@mrrisega/dsh-remote 0.6.0-beta.7 → 0.6.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/clients/dsh-remote/dsh-bridge.mjs +291 -4
- package/clients/dsh-remote/e2ee-client.mjs +711 -0
- package/clients/dsh-remote/e2ee-shim-script.js +697 -0
- package/clients/dsh-remote/e2ee-shim.mjs +158 -0
- package/clients/dsh-remote/test/e2ee-bridge.test.mjs +630 -0
- package/clients/dsh-remote/test/e2ee-client.test.mjs +365 -0
- package/clients/dsh-remote/test/e2ee-shim.test.mjs +507 -0
- package/clients/dsh-remote/test/e2ee-wecrypto-parity.test.mjs +200 -0
- package/clients/dsh-remote/test/lib-e2ee-shim.mjs +59 -0
- package/clients/dsh-remote/test/lib-native-e2ee.mjs +92 -0
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +36 -1
- package/packages/dsh-remote-web/lib/index.js +29 -1
- package/packages/dsh-remote-web/test/e2ee-state.test.mjs +141 -0
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +75 -0
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +1 -1
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bridge×router E2EE Phase-2 集成测试(全本地:内存/临时目录,不起任何真实服务):
|
|
4
|
+
* 本地 relay-router + 三个 bridge(同一账号 user 42,不同 deviceId)+ 本地 HTTP/WS echo 上游 +
|
|
5
|
+
* mock 账号 API(/api/e2ee-params 按 Bearer 内 phone claim 分流 on/off/401,/api/devices 201)。
|
|
6
|
+
*
|
|
7
|
+
* 覆盖:
|
|
8
|
+
* 1. 开关与回退:on → .e2ee-state.json {enabled:true} + tunnel-register caps=["e2ee-v2"]
|
|
9
|
+
* → /_devices 透传 caps;off/401 → 状态文件 reason 明确、caps 空、明文 v1 路径原样;
|
|
10
|
+
* 2. E2EE 控制通道握手(device 形态 /remote/<dev>/_e2ee/ctrl 与 channel 形态
|
|
11
|
+
* /remote/_e2ee/ctrl?device=):hello→hello-ack→probe→probe-ok(会话 verified);
|
|
12
|
+
* 3. E2EE HTTP:信封请求(方法/路径/头/正文)经 router 透明转发 → bridge 解封→上游→封回
|
|
13
|
+
* → 手机开包还原;篡改密文 → 显式 502 + x-dsh-e2ee-error,绝不静默降级;
|
|
14
|
+
* 4. E2EE WS 数据流(&e2ee=<sessId>&w= 标记):桥剥除标记连上游、逐消息加/解密、echo 一致;
|
|
15
|
+
* 5. 解封失败显式拒绝:disabled bridge 收到信封标记帧 → 502 e2ee_disabled(不误转发明文);
|
|
16
|
+
* 6. **Phase-3 手机端**:native.html 抽取的浏览器 WebCrypto(non-复制实现)跑真实
|
|
17
|
+
* 解锁握手 + HTTP 信封 + WS 数据流,与桥端互通;密码错误 → e2ee-error bad_key。
|
|
18
|
+
*
|
|
19
|
+
* 用法: node --test --test-concurrency=1 test/e2ee-bridge.test.mjs
|
|
20
|
+
*/
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { createHmac } from "node:crypto";
|
|
24
|
+
import http from "node:http";
|
|
25
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
26
|
+
import net from "node:net";
|
|
27
|
+
import os from "node:os";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
import test, { after, before } from "node:test";
|
|
31
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
32
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
33
|
+
import {
|
|
34
|
+
E2eeSession,
|
|
35
|
+
decodeHttpResponsePlain,
|
|
36
|
+
deriveMasterKey,
|
|
37
|
+
deriveShk,
|
|
38
|
+
envelopeRequestHeaders,
|
|
39
|
+
fetchE2eeParams,
|
|
40
|
+
newSessId,
|
|
41
|
+
parseWsE2eeParams,
|
|
42
|
+
randomB64,
|
|
43
|
+
randomHex
|
|
44
|
+
} from "../e2ee-client.mjs";
|
|
45
|
+
import { loadNativeWcCore, loadNativeHandoverCore } from "./lib-native-e2ee.mjs";
|
|
46
|
+
import { loadShimCore } from "./lib-e2ee-shim.mjs";
|
|
47
|
+
|
|
48
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
49
|
+
const ROOT = path.join(HERE, "..", "..", "..");
|
|
50
|
+
const ROUTER_SRC = path.join(ROOT, "packages/relay-router/src/index.mjs");
|
|
51
|
+
const BRIDGE_SRC = path.join(HERE, "..", "dsh-bridge.mjs");
|
|
52
|
+
|
|
53
|
+
const SECRET = "test-e2ee-secret-0123456789abcdef0123456789abcdef";
|
|
54
|
+
const PASSWORD = "e2ee-integration-password";
|
|
55
|
+
const SALT_B64 = Buffer.from(Array.from({ length: 16 }, (_, i) => 0xa0 + i)).toString("base64url");
|
|
56
|
+
const DEV_ON = "dev-eeeeon000001"; // e2ee-params → enabled:true
|
|
57
|
+
const DEV_OFF = "dev-eeeoff000002"; // e2ee-params → enabled:false
|
|
58
|
+
const DEV_401 = "dev-eee401000003"; // e2ee-params → 401
|
|
59
|
+
|
|
60
|
+
function signJwt(claims, ttlSec = 7200) {
|
|
61
|
+
const b64 = (o) => Buffer.from(JSON.stringify(o)).toString("base64url");
|
|
62
|
+
const header = b64({ alg: "HS256", typ: "JWT" });
|
|
63
|
+
const now = Math.floor(Date.now() / 1000);
|
|
64
|
+
const body = b64({ ...claims, iat: now, exp: now + ttlSec });
|
|
65
|
+
const sig = createHmac("sha256", SECRET).update(`${header}.${body}`).digest("base64url");
|
|
66
|
+
return `${header}.${body}.${sig}`;
|
|
67
|
+
}
|
|
68
|
+
const phoneJwt = signJwt({ sub: "42", phone: "13800000000", plan: "free" });
|
|
69
|
+
const COOKIE = "dsh_token=" + encodeURIComponent(phoneJwt);
|
|
70
|
+
|
|
71
|
+
const freePort = () => new Promise((resolve) => {
|
|
72
|
+
const s = net.createServer();
|
|
73
|
+
s.listen(0, "127.0.0.1", () => { const p = s.address().port; s.close(() => resolve(p)); });
|
|
74
|
+
});
|
|
75
|
+
const sleep = (ms) => delay(ms, null, { ref: false });
|
|
76
|
+
async function waitFor(desc, fn, timeoutMs = 20000) {
|
|
77
|
+
const t0 = Date.now();
|
|
78
|
+
let last;
|
|
79
|
+
while (Date.now() - t0 < timeoutMs) {
|
|
80
|
+
try { const r = await fn(); if (r) return r; } catch (e) { last = e; }
|
|
81
|
+
await sleep(200);
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`等待超时: ${desc}${last ? " — " + last.message : ""}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------- 进程/目录 ----------
|
|
87
|
+
const procs = [];
|
|
88
|
+
const tmpDirs = [];
|
|
89
|
+
const spawnProc = (name, args, env) => {
|
|
90
|
+
const p = spawn(process.execPath, args, { env: { ...process.env, ...env }, stdio: ["ignore", "pipe", "pipe"] });
|
|
91
|
+
const tail = [];
|
|
92
|
+
p.stdout.on("data", (d) => { const s = String(d); tail.push(s); });
|
|
93
|
+
p.stderr.on("data", (d) => { const s = String(d); tail.push(s); });
|
|
94
|
+
p._tail = () => tail.join("").slice(-4000);
|
|
95
|
+
procs.push(p);
|
|
96
|
+
return p;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ---------- 本地上游:HTTP echo + /ws-echo + 官方 html(/api/official 供 Phase-4 注入验证) ----------
|
|
100
|
+
const OFFICIAL_BODY = `<!doctype html>
|
|
101
|
+
<html lang="en">
|
|
102
|
+
<head><base href="/"><script>window.__ModuleLoader__ = { mode: "queue" }</script>
|
|
103
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
104
|
+
<title>DeepSeek Harness</title></head>
|
|
105
|
+
<body><div id="root"></div></body></html>`;
|
|
106
|
+
|
|
107
|
+
const upstream = http.createServer((req, res) => {
|
|
108
|
+
let body = "";
|
|
109
|
+
req.on("data", (c) => { body += c; });
|
|
110
|
+
req.on("end", () => {
|
|
111
|
+
// 官方 dsh web 特征 html(Phase-4:验证 e2ee 启用的桥端注入镜像 shim)
|
|
112
|
+
if (req.url === "/official" || req.url.startsWith("/api/official")) {
|
|
113
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
114
|
+
res.end(OFFICIAL_BODY);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
118
|
+
res.end(JSON.stringify({ method: req.method, url: req.url, ct: req.headers["content-type"] || "", auth: req.headers["x-dsh-test-auth"] || "", body }));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
let upstreamWss = null;
|
|
122
|
+
|
|
123
|
+
// mock 账号 API:按 Bearer JWT 的 phone claim 尾号分流(0001→on 0002→off 0003→401)。
|
|
124
|
+
// 客户端(桥/手机)把 {API_BASE}/api/e2ee-params 请求发到 /relay-api/api/e2ee-params,这里剥前缀后匹配。
|
|
125
|
+
const mockApi = http.createServer((req, res) => {
|
|
126
|
+
const send = (status, obj) => { res.writeHead(status, { "content-type": "application/json" }); res.end(JSON.stringify(obj)); };
|
|
127
|
+
const path = req.url.replace(/^\/relay-api/, "") || "/";
|
|
128
|
+
if (req.method === "GET" && path === "/api/e2ee-params") {
|
|
129
|
+
let phone = "";
|
|
130
|
+
try {
|
|
131
|
+
const auth = String(req.headers.authorization || "").replace(/^Bearer\s+/i, "");
|
|
132
|
+
phone = JSON.parse(Buffer.from(auth.split(".")[1] || "", "base64url").toString("utf8")).phone || "";
|
|
133
|
+
} catch { /* keep "" */ }
|
|
134
|
+
if (phone.endsWith("0003")) return send(401, { error: { code: "unauthorized" } });
|
|
135
|
+
if (!phone.endsWith("0001")) {
|
|
136
|
+
return send(200, { e2ee: { enabled: false, profile: "pbkdf2-sha256-600k", kdf: { alg: "pbkdf2-sha256", iter: 600000, dkLen: 32, hash: "sha256" }, salt: "", epoch: 0 } });
|
|
137
|
+
}
|
|
138
|
+
return send(200, { e2ee: { enabled: true, profile: "pbkdf2-sha256-600k", kdf: { alg: "pbkdf2-sha256", iter: 600000, dkLen: 32, hash: "sha256" }, salt: SALT_B64, epoch: 2 } });
|
|
139
|
+
}
|
|
140
|
+
if (req.method === "POST" && path === "/api/devices") return send(201, { device: { id: "dev" } });
|
|
141
|
+
return send(404, { error: { code: "not_found" } });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
let routerBase = "";
|
|
145
|
+
let apiBase = "";
|
|
146
|
+
const stateDirs = {};
|
|
147
|
+
|
|
148
|
+
before(async () => {
|
|
149
|
+
const [upPort, apiPort, routerPort] = [await freePort(), await freePort(), await freePort()];
|
|
150
|
+
await new Promise((r) => mockApi.listen(apiPort, "127.0.0.1", r));
|
|
151
|
+
await new Promise((r) => upstream.listen(upPort, "127.0.0.1", r));
|
|
152
|
+
// 单一 noServer WSS + 按 pathname 手工路由(ws 库同一 http server 挂多个 WSS 会互相抢占同一 socket):
|
|
153
|
+
// /ws-echo(既有 echo)、/api/events.mux(Phase-4 受保护前缀下的数据 WS echo)
|
|
154
|
+
upstreamWss = new WebSocketServer({ noServer: true });
|
|
155
|
+
upstreamWss.on("connection", (ws) => ws.on("message", (d, isBinary) => ws.send(d, { binary: isBinary })));
|
|
156
|
+
upstream.on("upgrade", (req, socket, head) => {
|
|
157
|
+
let pathname = "";
|
|
158
|
+
try { pathname = new URL(req.url || "/", "http://x").pathname; } catch { /* keep "" */ }
|
|
159
|
+
if (pathname === "/ws-echo" || pathname === "/api/events.mux") {
|
|
160
|
+
upstreamWss.handleUpgrade(req, socket, head, (ws) => upstreamWss.emit("connection", ws, req));
|
|
161
|
+
} else {
|
|
162
|
+
try { socket.destroy(); } catch { /* ignore */ }
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
spawnProc("router", [ROUTER_SRC], {
|
|
167
|
+
DSH_ENTERPRISE_JWT_SECRET: SECRET,
|
|
168
|
+
DSH_ROUTER_HOST: "127.0.0.1",
|
|
169
|
+
DSH_ROUTER_PORT: String(routerPort)
|
|
170
|
+
});
|
|
171
|
+
await waitFor("router 就绪", async () => (await fetch(`http://127.0.0.1:${routerPort}/`)).status === 404);
|
|
172
|
+
|
|
173
|
+
for (const k of ["a", "b", "c"]) {
|
|
174
|
+
const d = mkdtempSync(path.join(os.tmpdir(), `dsh-e2ee-${k}-`));
|
|
175
|
+
writeFileSync(path.join(d, "config.json"), "{}");
|
|
176
|
+
stateDirs[k] = d;
|
|
177
|
+
tmpDirs.push(d);
|
|
178
|
+
}
|
|
179
|
+
const spawnBridge = (deviceId, phone, dir) => spawnProc(`bridge-${deviceId.slice(-4)}`, [BRIDGE_SRC], {
|
|
180
|
+
DSH_BRIDGE_TUNNEL_URL: `ws://127.0.0.1:${routerPort}`,
|
|
181
|
+
DSH_BRIDGE_DEVICE_ID: deviceId,
|
|
182
|
+
DSH_BRIDGE_TOKEN: signJwt({ sub: "42", phone, plan: "free" }),
|
|
183
|
+
DSH_BRIDGE_PASSWORD: PASSWORD,
|
|
184
|
+
DSH_BRIDGE_UPSTREAM: `http://127.0.0.1:${upPort}`,
|
|
185
|
+
DSH_BRIDGE_API: `http://127.0.0.1:${apiPort}/relay-api`,
|
|
186
|
+
DSH_BRIDGE_CONFIG: path.join(dir, "config.json")
|
|
187
|
+
});
|
|
188
|
+
spawnBridge(DEV_ON, "13811110001", stateDirs.a);
|
|
189
|
+
spawnBridge(DEV_OFF, "13811110002", stateDirs.b);
|
|
190
|
+
spawnBridge(DEV_401, "13811110003", stateDirs.c);
|
|
191
|
+
|
|
192
|
+
routerBase = `http://127.0.0.1:${routerPort}`;
|
|
193
|
+
apiBase = `http://127.0.0.1:${apiPort}/relay-api`;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
after(() => {
|
|
197
|
+
for (const p of procs) { try { p.kill("SIGTERM"); } catch {} }
|
|
198
|
+
for (const d of tmpDirs) { try { rmSync(d, { recursive: true, force: true }); } catch {} }
|
|
199
|
+
try { upstreamWss?.close(); } catch {}
|
|
200
|
+
try { upstream.close(); } catch {}
|
|
201
|
+
try { mockApi.close(); } catch {}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const readState = (k) => {
|
|
205
|
+
try { return JSON.parse(readFileSync(path.join(stateDirs[k], ".e2ee-state.json"), "utf8")); } catch { return null; }
|
|
206
|
+
};
|
|
207
|
+
const devicesOf = async () => (await (await fetch(`${routerBase}/_devices`, { headers: { cookie: COOKIE } })).json()).devices || [];
|
|
208
|
+
|
|
209
|
+
/** 手机侧:拉参数(与 bridge 相同服务端)→ 派生 MK → 建手机端会话对象。 */
|
|
210
|
+
async function phonePrepare() {
|
|
211
|
+
const params = await fetchE2eeParams(apiBase, signJwt({ sub: "42", phone: "13811110001", plan: "free" }));
|
|
212
|
+
assert.equal(params.enabled, true);
|
|
213
|
+
const mk = deriveMasterKey(PASSWORD, params.salt, params.kdf);
|
|
214
|
+
return { mk, params };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 手机侧解锁:对任意 ctrl URL(device 或 channel 形态)跑 §5.2 全握手。
|
|
219
|
+
* @returns {Promise<{ctrl: WebSocket, client: E2eeSession, sessId: string}>}
|
|
220
|
+
*/
|
|
221
|
+
function unlock(ctrlWsUrl, { mk, params }) {
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
const sessId = newSessId();
|
|
224
|
+
const aB64 = randomB64(32);
|
|
225
|
+
const ctrl = new WebSocket(ctrlWsUrl, { headers: { cookie: COOKIE } });
|
|
226
|
+
let client = null;
|
|
227
|
+
let settled = false;
|
|
228
|
+
const done = (v) => { if (!settled) { settled = true; clearTimeout(timer); resolve(v); } };
|
|
229
|
+
const fail = (e) => { if (!settled) { settled = true; clearTimeout(timer); try { ctrl.terminate(); } catch {} reject(e); } };
|
|
230
|
+
const timer = setTimeout(() => fail(new Error("解锁握手超时")), 10000);
|
|
231
|
+
ctrl.on("error", (e) => fail(e));
|
|
232
|
+
ctrl.on("close", () => { if (!settled) fail(new Error("ctrl 提前关闭")); });
|
|
233
|
+
ctrl.on("open", () => {
|
|
234
|
+
ctrl.send(JSON.stringify({ v: 2, type: "e2ee-hello", role: "phone", s: sessId, a: aB64, salt: params.salt, profile: params.profile, ts: Date.now() }));
|
|
235
|
+
});
|
|
236
|
+
ctrl.on("message", (raw) => {
|
|
237
|
+
let msg;
|
|
238
|
+
try { msg = JSON.parse(String(raw)); } catch { return; }
|
|
239
|
+
if (msg.type === "e2ee-error") return fail(new Error(`e2ee-error: ${msg.code} ${msg.message || ""}`));
|
|
240
|
+
if (msg.type === "e2ee-hello-ack" && !client) {
|
|
241
|
+
const { shk, saltH } = deriveShk(mk, aB64, msg.b);
|
|
242
|
+
client = new E2eeSession({ sessId, shk, saltH, profile: params.profile, epoch: params.epoch });
|
|
243
|
+
const probe = client.seal({ kind: "ctrl", dir: "p2b", counter: 0, data: JSON.stringify({ p: "dsh-e2ee-probe-v1", t: Date.now(), c: 0 }) });
|
|
244
|
+
ctrl.send(JSON.stringify(probe));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (msg && msg.v === 2 && msg.k === "ctrl" && client) {
|
|
248
|
+
// probe-ok:开包校验通过 = 双方 MK 一致
|
|
249
|
+
client.open({ kind: "ctrl", dir: "b2p", env: msg, counter: msg.c ?? 0 });
|
|
250
|
+
done({ ctrl, client, sessId });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------- 测试 ----------
|
|
257
|
+
|
|
258
|
+
test("开关与回退:enabled bridge 上报 caps + 状态文件;off/401 明文回退原样", async () => {
|
|
259
|
+
// 等待三台 bridge 注册 + A 的 e2ee 状态落盘
|
|
260
|
+
await waitFor("三台 bridge 注册进 /_devices", async () => {
|
|
261
|
+
const devs = await devicesOf();
|
|
262
|
+
const ids = devs.map((d) => d.id);
|
|
263
|
+
return ids.includes(DEV_ON) && ids.includes(DEV_OFF) && ids.includes(DEV_401);
|
|
264
|
+
});
|
|
265
|
+
await waitFor("A .e2ee-state.json 已写", () => readState("a")?.enabled === true);
|
|
266
|
+
await waitFor("B .e2ee-state.json 已写", () => readState("b") !== null);
|
|
267
|
+
await waitFor("C .e2ee-state.json 已写", () => readState("c") !== null);
|
|
268
|
+
|
|
269
|
+
// 状态文件:{enabled,reason}
|
|
270
|
+
const stA = readState("a");
|
|
271
|
+
assert.equal(stA.enabled, true);
|
|
272
|
+
assert.equal(stA.reason, "ok");
|
|
273
|
+
assert.deepEqual(stA.caps, ["e2ee-v2"]);
|
|
274
|
+
const stB = readState("b");
|
|
275
|
+
assert.equal(stB.enabled, false);
|
|
276
|
+
assert.equal(stB.reason, "server_disabled");
|
|
277
|
+
const stC = readState("c");
|
|
278
|
+
assert.equal(stC.enabled, false);
|
|
279
|
+
assert.equal(stC.reason, "server_unauthorized");
|
|
280
|
+
|
|
281
|
+
// /_devices:router 原样透传 caps(§6.2)
|
|
282
|
+
const devs = await devicesOf();
|
|
283
|
+
const capsOf = (id) => (devs.find((d) => d.id === id) || {}).caps || [];
|
|
284
|
+
assert.deepEqual(capsOf(DEV_ON), ["e2ee-v2"]);
|
|
285
|
+
assert.deepEqual(capsOf(DEV_OFF), []);
|
|
286
|
+
assert.deepEqual(capsOf(DEV_401), []);
|
|
287
|
+
|
|
288
|
+
// 明文 v1 路径:三台都不带信封标记 → 照常 200(回退不破坏隧道)
|
|
289
|
+
for (const dev of [DEV_ON, DEV_OFF, DEV_401]) {
|
|
290
|
+
const r = await fetch(`${routerBase}/remote/${dev}/api/echo?plain=1`, { headers: { cookie: COOKIE } });
|
|
291
|
+
assert.equal(r.status, 200, `${dev} 明文应 200`);
|
|
292
|
+
const j = await r.json();
|
|
293
|
+
assert.equal(j.method, "GET");
|
|
294
|
+
assert.equal(j.url, "/api/echo?plain=1");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// disabled bridge 收到信封标记帧 → 显式 502 e2ee_disabled,不误转发明文
|
|
298
|
+
const off = new E2eeSession({ sessId: newSessId(), shk: Buffer.alloc(32, 7), saltH: Buffer.alloc(32, 8) });
|
|
299
|
+
const env = off.seal({ kind: "http", dir: "p2b", counter: 0, data: "x" });
|
|
300
|
+
const r = await fetch(`${routerBase}/remote/${DEV_OFF}/api/echo`, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: { ...envelopeRequestHeaders(env.s, "http"), cookie: COOKIE },
|
|
303
|
+
body: JSON.stringify(env)
|
|
304
|
+
});
|
|
305
|
+
assert.equal(r.status, 502);
|
|
306
|
+
assert.equal(r.headers.get("x-dsh-e2ee-error"), "e2ee_disabled");
|
|
307
|
+
assert.match(await r.text(), /⚠/);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("E2EE 解锁握手(device 形态)+ channel 形态可开,探针通过后会话可用", async () => {
|
|
311
|
+
const { mk, params } = await phonePrepare();
|
|
312
|
+
const { ctrl, client, sessId } = await unlock(`${routerBase}/remote/${DEV_ON}/_e2ee/ctrl`, { mk, params });
|
|
313
|
+
assert.equal(sessId.length, 32);
|
|
314
|
+
assert.ok(client.sessId);
|
|
315
|
+
// channel 形态:/remote/_e2ee/ctrl?device=<dev>(router CHANNEL_MARKERS 含 _e2ee)
|
|
316
|
+
const chan = await unlock(`${routerBase}/remote/_e2ee/ctrl?device=${DEV_ON}`, { mk, params });
|
|
317
|
+
assert.equal(chan.sessId.length, 32);
|
|
318
|
+
chan.ctrl.close(1000);
|
|
319
|
+
// 保留 A 的解锁会话供后续 http/ws 用例复用
|
|
320
|
+
globalThis.__unlocked = { mk, params, ctrl, client, sessId };
|
|
321
|
+
ctrl.on("close", () => { globalThis.__ctrlClosed = true; });
|
|
322
|
+
assert.equal(globalThis.__ctrlClosed, undefined);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("E2EE HTTP:信封请求解封→上游→封回往返一致;篡改密文 → 显式 502", async () => {
|
|
326
|
+
const u = globalThis.__unlocked;
|
|
327
|
+
assert.ok(u, "需要先解锁");
|
|
328
|
+
const { client } = u;
|
|
329
|
+
// 1) 正常往返:POST /api/echo?b=2,信封内带头/正文
|
|
330
|
+
const bodyPayload = JSON.stringify({ hello: "世界", n: 42 });
|
|
331
|
+
const plain = Buffer.from(JSON.stringify({
|
|
332
|
+
m: "POST",
|
|
333
|
+
p: "/api/echo?b=2",
|
|
334
|
+
h: { "content-type": "application/json; charset=utf-8", "x-dsh-test-auth": "secret-value" },
|
|
335
|
+
b: Buffer.from(bodyPayload, "utf8").toString("base64")
|
|
336
|
+
}), "utf8");
|
|
337
|
+
const envReq = client.seal({ kind: "http", dir: "p2b", counter: 1, data: plain });
|
|
338
|
+
const res = await fetch(`${routerBase}/remote/${DEV_ON}/api/echo?b=2`, {
|
|
339
|
+
method: "POST",
|
|
340
|
+
headers: { ...envelopeRequestHeaders(client.sessId, "http"), cookie: COOKIE },
|
|
341
|
+
body: JSON.stringify(envReq)
|
|
342
|
+
});
|
|
343
|
+
assert.equal(res.status, 200);
|
|
344
|
+
const ct = res.headers.get("content-type") || "";
|
|
345
|
+
assert.ok(ct.startsWith("application/vnd.dsh.e2ee-v2"), `外层应为信封 content-type, got ${ct}`);
|
|
346
|
+
assert.ok((res.headers.get("x-dsh-e2ee") || "").includes(`s=${client.sessId}`));
|
|
347
|
+
// 手机开包:http-resp 子密钥 = 请求 n
|
|
348
|
+
const envResp = JSON.parse(await res.text());
|
|
349
|
+
assert.equal(envResp.k, "http-resp");
|
|
350
|
+
const opened = client.open({ kind: "http-resp", dir: "b2p", env: envResp, counter: envResp.c, reqNonceB64: envReq.n });
|
|
351
|
+
const resp = decodeHttpResponsePlain(opened.data);
|
|
352
|
+
assert.equal(resp.status, 200);
|
|
353
|
+
const up = JSON.parse(resp.bodyBuffer.toString("utf8"));
|
|
354
|
+
assert.equal(up.method, "POST");
|
|
355
|
+
assert.equal(up.url, "/api/echo?b=2");
|
|
356
|
+
assert.equal(up.auth, "secret-value");
|
|
357
|
+
assert.equal(up.body, bodyPayload);
|
|
358
|
+
// resp 内头部:content-encoding 已被移入 enc(无则空),content-type 保留
|
|
359
|
+
assert.equal(resp.headers["content-type"], "application/json; charset=utf-8");
|
|
360
|
+
|
|
361
|
+
// 2) 篡改密文 → 502 + x-dsh-e2ee-error,不开包即失败、不回退明文
|
|
362
|
+
const envBad = client.seal({ kind: "http", dir: "p2b", counter: 2, data: plain });
|
|
363
|
+
const buf = Buffer.from(envBad.d, "base64url");
|
|
364
|
+
buf[5] ^= 0x55;
|
|
365
|
+
envBad.d = buf.toString("base64url");
|
|
366
|
+
const bad = await fetch(`${routerBase}/remote/${DEV_ON}/api/echo`, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers: { ...envelopeRequestHeaders(client.sessId, "http"), cookie: COOKIE },
|
|
369
|
+
body: JSON.stringify(envBad)
|
|
370
|
+
});
|
|
371
|
+
assert.equal(bad.status, 502);
|
|
372
|
+
assert.equal(bad.headers.get("x-dsh-e2ee-error"), "auth_failed");
|
|
373
|
+
const badText = await bad.text();
|
|
374
|
+
assert.match(badText, /⚠/);
|
|
375
|
+
assert.match(badText, /auth_failed|无法解密/);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("E2EE WS 数据流:&e2ee=&w= 标记 → 桥剥除标记连上游,逐消息加解密 echo 一致", async () => {
|
|
379
|
+
const u = globalThis.__unlocked;
|
|
380
|
+
assert.ok(u, "需要先解锁");
|
|
381
|
+
const { client, sessId } = u;
|
|
382
|
+
const w = randomHex(8);
|
|
383
|
+
const wsUrl = `${routerBase}/remote/${DEV_ON}/ws-echo?e2ee=${sessId}&w=${w}`;
|
|
384
|
+
const wsUrlObj = new URL(wsUrl);
|
|
385
|
+
// bridge 收到的 ws-open 帧 path 是 router 剥离 /remote/<dev> 后的上游路径(§6.2/§4.6)
|
|
386
|
+
const bridgePath = wsUrlObj.pathname.replace(/^\/remote\/[^/]+/, "") + wsUrlObj.search;
|
|
387
|
+
const parsed = parseWsE2eeParams(bridgePath);
|
|
388
|
+
assert.equal(parsed.sessId, sessId);
|
|
389
|
+
assert.equal(parsed.wsLabel, `/ws-echo?w=${w}`);
|
|
390
|
+
assert.equal(parsed.upstreamPath, "/ws-echo");
|
|
391
|
+
|
|
392
|
+
const data = new WebSocket(wsUrl.replace(/^http/, "ws"), { headers: { cookie: COOKIE } });
|
|
393
|
+
try {
|
|
394
|
+
const echoed = await new Promise((resolve, reject) => {
|
|
395
|
+
const t = setTimeout(() => reject(new Error("ws e2ee echo 超时")), 10000);
|
|
396
|
+
data.on("open", () => {
|
|
397
|
+
// 手机 → bridge:密封 w 信封(文本)
|
|
398
|
+
const env = client.seal({ kind: "w", dir: "p2b", counter: 0, data: "ping-e2ee-你好", wsLabel: parsed.wsLabel });
|
|
399
|
+
data.send(JSON.stringify(env));
|
|
400
|
+
});
|
|
401
|
+
data.on("message", (raw) => {
|
|
402
|
+
try {
|
|
403
|
+
const env = JSON.parse(String(raw));
|
|
404
|
+
if (env && env.k === "w") {
|
|
405
|
+
const opened = client.open({ kind: "w", dir: "b2p", env, counter: env.c, wsLabel: parsed.wsLabel });
|
|
406
|
+
clearTimeout(t);
|
|
407
|
+
resolve(opened.data.toString("utf8"));
|
|
408
|
+
}
|
|
409
|
+
} catch (e) { clearTimeout(t); reject(e); }
|
|
410
|
+
});
|
|
411
|
+
data.on("close", (code, reason) => { clearTimeout(t); reject(new Error(`ws e2ee 提前关闭 code=${code} reason=${reason}`)); });
|
|
412
|
+
data.on("error", (e) => { clearTimeout(t); reject(new Error(`ws e2ee error: ${e.message}`)); });
|
|
413
|
+
});
|
|
414
|
+
assert.equal(echoed, "ping-e2ee-你好");
|
|
415
|
+
} finally {
|
|
416
|
+
try { data.close(1000); } catch {}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ============================================================
|
|
421
|
+
// Phase-3 手机端:用 native.html 里抽取的「浏览器 WebCrypto」实现
|
|
422
|
+
// (而非测试内复制)跑真实解锁 + HTTP 信封 + WS 数据流,验证手机端与桥端互通。
|
|
423
|
+
// ============================================================
|
|
424
|
+
|
|
425
|
+
/** 手机端(浏览器 WebCrypto)解锁:hello→ack→deriveShk→探针;密码错 → bad_key。 */
|
|
426
|
+
function wcUnlock(ctrlWsUrl, { mkBytes, params }) {
|
|
427
|
+
return new Promise((resolve, reject) => {
|
|
428
|
+
const sessId = wc.wcNewSessId();
|
|
429
|
+
const aB64 = wc.wcRandomB64(32);
|
|
430
|
+
const ctrl = new WebSocket(ctrlWsUrl, { headers: { cookie: COOKIE } });
|
|
431
|
+
let client = null;
|
|
432
|
+
let settled = false;
|
|
433
|
+
const done = (v) => { if (!settled) { settled = true; clearTimeout(timer); try { ctrl.close(1000); } catch {} resolve(v); } };
|
|
434
|
+
const fail = (e) => { if (!settled) { settled = true; clearTimeout(timer); try { ctrl.terminate(); } catch {} reject(e); } };
|
|
435
|
+
const timer = setTimeout(() => fail(new Error("wc 解锁握手超时")), 10000);
|
|
436
|
+
ctrl.on("error", (e) => fail(e));
|
|
437
|
+
ctrl.on("close", () => { if (!settled) fail(new Error("wc ctrl 提前关闭")); });
|
|
438
|
+
ctrl.on("open", () => {
|
|
439
|
+
ctrl.send(JSON.stringify({ v: 2, type: "e2ee-hello", role: "phone", s: sessId, a: aB64, salt: params.salt, profile: params.profile, ts: Date.now() }));
|
|
440
|
+
});
|
|
441
|
+
ctrl.on("message", (raw) => {
|
|
442
|
+
(async () => {
|
|
443
|
+
let msg;
|
|
444
|
+
try { msg = JSON.parse(String(raw)); } catch { return; }
|
|
445
|
+
if (!msg || typeof msg !== "object") return;
|
|
446
|
+
if (msg.type === "e2ee-error") return fail(new Error(`e2ee-error: ${msg.code} ${msg.message || ""}`));
|
|
447
|
+
if (msg.type === "e2ee-hello-ack" && !client) {
|
|
448
|
+
const { shk, saltH } = await wc.wcDeriveShk(mkBytes, aB64, msg.b);
|
|
449
|
+
client = new wc.WcE2eeSession({ sessId, shk, saltH, profile: params.profile, epoch: params.epoch });
|
|
450
|
+
const probe = await client.seal({ kind: "ctrl", dir: wc.WC_DIR_P2B, counter: 0, data: JSON.stringify({ p: "dsh-e2ee-probe-v1", t: Date.now(), c: 0 }) });
|
|
451
|
+
ctrl.send(JSON.stringify(probe));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (client && msg.v === 2 && msg.k === "ctrl") {
|
|
455
|
+
const opened = await client.open({ kind: "ctrl", dir: wc.WC_DIR_B2P, env: msg, counter: msg.c ?? 0 });
|
|
456
|
+
if (JSON.parse(Buffer.from(opened.data).toString("utf8")).p !== "dsh-e2ee-probe-ok") return fail(new Error("wc probe-ok 载荷不符"));
|
|
457
|
+
done({ ctrl, client, sessId });
|
|
458
|
+
}
|
|
459
|
+
})().catch(fail);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
let wc;
|
|
465
|
+
|
|
466
|
+
test("Phase-3 手机 WebCrypto(native.html 抽取)解锁+HTTP+WS 与桥端互通", async () => {
|
|
467
|
+
wc = await loadNativeWcCore();
|
|
468
|
+
// 手机拉取参数(与桥同一服务端)→ WebCrypto 派生 MK
|
|
469
|
+
const params = await fetchE2eeParams(apiBase, signJwt({ sub: "42", phone: "13811110001", plan: "free" }));
|
|
470
|
+
assert.equal(params.enabled, true);
|
|
471
|
+
const mk = new Uint8Array(deriveMasterKey(PASSWORD, params.salt, params.kdf));
|
|
472
|
+
assert.equal(Buffer.from(await wc.wcDeriveMasterKey(PASSWORD, params.salt, params.kdf)).toString("hex"), Buffer.from(mk).toString("hex"), "手机/桥 MK 一致");
|
|
473
|
+
|
|
474
|
+
// 1) 解锁握手(device 形态)
|
|
475
|
+
const { client, sessId } = await wcUnlock(`${routerBase}/remote/${DEV_ON}/_e2ee/ctrl`, { mkBytes: mk, params });
|
|
476
|
+
assert.equal(sessId.length, 32);
|
|
477
|
+
|
|
478
|
+
// 2) 密码错误 → e2ee-error bad_key(可读文案路径)
|
|
479
|
+
const wrongMk = new Uint8Array(deriveMasterKey("wrong-password", params.salt, params.kdf));
|
|
480
|
+
await assert.rejects(() => wcUnlock(`${routerBase}/remote/${DEV_ON}/_e2ee/ctrl`, { mkBytes: wrongMk, params }), /bad_key|auth_failed/);
|
|
481
|
+
|
|
482
|
+
// 3) HTTP 信封往返(手机 seal → 桥解封 → 上游 → 封回 → 手机开包)
|
|
483
|
+
const bodyPayload = JSON.stringify({ hello: "phone-webcrypto", n: 7 });
|
|
484
|
+
const reqPlain = wc.wcEncodeHttpRequestPlain({
|
|
485
|
+
method: "POST",
|
|
486
|
+
path: "/api/echo?phone=1",
|
|
487
|
+
headers: { "content-type": "application/json; charset=utf-8", "x-dsh-test-auth": "phone-secret" },
|
|
488
|
+
bodyB64: Buffer.from(bodyPayload, "utf8").toString("base64")
|
|
489
|
+
});
|
|
490
|
+
const nBytes = new Uint8Array(Buffer.from("0102030405060708090a0b0c", "hex"));
|
|
491
|
+
const envReq = await client.seal({ kind: "http", dir: wc.WC_DIR_P2B, counter: 0, data: reqPlain, nonceBytes: nBytes });
|
|
492
|
+
const res = await fetch(`${routerBase}/remote/${DEV_ON}/api/echo?phone=1`, {
|
|
493
|
+
method: "POST",
|
|
494
|
+
headers: { ...envelopeRequestHeaders(sessId, "http"), cookie: COOKIE },
|
|
495
|
+
body: JSON.stringify(envReq)
|
|
496
|
+
});
|
|
497
|
+
assert.equal(res.status, 200);
|
|
498
|
+
assert.ok((res.headers.get("content-type") || "").startsWith("application/vnd.dsh.e2ee-v2"));
|
|
499
|
+
const envResp = JSON.parse(await res.text());
|
|
500
|
+
const opened = await client.open({ kind: "http-resp", dir: wc.WC_DIR_B2P, env: envResp, counter: envResp.c ?? 0, reqNonceB64: envReq.n });
|
|
501
|
+
const rp = wc.wcDecodeHttpResponsePlain(opened.data);
|
|
502
|
+
assert.equal(rp.status, 200);
|
|
503
|
+
const up = JSON.parse(Buffer.from(rp.bodyB64, "base64").toString("utf8"));
|
|
504
|
+
assert.equal(up.method, "POST");
|
|
505
|
+
assert.equal(up.url, "/api/echo?phone=1");
|
|
506
|
+
assert.equal(up.auth, "phone-secret");
|
|
507
|
+
assert.equal(up.body, bodyPayload);
|
|
508
|
+
|
|
509
|
+
// 4) WS 数据流:&e2ee=&w=(w=8B hex=16 字符);逐消息 w 信封 echo 一致
|
|
510
|
+
const w16 = wc.wcRandomHex(8);
|
|
511
|
+
const wsUrl = `${routerBase}/remote/${DEV_ON}/ws-echo?e2ee=${sessId}&w=${w16}`;
|
|
512
|
+
const wsUrlObj = new URL(wsUrl);
|
|
513
|
+
const bridgePath = wsUrlObj.pathname.replace(/^\/remote\/[^/]+/, "") + wsUrlObj.search;
|
|
514
|
+
const parsed = wc.wcParseWsE2eeParams(bridgePath);
|
|
515
|
+
assert.equal(parsed.sessId, sessId);
|
|
516
|
+
assert.equal(parsed.w, w16);
|
|
517
|
+
assert.equal(parsed.wsLabel, `/ws-echo?w=${w16}`);
|
|
518
|
+
|
|
519
|
+
const data = new WebSocket(wsUrl.replace(/^http/, "ws"), { headers: { cookie: COOKIE } });
|
|
520
|
+
try {
|
|
521
|
+
const echoed = await new Promise((resolve, reject) => {
|
|
522
|
+
const t = setTimeout(() => reject(new Error("wc ws echo 超时")), 10000);
|
|
523
|
+
data.on("open", async () => {
|
|
524
|
+
const env = await client.seal({ kind: "w", dir: wc.WC_DIR_P2B, counter: 0, data: "ping-e2ee-phone-你好", wsLabel: parsed.wsLabel });
|
|
525
|
+
data.send(JSON.stringify(env));
|
|
526
|
+
});
|
|
527
|
+
data.on("message", async (raw) => {
|
|
528
|
+
try {
|
|
529
|
+
const env = JSON.parse(String(raw));
|
|
530
|
+
if (env && env.k === "w") {
|
|
531
|
+
const openedMsg = await client.open({ kind: "w", dir: wc.WC_DIR_B2P, env, counter: env.c, wsLabel: parsed.wsLabel });
|
|
532
|
+
clearTimeout(t);
|
|
533
|
+
resolve(Buffer.from(openedMsg.data).toString("utf8"));
|
|
534
|
+
}
|
|
535
|
+
} catch (e) { clearTimeout(t); reject(e); }
|
|
536
|
+
});
|
|
537
|
+
data.on("close", (code, reason) => { clearTimeout(t); reject(new Error(`wc ws 提前关闭 code=${code} reason=${reason}`)); });
|
|
538
|
+
data.on("error", (e) => { clearTimeout(t); reject(new Error(`wc ws error: ${e.message}`)); });
|
|
539
|
+
});
|
|
540
|
+
assert.equal(echoed, "ping-e2ee-phone-你好");
|
|
541
|
+
} finally {
|
|
542
|
+
try { data.close(1000); } catch {}
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
// ============================================================
|
|
548
|
+
// Phase-4 镜像页 shim:交接单重建会话 → 真实本地 router+bridge 互通端到端
|
|
549
|
+
// (注入开关 / 灰度关闭零注入 / HTTP 信封 / WS 数据流,全部走真实进程)
|
|
550
|
+
// ============================================================
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* 把 'ws' 包实例适配成浏览器式 WebSocket 表面(供镜像 shim 的 SeE2eeWs 内层使用)。
|
|
554
|
+
* 浏览器中 Cookie 由同源自动携带;node 测试无 cookie jar → 适配器显式带 COOKIE 头。
|
|
555
|
+
*/
|
|
556
|
+
class WsPkgAdapter {
|
|
557
|
+
constructor(url) {
|
|
558
|
+
this.url = url;
|
|
559
|
+
this.readyState = 0;
|
|
560
|
+
this._ws = new WebSocket(url, { headers: { cookie: COOKIE } });
|
|
561
|
+
this.protocol = ""; this.extensions = ""; this.bufferedAmount = 0; this.binaryType = "blob";
|
|
562
|
+
const self = this;
|
|
563
|
+
this._ws.on("open", () => { self.readyState = 1; if (self.onopen) self.onopen({ type: "open", target: self }); });
|
|
564
|
+
this._ws.on("message", (d, isBinary) => { if (self.onmessage) self.onmessage({ type: "message", data: isBinary ? d : String(d), target: self }); });
|
|
565
|
+
this._ws.on("close", (code, reason) => {
|
|
566
|
+
self.readyState = 3;
|
|
567
|
+
if (self.onclose) self.onclose({ type: "close", code, reason: String(reason || ""), target: self });
|
|
568
|
+
});
|
|
569
|
+
this._ws.on("error", (e) => { if (self.onerror) self.onerror({ type: "error", message: e && e.message, target: self }); });
|
|
570
|
+
}
|
|
571
|
+
send(data) { this._ws.send(data); }
|
|
572
|
+
close(code, reason) { try { this._ws.close(code, reason); } catch (e) { /* ignore */ } }
|
|
573
|
+
addEventListener() {}
|
|
574
|
+
removeEventListener() {}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
test("Phase-4 镜像页 shim:注入+交接单重建+HTTP 信封/WS 真实互通;灰度关闭零注入", async () => {
|
|
578
|
+
const [wcMod, shimMod, nativeHo] = await Promise.all([loadNativeWcCore(), loadShimCore(), loadNativeHandoverCore()]);
|
|
579
|
+
const params = await fetchE2eeParams(apiBase, signJwt({ sub: "42", phone: "13811110001", plan: "free" }));
|
|
580
|
+
assert.equal(params.enabled, true);
|
|
581
|
+
const mk = new Uint8Array(deriveMasterKey(PASSWORD, params.salt, params.kdf));
|
|
582
|
+
// 手机端(浏览器 WebCrypto)解锁 → 已与桥端探针互认的会话
|
|
583
|
+
const { client, sessId } = await wcUnlock(`${routerBase}/remote/${DEV_ON}/_e2ee/ctrl`, { mkBytes: mk, params });
|
|
584
|
+
assert.equal(sessId.length, 32);
|
|
585
|
+
|
|
586
|
+
// 1) 交接单:native.html 写入函数(抽取真实代码)→ 镜像 shim 读取函数重建会话(字节一致)
|
|
587
|
+
const handoverRaw = nativeHo.dshE2eeHandoverEncode(client);
|
|
588
|
+
assert.ok(handoverRaw, "native 应能编码交接单");
|
|
589
|
+
const sess = shimMod.seSessionOfHandover(handoverRaw);
|
|
590
|
+
assert.ok(sess, "镜像 shim 应能解码交接单重建会话");
|
|
591
|
+
assert.equal(sess.sessId, sessId);
|
|
592
|
+
assert.equal(Buffer.from(sess.shk).toString("hex"), Buffer.from(client.shk).toString("hex"));
|
|
593
|
+
|
|
594
|
+
// 2) 注入:e2ee 启用的桥端 → 官方 html 响应含 shim;e2ee 关闭桥端(灰度默认关)→ 零注入
|
|
595
|
+
const docRes = await fetch(`${routerBase}/remote/${DEV_ON}/official`, { headers: { cookie: COOKIE } });
|
|
596
|
+
assert.equal(docRes.status, 200);
|
|
597
|
+
const docHtml = await docRes.text();
|
|
598
|
+
assert.ok(docHtml.includes("data-dsh-e2ee-shim"), "e2ee 启用的桥端应注入镜像 shim");
|
|
599
|
+
assert.ok(docHtml.includes("dsh-e2ee-badge"));
|
|
600
|
+
assert.ok(docHtml.includes("data-dsh-mobile-adapter"), "mobile-adapter 照常注入");
|
|
601
|
+
const offHtml = await (await fetch(`${routerBase}/remote/${DEV_OFF}/official`, { headers: { cookie: COOKIE } })).text();
|
|
602
|
+
assert.ok(!offHtml.includes("data-dsh-e2ee-shim"), "e2ee 关闭(灰度关)桥端不注入 shim");
|
|
603
|
+
assert.ok(offHtml.includes("data-dsh-mobile-adapter"));
|
|
604
|
+
|
|
605
|
+
// 3) HTTP 信封:镜像 shim 的 fetch 包装 → 真实 router+bridge → 上游 html(桥内先注入后加密)
|
|
606
|
+
const ctx = { sess, origin: routerBase };
|
|
607
|
+
const wrappedRes = await shimMod.seFetchWrapper(
|
|
608
|
+
fetch.bind(globalThis), ctx,
|
|
609
|
+
`${routerBase}/remote/${DEV_ON}/api/official?phase4=1`,
|
|
610
|
+
{ method: "GET", headers: { accept: "text/html", cookie: COOKIE } }
|
|
611
|
+
);
|
|
612
|
+
assert.equal(wrappedRes.status, 200);
|
|
613
|
+
const encBody = await wrappedRes.text();
|
|
614
|
+
assert.ok(encBody.includes("data-dsh-e2ee-shim"), "信封解密出的 html 应含注入 shim(mobile-adapter 同理)");
|
|
615
|
+
assert.ok(encBody.includes("data-dsh-mobile-adapter"));
|
|
616
|
+
|
|
617
|
+
// 4) 数据 WS:镜像 shim 的 SeE2eeWs 代理 → 真实 router+bridge → 上游 /api/events.mux 回显
|
|
618
|
+
const patchedWs = shimMod.seMakeWsCtor(WsPkgAdapter, { sess, origin: routerBase });
|
|
619
|
+
const dataWs = patchedWs(`${routerBase.replace(/^http/, "ws")}/remote/${DEV_ON}/api/events.mux?x=1`);
|
|
620
|
+
assert.ok(dataWs instanceof shimMod.SeE2eeWs, "受保护 /api 数据 WS 应套加密代理");
|
|
621
|
+
const echoed = await new Promise((resolve, reject) => {
|
|
622
|
+
const t = setTimeout(() => reject(new Error("shim ws echo 超时")), 12000);
|
|
623
|
+
dataWs.onerror = (e) => { clearTimeout(t); reject(new Error("shim ws error: " + ((e && e.message) || ""))); };
|
|
624
|
+
dataWs.onmessage = (ev) => { clearTimeout(t); resolve(ev.data); };
|
|
625
|
+
dataWs.onopen = () => dataWs.send("ping-shim-phase4-你好");
|
|
626
|
+
dataWs.onclose = (ev) => { if (!ev || ev.code !== 1000) { clearTimeout(t); reject(new Error("shim ws 提前关闭 code=" + (ev && ev.code) + " reason=" + (ev && ev.reason))); } };
|
|
627
|
+
});
|
|
628
|
+
assert.equal(echoed, "ping-shim-phase4-你好");
|
|
629
|
+
try { dataWs.close(1000); } catch { /* ignore */ }
|
|
630
|
+
});
|