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