client/client_chrome.go

353 lines
8.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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