rdsh-gateway 0.11.0 → 0.11.2
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/dist/join.d.ts +4 -0
- package/dist/join.js +49 -24
- package/package.json +1 -1
package/dist/join.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface JoinOptions {
|
|
|
36
36
|
host?: string;
|
|
37
37
|
port?: number;
|
|
38
38
|
};
|
|
39
|
+
/** host E2EE 静态密钥对目录(缺省 ~/.rdsh;**仅供测试注入**,生产不要传 —— 见 e2ee-key-store.ts) */
|
|
40
|
+
e2eeKeyDir?: string;
|
|
39
41
|
}
|
|
40
42
|
/** 注册/接入结果:解析出的 host token + 是否需 insecure + 生效的主机名(缺省=机器 hostname)。 */
|
|
41
43
|
export interface RegisterOutcome {
|
|
@@ -65,6 +67,8 @@ export interface StartJoinOptions {
|
|
|
65
67
|
role: JoinLockRole;
|
|
66
68
|
/** 锁文件路径(缺省 ~/.rdsh/join.lock;测试可注入临时路径) */
|
|
67
69
|
lockPath?: string;
|
|
70
|
+
/** E2EE 静态密钥对目录(缺省 ~/.rdsh;**仅供测试注入**,见 e2ee-key-store.ts) */
|
|
71
|
+
e2eeKeyDir?: string;
|
|
68
72
|
hooks?: JoinHooks;
|
|
69
73
|
/** DSH UI 兼容(缺省 trustE2EEAsLoopback=true;false 关闭 JS patch) */
|
|
70
74
|
dshUiCompat?: {
|
package/dist/join.js
CHANGED
|
@@ -110,7 +110,7 @@ export async function registerJoin(opts) {
|
|
|
110
110
|
let token;
|
|
111
111
|
if (opts.token !== undefined) {
|
|
112
112
|
// --token = join token(或旧 host token)→ register 端点换 host token
|
|
113
|
-
const e2eeKeyPair = loadOrCreateE2eeKeyPair();
|
|
113
|
+
const e2eeKeyPair = loadOrCreateE2eeKeyPair(opts.e2eeKeyDir);
|
|
114
114
|
const { hostToken } = await register(opts.hubUrl, opts.token, name, insecure, e2eeKeyPair.publicRaw.toString("base64url"));
|
|
115
115
|
token = hostToken;
|
|
116
116
|
persistToken(opts.hubUrl, token);
|
|
@@ -273,6 +273,19 @@ export function startJoin(opts) {
|
|
|
273
273
|
const uiCompat = { trustE2EEAsLoopback: opts.dshUiCompat?.trustE2EEAsLoopback !== false };
|
|
274
274
|
// 访问口令(feature 15):可变引用 → setAccessCode 运行中切换;null = gate off
|
|
275
275
|
const gate = { accessCode: opts.gateway?.accessCode ?? null };
|
|
276
|
+
/**
|
|
277
|
+
* raw 门禁:页面是否已授权。**实例级共享**(原为 per-raw-stream)。
|
|
278
|
+
*
|
|
279
|
+
* authorize 证明的是"**页面**知道口令",不是"某条 raw 流知道"——而页面注入脚本每页只跑一次
|
|
280
|
+
* (`direct.ts` 的 `__rdshDirectBootstrapped` 守卫)。原先声明在 `makeInnerDispatcher()` 内,
|
|
281
|
+
* 该方法**每条 raw 流都会调用一次**(`startRawStream`),于是新建流标志被重置为 false、
|
|
282
|
+
* 页面又不会重新 authorize ⇒ 该流上所有请求被 CLOSE 403 `raw stream not authorized`。
|
|
283
|
+
*
|
|
284
|
+
* 放在 `startJoin` 作用域 = **每个网关进程一份**:同一进程内共享(**含隧道重连**——`connect()`
|
|
285
|
+
* 嵌在 `startJoin` 内、重连不重建该作用域),跨进程仍隔离。
|
|
286
|
+
* 2026-10-04 修复,详见 `doc/fix/20261004-raw-authorize-per-stream/`。
|
|
287
|
+
*/
|
|
288
|
+
let rawAuthorized = false;
|
|
276
289
|
// raw 门禁 + 候选(24-direct-first):提供 direct 回调即启用候选端点;raw 门禁(authorize)仅设口令时生效
|
|
277
290
|
const rawGate = opts.direct !== undefined
|
|
278
291
|
? { candidates: opts.direct.candidates, mintTicket: opts.direct.mintTicket }
|
|
@@ -338,8 +351,6 @@ export function startJoin(opts) {
|
|
|
338
351
|
const patchSkipped = new Set();
|
|
339
352
|
/** rdsh WebView API 是否已注入过(每个进程只打一次日志,避免页面加载刷屏)。 */
|
|
340
353
|
let adapterInjectionLogged = false;
|
|
341
|
-
/** raw 门禁:当前 raw 流是否已被页面授权(动态判定:见 handleOpen 的条件,含运行中 gate 开关) */
|
|
342
|
-
let rawAuthorized = false;
|
|
343
354
|
/** 从转发头里取 rdsh_gate cookie(hub D12 白名单透传)。 */
|
|
344
355
|
function gateCookie(headers) {
|
|
345
356
|
const ck = headers["cookie"];
|
|
@@ -470,27 +481,19 @@ export function startJoin(opts) {
|
|
|
470
481
|
send(encodeFrame(FRAME_TYPE.ERROR, frame.streamId, jsonPayload({ code: "BAD_OPEN", message: "malformed open" })));
|
|
471
482
|
return;
|
|
472
483
|
}
|
|
473
|
-
// ---- raw
|
|
474
|
-
if (dio?.rawGate !== undefined) {
|
|
475
|
-
//
|
|
476
|
-
if (
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
+
// ---- raw 门禁(仅 raw inner dispatcher:dio.rawGate 定义时)----
|
|
485
|
+
if (dio?.rawGate !== undefined && gate.accessCode !== null && !rawAuthorized) {
|
|
486
|
+
// 授权前只接受 authorize 请求,否则 fail-closed 关流(修 raw 绕过,R7)
|
|
487
|
+
if (path.startsWith("/__rdsh/authorize")) {
|
|
488
|
+
const token = new URL(path, "http://rdsh.local").searchParams.get("token") ?? "";
|
|
489
|
+
if (token !== "" && verifyGateCookie(gate.accessCode, token)) {
|
|
490
|
+
rawAuthorized = true;
|
|
491
|
+
sendSyntheticHttp(send, frame.streamId, 204, {}, Buffer.alloc(0));
|
|
492
|
+
return;
|
|
484
493
|
}
|
|
485
|
-
send(encodeFrame(FRAME_TYPE.CLOSE, frame.streamId, jsonPayload({ code: 403, message: "raw stream not authorized" })));
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
// 候选 + 一次性直连票(无论是否设口令都提供;经 E2EE,hub 不可见,R3/R4)
|
|
489
|
-
if (path === "/__rdsh/direct-candidates" || path.startsWith("/__rdsh/direct-candidates?")) {
|
|
490
|
-
const body = Buffer.from(JSON.stringify({ candidates: dio.rawGate.candidates(), ticket: dio.rawGate.mintTicket() }));
|
|
491
|
-
sendSyntheticHttp(send, frame.streamId, 200, { "content-type": "application/json" }, body);
|
|
492
|
-
return;
|
|
493
494
|
}
|
|
495
|
+
send(encodeFrame(FRAME_TYPE.CLOSE, frame.streamId, jsonPayload({ code: 403, message: "raw stream not authorized" })));
|
|
496
|
+
return;
|
|
494
497
|
}
|
|
495
498
|
// ---- 访问口令 gate(仅 plain dispatcher:dio.gate=true 且已设 accessCode)----
|
|
496
499
|
if (dio?.gate === true && gate.accessCode !== null) {
|
|
@@ -514,6 +517,21 @@ export function startJoin(opts) {
|
|
|
514
517
|
return;
|
|
515
518
|
}
|
|
516
519
|
}
|
|
520
|
+
// ---- 直连候选 + 一次性票(**raw 与 plain 两条路径都要提供**)----
|
|
521
|
+
// 位置在**两道门禁之后**:raw 已过 rawGate,plain 已过访问口令(未过则上面已 return),
|
|
522
|
+
// 因此既不会泄露内网候选地址、也不会在未授权时签发直连票(R3/R4/R7)。
|
|
523
|
+
// 为什么 plain 也要提供:plain 页同样注入了直连 bootstrap(见 plainDispatcher 的 htmlInject),
|
|
524
|
+
// 缺了这个端点,页面里的 fetch("/__rdsh/direct-candidates") 会被转发给 dsh web → 404,
|
|
525
|
+
// 于是 window.__rdshDirectInfo 永不赋值 —— 即 2026-10-04 真机 "no __rdshDirectInfo" 的根因。
|
|
526
|
+
if (dio?.directCandidates !== undefined &&
|
|
527
|
+
(path === "/__rdsh/direct-candidates" || path.startsWith("/__rdsh/direct-candidates?"))) {
|
|
528
|
+
const body = Buffer.from(JSON.stringify({
|
|
529
|
+
candidates: dio.directCandidates.candidates(),
|
|
530
|
+
ticket: dio.directCandidates.mintTicket(),
|
|
531
|
+
}));
|
|
532
|
+
sendSyntheticHttp(send, frame.streamId, 200, { "content-type": "application/json" }, body);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
517
535
|
if (kind === "ws") {
|
|
518
536
|
openWsStream(frame.streamId, path, headers);
|
|
519
537
|
return;
|
|
@@ -552,7 +570,11 @@ export function startJoin(opts) {
|
|
|
552
570
|
delete outHeaders["transfer-encoding"];
|
|
553
571
|
if (!adapterInjectionLogged) {
|
|
554
572
|
adapterInjectionLogged = true;
|
|
555
|
-
|
|
573
|
+
// 按**实际注入的内容**打日志:raw 路径只注入直连 bootstrap,不含 WebView API。
|
|
574
|
+
// (旧文案一律说 "webview api injected",在 raw 路径上是假的,曾误导排查。)
|
|
575
|
+
console.log(inject.includes("__rdshWebViewApi")
|
|
576
|
+
? "[rdsh] html inject: webview api + direct bootstrap (plain path)"
|
|
577
|
+
: "[rdsh] html inject: direct bootstrap only (raw/E2EE path)");
|
|
556
578
|
}
|
|
557
579
|
send(encodeFrame(FRAME_TYPE.OPEN, streamId, jsonPayload({ kind: "http", status, reason: upRes.statusMessage, headers: outHeaders })));
|
|
558
580
|
sendChunkedBody(send, streamId, Buffer.from(html));
|
|
@@ -720,6 +742,8 @@ export function startJoin(opts) {
|
|
|
720
742
|
const plainDispatcher = makeInnerDispatcher(sendTunnelFrame, {
|
|
721
743
|
jsPatch: () => uiCompat.trustE2EEAsLoopback,
|
|
722
744
|
gate: true,
|
|
745
|
+
// plain 页也要能取直连候选:它同样注入了直连 bootstrap,端点由下方统一提供
|
|
746
|
+
directCandidates: rawGate,
|
|
723
747
|
htmlInject: () => {
|
|
724
748
|
// 直连 bootstrap 始终注入(join 模式);页面授权 token 仅设口令时生成(无口令则跳过 raw 授权、只取候选)
|
|
725
749
|
return RDSH_WEBVIEW_API + pageAuthorizeScript(gate.accessCode);
|
|
@@ -728,7 +752,7 @@ export function startJoin(opts) {
|
|
|
728
752
|
// host 端 E2EE 静态密钥对(持久化 ~/.rdsh/e2ee-key.json;join 注册时上送指纹)
|
|
729
753
|
let hostE2eeKeypair;
|
|
730
754
|
try {
|
|
731
|
-
hostE2eeKeypair = loadOrCreateE2eeKeyPair();
|
|
755
|
+
hostE2eeKeypair = loadOrCreateE2eeKeyPair(opts.e2eeKeyDir);
|
|
732
756
|
}
|
|
733
757
|
catch (err) {
|
|
734
758
|
releaseLockAndRethrow(err);
|
|
@@ -759,6 +783,7 @@ export function startJoin(opts) {
|
|
|
759
783
|
}, {
|
|
760
784
|
jsPatch: () => uiCompat.trustE2EEAsLoopback,
|
|
761
785
|
rawGate,
|
|
786
|
+
directCandidates: rawGate,
|
|
762
787
|
// raw 门禁的解锁脚本:与 plain 同一来源(pageAuthorizeScript)。
|
|
763
788
|
// raw 流不转发 cookie,这里是页面唯一的授权入口;缺了它 raw 门禁必然 fail-closed。
|
|
764
789
|
htmlInject: () => pageAuthorizeScript(gate.accessCode),
|