zy
Committed by GitHub

Merge pull request #33 from autoclaw-cc/fix/stealth-anti-detection

perf: 浏览器反检测模块全面优化 - 动态平台适配与指纹一致性
@@ -15,7 +15,7 @@ import requests @@ -15,7 +15,7 @@ import requests
15 import websockets.sync.client as ws_client 15 import websockets.sync.client as ws_client
16 16
17 from .errors import CDPError, ElementNotFoundError 17 from .errors import CDPError, ElementNotFoundError
18 -from .stealth import REALISTIC_UA, STEALTH_JS 18 +from .stealth import STEALTH_JS, build_ua_override
19 19
20 logger = logging.getLogger(__name__) 20 logger = logging.getLogger(__name__)
21 21
@@ -571,6 +571,7 @@ class Browser: @@ -571,6 +571,7 @@ class Browser:
571 self.port = port 571 self.port = port
572 self.base_url = f"http://{host}:{port}" 572 self.base_url = f"http://{host}:{port}"
573 self._cdp: CDPClient | None = None 573 self._cdp: CDPClient | None = None
  574 + self._chrome_version: str | None = None
574 575
575 def connect(self) -> None: 576 def connect(self) -> None:
576 """连接到 Chrome DevTools。""" 577 """连接到 Chrome DevTools。"""
@@ -578,7 +579,13 @@ class Browser: @@ -578,7 +579,13 @@ class Browser:
578 resp.raise_for_status() 579 resp.raise_for_status()
579 info = resp.json() 580 info = resp.json()
580 ws_url = info["webSocketDebuggerUrl"] 581 ws_url = info["webSocketDebuggerUrl"]
581 - logger.info("连接到 Chrome: %s", ws_url) 582 +
  583 + # 从 "Chrome/134.0.6998.88" 提取真实版本号,用于动态构建 UA
  584 + browser_str = info.get("Browser", "")
  585 + if "/" in browser_str:
  586 + self._chrome_version = browser_str.split("/", 1)[1]
  587 +
  588 + logger.info("连接到 Chrome: %s (version=%s)", ws_url, self._chrome_version)
582 self._cdp = CDPClient(ws_url) 589 self._cdp = CDPClient(ws_url)
583 590
584 def _setup_page(self, page: Page) -> Page: 591 def _setup_page(self, page: Page) -> Page:
@@ -586,7 +593,10 @@ class Browser: @@ -586,7 +593,10 @@ class Browser:
586 import contextlib 593 import contextlib
587 594
588 page.inject_stealth() 595 page.inject_stealth()
589 - page._send_session("Emulation.setUserAgentOverride", {"userAgent": REALISTIC_UA}) 596 + page._send_session(
  597 + "Emulation.setUserAgentOverride",
  598 + build_ua_override(self._chrome_version),
  599 + )
590 page._send_session( 600 page._send_session(
591 "Emulation.setDeviceMetricsOverride", 601 "Emulation.setDeviceMetricsOverride",
592 { 602 {
1 -"""反检测 JS 注入 + Chrome 启动参数,对应 go-rod/stealth。""" 1 +"""反检测配置:UA / Client Hints / JS 注入 / Chrome 启动参数。
2 2
3 -# 真实 Chrome UA(固定版本,避免每次随机导致指纹不一致)  
4 -REALISTIC_UA = ( 3 +关键原则:UA、navigator.platform、Client Hints、WebGL 等所有信号必须与实际平台一致。
  4 +"""
  5 +
  6 +from __future__ import annotations
  7 +
  8 +import platform as _platform
  9 +
  10 +# Chrome 版本号 — 定期更新以匹配主流版本(当前对应 2025 年中期稳定版)
  11 +_CHROME_VER = "136"
  12 +_CHROME_FULL_VER = "136.0.0.0"
  13 +
  14 +
  15 +def _build_platform_config() -> dict:
  16 + """根据实际操作系统生成一致的 UA / Client Hints / WebGL 配置。"""
  17 + system = _platform.system()
  18 +
  19 + brands = [
  20 + {"brand": "Chromium", "version": _CHROME_VER},
  21 + {"brand": "Google Chrome", "version": _CHROME_VER},
  22 + {"brand": "Not-A.Brand", "version": "24"},
  23 + ]
  24 + full_version_list = [
  25 + {"brand": "Chromium", "version": _CHROME_FULL_VER},
  26 + {"brand": "Google Chrome", "version": _CHROME_FULL_VER},
  27 + {"brand": "Not-A.Brand", "version": "24.0.0.0"},
  28 + ]
  29 +
  30 + if system == "Darwin":
  31 + arch = "arm" if _platform.machine() == "arm64" else "x86"
  32 + return {
  33 + "ua": (
  34 + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
  35 + "AppleWebKit/537.36 (KHTML, like Gecko) "
  36 + f"Chrome/{_CHROME_FULL_VER} Safari/537.36"
  37 + ),
  38 + "nav_platform": "MacIntel",
  39 + "ua_metadata": {
  40 + "brands": brands,
  41 + "fullVersionList": full_version_list,
  42 + "platform": "macOS",
  43 + "platformVersion": "14.5.0",
  44 + "architecture": arch,
  45 + "model": "",
  46 + "mobile": False,
  47 + "bitness": "64",
  48 + "wow64": False,
  49 + },
  50 + "webgl_vendor": "Apple Inc.",
  51 + "webgl_renderer": (
  52 + "ANGLE (Apple, ANGLE Metal Renderer: Apple M1, Unspecified Version)"
  53 + ),
  54 + }
  55 +
  56 + if system == "Windows":
  57 + return {
  58 + "ua": (
5 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " 59 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
6 "AppleWebKit/537.36 (KHTML, like Gecko) " 60 "AppleWebKit/537.36 (KHTML, like Gecko) "
7 - "Chrome/131.0.0.0 Safari/537.36"  
8 -) 61 + f"Chrome/{_CHROME_FULL_VER} Safari/537.36"
  62 + ),
  63 + "nav_platform": "Win32",
  64 + "ua_metadata": {
  65 + "brands": brands,
  66 + "fullVersionList": full_version_list,
  67 + "platform": "Windows",
  68 + "platformVersion": "15.0.0",
  69 + "architecture": "x86",
  70 + "model": "",
  71 + "mobile": False,
  72 + "bitness": "64",
  73 + "wow64": False,
  74 + },
  75 + "webgl_vendor": "Google Inc. (Intel)",
  76 + "webgl_renderer": (
  77 + "ANGLE (Intel, Intel(R) UHD Graphics 630 (CML GT2), Direct3D11)"
  78 + ),
  79 + }
  80 +
  81 + # Linux
  82 + return {
  83 + "ua": (
  84 + "Mozilla/5.0 (X11; Linux x86_64) "
  85 + "AppleWebKit/537.36 (KHTML, like Gecko) "
  86 + f"Chrome/{_CHROME_FULL_VER} Safari/537.36"
  87 + ),
  88 + "nav_platform": "Linux x86_64",
  89 + "ua_metadata": {
  90 + "brands": brands,
  91 + "fullVersionList": full_version_list,
  92 + "platform": "Linux",
  93 + "platformVersion": "6.5.0",
  94 + "architecture": "x86",
  95 + "model": "",
  96 + "mobile": False,
  97 + "bitness": "64",
  98 + "wow64": False,
  99 + },
  100 + "webgl_vendor": "Google Inc. (Mesa)",
  101 + "webgl_renderer": (
  102 + "ANGLE (Mesa, Mesa Intel(R) UHD Graphics 630 (CML GT2), OpenGL 4.6)"
  103 + ),
  104 + }
  105 +
  106 +
  107 +PLATFORM_CONFIG = _build_platform_config()
  108 +
  109 +# 向后兼容导出
  110 +REALISTIC_UA = PLATFORM_CONFIG["ua"]
  111 +
  112 +
  113 +def build_ua_override(chrome_full_ver: str | None = None) -> dict:
  114 + """构建 Emulation.setUserAgentOverride 参数。
  115 +
  116 + Args:
  117 + chrome_full_ver: Chrome 完整版本号(如 "134.0.6998.88"),
  118 + 从 CDP /json/version 接口获取。为 None 时使用默认值。
  119 +
  120 + Returns:
  121 + 可直接传给 Emulation.setUserAgentOverride 的参数字典。
  122 + """
  123 + ver = chrome_full_ver or _CHROME_FULL_VER
  124 + major = ver.split(".")[0]
  125 + system = _platform.system()
  126 +
  127 + brands = [
  128 + {"brand": "Chromium", "version": major},
  129 + {"brand": "Google Chrome", "version": major},
  130 + {"brand": "Not-A.Brand", "version": "24"},
  131 + ]
  132 + full_version_list = [
  133 + {"brand": "Chromium", "version": ver},
  134 + {"brand": "Google Chrome", "version": ver},
  135 + {"brand": "Not-A.Brand", "version": "24.0.0.0"},
  136 + ]
9 137
10 -# 反检测 JS 脚本:在页面加载时注入  
11 -STEALTH_JS = """ 138 + if system == "Darwin":
  139 + arch = "arm" if _platform.machine() == "arm64" else "x86"
  140 + ua = (
  141 + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
  142 + "AppleWebKit/537.36 (KHTML, like Gecko) "
  143 + f"Chrome/{ver} Safari/537.36"
  144 + )
  145 + nav_platform = "MacIntel"
  146 + ua_platform = "macOS"
  147 + platform_ver = "14.5.0"
  148 + elif system == "Windows":
  149 + arch = "x86"
  150 + ua = (
  151 + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
  152 + "AppleWebKit/537.36 (KHTML, like Gecko) "
  153 + f"Chrome/{ver} Safari/537.36"
  154 + )
  155 + nav_platform = "Win32"
  156 + ua_platform = "Windows"
  157 + platform_ver = "15.0.0"
  158 + else:
  159 + arch = "x86"
  160 + ua = (
  161 + "Mozilla/5.0 (X11; Linux x86_64) "
  162 + "AppleWebKit/537.36 (KHTML, like Gecko) "
  163 + f"Chrome/{ver} Safari/537.36"
  164 + )
  165 + nav_platform = "Linux x86_64"
  166 + ua_platform = "Linux"
  167 + platform_ver = "6.5.0"
  168 +
  169 + return {
  170 + "userAgent": ua,
  171 + "platform": nav_platform,
  172 + "userAgentMetadata": {
  173 + "brands": brands,
  174 + "fullVersionList": full_version_list,
  175 + "platform": ua_platform,
  176 + "platformVersion": platform_ver,
  177 + "architecture": arch,
  178 + "model": "",
  179 + "mobile": False,
  180 + "bitness": "64",
  181 + "wow64": False,
  182 + },
  183 + }
  184 +
  185 +# ---------------------------------------------------------------------------
  186 +# 反检测 JS 脚本模板($$占位符$$ 由 Python 替换为平台值)
  187 +# ---------------------------------------------------------------------------
  188 +_STEALTH_JS_TEMPLATE = """
12 (() => { 189 (() => {
13 - // 1. navigator.webdriver  
14 - Object.defineProperty(navigator, 'webdriver', {  
15 - get: () => undefined, 190 + // 1. navigator.webdriver — Proxy 包装原始 native getter,toString() 仍返回 [native code]
  191 + const wd = Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver');
  192 + if (wd && wd.get) {
  193 + Object.defineProperty(Navigator.prototype, 'webdriver', {
  194 + get: new Proxy(wd.get, { apply: () => false }),
16 configurable: true, 195 configurable: true,
17 }); 196 });
  197 + }
18 198
19 // 2. chrome.runtime 199 // 2. chrome.runtime
20 - if (!window.chrome) {  
21 - window.chrome = {};  
22 - } 200 + if (!window.chrome) window.chrome = {};
23 if (!window.chrome.runtime) { 201 if (!window.chrome.runtime) {
24 - window.chrome.runtime = {  
25 - connect: () => {},  
26 - sendMessage: () => {},  
27 - }; 202 + window.chrome.runtime = { connect: () => {}, sendMessage: () => {} };
28 } 203 }
29 204
30 - // 3. plugins  
31 - Object.defineProperty(navigator, 'plugins', {  
32 - get: () => {  
33 - return [  
34 - {  
35 - 0: {type: 'application/x-google-chrome-pdf'},  
36 - description: 'Portable Document Format',  
37 - filename: 'internal-pdf-viewer',  
38 - length: 1,  
39 - name: 'Chrome PDF Plugin',  
40 - },  
41 - {  
42 - 0: {type: 'application/pdf'},  
43 - description: '',  
44 - filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',  
45 - length: 1,  
46 - name: 'Chrome PDF Viewer',  
47 - },  
48 - {  
49 - 0: {type: 'application/x-nacl'},  
50 - description: '',  
51 - filename: 'internal-nacl-plugin',  
52 - length: 1,  
53 - name: 'Native Client', 205 + // 3. chrome.app — headless 缺失此对象,检测脚本会检查
  206 + if (!window.chrome.app) {
  207 + window.chrome.app = {
  208 + isInstalled: false,
  209 + InstallState: {
  210 + DISABLED: 'disabled',
  211 + INSTALLED: 'installed',
  212 + NOT_INSTALLED: 'not_installed',
54 }, 213 },
55 - ]; 214 + RunningState: {
  215 + CANNOT_RUN: 'cannot_run',
  216 + READY_TO_RUN: 'ready_to_run',
  217 + RUNNING: 'running',
56 }, 218 },
  219 + getDetails: function() {},
  220 + getIsInstalled: function() {},
  221 + installState: function() { return 'not_installed'; },
  222 + runningState: function() { return 'cannot_run'; },
  223 + };
  224 + }
  225 +
  226 + // 4. navigator.vendor — Chrome 应返回 "Google Inc."
  227 + Object.defineProperty(navigator, 'vendor', {
  228 + get: () => 'Google Inc.',
57 configurable: true, 229 configurable: true,
58 }); 230 });
59 231
  232 + // 5. plugins — 不覆盖,真实 Chrome 已有正确的 PluginArray
  233 +
60 // 4. languages 234 // 4. languages
61 Object.defineProperty(navigator, 'languages', { 235 Object.defineProperty(navigator, 'languages', {
62 get: () => ['zh-CN', 'zh', 'en-US', 'en'], 236 get: () => ['zh-CN', 'zh', 'en-US', 'en'],
@@ -72,13 +246,19 @@ STEALTH_JS = """ @@ -72,13 +246,19 @@ STEALTH_JS = """
72 : originalQuery(parameters); 246 : originalQuery(parameters);
73 } 247 }
74 248
75 - // 6. WebGL vendor/renderer  
76 - const getParameter = WebGLRenderingContext.prototype.getParameter;  
77 - WebGLRenderingContext.prototype.getParameter = function(parameter) {  
78 - if (parameter === 37445) return 'Intel Inc.';  
79 - if (parameter === 37446) return 'Intel Iris OpenGL Engine';  
80 - return getParameter.call(this, parameter); 249 + // 6. WebGL vendor/renderer — 与平台一致(同时覆盖 WebGL1 和 WebGL2)
  250 + const overrideWebGL = (proto) => {
  251 + const original = proto.getParameter;
  252 + proto.getParameter = function(p) {
  253 + if (p === 37445) return '$$WEBGL_VENDOR$$';
  254 + if (p === 37446) return '$$WEBGL_RENDERER$$';
  255 + return original.call(this, p);
81 }; 256 };
  257 + };
  258 + overrideWebGL(WebGLRenderingContext.prototype);
  259 + if (typeof WebGL2RenderingContext !== 'undefined') {
  260 + overrideWebGL(WebGL2RenderingContext.prototype);
  261 + }
82 262
83 // 7. hardwareConcurrency — 随机 4 或 8 263 // 7. hardwareConcurrency — 随机 4 或 8
84 Object.defineProperty(navigator, 'hardwareConcurrency', { 264 Object.defineProperty(navigator, 'hardwareConcurrency', {
@@ -109,18 +289,18 @@ STEALTH_JS = """ @@ -109,18 +289,18 @@ STEALTH_JS = """
109 window.chrome.loadTimes = function() { return {}; }; 289 window.chrome.loadTimes = function() { return {}; };
110 } 290 }
111 291
112 - // 11. outerWidth/outerHeight — 与 innerWidth/innerHeight 对齐  
113 - Object.defineProperty(window, 'outerWidth', {  
114 - get: () => window.innerWidth,  
115 - configurable: true,  
116 - });  
117 - Object.defineProperty(window, 'outerHeight', {  
118 - get: () => window.innerHeight,  
119 - configurable: true,  
120 - }); 292 + // 11. outerWidth/outerHeight — 不覆盖
  293 + // 正常浏览器 outer > inner(有标题栏/工具栏),设为相等反而暴露自动化特征
  294 +
121 })(); 295 })();
122 """ 296 """
123 297
  298 +STEALTH_JS = (
  299 + _STEALTH_JS_TEMPLATE
  300 + .replace("$$WEBGL_VENDOR$$", PLATFORM_CONFIG["webgl_vendor"])
  301 + .replace("$$WEBGL_RENDERER$$", PLATFORM_CONFIG["webgl_renderer"])
  302 +)
  303 +
124 # Chrome 启动参数(反检测相关) 304 # Chrome 启动参数(反检测相关)
125 STEALTH_ARGS = [ 305 STEALTH_ARGS = [
126 "--disable-blink-features=AutomationControlled", 306 "--disable-blink-features=AutomationControlled",