# -*- coding: utf-8 -*- """ 决定性实验 A —— 把真机页面**组好但没发出**的 verify 包, 分别交给 Python / 浏览器 代发。 要回答的问题 (har/1ms_dianxuan/PROGRESS.md §五 1(a)): 12 到底是"**谁在发这个 HTTP**"造成的, 还是"**发出去的包**"造成的? 做法: 1. 在真机页面的 **顶层页 + 验证码 iframe** 里 hook 掉 XMLHttpRequest.send / fetch, 让腾讯 widget 照常组包, 但请求**不真正发出** (拦在 `cap_union_new_verify`)。 2. 用 CDP 真实鼠标把验证码答完 -> widget 组包 -> 被拦下, 我们拿到**原封不动的** body。 3. 把这个 body 发给 `cap_union_new_verify`: --send-by python : httpx 按浏览器同款头发出 (实验组) --send-by browser : 同 iframe 里 fetch 发出 (对照组: 证明这个包本身有效且未被消耗) 4. 判读: * 两组都 0 -> "谁在发"无关, 12 只剩"包内容"(H-LAYOUT) -> 下一步去取槽位表 * python 12, 浏览器 0 -> 存在"会话↔请求方"绑定, 需换实验 A' * 浏览器也非 0 -> 拦截本身污染了会话, 实验无效 用法: .venv/bin/python scripts/experiment_relay_verify.py --send-by python .venv/bin/python scripts/experiment_relay_verify.py --send-by browser """ from __future__ import annotations import argparse import base64 import importlib.util import json import subprocess import sys import time from pathlib import Path ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) import httpx # noqa: E402 def _bridge(): spec = importlib.util.spec_from_file_location("bridge_1ms", ROOT / "scripts" / "bridge_1ms_captcha.py") mod = importlib.util.module_from_spec(spec) sys.argv = ["experiment"] spec.loader.exec_module(mod) return mod TRAP_JS = r"""(() => { if (window.__vtrapInstalled) return {already: true}; window.__vtrapInstalled = true; window.__vtrap = null; const isVerify = (u) => typeof u === 'string' && u.indexOf('cap_union_new_verify') >= 0; const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; const origSRH = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.open = function (m, u) { this.__vUrl = u; this.__vMethod = m; this.__vHdrs = {}; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function (k, v) { try { if (isVerify(this.__vUrl)) this.__vHdrs[k] = v; } catch (e) {} return origSRH.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { if (isVerify(this.__vUrl)) { window.__vtrap = { via: 'xhr', url: String(this.__vUrl), method: this.__vMethod || 'POST', headers: this.__vHdrs || {}, body: String(body), at: Date.now() }; return; // 故意不发出 } return origSend.apply(this, arguments); }; const origFetch = window.fetch; if (typeof origFetch === 'function') { window.fetch = function (input, init) { const u = (typeof input === 'string') ? input : (input && input.url); if (isVerify(u)) { const body = (init && init.body) || ''; window.__vtrap = { via: 'fetch', url: String(u), method: (init && init.method) || 'GET', headers: (init && init.headers) || {}, body: String(body), at: Date.now() }; return Promise.reject(new Error('__vtrap_blocked__')); } return origFetch.apply(this, arguments); }; } return {installed: true}; })()""" READ_TRAP = "(() => ({trap: window.__vtrap || null, installed: !!window.__vtrapInstalled}))()" def send_by_python(url: str, body: str, headers: dict, want_origin: bool) -> dict: h = { "Content-Type": headers.get("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"), "Accept": headers.get("Accept", "application/json, text/javascript, */*; q=0.01"), "Referer": "https://turing.captcha.gtimg.com/", "User-Agent": ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36"), "sec-ch-ua-platform": '"macOS"', "sec-ch-ua-mobile": "?0", "sec-ch-ua": '"Google Chrome";v="155", "Chromium";v="155", "Not(A:Brand";v="24"', } for k, v in (headers or {}).items(): h[k] = v if want_origin: h["Origin"] = "https://turing.captcha.gtimg.com" with httpx.Client(timeout=20.0, trust_env=False) as c: r = c.post(url, content=body.encode("utf-8"), headers=h) return {"status": r.status_code, "text": r.text, "sent_headers": h} def main() -> int: ap = argparse.ArgumentParser(description="把真机组好的 verify 包交给 Python/浏览器 代发") ap.add_argument("--send-by", choices=["python", "browser"], default="python") ap.add_argument("--rounds", type=int, default=8, help="最多刷几轮去等一个文字点选题") ap.add_argument("--origin", action="store_true", help="Python 代发时附带 Origin 头") args = ap.parse_args() b = _bridge() # 1) 唤起 widget ens = b.cdp_eval(b.EXPR_ENSURE, b.PAGE)["result"] print(f"[1] 唤起 widget: {ens}") time.sleep(2.0) # 2) 装陷阱 (顶层页 + iframe; 谁发都拦得住) for ctx in (b.PAGE, b.IFRAME): try: print(f"[2] 装陷阱 {ctx}: {b.cdp_eval(TRAP_JS, ctx)['result']}") except Exception as exc: print(f"[2] 装陷阱 {ctx} 失败: {exc}") # 3) 跑桥接去答题 (回调必定超时 —— 因为 verify 被我们拦下了, 这正是预期) print("[3] 让真机把题答完 (verify 会被拦下, 所以回调超时属预期) ...") p = subprocess.run([sys.executable, str(ROOT / "scripts" / "bridge_1ms_captcha.py"), "--only", "text", "--rounds", str(args.rounds), "--solves", "1"], cwd=str(ROOT), capture_output=True, text=True, timeout=600) tail = [ln for ln in p.stdout.splitlines() if ln.strip()][-8:] print(" " + "\n ".join(tail)) # 4) 取包 trap = None for ctx in (b.IFRAME, b.PAGE): try: r = b.cdp_eval(READ_TRAP, ctx)["result"] except Exception as exc: print(f"[4] 读陷阱 {ctx} 失败: {exc}") continue print(f"[4] {ctx}: installed={r.get('installed')} trap={'有' if r.get('trap') else '无'}") if r.get("trap"): trap = r["trap"] break if not trap: print("❌ 没拦到 verify 包 (可能本轮没抽到可解的题, 或 iframe 被重建)") return 1 body = trap["body"] print(f"[4] 拦到 verify 包: via={trap['via']} url={trap['url'][:80]} body={len(body)}B") print(f" headers={trap.get('headers')}") # 5) 代发 if args.send_by == "python": print("[5] Python(httpx) 代发同一包 ...") res = send_by_python(trap["url"], body, trap.get("headers") or {}, args.origin) else: print("[5] 浏览器(同 iframe) 代发同一包 ...") js = ("(async()=>{const body=atob('%s');" "const r=await fetch('%s',{method:'POST',headers:" "{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8'," "'Accept':'application/json, text/javascript, */*; q=0.01'},body});" "return {status:r.status, text: await r.text()};})()" % (base64.b64encode(body.encode()).decode(), trap["url"])) tmp = Path("/tmp/1ms_bridge/relay_browser.js") tmp.parent.mkdir(parents=True, exist_ok=True) tmp.write_text(js, encoding="utf-8") res = b.cdp_eval(js, b.IFRAME)["result"] print(f"[6] 代发结果: status={res.get('status')}") print(f" body={str(res.get('text'))[:400]}") try: code = json.loads(res.get("text") or "{}").get("errorCode") except Exception: code = None print(f"\n>>> errorCode = {code} (sender={args.send_by})") print(">>> 判读: 0 => '谁在发'无关, 12 只剩包内容(H-LAYOUT); 12 => 还需排查请求方绑定") return 0 if __name__ == "__main__": raise SystemExit(main())