http/chrome.go
Star e5b45bd7ff add go-rod/stealth for chrome
add httpproxy, useragent and some chrome config
2026-03-23 00:05:41 +08:00

308 lines
8.4 KiB
Go
Raw 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.

package http
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"apigo.cc/gojs"
"apigo.cc/gojs/goja"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/launcher/flags"
"github.com/ssgo/u"
"github.com/ysmood/fetchup"
)
type Chrome struct {
id string
chromeURL string
chromePath string
launcher *launcher.Launcher
browser *rod.Browser
}
var chromes = map[string]*Chrome{}
var chromesLock sync.Mutex
func (ch *Chrome) Close(vm *goja.Runtime) {
// logger := gojs.GetLogger(vm)
if ch.browser != nil {
// ver, _ := ch.browser.Version()
// logger.Info("关闭Chrome浏览器", "id", ch.id, "browser", ver.Product, "userAgent", ver.UserAgent, "chromeURL", ch.chromeURL, "chromePath", ch.chromePath)
ch.browser.Close()
ch.browser = nil
}
if ch.launcher != nil {
ch.launcher.Cleanup()
ch.launcher = nil
}
chromesLock.Lock()
delete(chromes, ch.id)
chromesLock.Unlock()
}
func CloseAllChrome(vm *goja.Runtime) {
n := len(chromes)
if n > 0 {
for _, ch := range chromes {
ch.Close(vm)
}
logger := gojs.GetLogger(vm)
logger.Info("关闭所有未主动关闭的Chrome浏览器", "count", n)
}
}
type ChromeInfo struct {
ProtocolVersion string
Product string
Revision string
UserAgent string
JsVersion string
ChromeURL string
ChromePath string
}
type ChromeOption struct {
ChromeURL string
ChromePath string
ChromeDownloadMirror string
ChromeDownloadPath string
ChromeOption []string
ShowWindow bool
HttpProxy string
UserAgent string
}
func (ch *Chrome) Info(showWindow *bool, vm *goja.Runtime) (ChromeInfo, error) {
ver, err := ch.browser.Version()
if err != nil {
return ChromeInfo{
ChromeURL: ch.chromeURL,
ChromePath: ch.chromePath,
}, err
}
return ChromeInfo{
ProtocolVersion: ver.ProtocolVersion,
Product: ver.Product,
Revision: ver.Revision,
UserAgent: ver.UserAgent,
JsVersion: ver.JsVersion,
ChromeURL: ch.chromeURL,
ChromePath: ch.chromePath,
}, nil
}
var hostConf = map[string]struct {
urlPrefix string
zipName string
}{
"darwin_amd64": {"Mac", "chrome-mac.zip"},
"darwin_arm64": {"Mac_Arm", "chrome-mac.zip"},
"linux_amd64": {"Linux_x64", "chrome-linux.zip"},
"windows_386": {"Win", "chrome-win.zip"},
"windows_amd64": {"Win_x64", "chrome-win.zip"},
}[runtime.GOOS+"_"+runtime.GOARCH]
func StartChrome(opt *ChromeOption, vm *goja.Runtime) (*Chrome, error) {
if opt == nil {
opt = &ChromeOption{}
}
if opt.ChromeURL == "" {
opt.ChromeURL = conf.ChromeURL
}
if opt.ChromePath == "" {
opt.ChromePath = conf.ChromePath
}
if opt.ChromeDownloadMirror == "" {
opt.ChromeDownloadMirror = conf.ChromeDownloadMirror
}
if opt.ChromeDownloadPath == "" {
opt.ChromeDownloadPath = conf.ChromeDownloadPath
}
if opt.HttpProxy == "" {
opt.HttpProxy = conf.HttpProxy
}
if opt.UserAgent == "" {
opt.UserAgent = conf.UserAgent
}
if opt.ChromeOption == nil {
opt.ChromeOption = conf.ChromeOption
}
// logger := gojs.GetLogger(vm)
ch := &Chrome{}
ch.id = u.UniqueId()
ch.browser = rod.New()
ch.chromeURL = opt.ChromeURL
if opt.ChromeURL == "" {
// 使用本地Chrome
ch.launcher = launcher.New()
if opt.ChromePath != "" {
ch.launcher.Bin(opt.ChromePath)
ch.chromePath = opt.ChromePath
} else if path, has := launcher.LookPath(); has {
ch.launcher.Bin(path)
ch.chromePath = path
} else {
// 自动下载Chrome
lb := launcher.NewBrowser()
// 默认优先级:淘宝(NPM) -> Playwright -> Google
lb.Hosts = []launcher.Host{launcher.HostNPM, launcher.HostPlaywright, launcher.HostGoogle}
if opt.ChromeDownloadMirror != "" {
// 定义私有镜像 Host 函数
var customHost launcher.Host = func(rev int) string {
// 构造逻辑完全对齐 rod 官方Prefix/OS_Arch/Revision/ZipName
return fmt.Sprintf("%s/%s/%d/%s",
strings.TrimSuffix(opt.ChromeDownloadMirror, "/"), // 去掉末尾的斜杠,防止拼接出双斜杠
hostConf.urlPrefix,
rev,
hostConf.zipName,
)
}
// 将自定义镜像插入到最前面
lb.Hosts = append([]launcher.Host{customHost}, lb.Hosts...)
}
var path string
var err error
// --- 核心修改区:只有指定了路径才走手动逻辑 ---
if opt.ChromeDownloadPath != "" {
rev := lb.Revision
// 构造存放目录和最终二进制路径
destDir := filepath.Join(opt.ChromeDownloadPath, fmt.Sprintf("chromium-%d", rev))
var binPath string
switch runtime.GOOS {
case "darwin": // macOS
binPath = filepath.Join(destDir, "Chromium.app/Contents/MacOS/Chromium")
case "windows": // Windows
binPath = filepath.Join(destDir, "chrome.exe")
default: // linux
binPath = filepath.Join(destDir, "chrome")
}
// --- 检查本地是否存在 (实现跳过下载) ---
if _, err = os.Stat(binPath); err == nil {
path = binPath
} else {
// --- 手动触发 fetchup 下载流程 ---
urls := []string{}
for _, host := range lb.Hosts {
urls = append(urls, host(rev))
}
// 使用你发现的 fetchup 逻辑,直接下载到指定的 destDir
fu := fetchup.New(destDir, urls...)
// 如果你设置了代理,也可以传给 fu.HttpClient
err = fu.Fetch()
if err == nil {
// 自动处理解压后的目录层级缩减
_ = fetchup.StripFirstDir(destDir)
path = binPath
// 修正 Linux 权限
if runtime.GOOS != "windows" {
_ = os.Chmod(path, 0755)
}
}
}
} else {
// 如果没有指定自定义路径,直接走 Rod 默认逻辑(会走 ENV 和默认缓存)
path, err = lb.Get()
}
if err != nil {
ch.Close(vm)
return nil, gojs.Err(fmt.Errorf("自动下载浏览器失败: %w", err))
}
ch.launcher.Bin(path)
ch.chromePath = path
}
// 基础防御参数
ch.launcher.
Set("disable-dev-shm-usage").
Set("single-process"). // 保留你原有的逻辑
Set("disable-blink-features", "AutomationControlled").
Set("no-sandbox").
Set("disable-infobars").
Delete("enable-automation") // 彻底删除自动化控制标志
// --- 动态 Headless 逻辑 ---
if opt.ShowWindow {
ch.launcher.Headless(false)
} else {
// 使用新版无头模式,不要调用 .Headless(true)
ch.launcher.Set("headless", "new")
}
// --- 动态 User-Agent 优化 ---
if opt.UserAgent != "" {
ch.launcher.Set("user-agent", opt.UserAgent)
} else {
// 建议版本号保持在 130+,目前 146 有点过于领先(当前稳定版大约在 134 左右)
const chromeVersion = "134.0.0.0"
switch runtime.GOOS {
case "darwin": // macOS
ch.launcher.Set("user-agent", fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", chromeVersion))
case "linux":
// 建议 Linux 下还是用 Linux 的 UA减少被识别为“伪装者”的风险
ch.launcher.Set("user-agent", fmt.Sprintf("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", chromeVersion))
default: // windows
ch.launcher.Set("user-agent", fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", chromeVersion))
}
}
if opt.HttpProxy != "" {
ch.launcher.Proxy(opt.HttpProxy)
}
// 操作系统特定兼容
switch runtime.GOOS {
case "linux":
ch.launcher.Set("disable-setuid-sandbox")
case "windows":
ch.launcher.Set("disable-features=RendererCodeIntegrity")
}
if opt.ChromeOption != nil {
for _, opt := range opt.ChromeOption {
a := u.SplitTrimN(opt, "=", 2)
if len(a) == 2 {
ch.launcher.Set(flags.Flag(a[0]), a[1])
} else {
ch.launcher.Set(flags.Flag(opt))
}
}
}
if localChromeURL, err := ch.launcher.Launch(); err != nil {
ch.Close(vm)
return nil, gojs.Err(err)
} else {
ch.chromeURL = localChromeURL
if ch.chromePath == "" {
ch.chromePath = ch.launcher.Get(flags.Bin)
}
}
}
ch.browser.ControlURL(ch.chromeURL)
if err := ch.browser.Connect(); err != nil {
return nil, gojs.Err(err)
}
// ver, _ := ch.browser.Version()
// logger.Info("启动Chrome浏览器", "id", ch.id, "browser", ver.Product, "userAgent", ver.UserAgent, "chromeURL", ch.chromeURL, "chromePath", ch.chromePath)
chromesLock.Lock()
chromes[ch.id] = ch
chromesLock.Unlock()
return ch, nil
}