client/client_chrome.go

353 lines
8.3 KiB
Go
Raw Permalink Normal View History

//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"
}