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": "",
|
||||
"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"
|
||||
}
|
||||
+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)
|
||||
|
||||
|
||||
# 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:
|
||||
|
||||
@@ -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,
|
||||
"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": ["<all_urls>"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_start",
|
||||
"all_frames": true,
|
||||
"world": "MAIN",
|
||||
"match_about_blank": true
|
||||
"all_frames": 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