From 62b6ea8ea27349626ac4a20aeccefaf00a714d82 Mon Sep 17 00:00:00 2001 From: Chaos Date: Sun, 12 Jul 2026 00:58:28 +0800 Subject: [PATCH] Harden Turnstile solve path for --fast/Xvfb runs Add cf_sleep, multi-round shadow clicks, richer diagnostics, and longer profile/CF budgets so tokenLen=0 fails less on headless servers. --- README.md | 2 +- config.example.json | 10 +- config.json | 8 +- grok_register_ttk.py | 395 ++++++++++++++++++++++++++--------- register_cli.py | 9 +- turnstilePatch/content.js | 92 ++++++-- turnstilePatch/manifest.json | 4 +- 7 files changed, 391 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index dfadd81..31eb8d5 100644 --- a/README.md +++ b/README.md @@ -544,7 +544,7 @@ curl -sS http://127.0.0.1:8317/v1/chat/completions \ | 协议 verify/approve 失败 | 会话态变化 / 风控;看日志后自动回退浏览器 | | 一直 `authorization_pending` | 浏览器路径未完成 consent;需到「设备已授权」且 token 200 | | Cloudflare / Turnstile | 必须用 **Chromium + turnstilePatch**(Chrome 无法 `--load-extension`,token 常为 0);配合 `./start.sh` 自动 Xvfb | -| Turnstile token 长度=0 | 日志若 `skip turnstilePatch on Google Chrome`:`sudo apt install -y chromium` 后重跑;或 `browser_path` 指向 chromium | +| Turnstile token 长度=0 | ① 日志若 `skip turnstilePatch` / 选中 Chrome:装 Chromium 或设 `browser_path`;② 扩展已加载仍为 0:代码侧已用 `cf_sleep`(不受 `--fast` 0.15x 过度压缩)+ 多轮 shadow 点击;可再 `--no-fast` 或加大 `turnstile_max_rounds`/`profile_timeout`;③ 看日志 `Turnstile 初始态 iframe=` 是否为 0(widget 未挂载 / IP 被拦) | | 浏览器 connection fails / no interface | 无 DISPLAY:`./start.sh` 自动 Xvfb / Python headless | | Hotmail 收不到码 | 检查四段凭证、ClientID/Token、IMAP 主机与 alias 计数 | | 有 token 但无 grok-4.5 | `cpa_base_url` 是否为 `cli-chat-proxy` | diff --git a/config.example.json b/config.example.json index 420717f..6d00d12 100644 --- a/config.example.json +++ b/config.example.json @@ -95,10 +95,18 @@ "code_form_timeout": 180, "// profile_timeout": "资料页(姓名密码生日)超时(秒)", "profile_timeout": 240, - "// turnstile_retry_limit": "Turnstile 失败重试次数", + "// turnstile_retry_limit": "Turnstile 失败重试次数(账号级)", "turnstile_retry_limit": 3, "// turnstile_stuck_timeout": "Turnstile 卡住判定超时(秒)", "turnstile_stuck_timeout": 150, + "// turnstile_fast_fail_count": "资料页内连续 getTurnstileToken 失败几次后放弃(服务器建议 4)", + "turnstile_fast_fail_count": 4, + "// turnstile_retry_after_sec": "资料页等待 CF 多久后触发二次复用(秒)", + "turnstile_retry_after_sec": 12, + "// turnstile_retry_gap_sec": "两次二次复用之间的最小间隔(秒)", + "turnstile_retry_gap_sec": 8, + "// turnstile_max_rounds": "单次 getTurnstileToken 内 click/poll 轮数", + "turnstile_max_rounds": 8, "// sso_timeout_base / sso_timeout_max / sso_progress_extension": "等待 sso cookie 的基础/上限/有进度时延长(秒)", "sso_timeout_base": 240, "sso_timeout_max": 480, diff --git a/config.json b/config.json index 3f440ee..b39f23a 100644 --- a/config.json +++ b/config.json @@ -98,8 +98,8 @@ "gmail_imap_fallback": true, "gmail_imap_fallback_timeout_sec": 45, "gmail_imap_fallback_resend_sec": 10, - "turnstile_fast_fail_count": 2, - "turnstile_retry_after_sec": 8, - "turnstile_retry_gap_sec": 5, - "turnstile_max_rounds": 4 + "turnstile_fast_fail_count": 4, + "turnstile_retry_after_sec": 12, + "turnstile_retry_gap_sec": 8, + "turnstile_max_rounds": 8 } diff --git a/grok_register_ttk.py b/grok_register_ttk.py index 0dbe582..426ccfd 100644 --- a/grok_register_ttk.py +++ b/grok_register_ttk.py @@ -1144,6 +1144,29 @@ def human_sleep(mean_seconds, cancel_callback=None): sleep_with_cancel(delay, cancel_callback) +def cf_sleep(seconds, cancel_callback=None, *, floor: float = 0.6): + """Turnstile/Cloudflare 专用等待:fast 模式仍保留足够墙钟时间。 + + human_sleep 在 --fast 下会被压到 ~0.15x,导致 CF iframe 根本来不及加载/解题。 + 这里仅轻度缩放,并强制 floor,避免 token 恒为 0。 + """ + try: + sec = float(seconds) + except Exception: + sec = 1.0 + scale = float(PERF_FLAGS.get("sleep_scale", 1.0) or 1.0) + if PERF_FLAGS.get("fast"): + # 最多压到 0.55x,且不低于 floor + scale = max(0.55, min(scale, 1.0)) + delay = max(float(floor), sec * scale) + # 轻微抖动,避免机械节奏 + try: + delay = max(float(floor), min(delay * 1.35, random.gauss(delay, delay * 0.12))) + except Exception: + pass + sleep_with_cancel(delay, cancel_callback) + + def get_domains(api_key=None): headers = {} @@ -3713,32 +3736,11 @@ return 'clicked'; raise Exception("验证码已获取,但自动填写/提交失败") -def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 4): - """Try to obtain a Turnstile token. - - max_rounds controls click/reset attempts; each round waits longer for CF. - CDP stealth should already be applied (webdriver hidden). - """ - page = _get_page() - if page is None: - raise Exception("页面未就绪,无法执行 Turnstile") - apply_stealth_patches(page, log_callback=None) - +def _read_turnstile_token(page): + """Read cf-turnstile-response / turnstile.getResponse if present.""" try: - page.run_js( - "try { if (window.turnstile && typeof turnstile.reset === 'function') turnstile.reset(); } catch(e) {}" - ) - except Exception: - pass - - rounds = max(2, int(max_rounds or 4)) - # Give CF more time: ~1.2s * rounds, plus initial attach wait - human_sleep(0.8 if not PERF_FLAGS.get("fast") else 0.35, cancel_callback) - for round_i in range(0, rounds): - raise_if_cancelled(cancel_callback) - try: - token = page.run_js( - """ + token = page.run_js( + """ try { const byInput = String((document.querySelector('input[name="cf-turnstile-response"]') || {}).value || '').trim(); if (byInput) return byInput; @@ -3749,77 +3751,246 @@ try { } return ''; } catch(e) { return ''; } - """ - ) - token = str(token or "").strip() + """ + ) + return str(token or "").strip() + except Exception: + return "" + + +def _turnstile_diag(page) -> dict: + """Lightweight CF widget diagnostics for logs (no secrets).""" + try: + info = page.run_js( + """ +try { + const input = document.querySelector('input[name="cf-turnstile-response"], textarea[name="cf-turnstile-response"]'); + const iframes = Array.from(document.querySelectorAll( + 'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]' + )); + const widgets = Array.from(document.querySelectorAll('div.cf-turnstile, [data-sitekey]')); + const srcs = iframes.slice(0, 3).map((f) => { + try { return String(f.src || '').slice(0, 120); } catch(e) { return ''; } + }); + return { + hasInput: !!input, + tokenLen: input ? String(input.value || '').trim().length : 0, + iframeCount: iframes.length, + widgetCount: widgets.length, + sitekey: widgets[0] ? String(widgets[0].getAttribute('data-sitekey') || '').slice(0, 16) : '', + iframeSrc0: srcs[0] || '', + turnstileApi: !!(window.turnstile && typeof turnstile.getResponse === 'function'), + webdriver: !!navigator.webdriver, + }; +} catch(e) { + return { err: String(e && e.message || e) }; +} + """ + ) + return info if isinstance(info, dict) else {"raw": str(info)[:200]} + except Exception as exc: + return {"err": str(exc)[:160]} + + +def _click_turnstile_widget(page, log_callback=None) -> str: + """Best-effort click path for interactive Turnstile checkbox. + + Returns a short tag describing which path succeeded (or 'none'). + """ + # Path A: shadow-root checkbox under cf-turnstile-response parent + try: + challenge_input = page.ele("@name=cf-turnstile-response", timeout=0.6) + except Exception: + challenge_input = None + if challenge_input: + iframe = None + try: + wrapper = challenge_input.parent() + iframe = wrapper.shadow_root.ele("tag:iframe") + except Exception: + iframe = None + if iframe is None: + try: + iframe = page.ele( + 'css:iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]', + timeout=0.6, + ) + 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); +try { + Object.defineProperty(MouseEvent.prototype, 'screenX', { get: function(){ return sx; } }); + Object.defineProperty(MouseEvent.prototype, 'screenY', { get: function(){ return sy; } }); +} catch(e) {} + """ + ) + except Exception: + pass + # A1: body.shadow_root input / .mark + 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") + if btn: + try: + btn.click() + except Exception: + try: + btn.click(by_js=True) + except Exception: + page.actions.click(btn) + return "shadow-checkbox" + except Exception: + pass + # A2: click iframe element itself (center) + try: + iframe.click() + return "iframe-click" + except Exception: + pass + try: + page.actions.click(iframe) + return "iframe-actions" + except Exception: + pass + + # Path B: DOM query + synthetic pointer events + try: + clicked = page.run_js( + """ +function firePointer(el) { + if (!el) return false; + try { el.scrollIntoView({block:'center', inline:'center'}); } catch(e) {} + const rect = el.getBoundingClientRect(); + const x = rect.left + Math.max(8, Math.min(rect.width - 8, rect.width * 0.35)); + const y = rect.top + Math.max(8, Math.min(rect.height - 8, rect.height * 0.5)); + const opts = {bubbles:true, cancelable:true, view:window, clientX:x, clientY:y, button:0}; + for (const t of ['pointerover','pointerenter','mouseover','mouseenter','mousemove','pointerdown','mousedown','pointerup','mouseup','click']) { + try { + const Ev = t.startsWith('pointer') ? PointerEvent : MouseEvent; + el.dispatchEvent(new Ev(t, opts)); + } catch(e) { + try { el.dispatchEvent(new MouseEvent(t, opts)); } catch(e2) {} + } + } + try { if (typeof el.click === 'function') el.click(); } catch(e) {} + return true; +} +const nodes = Array.from(document.querySelectorAll( + 'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], div.cf-turnstile, [data-sitekey]' +)); +let ok = false; +for (const n of nodes) { + if (firePointer(n)) ok = true; +} +return ok; + """ + ) + if clicked: + return "js-pointer" + except Exception: + pass + return "none" + + +def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 4): + """Try to obtain a Turnstile token. + + max_rounds controls click/reset attempts; each round waits longer for CF. + CDP stealth should already be applied (webdriver hidden). + + Note: --fast compresses human_sleep heavily; this function uses cf_sleep + so CF still has real wall-clock time to attach iframe + solve. + """ + page = _get_page() + if page is None: + raise Exception("页面未就绪,无法执行 Turnstile") + apply_stealth_patches(page, log_callback=None) + + rounds = max(3, int(max_rounds or 4)) + # Initial attach wait — managed / auto modes often fill without click + cf_sleep(1.8 if not PERF_FLAGS.get("fast") else 1.2, cancel_callback, floor=1.0) + + # Passive poll first (don't reset — reset often kills in-flight solve) + for _ in range(4): + raise_if_cancelled(cancel_callback) + token = _read_turnstile_token(page) + if len(token) >= 80: + if log_callback: + log_callback(f"[*] Turnstile 已通过(被动),token长度={len(token)}") + return token + cf_sleep(0.9, cancel_callback, floor=0.7) + + diag0 = _turnstile_diag(page) + if log_callback: + log_callback( + f"[Debug] Turnstile 初始态: tokenLen={diag0.get('tokenLen', 0)} " + f"iframe={diag0.get('iframeCount', 0)} widget={diag0.get('widgetCount', 0)} " + f"api={diag0.get('turnstileApi')} webdriver={diag0.get('webdriver')} " + f"sitekey={diag0.get('sitekey', '')!r}" + ) + + last_click = "none" + 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 - challenge_input = page.ele("@name=cf-turnstile-response", timeout=0.5) - if challenge_input: - wrapper = challenge_input.parent() - iframe = None + # Only reset when previous click rounds clearly failed (not on first) + if round_i >= 2 and round_i % 2 == 0: try: - iframe = wrapper.shadow_root.ele("tag:iframe") + page.run_js( + "try { if (window.turnstile && typeof turnstile.reset === 'function') turnstile.reset(); } catch(e) {}" + ) + if log_callback: + log_callback(f"[Debug] Turnstile reset (round={round_i + 1})") + cf_sleep(1.0, cancel_callback, floor=0.8) except Exception: - iframe = None - if iframe is None: - try: - iframe = page.ele( - 'css:iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]', - timeout=0.5, - ) - 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 - clicked = False - try: - body_sr = iframe.ele("tag:body").shadow_root - btn = body_sr.ele("tag:input") or body_sr.ele("css:.mark") - if btn: - btn.click() - clicked = True - except Exception: - pass - if not clicked: - try: - iframe.click() - except Exception: - pass - else: - # 兜底:尝试触发页面上可见的 Turnstile 容器 / iframe - page.run_js( - """ -const nodes = Array.from(document.querySelectorAll( - 'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], div.cf-turnstile, [data-sitekey]' -)); -for (const n of nodes) { - try { n.scrollIntoView({block:'center', inline:'center'}); } catch(e) {} - try { if (typeof n.click === 'function') n.click(); } catch(e) {} -} - """ - ) - except Exception: - pass - # Progressive wait: later rounds wait a bit longer for CF challenge - human_sleep(0.9 + 0.25 * round_i, cancel_callback) + pass - raise Exception("Turnstile 获取 token 失败") + last_click = _click_turnstile_widget(page, log_callback=log_callback) + if log_callback and (round_i == 0 or last_click != "none"): + log_callback(f"[Debug] Turnstile click path={last_click} round={round_i + 1}/{rounds}") + + # After click: poll more densely for token + poll_budget = 3.2 + 0.7 * round_i + poll_deadline = time.time() + poll_budget + while time.time() < poll_deadline: + raise_if_cancelled(cancel_callback) + token = _read_turnstile_token(page) + if len(token) >= 80: + if log_callback: + log_callback(f"[*] Turnstile 已通过,token长度={len(token)}") + return token + cf_sleep(0.55, cancel_callback, floor=0.45) + except Exception as exc: + if log_callback and round_i == 0: + log_callback(f"[Debug] Turnstile round error: {exc}") + + # Progressive gap between rounds + cf_sleep(1.1 + 0.35 * round_i, cancel_callback, floor=0.9) + + diag1 = _turnstile_diag(page) + if log_callback: + log_callback( + f"[Debug] Turnstile 失败态: tokenLen={diag1.get('tokenLen', 0)} " + f"iframe={diag1.get('iframeCount', 0)} last_click={last_click} " + f"src={str(diag1.get('iframeSrc0', ''))[:80]!r}" + ) + raise Exception( + f"Turnstile 获取 token 失败 (tokenLen={diag1.get('tokenLen', 0)}, " + f"iframe={diag1.get('iframeCount', 0)}, click={last_click})" + ) def build_profile(): @@ -3854,11 +4025,18 @@ def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None dump_state(page, "profile-form") take_screenshot(page, "profile-form") given_name, family_name, password = build_profile() - # 预热 Turnstile:等 iframe 初始化;CDP stealth 会尝试自动点 checkbox + # 预热 Turnstile:等 iframe 初始化;CDP stealth / extension 会尝试自动点 checkbox if log_callback: log_callback("[*] 预热 Turnstile...") - # fast mode scales human_sleep; keep enough time for turnstile iframe to attach - human_sleep(1.6 if not PERF_FLAGS.get("fast") else 0.8, cancel_callback) + # 必须用 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) + # 被动预读一次,managed 模式常自动出 token + try: + pre = _read_turnstile_token(page) + if pre and len(pre) >= 80 and log_callback: + log_callback(f"[*] 预热阶段已有 token,长度={len(pre)}") + except Exception: + pass deadline = time.time() + timeout form_filled_once = False wait_cf_since = None @@ -3866,22 +4044,22 @@ def fill_profile_and_submit(timeout=120, log_callback=None, cancel_callback=None last_cf_log_at = 0.0 cf_token_fail_streak = 0 try: - # Slightly more patient by default — Chrome CDP stealth still needs CF time - max_cf_token_fails = max(1, int(config.get("turnstile_fast_fail_count", 3) or 3)) + # 服务器 Xvfb 上 CF 常需多轮;默认比桌面更耐心 + max_cf_token_fails = max(2, int(config.get("turnstile_fast_fail_count", 4) or 4)) except Exception: - max_cf_token_fails = 3 + max_cf_token_fails = 4 try: - cf_retry_after = float(config.get("turnstile_retry_after_sec", 10) or 10) + cf_retry_after = float(config.get("turnstile_retry_after_sec", 12) or 12) except Exception: - cf_retry_after = 10.0 + cf_retry_after = 12.0 try: - cf_retry_gap = float(config.get("turnstile_retry_gap_sec", 6) or 6) + cf_retry_gap = float(config.get("turnstile_retry_gap_sec", 8) or 8) except Exception: - cf_retry_gap = 6.0 + cf_retry_gap = 8.0 try: - turnstile_rounds = max(4, int(config.get("turnstile_max_rounds", 6) or 6)) + turnstile_rounds = max(5, int(config.get("turnstile_max_rounds", 8) or 8)) except Exception: - turnstile_rounds = 6 + turnstile_rounds = 8 while time.time() < deadline: raise_if_cancelled(cancel_callback) @@ -3972,6 +4150,14 @@ return 'filled-no-submit'; waited = now - wait_cf_since if wait_cf_since else 0 log_callback(f"[*] 资料已填写,等待 Cloudflare... token长度={token_len} waited={waited:.0f}s") last_cf_log_at = now + # 卡住前先轻点一次 widget(不 reset),给 managed/interactive 更多时间 + if wait_cf_since and now - wait_cf_since >= 3.0 and now - last_cf_retry_at >= 4.0: + try: + path = _click_turnstile_widget(page) + if path != "none" and log_callback and (now - last_cf_log_at >= 4): + log_callback(f"[Debug] 等待期轻点 Turnstile path={path}") + except Exception: + pass # 卡住后自动二次复用 Turnstile 组件 if now - wait_cf_since >= cf_retry_after and now - last_cf_retry_at >= cf_retry_gap: if log_callback: @@ -4003,7 +4189,7 @@ return String(cfInput.value || '').trim().length; if cf_token_fail_streak >= max_cf_token_fails: raise Exception(f"Turnstile 连续失败,快速放弃资料页: {cf_exc}") last_cf_retry_at = now - human_sleep(0.8, cancel_callback) + cf_sleep(1.0, cancel_callback, floor=0.7) continue if filled in ("ready-to-submit", "filled-no-submit"): @@ -4058,6 +4244,11 @@ return 'submitted'; waited = now - wait_cf_since if wait_cf_since else 0 log_callback(f"[*] 等待 Cloudflare 后再提交... token长度={token_len} waited={waited:.0f}s") last_cf_log_at = now + if wait_cf_since and now - wait_cf_since >= 3.0 and now - last_cf_retry_at >= 4.0: + try: + _click_turnstile_widget(page) + except Exception: + pass if now - wait_cf_since >= cf_retry_after and now - last_cf_retry_at >= cf_retry_gap: if log_callback: log_callback("[*] 提交前仍卡住,自动再次复用 Turnstile...") @@ -4088,7 +4279,7 @@ return String(cfInput.value || '').trim().length; if cf_token_fail_streak >= max_cf_token_fails: raise Exception(f"Turnstile 连续失败,快速放弃资料页: {cf_exc}") last_cf_retry_at = now - human_sleep(0.8, cancel_callback) + cf_sleep(1.0, cancel_callback, floor=0.7) continue if submit_state == "submitted": diff --git a/register_cli.py b/register_cli.py index b06feb3..d3d0cf4 100644 --- a/register_cli.py +++ b/register_cli.py @@ -377,10 +377,11 @@ def register_one( sso_timeout = int(cfg.get("sso_timeout_base", 120) or 120) except Exception: sso_timeout = 120 - # Fast-fail band: keep CF stalls from dominating wall clock. - # batch10 lesson: success profiles finish in ~6–18s; 40–50s CF fail is enough. - profile_timeout = max(35, min(profile_timeout, 70)) - sso_timeout = max(30, min(sso_timeout, 90)) + # Profile page is dominated by Turnstile on Xvfb/servers. + # Old 70s hard-cap + --fast human_sleep scale caused tokenLen=0 within ~18s. + # Allow enough wall-clock for multi-round CF (cf_sleep is only lightly scaled). + profile_timeout = max(60, min(profile_timeout, 150)) + sso_timeout = max(30, min(sso_timeout, 120)) log(worker_id, "4. 填写资料") profile = reg.fill_profile_and_submit( diff --git a/turnstilePatch/content.js b/turnstilePatch/content.js index a54e1e9..92cffde 100644 --- a/turnstilePatch/content.js +++ b/turnstilePatch/content.js @@ -3,6 +3,8 @@ // / iframe postMessage spam) were observed to break xAI signup with: // [permission_denied] HTTP 403 // Keep webdriver hide only. Auto-click remains best-effort and local-only. +// Note: CF challenge iframes are cross-origin — contentDocument click usually fails; +// host-page click + Python CDP shadow path do the real work. (function () { "use strict"; @@ -30,41 +32,101 @@ } } catch (e) {} - // 2. Best-effort Turnstile checkbox click (same-origin only; no postMessage spam) + function firePointer(el) { + if (!el) return; + try { + el.scrollIntoView({ block: "center", inline: "center" }); + } catch (e) {} + var rect; + try { + rect = el.getBoundingClientRect(); + } catch (e) { + rect = null; + } + var x = 12; + var y = 12; + if (rect && rect.width > 0) { + x = rect.left + Math.max(6, Math.min(rect.width - 6, rect.width * 0.35)); + y = rect.top + Math.max(6, Math.min(rect.height - 6, rect.height * 0.5)); + } + var opts = { + bubbles: true, + cancelable: true, + view: window, + clientX: x, + clientY: y, + button: 0, + }; + var types = [ + "pointerover", + "mouseover", + "mousemove", + "pointerdown", + "mousedown", + "pointerup", + "mouseup", + "click", + ]; + for (var i = 0; i < types.length; i++) { + var t = types[i]; + try { + var Ev = t.indexOf("pointer") === 0 ? PointerEvent : MouseEvent; + el.dispatchEvent(new Ev(t, opts)); + } catch (e) { + try { + el.dispatchEvent(new MouseEvent(t, opts)); + } catch (e2) {} + } + } + try { + if (typeof el.click === "function") el.click(); + } catch (e) {} + } + + // 2. Best-effort Turnstile interaction (same-origin iframe + host widget) function tryClickTurnstile() { try { var iframes = document.querySelectorAll( 'iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]' ); for (var i = 0; i < iframes.length; i++) { + // Host-side synthetic click (works even when iframe is cross-origin) + firePointer(iframes[i]); try { var body = iframes[i].contentDocument || iframes[i].contentWindow.document; - var checkbox = body.querySelector('input[type="checkbox"], .mark'); + if (!body) continue; + var checkbox = body.querySelector( + 'input[type="checkbox"], .mark, label, [role="checkbox"]' + ); if (checkbox && !checkbox.checked) { - checkbox.click(); + firePointer(checkbox); } } catch (e) { - // cross-origin: skip + // cross-origin: host click above is enough } } + var widgets = document.querySelectorAll( + "div.cf-turnstile, [data-sitekey], input[name='cf-turnstile-response']" + ); + for (var j = 0; j < widgets.length; j++) { + firePointer(widgets[j]); + } } catch (e) {} } - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", function () { - var n = 0; - var t = setInterval(function () { - n++; - tryClickTurnstile(); - if (n > 80) clearInterval(t); - }, 500); - }); - } else { + function startLoop() { var n = 0; var t = setInterval(function () { n++; tryClickTurnstile(); - if (n > 80) clearInterval(t); + // ~90s of best-effort clicks (was 40s) + if (n > 180) clearInterval(t); }, 500); } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", startLoop); + } else { + startLoop(); + } })(); diff --git a/turnstilePatch/manifest.json b/turnstilePatch/manifest.json index 6bab5c7..0adbd32 100644 --- a/turnstilePatch/manifest.json +++ b/turnstilePatch/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, "name": "Turnstile Patch", - "version": "1.2.0", - "description": "Minimal webdriver hide for Turnstile (no aggressive API patches)", + "version": "1.3.0", + "description": "Minimal webdriver hide + best-effort host click for Turnstile", "content_scripts": [ { "matches": [