353 lines
8.3 KiB
Go
353 lines
8.3 KiB
Go
|
//go:build !cgo
|
|||
|
// +build !cgo
|
|||
|
|
|||
|
package client
|
|||
|
|
|||
|
import (
|
|||
|
"bufio"
|
|||
|
"context"
|
|||
|
_ "embed"
|
|||
|
"fmt"
|
|||
|
"net/url"
|
|||
|
"os"
|
|||
|
"path/filepath"
|
|||
|
"reflect"
|
|||
|
"runtime"
|
|||
|
"strconv"
|
|||
|
"strings"
|
|||
|
|
|||
|
chromeRuntime "github.com/chromedp/cdproto/runtime"
|
|||
|
"github.com/chromedp/chromedp"
|
|||
|
"github.com/ssgo/tool/watcher"
|
|||
|
"github.com/ssgo/u"
|
|||
|
)
|
|||
|
|
|||
|
type Webview struct {
|
|||
|
id uint64
|
|||
|
ctx context.Context
|
|||
|
cancel context.CancelFunc
|
|||
|
isRunning bool
|
|||
|
// isAsync bool
|
|||
|
isDebug bool
|
|||
|
webRoot string
|
|||
|
watcher *watcher.Watcher
|
|||
|
html string
|
|||
|
url string
|
|||
|
closeChan chan bool
|
|||
|
width int
|
|||
|
height int
|
|||
|
title string
|
|||
|
evals []string
|
|||
|
bindFuncs map[string]any
|
|||
|
}
|
|||
|
|
|||
|
var chromeVersion = -1
|
|||
|
|
|||
|
func (w *Webview) New(isDebug bool) {
|
|||
|
if chromeVersion == -1 {
|
|||
|
chromeVersion = getChromeVersion()
|
|||
|
}
|
|||
|
w.isDebug = isDebug
|
|||
|
}
|
|||
|
|
|||
|
func (w *Webview) close() {
|
|||
|
// fmt.Println("====== w.Close()")
|
|||
|
if w.isRunning {
|
|||
|
w.isRunning = false
|
|||
|
// w.closeChan = make(chan bool, 1)
|
|||
|
// fmt.Println("====== w.w.Terminate()")
|
|||
|
w.cancel()
|
|||
|
// fmt.Println("====== w.w.Terminate() ok")
|
|||
|
// <-w.closeChan
|
|||
|
// w.closeChan = nil
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func (w *Webview) doClose() {
|
|||
|
// fmt.Println("====== w.doClose()")
|
|||
|
if w.watcher != nil {
|
|||
|
w.watcher.Stop()
|
|||
|
w.watcher = nil
|
|||
|
}
|
|||
|
windowsLock.Lock()
|
|||
|
delete(windows, w.id)
|
|||
|
n := len(windows)
|
|||
|
windowsLock.Unlock()
|
|||
|
|
|||
|
// fmt.Println("====== w.w.Destroy()")
|
|||
|
if w.cancel != nil {
|
|||
|
w.cancel()
|
|||
|
}
|
|||
|
// fmt.Println("====== w.w.Destroy() ok")
|
|||
|
|
|||
|
// 解除 w.Close 的等待
|
|||
|
if w.closeChan != nil {
|
|||
|
w.closeChan <- true
|
|||
|
}
|
|||
|
|
|||
|
// 解除 gojs.WaitAll 的等待
|
|||
|
if waitChan != nil && n == 0 {
|
|||
|
waitChan <- true
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func (w *Webview) loadHtml(html string) {
|
|||
|
w.url = "data:text/html;charset=utf-8," + url.PathEscape(html)
|
|||
|
if w.ctx != nil {
|
|||
|
chromedp.Run(w.ctx, chromedp.Navigate(w.url))
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func (w *Webview) loadURL(url string) {
|
|||
|
if url != "" {
|
|||
|
if strings.Contains(url, "://") {
|
|||
|
// chromedp.Run(w.ctx, chromedp.Navigate(url))
|
|||
|
w.url = url
|
|||
|
} else {
|
|||
|
if !filepath.IsAbs(url) {
|
|||
|
curPath, _ := os.Getwd()
|
|||
|
url = filepath.Join(curPath, url)
|
|||
|
}
|
|||
|
w.webRoot = filepath.Dir(url)
|
|||
|
w.url = "file://" + url
|
|||
|
// chromedp.Run(w.ctx, chromedp.Navigate(url))
|
|||
|
}
|
|||
|
} else {
|
|||
|
// chromedp.Run(w.ctx, chromedp.Navigate("about:blank"))
|
|||
|
w.url = "about:blank"
|
|||
|
}
|
|||
|
|
|||
|
if w.ctx != nil {
|
|||
|
chromedp.Run(w.ctx, chromedp.Navigate(w.url))
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func (w *Webview) reload() {
|
|||
|
if w.html != "" {
|
|||
|
w.loadHtml(w.html)
|
|||
|
} else {
|
|||
|
w.loadURL(w.url)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func (w *Webview) setTitle(title string) {
|
|||
|
w.title = title
|
|||
|
if w.ctx != nil {
|
|||
|
chromedp.Run(w.ctx, chromedp.Evaluate(`document.title = "`+w.title+`"`, nil))
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func (w *Webview) setSize(width int, height int, sizeMode string) {
|
|||
|
w.width = width
|
|||
|
w.height = height
|
|||
|
if w.ctx != nil {
|
|||
|
// chromedp.Run(w.ctx, chromedp.EmulateViewport(int64(w.width), int64(w.height)))
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func (w *Webview) eval(code string) any {
|
|||
|
if w.ctx == nil {
|
|||
|
w.evals = append(w.evals, code)
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
var result any
|
|||
|
chromedp.Run(w.ctx, chromedp.Evaluate(code, &result))
|
|||
|
return result
|
|||
|
}
|
|||
|
|
|||
|
func (w *Webview) Dispatch(f func()) {
|
|||
|
// w.w.Dispatch(f)
|
|||
|
f()
|
|||
|
}
|
|||
|
|
|||
|
func (w *Webview) Run() {
|
|||
|
// chromeVersion = 0
|
|||
|
if chromeVersion > 0 {
|
|||
|
// 根据版本调整配置
|
|||
|
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
|||
|
chromedp.Flag("headless", false), // 显示窗口
|
|||
|
chromedp.Flag("window-size", fmt.Sprintf("%d,%d", w.width, w.height)), // 窗口尺寸
|
|||
|
chromedp.Flag("app", w.url), // 直接启动到目标页
|
|||
|
chromedp.Flag("disable-notifications", true), // 禁用通知
|
|||
|
chromedp.Flag("no-first-run", true), // 跳过首次运行
|
|||
|
chromedp.Flag("no-default-browser-check", true), // 禁用默认浏览器检查
|
|||
|
chromedp.Flag("disable-component-update", true), // 禁用组件更新
|
|||
|
chromedp.Flag("disable-background-networking", true), // 禁用后台网络
|
|||
|
chromedp.Flag("disable-client-side-phishing-detection", true), // 禁用钓鱼检测
|
|||
|
)
|
|||
|
|
|||
|
if chromeVersion >= 115 {
|
|||
|
// 新版本
|
|||
|
opts = append(opts,
|
|||
|
chromedp.Flag("enable-automation", false),
|
|||
|
// chromedp.Flag("excludeSwitches", []string{"enable-automation"}),
|
|||
|
chromedp.Flag("useAutomationExtension", false),
|
|||
|
)
|
|||
|
} else if chromeVersion >= 90 {
|
|||
|
// Chrome 90+ 参数
|
|||
|
opts = append(opts,
|
|||
|
chromedp.Flag("disable-blink-features", "AutomationControlled"),
|
|||
|
)
|
|||
|
} else {
|
|||
|
// Chrome 89 及以下参数
|
|||
|
opts = append(opts,
|
|||
|
chromedp.Flag("disable-infobars", true),
|
|||
|
chromedp.Flag("disable-extensions", true),
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
allocCtx, cancel1 := chromedp.NewExecAllocator(context.Background(), opts...)
|
|||
|
ctx, cancel2 := chromedp.NewContext(allocCtx)
|
|||
|
w.ctx = ctx
|
|||
|
w.cancel = func() {
|
|||
|
cancel2()
|
|||
|
cancel1()
|
|||
|
}
|
|||
|
|
|||
|
codes := []string{getDetectionScript(chromeVersion)}
|
|||
|
if w.evals != nil {
|
|||
|
codes = append(codes, strings.Join(w.evals, "\n"))
|
|||
|
}
|
|||
|
|
|||
|
chromedp.ListenTarget(w.ctx, func(ev any) {
|
|||
|
if ev, ok := ev.(*chromeRuntime.EventBindingCalled); ok {
|
|||
|
if fn, ok := w.bindFuncs[ev.Name]; ok {
|
|||
|
fnV := reflect.ValueOf(fn)
|
|||
|
if fnV.Kind() == reflect.Func {
|
|||
|
var args []reflect.Value
|
|||
|
if ev.Payload != "" {
|
|||
|
argsV := u.FinalValue(reflect.ValueOf(u.UnJson(ev.Payload, nil)))
|
|||
|
if argsV.Kind() == reflect.Slice {
|
|||
|
for i := 0; i < fnV.Type().NumIn(); i++ {
|
|||
|
argType := fnV.Type().In(i)
|
|||
|
argValueP := reflect.New(argType)
|
|||
|
argValue := argValueP.Elem()
|
|||
|
if i < argsV.Len() {
|
|||
|
u.Convert(argsV.Index(i), argValueP)
|
|||
|
}
|
|||
|
args = append(args, argValue)
|
|||
|
}
|
|||
|
}
|
|||
|
go func() {
|
|||
|
_ = fnV.Call(args)
|
|||
|
}()
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
})
|
|||
|
|
|||
|
actions := []chromedp.Action{}
|
|||
|
|
|||
|
for k, _ := range w.bindFuncs {
|
|||
|
actions = append(actions, chromeRuntime.AddBinding(k))
|
|||
|
codes = append(codes, fmt.Sprintf("window.__%s = %s; window.%s = function () { __%s(JSON.stringify(Array.from(arguments))) }", k, k, k, k))
|
|||
|
}
|
|||
|
codes = append(codes, appCode)
|
|||
|
actions = append(actions, chromedp.Evaluate(strings.Join(codes, "\n"), nil))
|
|||
|
|
|||
|
exitChan := make(chan bool, 1)
|
|||
|
chromedp.Run(w.ctx, actions...)
|
|||
|
|
|||
|
go func() {
|
|||
|
<-w.ctx.Done()
|
|||
|
exitChan <- true
|
|||
|
}()
|
|||
|
|
|||
|
<-exitChan
|
|||
|
} else {
|
|||
|
if runtime.GOOS == "windows" {
|
|||
|
u.RunCommand("start", w.url)
|
|||
|
} else if runtime.GOOS == "darwin" {
|
|||
|
u.RunCommand("open", w.url)
|
|||
|
} else {
|
|||
|
u.RunCommand("xdg-open", w.url)
|
|||
|
}
|
|||
|
|
|||
|
fmt.Println("URL: ", w.url)
|
|||
|
fmt.Println("Press Enter to exit...")
|
|||
|
reader := bufio.NewReader(os.Stdin)
|
|||
|
_, _ = reader.ReadString('\n')
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func (w *Webview) binds() {
|
|||
|
w.bindFuncs = map[string]any{}
|
|||
|
bindsLock.RLock()
|
|||
|
for k, v := range binds {
|
|||
|
w.bindFuncs[k] = v
|
|||
|
}
|
|||
|
bindsLock.RUnlock()
|
|||
|
|
|||
|
w.bindFuncs["__closeWindow"] = w.close
|
|||
|
w.bindFuncs["__setTitle"] = w.setTitle
|
|||
|
w.bindFuncs["__setSize"] = w.setSize
|
|||
|
w.bindFuncs["__eval"] = w.eval
|
|||
|
}
|
|||
|
|
|||
|
// 获取浏览器版本(精确到主版本号)
|
|||
|
func getChromeVersion() int {
|
|||
|
var ua string
|
|||
|
ctx, cancel := chromedp.NewContext(context.Background())
|
|||
|
defer cancel()
|
|||
|
if err := chromedp.Run(ctx,
|
|||
|
chromedp.Evaluate("navigator.userAgent", &ua),
|
|||
|
); err != nil {
|
|||
|
return 0
|
|||
|
}
|
|||
|
|
|||
|
// 解析示例:Mozilla/5.0... Chrome/124.0.0.0...
|
|||
|
start := strings.Index(ua, "Chrome/")
|
|||
|
if start == -1 {
|
|||
|
return 0
|
|||
|
}
|
|||
|
|
|||
|
versionPart := ua[start+7:]
|
|||
|
end := strings.Index(versionPart, ".")
|
|||
|
if end == -1 {
|
|||
|
return 0
|
|||
|
}
|
|||
|
|
|||
|
version, _ := strconv.Atoi(versionPart[:end])
|
|||
|
return version
|
|||
|
}
|
|||
|
|
|||
|
// 生成版本自适应的检测脚本
|
|||
|
func getDetectionScript(version int) string {
|
|||
|
baseJS := `
|
|||
|
(() => {
|
|||
|
// 核心属性修改
|
|||
|
Object.defineProperty(navigator, 'webdriver', {
|
|||
|
get: () => false,
|
|||
|
configurable: true
|
|||
|
});
|
|||
|
|
|||
|
// 通用内存清理
|
|||
|
const legacyProps = ['$cdc', '__driver_evaluate'];
|
|||
|
const modernProps = ['_cdc', '__driver', '__automation'];
|
|||
|
[...legacyProps, ...modernProps].forEach(p => {
|
|||
|
try { delete window[p] } catch(e){}
|
|||
|
try { delete document[p] } catch(e){}
|
|||
|
});`
|
|||
|
|
|||
|
if version >= 120 {
|
|||
|
// 修复点:将 JS 模板字符串改为普通字符串拼接
|
|||
|
baseJS += `
|
|||
|
// 伪造 Chrome 运行时
|
|||
|
window.chrome = {
|
|||
|
runtime: {
|
|||
|
id: 'mock_extension',
|
|||
|
getManifest: () => ({ version: '1.0' }),
|
|||
|
getURL: function(path) {
|
|||
|
return 'chrome-extension://mock_extension/' + path;
|
|||
|
},
|
|||
|
onMessage: { addListener: function(){} }
|
|||
|
}
|
|||
|
};`
|
|||
|
}
|
|||
|
|
|||
|
baseJS += "\n})();"
|
|||
|
return baseJS + "\n"
|
|||
|
}
|