diff --git a/config.json b/config.json index 3450cb8..0e0d2bd 100644 --- a/config.json +++ b/config.json @@ -10,7 +10,7 @@ "proxy": "", "enable_nsfw": true, "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_local_token_file": "", "grok2api_pool_name": "ssoBasic", @@ -102,7 +102,8 @@ "turnstile_retry_after_sec": 10, "turnstile_retry_gap_sec": 8, "turnstile_max_rounds": 20, - "browser_prefer": "chrome", - "browser_path": "/usr/bin/google-chrome-stable", - "browser_headless": false + "browser_prefer": "auto", + "browser_path": "", + "browser_headless": false, + "stealth_mode": "off" } diff --git a/grok_register_ttk.py b/grok_register_ttk.py index 2f06f0d..1b567ab 100644 --- a/grok_register_ttk.py +++ b/grok_register_ttk.py @@ -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) -# Base flags always safe. Keep close to Git-creat7/grokRegister-cpa which only -# uses auto_port + extension (no slim flags) on real headed desktops. +# Headed flags from grok-register.zip (ReinerBRO): no --disable-images, no AutomationControlled. +# Images must stay ON or Turnstile iframe often fails to paint. 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", - "--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 -# blank CF widgets (iframe=0 / token=0) — especially --disable-software-rasterizer. +# Headless / Linux-server extras only. --disable-software-rasterizer blanks CF on headed. CHROMIUM_HEADLESS_EXTRA_FLAGS = [ + "--no-sandbox", + "--disable-dev-shm-usage", "--disable-gpu", "--disable-software-rasterizer", - "--disable-background-networking", + "--mute-audio", ] # 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(): """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: - 1. config browser_path (absolute) + 1. config browser_path (absolute, must exist) 2. env BROWSER_PATH / CHROME_PATH / CHROMIUM_PATH - 3. browser_prefer=chromium|chrome|auto - auto → Chromium first when turnstilePatch exists (needed for CF) - 4. any remaining candidate + 3. browser_prefer=chromium|chrome → auto-detect candidates + 4. browser_prefer=auto/system → None (system default, zip style) 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) 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" - # env override env_pref = (os.environ.get("BROWSER_PREFER") or "").strip().lower() if 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"): 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) else: - # auto: extension needs Chromium; without extension Chrome is fine - if has_ext: - order = list(CHROMIUM_CANDIDATES) + list(CHROME_CANDIDATES) - else: - order = list(CHROME_CANDIDATES) + list(CHROMIUM_CANDIDATES) + order = list(CHROME_CANDIDATES) + list(CHROMIUM_CANDIDATES) 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. config.stealth_mode: off | minimal(default) | full """ - mode = str((config.get("stealth_mode") if isinstance(config, dict) else "") or "minimal").strip().lower() - if mode in ("0", "false", "off", "none", "disabled"): + # zip 默认不注入 CDP stealth(靠扩展);未配置时也走 off,避免和 content.js 叠指纹 + 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 page = page if page is not None else _get_page() if page is None: @@ -1097,7 +1100,14 @@ def apply_stealth_patches(page=None, log_callback=None) -> bool: 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: None → resolve from config/env/auto (no DISPLAY → headless) @@ -1108,30 +1118,26 @@ def create_browser_options(headless=None): options.auto_port() 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) - min_browser = str(os.environ.get("GROK_MIN_BROWSER", "") or "").strip().lower() in ( - "1", "true", "yes", "on", - ) - if min_browser: - print(" [browser] minimal flags mode (Git-creat7 style)", flush=True) - options.set_argument("--disable-blink-features=AutomationControlled") - options.set_argument("--no-first-run") - if use_hl: - options.set_argument("--disable-gpu") - else: - for flag in CHROMIUM_BASE_FLAGS: + + # grok-register.zip headed flags (CF-friendly) + for flag in CHROMIUM_BASE_FLAGS: + options.set_argument(flag) + + # Linux / headless-only extras (Windows headed stays minimal like the zip) + if use_hl or sys.platform != "win32": + for flag in CHROMIUM_HEADLESS_EXTRA_FLAGS: + if flag in ("--disable-gpu", "--disable-software-rasterizer") and not use_hl: + # headed Xvfb/desktop: keep software rasterizer so CF iframe paints + continue 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: options.set_pref("credentials_enable_service", False) except Exception: 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() if ua: try: @@ -1142,6 +1148,9 @@ def create_browser_options(headless=None): except Exception: pass 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() if browser_path: try: @@ -1149,20 +1158,21 @@ def create_browser_options(headless=None): print(f" [browser] path={browser_path}", flush=True) except Exception: 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): - # Google Chrome blocks CLI unpacked extension loading; Chromium still allows it. - 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: + try: options.add_extension(EXTENSION_PATH) print(f" [ext] turnstilePatch loaded: {EXTENSION_PATH}", flush=True) - # Apply config.json "proxy" to Chromium. Without this, only HTTP helpers - # used get_proxies(); the browser itself fell through to system/env proxy. + except Exception as ext_exc: + 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() if proxy: try: @@ -1173,7 +1183,6 @@ def create_browser_options(headless=None): if host: port = u.port or (443 if (u.scheme or "http") == "https" else 80) scheme = u.scheme or "http" - # Chromium --proxy-server cannot embed user:pass options.set_argument(f"--proxy-server={scheme}://{host}:{port}") except Exception as 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) else: print( - f" [browser] headed DISPLAY={os.environ.get('DISPLAY', '')!r} " - f"(flags=base-only, no disable-software-rasterizer)", + f" [browser] headed (zip-style minimal flags) " + f"DISPLAY={os.environ.get('DISPLAY', '')!r}", flush=True, ) return options @@ -4421,40 +4430,11 @@ return ok; return "none" -def getTurnstileToken(log_callback=None, cancel_callback=None, max_rounds: int = 20): - """batch100-era solver (55f56d2/c56d0d2) with longer waits. - - 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) - +def _read_turnstile_token_simple(page) -> str: + """Light token read used by solver (input + turnstile.getResponse).""" try: - 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( - """ + token = page.run_js( + """ try { const byInput = String((document.querySelector('input[name="cf-turnstile-response"]') || {}).value || '').trim(); if (byInput) return byInput; @@ -4463,21 +4443,217 @@ try { } 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 log_callback: log_callback(f"[*] Turnstile 已通过,token长度={len(token)}") return token - path = _click_turnstile_widget(page, log_callback=log_callback) - if log_callback and (round_i == 0 or round_i % 3 == 0): - log_callback(f"[Debug] Turnstile click path={path} round={round_i+1}/{rounds}") + ui = _turnstile_ui_state(page) + # After a click: prefer waiting over re-clicking (CF interactive fails on spam) + 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: pass - # Progressive wait like c56d0d2, but wall-clock via cf_sleep - cf_sleep(0.9 + 0.15 * min(round_i, 6), cancel_callback, floor=0.8) + cf_sleep(1.0, cancel_callback, floor=0.9) 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. """ page = _get_page() + # zip 方案:靠 turnstilePatch 扩展,不额外 CDP stealth(stealth_mode=off) apply_stealth_patches(page, log_callback=log_callback) check_timeout(time.time()) - # Cookie overlay often reappears on profile step and blanks Turnstile (iframe=0). dismiss_cookie_banner(page, log_callback=log_callback) dump_state(page, "profile-form") take_screenshot(page, "profile-form") given_name, family_name, password = build_profile() - # 预热 Turnstile:cookie 只关一次(sticky),然后给 Verifying 时间 + # zip: 预热 Turnstile — 等 2 秒让 iframe 初始化,插件会自动点击 checkbox if log_callback: log_callback("[*] 预热 Turnstile...") try: - dismiss_cookie_banner(page, log_callback=log_callback) + human_sleep(2, cancel_callback) except Exception: - pass - # 必须用 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 + cf_sleep(2.0, cancel_callback, floor=2.0) deadline = time.time() + timeout form_filled_once = False wait_cf_since = None @@ -4792,15 +4952,34 @@ return 'submitted'; now = time.time() if wait_cf_since is None: 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): token_len = submit_state.split(":", 1)[1] if ":" in submit_state else "0" waited = now - wait_cf_since if wait_cf_since else 0 log_callback( 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 - 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: _click_turnstile_widget(page) except Exception: diff --git a/turnstilePatch/content.js b/turnstilePatch/content.js new file mode 100644 index 0000000..2e9b993 --- /dev/null +++ b/turnstilePatch/content.js @@ -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); + } +})(); diff --git a/turnstilePatch/manifest.json b/turnstilePatch/manifest.json index 68e00dc..f153b00 100644 --- a/turnstilePatch/manifest.json +++ b/turnstilePatch/manifest.json @@ -1,16 +1,15 @@ { - "manifest_version": 3, - "name": "Turnstile Patcher", - "version": "2.1", - "description": "Patch CDP MouseEvent.screenX/screenY for Turnstile (ReinerBRO / Git-creat7)", + "manifest_version": 2, + "name": "Turnstile Patch", + "version": "1.0.0", + "description": "Patch browser automation detection for Turnstile", "content_scripts": [ { - "js": ["./script.js"], "matches": [""], + "js": ["content.js"], "run_at": "document_start", - "all_frames": true, - "world": "MAIN", - "match_about_blank": true + "all_frames": true } - ] + ], + "permissions": ["activeTab"] } diff --git a/turnstilePatch/script.js b/turnstilePatch/script.js deleted file mode 100644 index f1c1d5d..0000000 --- a/turnstilePatch/script.js +++ /dev/null @@ -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) {}