diff --git a/docs/design/cloudflare-handling.md b/docs/design/cloudflare-handling.md new file mode 100644 index 00000000..e5aef947 --- /dev/null +++ b/docs/design/cloudflare-handling.md @@ -0,0 +1,42 @@ +# CF-1 Cloudflare 验证处理(调研) + +## 1. 复现(2026-09-30,HTTP 5 次、浏览器 5 次,间隔 ≥15s) +| 请求 | 结果 | +| --- | --- | +| HTTP(Deno,Firefox UA)观众 / 憨憨搜索页,无 Cookie、仅 cf_clearance、补齐浏览器请求头 | 全部 302 → login.php(`server: cloudflare`,无 `cf-mitigated`):**没被挑战** | +| 浏览器,观众账号 profile(已有 cf_clearance)搜索页 | 200,直接出结果 | +| 浏览器,全新 profile:憨憨 | 无挑战、无 CF Cookie | +| 浏览器,全新 profile:观众 ×2 | 首次导航 **403(挑战)**,1–5 秒内**不点即过**,拿到 `cf_clearance`(Cookie 期限 1 年;真实有效期由站点 Challenge Passage 决定,不可见) | + +结论:观众有 CF 挑战(非交互 managed/JS 类,挑战页类型未抓到),固定 Windows 身份的 camofox 能自动通过;憨憨未见。 + +**关键发现:两端出口不一致**。`cdn-cgi/trace`:浏览器(站点未开代理 → 直连)出口 HKG;app 的 HTTP 端走 `globalThis.fetch`,而 Deno 会读容器的 `HTTPS_PROXY`(override 里的 mihomo 中继)→ 出口 DFW。BR-4 §11.5 的"按站点统一出口"在 HTTP 侧没生效;代理换节点也是"随机出 CF"的可能来源。 + +**分类缺陷**:HTTP 遇挑战(403)在 spider `fetchPage` 被归为 `auth_invalid` → 账号标失效、开"登录失效"事项、触发重登;503 挑战被归为维护。 + +## 2. 试验:浏览器 cf_clearance + 同 UA 给 HTTP +HTTP 本身未被挑战,带不带 cf_clearance 都是 302,**本次无法判定**。另因出口不同(HKG vs DFW),即使被挑战也不满足"同 IP"前提。MP 的 HTTP 端是 requests/httpx(非浏览器 TLS),照样复用浏览器取得的 cookie+UA,说明多数 PT 站实际只绑 UA+IP。把"首次复用重试"本身作为在线验证,按站点记结果。 + +## 3. 方案对比 +| | 成功率 | 请求成本 | 礼貌 | 改动 | 与 BR-4 | +| --- | --- | --- | --- | --- | --- | +| A 同步 cf_clearance+UA,HTTP 重试 1 次 | 取决于站点是否绑 TLS | 最低 | 好 | 小 | 需改"仅浏览器"规则 | +| B 该请求改由浏览器执行 + N 小时走浏览器标记 | 高 | 中(整页加载) | 好 | 中 | 一致 | +| C 站点长期全走浏览器 | 高 | 高 | 一般 | 中 | 一致 | +| D FlareSolverr | 与 camofox 相当 | 多一容器 | — | 大 | 不需要(用户否决) | + +**推荐 A→B 级联**(照 MP,camofox 即仿真浏览器): +1. **触发**:`http.ts` 识别 `cf-mitigated: challenge` 或 403/503 + `underChallenge(html)` → 新错误 `SiteError("challenge")`;不再算凭据错误。 +2. **过挑战**:用该账号 profile 开浏览器会话打开同一 URL。① 等自动通过(≤15s,轮询标题 / cf_clearance);② 未过 → `browser.solve_challenge` 视觉子流程,只允许"我是人类"复选框 / Turnstile 单步点击,maxSteps 小、最多 1–2 次;③ 图片选择 / 输码类 → 会话转 `waiting(challenge_unsolved)`,生成待处理事项"观众需要你在原站视图里过一次验证",用户在桌面(同一会话)过完,交还即继续。约束:只处理用户已登录账号的站点;同站单次触发只处理一次,30 分钟节流;不接打码服务。 +3. **同步**:读回 cf_clearance + `navigator.userAgent`,前提是账号 UA 来源 = browser 且两端出口相同;HTTP 重试 1 次。 +4. **回退**:仍被挑战 → 本次请求改为浏览器导航取 `outerHTML` 交给现有解析器;站点记 `browserMode` 6 小时(续期 / 到期再试 HTTP);记 `cfReuse = fails`,以后跳过第 3 步。 +5. **展示**:站点页显示"Cloudflare:浏览器代理中,至 hh:mm / 需人工验证 / 复用有效"。 + +## 4. 实现清单 +- `packages/adapters/src/sites/{http.ts,types.ts,spider/adapter.ts}`:`challenge` 错误类,修 403 误判。 +- `sources-identity/runtime.ts`:非代理站点显式 `Deno.createHttpClient({proxy:null})` 直连(或浏览器也按 env 代理),两端同出口;`siteFetch` 挂挑战钩子(重试 / 浏览器执行)。 +- `service/credentials.ts`、`browser/cookies.ts`:`splitBrowserOnlyCookies` 改为"同 UA 同出口时 cf_clearance 进 HTTP Cookie",`__cf_bm` 仍不带。 +- `site.config.cloudflare = {browserUntil, cfReuse, lastChallengeAt, lastSolvedBy}`;`snapshot.ts` 不再对 challenge 调 `onAuthError`。 +- `browser/service.ts`:新增 `fetchViaBrowser(siteId, url)`(profile 锁、限流令牌、`site_disabled` 检查);`enterWaiting` / 待处理事项、桌面接管(WT-1)、BR-3 重连沿用。 +- `agent/challenge.ts`:无父 Agent 时由系统发起子流程(预算来自设置);Turnstile 在跨域 iframe,`click_at` 需插件提供真实鼠标坐标点击。 +- 测试:挑战分类、级联状态机、出口一致。