Align Turnstile with zip refs and harden interactive CF path

Restore MV2 turnstilePatch (content.js), zip-style headed browser flags,
system-default browser path, and interactive-safe solver that skips reset
while the human checkbox is up to avoid 验证失败 after the button appears.
This commit is contained in:
chaos committed 2026-07-12 07:04:38 +08:00
1 parent c87b01967f
commit 60e24b89c1
5 files changed
+449 -157

No files matched your search

+5 -4
View File
@@ -10,7 +10,7 @@
"proxy": "", "proxy": "",
"enable_nsfw": true, "enable_nsfw": true,
"register_count": 1, "register_count": 1,
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"grok2api_auto_add_local": false, "grok2api_auto_add_local": false,
"grok2api_local_token_file": "", "grok2api_local_token_file": "",
"grok2api_pool_name": "ssoBasic", "grok2api_pool_name": "ssoBasic",
@@ -102,7 +102,8 @@
"turnstile_retry_after_sec": 10, "turnstile_retry_after_sec": 10,
"turnstile_retry_gap_sec": 8, "turnstile_retry_gap_sec": 8,
"turnstile_max_rounds": 20, "turnstile_max_rounds": 20,
"browser_prefer": "chrome", "browser_prefer": "auto",
"browser_path": "/usr/bin/google-chrome-stable", "browser_path": "",
"browser_headless": false "browser_headless": false,
"stealth_mode": "off"
} }
+299 -120
View File
@@ -613,23 +613,25 @@ def add_token_to_grok2api_pools(raw_token, email="", log_callback=None):
_add_token_to_grok2api_pools_sync(raw_token, email=email, log_callback=log_callback) _add_token_to_grok2api_pools_sync(raw_token, email=email, log_callback=log_callback)
# Base flags always safe. Keep close to Git-creat7/grokRegister-cpa which only # Headed flags from grok-register.zip (ReinerBRO): no --disable-images, no AutomationControlled.
# uses auto_port + extension (no slim flags) on real headed desktops. # Images must stay ON or Turnstile iframe often fails to paint.
CHROMIUM_BASE_FLAGS = [ CHROMIUM_BASE_FLAGS = [
"--no-sandbox",
"--disable-dev-shm-usage",
# Keep images ON: Turnstile/CF widgets often need them; was a common fail cause.
"--mute-audio",
"--no-first-run", "--no-first-run",
"--disable-blink-features=AutomationControlled", "--no-default-browser-check",
"--disable-background-networking",
"--disable-component-update",
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-renderer-backgrounding",
] ]
# Extra flags only for true headless (no DISPLAY). Under Xvfb/headed these can # Headless / Linux-server extras only. --disable-software-rasterizer blanks CF on headed.
# blank CF widgets (iframe=0 / token=0) — especially --disable-software-rasterizer.
CHROMIUM_HEADLESS_EXTRA_FLAGS = [ CHROMIUM_HEADLESS_EXTRA_FLAGS = [
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu", "--disable-gpu",
"--disable-software-rasterizer", "--disable-software-rasterizer",
"--disable-background-networking", "--mute-audio",
] ]
# Back-compat alias used by older call sites / register_cli patches # Back-compat alias used by older call sites / register_cli patches
@@ -1001,12 +1003,14 @@ def _first_existing_browser(candidates) -> str | None:
def resolve_browser_path(): def resolve_browser_path():
"""Pick Chromium/Chrome for registration. """Pick Chromium/Chrome for registration.
Zip style (grok-register / protocol_cpa): do NOT set browser_path unless
config/env explicitly points at a real binary — let DrissionPage use system Chrome.
Priority: Priority:
1. config browser_path (absolute) 1. config browser_path (absolute, must exist)
2. env BROWSER_PATH / CHROME_PATH / CHROMIUM_PATH 2. env BROWSER_PATH / CHROME_PATH / CHROMIUM_PATH
3. browser_prefer=chromium|chrome|auto 3. browser_prefer=chromium|chrome → auto-detect candidates
auto → Chromium first when turnstilePatch exists (needed for CF) 4. browser_prefer=auto/system → None (system default, zip style)
4. any remaining candidate
Never returns /usr/bin/snap (common snap symlink collapse bug). Never returns /usr/bin/snap (common snap symlink collapse bug).
""" """
@@ -1028,24 +1032,22 @@ def resolve_browser_path():
print(f" [browser] ignore invalid {env_key}={env_path!r}", flush=True) print(f" [browser] ignore invalid {env_key}={env_path!r}", flush=True)
prefer = str((config.get("browser_prefer") if isinstance(config, dict) else "") or "auto").strip().lower() prefer = str((config.get("browser_prefer") if isinstance(config, dict) else "") or "auto").strip().lower()
if prefer in ("", "default", "detect"): if prefer in ("", "default", "detect", "system"):
prefer = "auto" prefer = "auto"
# env override
env_pref = (os.environ.get("BROWSER_PREFER") or "").strip().lower() env_pref = (os.environ.get("BROWSER_PREFER") or "").strip().lower()
if env_pref: if env_pref:
prefer = env_pref prefer = env_pref
has_ext = os.path.isdir(EXTENSION_PATH) # Zip style: leave path empty so Chromium() uses OS default Chrome.
if prefer in ("auto", "system"):
return None
if prefer in ("chrome", "google-chrome", "google"): if prefer in ("chrome", "google-chrome", "google"):
order = list(CHROME_CANDIDATES) + list(CHROMIUM_CANDIDATES) order = list(CHROME_CANDIDATES) + list(CHROMIUM_CANDIDATES)
elif prefer in ("chromium", "chromeium"): # typo tolerant elif prefer in ("chromium", "chromeium"):
order = list(CHROMIUM_CANDIDATES) + list(CHROME_CANDIDATES) order = list(CHROMIUM_CANDIDATES) + list(CHROME_CANDIDATES)
else: else:
# auto: extension needs Chromium; without extension Chrome is fine order = list(CHROME_CANDIDATES) + list(CHROMIUM_CANDIDATES)
if has_ext:
order = list(CHROMIUM_CANDIDATES) + list(CHROME_CANDIDATES)
else:
order = list(CHROME_CANDIDATES) + list(CHROMIUM_CANDIDATES)
return _first_existing_browser(order) return _first_existing_browser(order)
@@ -1057,8 +1059,9 @@ def apply_stealth_patches(page=None, log_callback=None) -> bool:
previously produced xAI [permission_denied] HTTP 403 on signup APIs. previously produced xAI [permission_denied] HTTP 403 on signup APIs.
config.stealth_mode: off | minimal(default) | full config.stealth_mode: off | minimal(default) | full
""" """
mode = str((config.get("stealth_mode") if isinstance(config, dict) else "") or "minimal").strip().lower() # zip 默认不注入 CDP stealth(靠扩展);未配置时也走 off,避免和 content.js 叠指纹
if mode in ("0", "false", "off", "none", "disabled"): mode = str((config.get("stealth_mode") if isinstance(config, dict) else "") or "off").strip().lower()
if mode in ("0", "false", "off", "none", "disabled", ""):
return False return False
page = page if page is not None else _get_page() page = page if page is not None else _get_page()
if page is None: if page is None:
@@ -1097,7 +1100,14 @@ def apply_stealth_patches(page=None, log_callback=None) -> bool:
def create_browser_options(headless=None): def create_browser_options(headless=None):
"""Build ChromiumOptions for register / TabPool. """Build ChromiumOptions — aligned with grok-register.zip / protocol_cpa zip.
Reference (both zips):
options.auto_port()
--no-first-run / --no-default-browser-check
always options.add_extension(turnstilePatch)
no browser_path override (system Chrome/Chromium)
no --disable-images (breaks Turnstile)
headless: headless:
None → resolve from config/env/auto (no DISPLAY → headless) None → resolve from config/env/auto (no DISPLAY → headless)
@@ -1108,30 +1118,26 @@ def create_browser_options(headless=None):
options.auto_port() options.auto_port()
options.set_timeouts(base=1) options.set_timeouts(base=1)
# Decide headless first so we can choose flag set (headed ≈ reference repo).
use_hl = should_use_headless(headless, log_hint=True) use_hl = should_use_headless(headless, log_hint=True)
min_browser = str(os.environ.get("GROK_MIN_BROWSER", "") or "").strip().lower() in (
"1", "true", "yes", "on", # grok-register.zip headed flags (CF-friendly)
) for flag in CHROMIUM_BASE_FLAGS:
if min_browser: options.set_argument(flag)
print(" [browser] minimal flags mode (Git-creat7 style)", flush=True)
options.set_argument("--disable-blink-features=AutomationControlled") # Linux / headless-only extras (Windows headed stays minimal like the zip)
options.set_argument("--no-first-run") if use_hl or sys.platform != "win32":
if use_hl: for flag in CHROMIUM_HEADLESS_EXTRA_FLAGS:
options.set_argument("--disable-gpu") if flag in ("--disable-gpu", "--disable-software-rasterizer") and not use_hl:
else: # headed Xvfb/desktop: keep software rasterizer so CF iframe paints
for flag in CHROMIUM_BASE_FLAGS: continue
options.set_argument(flag) options.set_argument(flag)
if use_hl:
for flag in CHROMIUM_HEADLESS_EXTRA_FLAGS:
options.set_argument(flag)
# Do NOT pass --excludeSwitches as a CLI arg (invalid); only real prefs.
try: try:
options.set_pref("credentials_enable_service", False) options.set_pref("credentials_enable_service", False)
except Exception: except Exception:
pass pass
# Force normal Chrome UA — default headless UA contains "HeadlessChrome"
# which Cloudflare often hard-blocks (Attention Required / blocked page). # Optional UA override (zip uses Chrome/138). Empty config → browser default.
ua = (get_user_agent() or "").strip() ua = (get_user_agent() or "").strip()
if ua: if ua:
try: try:
@@ -1142,6 +1148,9 @@ def create_browser_options(headless=None):
except Exception: except Exception:
pass pass
print(f" [browser] user-agent={ua[:72]}...", flush=True) print(f" [browser] user-agent={ua[:72]}...", flush=True)
# Only set browser_path when config/env points to a real existing binary.
# Zip never hardcodes path; empty browser_path → system default Chrome.
browser_path = resolve_browser_path() browser_path = resolve_browser_path()
if browser_path: if browser_path:
try: try:
@@ -1149,20 +1158,21 @@ def create_browser_options(headless=None):
print(f" [browser] path={browser_path}", flush=True) print(f" [browser] path={browser_path}", flush=True)
except Exception: except Exception:
pass pass
else:
print(" [browser] path=system-default (zip style)", flush=True)
# BOTH zips always load turnstilePatch — do not skip for Google Chrome.
# DrissionPage add_extension works with the temp profile even on Chrome.
if os.path.exists(EXTENSION_PATH): if os.path.exists(EXTENSION_PATH):
# Google Chrome blocks CLI unpacked extension loading; Chromium still allows it. try:
if _is_google_chrome_path(browser_path):
print(
" [ext] Chrome: minimal CDP stealth only (extension blocked)\n"
" 建议安装 Chromium 或 config browser_prefer=chromium / browser_path=/usr/bin/chromium\n"
" 否则 Turnstile token 常为 0(无头/Xvfb 服务器上尤其明显)",
flush=True,
)
else:
options.add_extension(EXTENSION_PATH) options.add_extension(EXTENSION_PATH)
print(f" [ext] turnstilePatch loaded: {EXTENSION_PATH}", flush=True) print(f" [ext] turnstilePatch loaded: {EXTENSION_PATH}", flush=True)
# Apply config.json "proxy" to Chromium. Without this, only HTTP helpers except Exception as ext_exc:
# used get_proxies(); the browser itself fell through to system/env proxy. print(f" [ext] turnstilePatch load failed: {ext_exc}", flush=True)
else:
print(f" [ext] turnstilePatch missing: {EXTENSION_PATH}", flush=True)
# Apply config.json "proxy" to Chromium.
proxy = (config.get("proxy") or "").strip() proxy = (config.get("proxy") or "").strip()
if proxy: if proxy:
try: try:
@@ -1173,7 +1183,6 @@ def create_browser_options(headless=None):
if host: if host:
port = u.port or (443 if (u.scheme or "http") == "https" else 80) port = u.port or (443 if (u.scheme or "http") == "https" else 80)
scheme = u.scheme or "http" scheme = u.scheme or "http"
# Chromium --proxy-server cannot embed user:pass
options.set_argument(f"--proxy-server={scheme}://{host}:{port}") options.set_argument(f"--proxy-server={scheme}://{host}:{port}")
except Exception as e: except Exception as e:
print(f" [proxy] set browser proxy failed: {e}") print(f" [proxy] set browser proxy failed: {e}")
@@ -1183,8 +1192,8 @@ def create_browser_options(headless=None):
print(" [browser] headless=True", flush=True) print(" [browser] headless=True", flush=True)
else: else:
print( print(
f" [browser] headed DISPLAY={os.environ.get('DISPLAY', '')!r} " f" [browser] headed (zip-style minimal flags) "
f"(flags=base-only, no disable-software-rasterizer)", f"DISPLAY={os.environ.get('DISPLAY', '')!r}",
flush=True, flush=True,
) )
return options return options
@@ -4421,40 +4430,11 @@ return ok;
return "none" return "none"
def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 20): def _read_turnstile_token_simple(page) -> str:
"""batch100-era solver (55f56d2/c56d0d2) with longer waits. """Light token read used by solver (input + turnstile.getResponse)."""
Historical success: Google Chrome + CDP stealth, Accept All Cookies,
reset + shadow checkbox click, token ~773.
"""
page = _get_page()
if page is None:
raise Exception("页面未就绪,无法执行 Turnstile")
apply_stealth_patches(page, log_callback=None)
try: try:
page.run_js( token = page.run_js(
"try { if (window.turnstile && typeof turnstile.reset === 'function') turnstile.reset(); } catch(e) {}" """
)
except Exception:
pass
rounds = max(8, int(max_rounds or 20))
# Initial settle for widget attach
cf_sleep(1.2 if not PERF_FLAGS.get("fast") else 0.9, cancel_callback, floor=0.7)
for round_i in range(0, rounds):
raise_if_cancelled(cancel_callback)
try:
token = _read_turnstile_token(page)
if len(token) >= 80:
if log_callback:
log_callback(f"[*] Turnstile 已通过,token长度={len(token)}")
return token
# Historical pure JS read path as well
token = page.run_js(
"""
try { try {
const byInput = String((document.querySelector('input[name="cf-turnstile-response"]') || {}).value || '').trim(); const byInput = String((document.querySelector('input[name="cf-turnstile-response"]') || {}).value || '').trim();
if (byInput) return byInput; if (byInput) return byInput;
@@ -4463,21 +4443,217 @@ try {
} }
return ''; return '';
} catch(e) { return ''; } } catch(e) { return ''; }
""" """
)
return str(token or "").strip()
except Exception:
return ""
def _turnstile_ui_state(page) -> dict:
"""Detect managed / interactive / failed Turnstile UI (best-effort)."""
try:
info = page.run_js(
"""
try {
const body = ((document.body && document.body.innerText) || '').slice(0, 6000);
const lower = body.toLowerCase();
const input = document.querySelector('input[name="cf-turnstile-response"]');
const tokenLen = input ? String(input.value || '').trim().length : 0;
let iframeCount = 0;
try {
iframeCount = document.querySelectorAll(
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]'
).length;
} catch (e) {}
// open shadow iframes
try {
const all = document.querySelectorAll('*');
for (const el of all) {
if (el.shadowRoot) {
iframeCount += el.shadowRoot.querySelectorAll(
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]'
).length;
}
}
} catch (e) {}
const interactive = /确认您是真人|verify you are human|确认您是人类|i am human|请完成安全检查/i.test(body);
const verifying = /正在验证|verifying|checking your browser|just a moment/i.test(body);
const failed = /验证失败|verification failed|try again|请重试|失败,请/i.test(body);
return {
tokenLen: tokenLen,
iframeCount: iframeCount,
interactive: interactive,
verifying: verifying,
failed: failed,
};
} catch (e) {
return { err: String(e && e.message || e) };
}
"""
)
return info if isinstance(info, dict) else {}
except Exception as exc:
return {"err": str(exc)[:120]}
def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 20):
"""Turnstile solver (zip base + interactive-safe).
Critical fixes vs naive zip loop:
- Do NOT reset when interactive checkbox already shown (reset kills progress → 验证失败)
- Click at most once every few seconds; after click WAIT for token
- Prefer real mouse actions over pure CDP ele.click spam
"""
page = _get_page()
if page is None:
raise Exception("页面未就绪,无法执行 Turnstile")
# Only reset when widget is missing / hard-failed — never while interactive is up.
ui0 = _turnstile_ui_state(page)
should_reset = False
if ui0.get("failed"):
should_reset = True
elif not ui0.get("iframeCount") and not ui0.get("tokenLen"):
should_reset = True
if should_reset:
try:
page.run_js(
"try { if (window.turnstile && typeof turnstile.reset === 'function') turnstile.reset(); } catch(e) {}"
) )
token = str(token or "").strip() if log_callback:
log_callback("[Debug] Turnstile reset (widget missing/failed)")
except Exception:
pass
elif log_callback and (ui0.get("interactive") or ui0.get("verifying")):
log_callback(
f"[Debug] Turnstile 已有 UI interactive={ui0.get('interactive')} "
f"verifying={ui0.get('verifying')} — 跳过 reset,避免清掉真人勾选"
)
rounds = max(1, int(max_rounds or 20))
last_click_at = 0.0
click_count = 0
for round_i in range(0, rounds):
raise_if_cancelled(cancel_callback)
try:
token = _read_turnstile_token_simple(page)
if len(token) >= 80: if len(token) >= 80:
if log_callback: if log_callback:
log_callback(f"[*] Turnstile 已通过,token长度={len(token)}") log_callback(f"[*] Turnstile 已通过,token长度={len(token)}")
return token return token
path = _click_turnstile_widget(page, log_callback=log_callback) ui = _turnstile_ui_state(page)
if log_callback and (round_i == 0 or round_i % 3 == 0): # After a click: prefer waiting over re-clicking (CF interactive fails on spam)
log_callback(f"[Debug] Turnstile click path={path} round={round_i+1}/{rounds}") now = time.time()
min_click_gap = 4.5 if ui.get("interactive") else 2.8
if ui.get("verifying") and not ui.get("failed"):
if log_callback and round_i % 3 == 0:
log_callback("[Debug] Turnstile verifying… 等待 token,不点击")
cf_sleep(1.2, cancel_callback, floor=1.0)
continue
# Only click when cool-down elapsed
if now - last_click_at < min_click_gap and click_count > 0:
cf_sleep(0.8, cancel_callback, floor=0.7)
continue
# Zip path: input → parent.shadow_root → iframe → patch screenX → body.shadow_root → input
challenge_input = None
try:
challenge_input = page.ele("@name=cf-turnstile-response", timeout=0.5)
except Exception:
challenge_input = None
clicked = False
if challenge_input:
iframe = None
try:
wrapper = challenge_input.parent()
iframe = wrapper.shadow_root.ele("tag:iframe")
except Exception:
iframe = None
if iframe:
try:
iframe.run_js(
"""
window.dtp = 1;
function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
let sx = getRandomInt(800, 1200);
let sy = getRandomInt(400, 700);
Object.defineProperty(MouseEvent.prototype, 'screenX', { value: sx });
Object.defineProperty(MouseEvent.prototype, 'screenY', { value: sy });
"""
)
except Exception:
pass
# Prefer real mouse path (less likely to trip interactive fail than ele.click spam)
try:
body_sr = iframe.ele("tag:body").shadow_root
btn = (
body_sr.ele("tag:input")
or body_sr.ele("css:.mark")
or body_sr.ele("css:label")
or body_sr.ele("css:[role='checkbox']")
)
if btn:
try:
page.actions.move_to(btn).click()
clicked = True
path = "actions-checkbox"
except Exception:
try:
btn.click()
clicked = True
path = "shadow-checkbox"
except Exception:
path = "none"
if clicked:
click_count += 1
last_click_at = time.time()
if log_callback:
log_callback(
f"[Debug] Turnstile click path={path} "
f"round={round_i+1}/{rounds} clicks={click_count}"
)
# After interactive click, wait longer for token / fail UI
cf_sleep(3.5 if ui.get("interactive") else 2.2, cancel_callback, floor=2.0)
continue
except Exception:
pass
# iframe center click as last resort
try:
page.actions.move_to(iframe).click()
clicked = True
click_count += 1
last_click_at = time.time()
if log_callback:
log_callback(
f"[Debug] Turnstile click path=iframe-actions "
f"round={round_i+1}/{rounds}"
)
cf_sleep(3.0, cancel_callback, floor=2.0)
continue
except Exception:
pass
if not clicked:
# Soft fallback: one JS click, not every round
if click_count == 0 or (now - last_click_at) >= 5.0:
page.run_js(
"""
const nodes = Array.from(document.querySelectorAll('div,span,iframe')).filter((n) => {
const txt = (n.className || '') + ' ' + (n.id || '') + ' ' + (n.getAttribute?.('src') || '');
return String(txt).toLowerCase().includes('turnstile');
});
if (nodes.length && typeof nodes[0].click === 'function') nodes[0].click();
"""
)
click_count += 1
last_click_at = time.time()
if log_callback and (round_i == 0 or round_i % 5 == 0):
log_callback(f"[Debug] Turnstile click path=js-fallback round={round_i+1}/{rounds}")
except Exception: except Exception:
pass pass
# Progressive wait like c56d0d2, but wall-clock via cf_sleep cf_sleep(1.0, cancel_callback, floor=0.9)
cf_sleep(0.9 + 0.15 * min(round_i, 6), cancel_callback, floor=0.8)
raise Exception("Turnstile 获取 token 失败") raise Exception("Turnstile 获取 token 失败")
@@ -4516,36 +4692,20 @@ def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None
→ re-fill token into hidden input → submit. → re-fill token into hidden input → submit.
""" """
page = _get_page() page = _get_page()
# zip 方案:靠 turnstilePatch 扩展,不额外 CDP stealth(stealth_mode=off)
apply_stealth_patches(page, log_callback=log_callback) apply_stealth_patches(page, log_callback=log_callback)
check_timeout(time.time()) check_timeout(time.time())
# Cookie overlay often reappears on profile step and blanks Turnstile (iframe=0).
dismiss_cookie_banner(page, log_callback=log_callback) dismiss_cookie_banner(page, log_callback=log_callback)
dump_state(page, "profile-form") dump_state(page, "profile-form")
take_screenshot(page, "profile-form") take_screenshot(page, "profile-form")
given_name, family_name, password = build_profile() given_name, family_name, password = build_profile()
# 预热 Turnstile:cookie 只关一次(sticky),然后给 Verifying 时间 # zip: 预热 Turnstile — 等 2 秒让 iframe 初始化,插件会自动点击 checkbox
if log_callback: if log_callback:
log_callback("[*] 预热 Turnstile...") log_callback("[*] 预热 Turnstile...")
try: try:
dismiss_cookie_banner(page, log_callback=log_callback) human_sleep(2, cancel_callback)
except Exception: except Exception:
pass cf_sleep(2.0, cancel_callback, floor=2.0)
# 必须用 cf_sleep:--fast 下 human_sleep 会把预热压到 <0.3s,iframe 来不及挂载
cf_sleep(2.4 if not PERF_FLAGS.get("fast") else 1.6, cancel_callback, floor=1.2)
try:
pre = _read_turnstile_token(page)
if pre and len(pre) >= 80 and log_callback:
log_callback(f"[*] 预热阶段已有 token,长度={len(pre)}")
else:
diag_pre = _turnstile_diag(page)
if log_callback:
log_callback(
f"[Debug] 预热后 Turnstile: tokenLen={diag_pre.get('tokenLen', 0)} "
f"hasInput={diag_pre.get('hasInput')} api={diag_pre.get('turnstileApi')} "
f"iframe={diag_pre.get('iframeCount', 0)}"
)
except Exception:
pass
deadline = time.time() + timeout deadline = time.time() + timeout
form_filled_once = False form_filled_once = False
wait_cf_since = None wait_cf_since = None
@@ -4792,15 +4952,34 @@ return 'submitted';
now = time.time() now = time.time()
if wait_cf_since is None: if wait_cf_since is None:
wait_cf_since = now wait_cf_since = now
ui = _turnstile_ui_state(page)
if log_callback and (last_cf_log_at <= 0 or now - last_cf_log_at >= 5): if log_callback and (last_cf_log_at <= 0 or now - last_cf_log_at >= 5):
token_len = submit_state.split(":", 1)[1] if ":" in submit_state else "0" token_len = submit_state.split(":", 1)[1] if ":" in submit_state else "0"
waited = now - wait_cf_since if wait_cf_since else 0 waited = now - wait_cf_since if wait_cf_since else 0
log_callback( log_callback(
f"[*] 等待 Cloudflare 人机验证通过后再提交... " f"[*] 等待 Cloudflare 人机验证通过后再提交... "
f"当前token长度={token_len} waited={waited:.0f}s" f"当前token长度={token_len} waited={waited:.0f}s "
f"interactive={ui.get('interactive')} verifying={ui.get('verifying')} failed={ui.get('failed')}"
) )
last_cf_log_at = now last_cf_log_at = now
if wait_cf_since and now - wait_cf_since >= 3.0 and now - last_cf_retry_at >= 3.0: # Interactive checkbox 已出现:不要每 3s 狂点,也不要立刻 reset 复用
if ui.get("verifying") and not ui.get("failed"):
sleep_with_cancel(1.2, cancel_callback)
continue
if ui.get("interactive") and not ui.get("failed"):
# 轻点一次后耐心等 token;间隔拉长
if wait_cf_since and now - last_cf_retry_at >= 6.0:
try:
_click_turnstile_widget(page)
last_cf_retry_at = now
if log_callback:
log_callback("[Debug] interactive 模式:轻点一次 checkbox,等待通过…")
except Exception:
pass
sleep_with_cancel(1.5, cancel_callback)
continue
# managed / 无 UI:沿用 zip 二次复用(内部已避免误 reset)
if wait_cf_since and now - wait_cf_since >= 5.0 and now - last_cf_retry_at >= 5.0:
try: try:
_click_turnstile_widget(page) _click_turnstile_widget(page)
except Exception: except Exception:
+137
View File
@@ -0,0 +1,137 @@
// Turnstile Patch - 隐藏自动化标识,加速 Turnstile 验证
// 在 document_start 阶段执行,确保在页面脚本之前生效
// Source: grok-register / grok_reg-protocol_cpa (zip reference)
(function () {
"use strict";
// 1. 隐藏 navigator.webdriver 标识
// Chrome 自动化模式下 navigator.webdriver = true,Turnstile 会检测此属性
try {
Object.defineProperty(navigator, "webdriver", {
get: function () {
return false;
},
configurable: true,
});
} catch (e) {}
// 2. 移除 Chrome 自动化相关的 Runtime 属性
try {
if (window.chrome && window.chrome.runtime) {
delete window.chrome.runtime.onConnect;
delete window.chrome.runtime.onMessage;
}
} catch (e) {}
// 3. 覆盖 permissions.query,隐藏 notifications 权限异常
try {
var origQuery = navigator.permissions.query.bind(navigator.permissions);
navigator.permissions.query = function (params) {
if (params.name === "notifications") {
return Promise.resolve({ state: Notification.permission });
}
return origQuery(params);
};
} catch (e) {}
// 4. 修补 plugin 数量,模拟正常浏览器
try {
Object.defineProperty(navigator, "plugins", {
get: function () {
return [1, 2, 3, 4, 5];
},
configurable: true,
});
} catch (e) {}
// 5. 修补 languages 属性
try {
Object.defineProperty(navigator, "languages", {
get: function () {
return ["en-US", "en"];
},
configurable: true,
});
} catch (e) {}
// 6. CDP MouseEvent.screenX/screenY 与 client 相同会被 CF 检测(Chromium 40280325)
// 两个 zip 的 getTurnstileToken 也会在 iframe 内再补一次;扩展层提前全局打补丁更稳
try {
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var _sx = getRandomInt(800, 1200);
var _sy = getRandomInt(400, 600);
Object.defineProperty(MouseEvent.prototype, "screenX", {
configurable: true,
get: function () {
return _sx + (this.clientX || 0);
},
});
Object.defineProperty(MouseEvent.prototype, "screenY", {
configurable: true,
get: function () {
return _sy + (this.clientY || 0);
},
});
} catch (e) {}
// 7. 页面加载完成后,自动监控并点击 Turnstile 复选框
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", autoClickTurnstile);
} else {
autoClickTurnstile();
}
function autoClickTurnstile() {
// 定时检查 Turnstile iframe 是否出现
var checkCount = 0;
var maxChecks = 100; // 最多检查 100 次(约 50 秒)
var timer = setInterval(function () {
checkCount++;
if (checkCount > maxChecks) {
clearInterval(timer);
return;
}
try {
// 查找 Turnstile iframe
var iframes = document.querySelectorAll(
'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]'
);
for (var i = 0; i < iframes.length; i++) {
var iframe = iframes[i];
try {
// 尝试访问 iframe 内部的 checkbox
var body = iframe.contentDocument || iframe.contentWindow.document;
var checkbox = body.querySelector(
'input[type="checkbox"], .mark, #cf-chl-widget-nomu1_resp'
);
if (checkbox && !checkbox.checked) {
checkbox.click();
}
} catch (e) {
// 跨域限制,尝试通过 postMessage 触发
try {
iframe.contentWindow.postMessage(
{ type: "turnstile-auto-click" },
"*"
);
} catch (e2) {}
}
}
// 也尝试直接操作 Turnstile API
if (
window.turnstile &&
typeof window.turnstile.getResponse === "function"
) {
var resp = window.turnstile.getResponse();
if (resp && resp.length > 0) {
clearInterval(timer); // 已获得 token,停止检查
}
}
} catch (e) {}
}, 500);
}
})();
+8 -9
View File
@@ -1,16 +1,15 @@
{ {
"manifest_version": 3, "manifest_version": 2,
"name": "Turnstile Patcher", "name": "Turnstile Patch",
"version": "2.1", "version": "1.0.0",
"description": "Patch CDP MouseEvent.screenX/screenY for Turnstile (ReinerBRO / Git-creat7)", "description": "Patch browser automation detection for Turnstile",
"content_scripts": [ "content_scripts": [
{ {
"js": ["./script.js"],
"matches": ["<all_urls>"], "matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start", "run_at": "document_start",
"all_frames": true, "all_frames": true
"world": "MAIN",
"match_about_blank": true
} }
] ],
"permissions": ["activeTab"]
} }
-24
View File
@@ -1,24 +0,0 @@
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// CDP Input.dispatchMouseEvent creates MouseEvent where
// .screenX/.screenY equal .clientX/.clientY. Cloudflare Turnstile detects
// this (Chromium issue 40280325). Patch with realistic screen coords.
// Source: ObjectAscended/CDP-bug-MouseEvent-.screenX-.screenY-patcher
// Ported from ReinerBRO/grok-register + Git-creat7/grokRegister-cpa.
// old method wouldn't work on 4k screens
let screenX = getRandomInt(800, 1200);
let screenY = getRandomInt(400, 600);
Object.defineProperty(MouseEvent.prototype, "screenX", { value: screenX });
Object.defineProperty(MouseEvent.prototype, "screenY", { value: screenY });
// Best-effort: also cover PointerEvent if present (CF may read either).
try {
if (typeof PointerEvent !== "undefined" && PointerEvent.prototype) {
Object.defineProperty(PointerEvent.prototype, "screenX", { value: screenX });
Object.defineProperty(PointerEvent.prototype, "screenY", { value: screenY });
}
} catch (e) {}