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:
1 parent
c87b01967f
commit
60e24b89c1
5 files changed
+449
-157
No files matched your search
+5
-4
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
@@ -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) {}
|
|
||||||
Reference in new issue
Block a user