- browsercfg 接进组合根:配置读 $DATA_DIR/config/plugins/browser.json(零 env)→ 探测(本机
Chromium → 已在跑的 CDP)→ 不可用就不注册(原因码入日志)
- 自研最小出站 CDP 客户端(不 Enabled Runtime、隔离世界求值、不发 --enable-automation)+
导航 + 截图 0600 落盘 + 指纹自检;工具 browser(open/screenshot/info,owner-only)
- 3 个变异自检(启动恢复 --enable-automation/无视可用性/空闲回收失效)
- 真机对照(两站各 1 次,**如实结论**):两侧都 pass ⇒ 「headless 被拦 / headful 通过」**未被证明**;
但测到两处硬差异:无头 UA 带 HeadlessChrome、screen 800 vs 2632;两模式 webdriver 均 false
(把 L6 的推断变成实测)。跑完 0 残留 Chrome、无对外监听端口
⚠️ 未做:MCP 自动接入(口径委派未落地)/自动注入上下文/面板页/搜索降级/docker 配方/Windows 用
pipe(Go ExtraFiles 不支持);SSRF 落了第三份同策实现待收口;浏览器会话未挂进程关闭链
61 lines
2.9 KiB
Go
61 lines
2.9 KiB
Go
package browser
|
||
|
||
import "context"
|
||
|
||
// Fingerprint 是一次「这台浏览器到底长什么样」的自检读数,对应
|
||
// `DevDocs/reference/browser-stealth/10 §4.1` 的落地自检项 L2~L5。它存在的意义是
|
||
// **可证伪**:有头到底有没有生效、webdriver 有没有被点亮、屏幕/时区/语言与设定是否
|
||
// 一致 —— 全靠这组**页面自报**的数字,而不是我们以为的启动参数。
|
||
type Fingerprint struct {
|
||
// Headless 来自本次会话的启动参数(我们有权威来源:参数是我们自己拼的),
|
||
// 不靠嗅探 UA —— `--headless=new` 的 UA **不含** Headless 字样(reference/05)。
|
||
Headless bool `json:"headless"`
|
||
// Webdriver 是 `navigator.webdriver`:false 才正常(我们不传 --enable-automation)。
|
||
Webdriver bool `json:"webdriver"`
|
||
// UserAgent / Platform 是页面自报的 UA 与平台串(原样,不做任何改写)。
|
||
UserAgent string `json:"userAgent"`
|
||
Platform string `json:"platform"`
|
||
// Language / Languages 是页面自报的语言列表(应与启动的 --lang 一致)。
|
||
Language string `json:"language"`
|
||
Languages []string `json:"languages"`
|
||
// Timezone 是 `Intl.DateTimeFormat().resolvedOptions().timeZone`(应与出口 IP 同源)。
|
||
Timezone string `json:"timezone"`
|
||
// ScreenWidth / ScreenHeight / ColorDepth 是 `screen.*`(应与 Xvfb 参数或真实桌面一致)。
|
||
ScreenWidth int `json:"screenWidth"`
|
||
ScreenHeight int `json:"screenHeight"`
|
||
ColorDepth int `json:"colorDepth"`
|
||
// OuterWidth / OuterHeight 是窗口外框尺寸:有头下应 > 0(无头常见 0)。
|
||
OuterWidth int `json:"outerWidth"`
|
||
OuterHeight int `json:"outerHeight"`
|
||
// DevicePixelRatio / HardwareConcurrency 是两条常见的指纹量。
|
||
DevicePixelRatio float64 `json:"devicePixelRatio"`
|
||
HardwareConcurrency int `json:"hardwareConcurrency"`
|
||
}
|
||
|
||
// fingerprintExpr 一次求值取回整组读数:分多次往返会让页面中途重定向后的读数互相
|
||
// 矛盾(一半是旧页、一半是新页)。它只在**隔离世界**里跑(session.evaluate)。
|
||
const fingerprintExpr = `(() => ({
|
||
webdriver: navigator.webdriver === true,
|
||
userAgent: navigator.userAgent,
|
||
platform: navigator.platform,
|
||
language: navigator.language,
|
||
languages: navigator.languages,
|
||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||
screenWidth: screen.width,
|
||
screenHeight: screen.height,
|
||
colorDepth: screen.colorDepth,
|
||
outerWidth: window.outerWidth,
|
||
outerHeight: window.outerHeight,
|
||
devicePixelRatio: window.devicePixelRatio,
|
||
hardwareConcurrency: navigator.hardwareConcurrency
|
||
}))()`
|
||
|
||
// readFingerprint 读一次页面指纹。Headless 取自会话的启动参数,其余取自页面自报。
|
||
func readFingerprint(ctx context.Context, s *session) (Fingerprint, error) {
|
||
fp := Fingerprint{Headless: s.headless}
|
||
if err := s.evaluate(ctx, fingerprintExpr, &fp); err != nil {
|
||
return Fingerprint{}, err
|
||
}
|
||
return fp, nil
|
||
}
|