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 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 门禁 + 候选端点(仅 raw inner dispatcher:dio.rawGate 定义时)----
485
- if (dio?.rawGate !== undefined) {
486
- // raw 门禁(仅设口令时):授权前只接受 authorize 请求,否则 fail-closed 关流(修 raw 绕过,R7)
487
- if (gate.accessCode !== null && !rawAuthorized) {
488
- if (path.startsWith("/__rdsh/authorize")) {
489
- const token = new URL(path, "http://rdsh.local").searchParams.get("token") ?? "";
490
- if (token !== "" && verifyGateCookie(gate.accessCode, token)) {
491
- rawAuthorized = true;
492
- sendSyntheticHttp(send, frame.streamId, 204, {}, Buffer.alloc(0));
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
- console.log("[rdsh] rdsh webview api injected (window.__rdshWebViewApi v1)");
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
- handleFrame(frame);
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
- setTimeout(connect, reconnectDelay + Math.random() * 500);
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", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rdsh-gateway",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
4
4
  "description": "rdsh host-side component: LAN auth gateway + outbound tunnel endpoint (spawns dsh web)",
5
5
  "license": "MIT",
6
6
  "type": "module",