143 lines
7.0 KiB
Markdown
143 lines
7.0 KiB
Markdown
# XHR/fetch Live Session Bridge
|
||
|
||
本文件用于约束 Node.js 补环境中的 `XMLHttpRequest`、`fetch`、`Request`、`Response`、`Headers` 与 `navigator.sendBeacon`。当最终项目会发送真实请求,或这些 API 影响动态资源、Cookie、challenge、telemetry、resource timing、writer 分支时,JS 侧网络 API 必须复用最终确认的 TLS 指纹兼容 Session。
|
||
|
||
## 硬性规则
|
||
|
||
- `offline-fixture` 只能用于离线诊断:可以返回 fixture / 默认响应,但必须明确标注“不发送真实网络请求”,不得宣称 TLS 指纹问题已解决。
|
||
- `live-session-bridge` 用于真实验证:JS `XMLHttpRequest` / `fetch` / `sendBeacon` 必须把请求交给同一个 CycleTLS / impers / curl-cffi-node / curl_cffi / cffi_curl / cyCronet Session 发送。
|
||
- 不得在补环境网络对象内部直接调用 Node 宿主 `fetch`、`http`、`https`、`axios`、`undici` 或 Python 普通 `requests` 绕过已确认 TLS 客户端。
|
||
- 如果最终用户选择 Python `curl_cffi`,`final.py` 必须持有唯一 `curl_cffi.requests.Session()`;Node JS runtime 通过 stdio / IPC / 嵌入式 bridge 请求 Python 发送,Node 侧只实现浏览器 API 语义与桥接协议。
|
||
- Bridge 只能作为唯一 `final.js` / `final.py` 入口内部的受控模块存在,不得交付另一个手动启动入口、临时 HTTP server、调试 bridge 或常驻本地服务。
|
||
- XHR/fetch 对象本身不得通过 `_` / `__` 自有属性保存状态;状态用 addon / xbs private API 或模块级 `WeakMap`。
|
||
- Session Bridge 只证明“由哪个客户端发送”,不能证明请求语义正确。真实请求前必须继续按 `xhr-fetch-semantics-audit.md` 比较浏览器与 Node 的 no-send network transcript。
|
||
- 代码里出现 `curl_cffi`、`IPC`、`sessionBridge` 等关键词不算验收证据;必须有运行时 `xhr-fetch-bridge-audit.json` 和机器生成的 `xhr-fetch-semantics-audit.json`。
|
||
|
||
## 推荐架构
|
||
|
||
```text
|
||
目标 JS XMLHttpRequest/fetch
|
||
-> src/env/network/xhr.js / fetch.js 浏览器语义层
|
||
-> session bridge adapter
|
||
-> final.js/final.py 持有的同一 TLS Session
|
||
-> 目标网络请求
|
||
-> response envelope
|
||
-> JS XHR readyState / fetch Response / resource timing / Cookie 同步
|
||
```
|
||
|
||
Python `curl_cffi` 场景:
|
||
|
||
```text
|
||
final.py
|
||
create curl_cffi.requests.Session()
|
||
spawn Node runtime child process
|
||
read NDJSON bridge request from child stdout / pipe
|
||
session.request(...)
|
||
write NDJSON bridge response back to child stdin / pipe
|
||
finally session.close()
|
||
```
|
||
|
||
Node 侧不得自行发目标 URL 请求;只发 bridge envelope。Python 侧不得创建第二个 Session 给 XHR/fetch,必须复用最终请求同一个 session。
|
||
|
||
## Bridge 请求 envelope
|
||
|
||
至少记录:
|
||
|
||
```json
|
||
{
|
||
"id": "bridge-req-1",
|
||
"api": "XMLHttpRequest.send",
|
||
"method": "POST",
|
||
"url": "https://example.com/api",
|
||
"headers": {},
|
||
"bodyEncoding": "utf8/base64/bytes/none",
|
||
"body": "",
|
||
"credentials": "include/same-origin/omit",
|
||
"referrer": "",
|
||
"mode": "cors/same-origin/no-cors",
|
||
"timeoutMs": 30000,
|
||
"baselineId": "fp-001"
|
||
}
|
||
```
|
||
|
||
Bridge response 至少记录:
|
||
|
||
```json
|
||
{
|
||
"id": "bridge-req-1",
|
||
"ok": true,
|
||
"status": 200,
|
||
"statusText": "OK",
|
||
"headers": {},
|
||
"bodyEncoding": "utf8/base64/bytes",
|
||
"body": "",
|
||
"timing": {},
|
||
"setCookieApplied": true
|
||
}
|
||
```
|
||
|
||
## 浏览器语义同步
|
||
|
||
- `XMLHttpRequest.open()`、`setRequestHeader()`、`send()`、`abort()`、`timeout`、`responseType`、`readyState`、`status`、`responseText`、`responseURL`、`getResponseHeader()`、`getAllResponseHeaders()` 按目标浏览器采样实现。
|
||
- `fetch()` 返回 `Promise<Response>`,`Headers`、`Request`、`Response.clone()`、`text()`、`json()`、`arrayBuffer()`、body used 状态应可审计。
|
||
- `Set-Cookie` 由 session client 写回 Cookie jar,并同步到 JS cookie store;`xhr.getResponseHeader("set-cookie")` 和 `fetch Response.headers.get("set-cookie")` 默认应为 `null`,除非目标浏览器 baseline 证明可见。
|
||
- `document.cookie` 写入必须进入同一 Cookie jar 或在 bridge 请求前合并;服务端 `Set-Cookie` 必须更新同一 jar 和 JS cookie store。
|
||
- resource timing 如果被目标检测,必须在 `PerformanceResourceTiming` 中记录与真实浏览器 baseline 一致的最小字段和可见时机。
|
||
- CORS、redirect、referrer、credentials、cache、keepalive、sendBeacon 是否完整实现取决于目标路径;未覆盖时在矩阵中标 `needs-browser-baseline` 或 `native-capability-gap`,不得静默 mock。
|
||
|
||
## 必须产物
|
||
|
||
真实请求场景至少生成:
|
||
|
||
```text
|
||
case/notes/xhr-fetch-session-bridge.md
|
||
case/tmp/xhr-fetch-bridge-audit.json
|
||
case/fixtures/browser-network-transcript.ndjson
|
||
case/tmp/node-network-transcript.ndjson
|
||
case/tmp/xhr-fetch-semantics-audit.json
|
||
```
|
||
|
||
`xhr-fetch-session-bridge.md` 记录:
|
||
|
||
- 网络模式:`offline-fixture` / `live-session-bridge` / 不涉及。
|
||
- 最终 TLS Session 客户端:CycleTLS / impers / curl-cffi-node / curl_cffi / cffi_curl / cyCronet。
|
||
- Session 持有者:`final.js` / `final.py`。
|
||
- JS bridge 文件:例如 `result/src/env/network/xhr.js`、`result/src/env/network/fetch.js`。
|
||
- 请求层文件:例如 `result/src/request/client.js` 或 `result/src/request/session_client.py`。
|
||
- Cookie / Set-Cookie 同步策略。
|
||
- readyState / Promise / event 顺序验证。
|
||
- resource timing 是否涉及。
|
||
- 离线 fixture 是否仅用于诊断。
|
||
- Session id、运行时 bridge round trip 数、`sameSessionVerified`、`runtimeSourceHash`。
|
||
- no-send 请求语义审计结果:请求来源、Header 顺序、body SHA256、status/responseURL、事件顺序、reload realm 清理。
|
||
|
||
`xhr-fetch-bridge-audit.json` 必须由运行时 bridge 在至少一次受控 round trip 后写出:
|
||
|
||
```json
|
||
{
|
||
"schemaVersion": "xhr-fetch-session-bridge-audit/v2",
|
||
"generatedBy": "runtime-session-bridge/v2",
|
||
"mode": "live-session-bridge",
|
||
"tlsClient": "curl_cffi",
|
||
"sessionOwner": "final.py",
|
||
"sessionId": "session-1",
|
||
"transportRoundTrips": 1,
|
||
"sameSessionVerified": true,
|
||
"runtimeSourceHash": "...",
|
||
"cookieSyncVerified": true
|
||
}
|
||
```
|
||
|
||
不得在没有实际 bridge round trip 时生成该文件。Node no-send transcript 和最终请求 session 必须记录同一 session identity 或可验证的 session lineage。
|
||
|
||
## 检查命令
|
||
|
||
```bash
|
||
node scripts/check_xhr_fetch_session_bridge.js --case-dir case --markdown
|
||
node scripts/check_xhr_fetch_session_bridge.js --case-dir case --require-live --json
|
||
node scripts/check_xhr_fetch_session_bridge.js --case-dir case --tls-client curl_cffi --require-live --markdown
|
||
node scripts/check_xhr_fetch_semantics.js --case-dir case --require --require-no-send --out case/tmp/xhr-fetch-semantics-audit.json --markdown
|
||
```
|
||
|
||
检查失败时,下一步只能补 live session bridge、补 Cookie 同步、修正 XHR/fetch 浏览器语义、改为明确不发真实请求,或记录用户豁免。不得继续真实请求验证或写“TLS 指纹已解决”。
|