rdsh-gateway 0.11.1 → 0.11.3
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/direct.js +3 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/join.d.ts +10 -0
- package/dist/join.js +53 -24
- package/package.json +1 -1
package/dist/direct.js
CHANGED
|
@@ -29,6 +29,9 @@ export async function startDirect(opts) {
|
|
|
29
29
|
actualPort,
|
|
30
30
|
candidates: () => lanCandidates(actualPort),
|
|
31
31
|
stop: async () => {
|
|
32
|
+
// 先强制断开所有连接(含 keep-alive):否则有活跃连接时 server.close() 的回调
|
|
33
|
+
// 永不触发,面板「断开」的 RPC 会一直挂住、按钮卡在 loading。
|
|
34
|
+
gateway.server.closeAllConnections();
|
|
32
35
|
await new Promise((resolve) => gateway.server.close(() => resolve()));
|
|
33
36
|
gateway.dispose();
|
|
34
37
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -23,13 +23,14 @@ export { loginPageHtml } from "./login-page.ts";
|
|
|
23
23
|
export { installService, uninstallService, serviceStatus, startService, stopService, restartService, systemdUnit, launchdPlist, SERVICE_NAME, JOIN_SERVICE_NAME, HOST_SERVICE_NAME, HUB_SERVICE_NAME } from "./service.ts";
|
|
24
24
|
export type { ServiceSpec } from "./service.ts";
|
|
25
25
|
export declare const NAME = "rdsh-gateway";
|
|
26
|
-
export { join, startJoin, registerJoin, detectInsecure, selfRevoke, patchLoopbackJs } from "./join.ts";
|
|
26
|
+
export { join, startJoin, registerJoin, detectInsecure, selfRevoke, patchLoopbackJs, hubRequest } from "./join.ts";
|
|
27
27
|
export type { JoinOptions, RegisterOutcome, JoinState, JoinHooks, StartJoinOptions, JoinHandle } from "./join.ts";
|
|
28
28
|
export { startDirect, lanCandidates, isPrivateIpv4 } from "./direct.ts";
|
|
29
29
|
export type { DirectOptions, DirectHandle } from "./direct.ts";
|
|
30
30
|
export { createDirectTicketManager } from "./direct-ticket.ts";
|
|
31
31
|
export type { DirectTicketManager } from "./direct-ticket.ts";
|
|
32
32
|
export { loadOrCreateDirectSecret, directSecretPath } from "./direct-secret.ts";
|
|
33
|
-
export { readPersistedToken, clearPersistedToken } from "./token-store.ts";
|
|
33
|
+
export { readPersistedToken, persistToken, clearPersistedToken } from "./token-store.ts";
|
|
34
34
|
export { acquireJoinLock, releaseJoinLock, readJoinLock, JOIN_LOCK_PATH } from "./lock.ts";
|
|
35
35
|
export type { JoinLock, JoinLockRole } from "./lock.ts";
|
|
36
|
+
export { loadOrCreateE2eeKeyPair, e2eeKeyFilePath } from "./e2ee-key-store.ts";
|
package/dist/index.js
CHANGED
|
@@ -15,10 +15,11 @@ export { loadTls } from "./tls.js";
|
|
|
15
15
|
export { loginPageHtml } from "./login-page.js";
|
|
16
16
|
export { installService, uninstallService, serviceStatus, startService, stopService, restartService, systemdUnit, launchdPlist, SERVICE_NAME, JOIN_SERVICE_NAME, HOST_SERVICE_NAME, HUB_SERVICE_NAME } from "./service.js";
|
|
17
17
|
export const NAME = "rdsh-gateway";
|
|
18
|
-
export { join, startJoin, registerJoin, detectInsecure, selfRevoke, patchLoopbackJs } from "./join.js";
|
|
18
|
+
export { join, startJoin, registerJoin, detectInsecure, selfRevoke, patchLoopbackJs, hubRequest } from "./join.js";
|
|
19
19
|
export { startDirect, lanCandidates, isPrivateIpv4 } from "./direct.js";
|
|
20
20
|
export { createDirectTicketManager } from "./direct-ticket.js";
|
|
21
21
|
export { loadOrCreateDirectSecret, directSecretPath } from "./direct-secret.js";
|
|
22
|
-
export { readPersistedToken, clearPersistedToken } from "./token-store.js";
|
|
22
|
+
export { readPersistedToken, persistToken, clearPersistedToken } from "./token-store.js";
|
|
23
23
|
export { acquireJoinLock, releaseJoinLock, readJoinLock, JOIN_LOCK_PATH } from "./lock.js";
|
|
24
|
+
export { loadOrCreateE2eeKeyPair, e2eeKeyFilePath } from "./e2ee-key-store.js";
|
|
24
25
|
//# sourceMappingURL=index.js.map
|
package/dist/join.d.ts
CHANGED
|
@@ -111,6 +111,16 @@ export interface JoinHandle {
|
|
|
111
111
|
}
|
|
112
112
|
/** 探测 hub 是否需 insecure:以严格校验握手一次;证书错误 → true(需 insecure)。 */
|
|
113
113
|
export declare function detectInsecure(hubUrl: string): Promise<boolean>;
|
|
114
|
+
/** hub HTTP 调用(node:https 支持自签跳过校验 —— undici fetch 不受 NODE_TLS_REJECT_UNAUTHORIZED 影响)。 */
|
|
115
|
+
export declare function hubRequest(baseUrl: string, path: string, opts: {
|
|
116
|
+
method: string;
|
|
117
|
+
insecure: boolean;
|
|
118
|
+
body?: unknown;
|
|
119
|
+
}): Promise<{
|
|
120
|
+
ok: boolean;
|
|
121
|
+
status: number;
|
|
122
|
+
body: Record<string, unknown>;
|
|
123
|
+
}>;
|
|
114
124
|
/** 调用 hub self-revoke 注销本机(host 持自己的 host token)。`rdsh host leave` 使用。 */
|
|
115
125
|
export declare function selfRevoke(hubUrl: string, token: string, insecure: boolean): Promise<void>;
|
|
116
126
|
/** 解析 host token(--token 注册 > 持久化复用)+ 自动检测证书;供 CLI 配置命令与 join() 复用。 */
|
package/dist/join.js
CHANGED
|
@@ -64,7 +64,7 @@ const PONG_TIMEOUT_MS = 10_000;
|
|
|
64
64
|
const RECONNECT_BASE_MS = 1_000;
|
|
65
65
|
const RECONNECT_MAX_MS = 60_000;
|
|
66
66
|
/** hub HTTP 调用(node:https 支持自签跳过校验 —— undici fetch 不受 NODE_TLS_REJECT_UNAUTHORIZED 影响)。 */
|
|
67
|
-
function hubRequest(baseUrl, path, opts) {
|
|
67
|
+
export function hubRequest(baseUrl, path, opts) {
|
|
68
68
|
const url = new URL(baseUrl + path);
|
|
69
69
|
const lib = url.protocol === "https:" ? httpsRequest : httpRequest;
|
|
70
70
|
return new Promise((resolve, reject) => {
|
|
@@ -481,27 +481,19 @@ export function startJoin(opts) {
|
|
|
481
481
|
send(encodeFrame(FRAME_TYPE.ERROR, frame.streamId, jsonPayload({ code: "BAD_OPEN", message: "malformed open" })));
|
|
482
482
|
return;
|
|
483
483
|
}
|
|
484
|
-
// ---- raw
|
|
485
|
-
if (dio?.rawGate !== undefined) {
|
|
486
|
-
//
|
|
487
|
-
if (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
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;
|
|
495
493
|
}
|
|
496
|
-
send(encodeFrame(FRAME_TYPE.CLOSE, frame.streamId, jsonPayload({ code: 403, message: "raw stream not authorized" })));
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
// 候选 + 一次性直连票(无论是否设口令都提供;经 E2EE,hub 不可见,R3/R4)
|
|
500
|
-
if (path === "/__rdsh/direct-candidates" || path.startsWith("/__rdsh/direct-candidates?")) {
|
|
501
|
-
const body = Buffer.from(JSON.stringify({ candidates: dio.rawGate.candidates(), ticket: dio.rawGate.mintTicket() }));
|
|
502
|
-
sendSyntheticHttp(send, frame.streamId, 200, { "content-type": "application/json" }, body);
|
|
503
|
-
return;
|
|
504
494
|
}
|
|
495
|
+
send(encodeFrame(FRAME_TYPE.CLOSE, frame.streamId, jsonPayload({ code: 403, message: "raw stream not authorized" })));
|
|
496
|
+
return;
|
|
505
497
|
}
|
|
506
498
|
// ---- 访问口令 gate(仅 plain dispatcher:dio.gate=true 且已设 accessCode)----
|
|
507
499
|
if (dio?.gate === true && gate.accessCode !== null) {
|
|
@@ -525,6 +517,21 @@ export function startJoin(opts) {
|
|
|
525
517
|
return;
|
|
526
518
|
}
|
|
527
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
|
+
}
|
|
528
535
|
if (kind === "ws") {
|
|
529
536
|
openWsStream(frame.streamId, path, headers);
|
|
530
537
|
return;
|
|
@@ -563,7 +570,11 @@ export function startJoin(opts) {
|
|
|
563
570
|
delete outHeaders["transfer-encoding"];
|
|
564
571
|
if (!adapterInjectionLogged) {
|
|
565
572
|
adapterInjectionLogged = true;
|
|
566
|
-
|
|
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)");
|
|
567
578
|
}
|
|
568
579
|
send(encodeFrame(FRAME_TYPE.OPEN, streamId, jsonPayload({ kind: "http", status, reason: upRes.statusMessage, headers: outHeaders })));
|
|
569
580
|
sendChunkedBody(send, streamId, Buffer.from(html));
|
|
@@ -731,6 +742,8 @@ export function startJoin(opts) {
|
|
|
731
742
|
const plainDispatcher = makeInnerDispatcher(sendTunnelFrame, {
|
|
732
743
|
jsPatch: () => uiCompat.trustE2EEAsLoopback,
|
|
733
744
|
gate: true,
|
|
745
|
+
// plain 页也要能取直连候选:它同样注入了直连 bootstrap,端点由下方统一提供
|
|
746
|
+
directCandidates: rawGate,
|
|
734
747
|
htmlInject: () => {
|
|
735
748
|
// 直连 bootstrap 始终注入(join 模式);页面授权 token 仅设口令时生成(无口令则跳过 raw 授权、只取候选)
|
|
736
749
|
return RDSH_WEBVIEW_API + pageAuthorizeScript(gate.accessCode);
|
|
@@ -770,6 +783,7 @@ export function startJoin(opts) {
|
|
|
770
783
|
}, {
|
|
771
784
|
jsPatch: () => uiCompat.trustE2EEAsLoopback,
|
|
772
785
|
rawGate,
|
|
786
|
+
directCandidates: rawGate,
|
|
773
787
|
// raw 门禁的解锁脚本:与 plain 同一来源(pageAuthorizeScript)。
|
|
774
788
|
// raw 流不转发 cookie,这里是页面唯一的授权入口;缺了它 raw 门禁必然 fail-closed。
|
|
775
789
|
htmlInject: () => pageAuthorizeScript(gate.accessCode),
|
|
@@ -940,8 +954,15 @@ export function startJoin(opts) {
|
|
|
940
954
|
client.terminate();
|
|
941
955
|
return;
|
|
942
956
|
}
|
|
943
|
-
for (const frame of frames)
|
|
944
|
-
|
|
957
|
+
for (const frame of frames) {
|
|
958
|
+
// 单帧处理异常不能崩宿主进程:对端(含旧版客户端)可能发来不完整/不兼容的帧。
|
|
959
|
+
try {
|
|
960
|
+
handleFrame(frame);
|
|
961
|
+
}
|
|
962
|
+
catch (err) {
|
|
963
|
+
log("error", `frame handling failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
945
966
|
});
|
|
946
967
|
client.on("close", () => {
|
|
947
968
|
cleanupStreams();
|
|
@@ -960,7 +981,15 @@ export function startJoin(opts) {
|
|
|
960
981
|
}
|
|
961
982
|
setState("reconnecting", { delayMs: reconnectDelay });
|
|
962
983
|
log("info", `tunnel lost — reconnecting in ${Math.round(reconnectDelay / 1000)}s...`);
|
|
963
|
-
|
|
984
|
+
// 定时器里的异常不会被上层捕获 —— 必须自己兜住,否则一次重连失败就崩掉宿主进程。
|
|
985
|
+
setTimeout(() => {
|
|
986
|
+
try {
|
|
987
|
+
connect();
|
|
988
|
+
}
|
|
989
|
+
catch (err) {
|
|
990
|
+
log("error", `reconnect failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
991
|
+
}
|
|
992
|
+
}, reconnectDelay + Math.random() * 500);
|
|
964
993
|
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
|
|
965
994
|
});
|
|
966
995
|
client.on("error", () => {
|