Skip to content

Commit c26fa98

Browse files
authored
Merge pull request #33 from autoclaw-cc/fix/stealth-anti-detection
perf: 浏览器反检测模块全面优化 - 动态平台适配与指纹一致性
2 parents 41c2e11 + 406e059 commit c26fa98

File tree

2 files changed

+255
-65
lines changed

2 files changed

+255
-65
lines changed

scripts/xhs/cdp.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import websockets.sync.client as ws_client
1616

1717
from .errors import CDPError, ElementNotFoundError
18-
from .stealth import REALISTIC_UA, STEALTH_JS
18+
from .stealth import STEALTH_JS, build_ua_override
1919

2020
logger = logging.getLogger(__name__)
2121

@@ -571,22 +571,32 @@ def __init__(self, host: str = "127.0.0.1", port: int = 9222) -> None:
571571
self.port = port
572572
self.base_url = f"http://{host}:{port}"
573573
self._cdp: CDPClient | None = None
574+
self._chrome_version: str | None = None
574575

575576
def connect(self) -> None:
576577
"""连接到 Chrome DevTools。"""
577578
resp = requests.get(f"{self.base_url}/json/version", timeout=5)
578579
resp.raise_for_status()
579580
info = resp.json()
580581
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)
582589
self._cdp = CDPClient(ws_url)
583590

584591
def _setup_page(self, page: Page) -> Page:
585592
"""为 Page 对象注入 stealth、UA、viewport,并启用必要的 CDP domain。"""
586593
import contextlib
587594

588595
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+
)
590600
page._send_session(
591601
"Emulation.setDeviceMetricsOverride",
592602
{

scripts/xhs/stealth.py

Lines changed: 242 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,236 @@
1-
"""反检测 JS 注入 + Chrome 启动参数,对应 go-rod/stealth。"""
1+
"""反检测配置:UA / Client Hints / JS 注入 / Chrome 启动参数
22
3-
# 真实 Chrome UA(固定版本,避免每次随机导致指纹不一致)
4-
REALISTIC_UA = (
5-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
6-
"AppleWebKit/537.36 (KHTML, like Gecko) "
7-
"Chrome/131.0.0.0 Safari/537.36"
8-
)
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": (
59+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
60+
"AppleWebKit/537.36 (KHTML, like Gecko) "
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+
]
9137

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 = """
12189
(() => {
13-
// 1. navigator.webdriver
14-
Object.defineProperty(navigator, 'webdriver', {
15-
get: () => undefined,
16-
configurable: true,
17-
});
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 }),
195+
configurable: true,
196+
});
197+
}
18198
19199
// 2. chrome.runtime
20-
if (!window.chrome) {
21-
window.chrome = {};
22-
}
200+
if (!window.chrome) window.chrome = {};
23201
if (!window.chrome.runtime) {
24-
window.chrome.runtime = {
25-
connect: () => {},
26-
sendMessage: () => {},
202+
window.chrome.runtime = { connect: () => {}, sendMessage: () => {} };
203+
}
204+
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',
213+
},
214+
RunningState: {
215+
CANNOT_RUN: 'cannot_run',
216+
READY_TO_RUN: 'ready_to_run',
217+
RUNNING: 'running',
218+
},
219+
getDetails: function() {},
220+
getIsInstalled: function() {},
221+
installState: function() { return 'not_installed'; },
222+
runningState: function() { return 'cannot_run'; },
27223
};
28224
}
29225
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',
54-
},
55-
];
56-
},
226+
// 4. navigator.vendor — Chrome 应返回 "Google Inc."
227+
Object.defineProperty(navigator, 'vendor', {
228+
get: () => 'Google Inc.',
57229
configurable: true,
58230
});
59231
232+
// 5. plugins — 不覆盖,真实 Chrome 已有正确的 PluginArray
233+
60234
// 4. languages
61235
Object.defineProperty(navigator, 'languages', {
62236
get: () => ['zh-CN', 'zh', 'en-US', 'en'],
@@ -72,13 +246,19 @@
72246
: originalQuery(parameters);
73247
}
74248
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);
256+
};
81257
};
258+
overrideWebGL(WebGLRenderingContext.prototype);
259+
if (typeof WebGL2RenderingContext !== 'undefined') {
260+
overrideWebGL(WebGL2RenderingContext.prototype);
261+
}
82262
83263
// 7. hardwareConcurrency — 随机 4 或 8
84264
Object.defineProperty(navigator, 'hardwareConcurrency', {
@@ -109,18 +289,18 @@
109289
window.chrome.loadTimes = function() { return {}; };
110290
}
111291
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+
121295
})();
122296
"""
123297

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+
124304
# Chrome 启动参数(反检测相关)
125305
STEALTH_ARGS = [
126306
"--disable-blink-features=AutomationControlled",

0 commit comments

Comments
 (0)