rdsh-gateway 0.5.0 → 0.6.0

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/config.d.ts CHANGED
@@ -30,6 +30,13 @@ export interface RdshConfig {
30
30
  hub?: string;
31
31
  name?: string;
32
32
  insecure?: boolean;
33
+ /** DSH UI 兼容开关(跟随 E2EE;trustE2EEAsLoopback 默认 true) */
34
+ dshUiCompat?: DshUiCompat;
35
+ }
36
+ /** DSH UI 兼容:把经隧道访问的前端 isLoopback 判定视为 loopback,使 Models/设置持久化可用。 */
37
+ export interface DshUiCompat {
38
+ /** E2EE 激活(或宿主启用)时 patch JS;false = 保持 DSH 原样(共享 host/敏感场景) */
39
+ trustE2EEAsLoopback?: boolean;
33
40
  }
34
41
  export declare const DEFAULT_HOST_CONFIG_PATH: string;
35
42
  /** 解析配置文件路径(--config > $RDSH_CONFIG > 默认 host.json)。 */
package/dist/config.js CHANGED
@@ -21,6 +21,7 @@ const DEFAULTS = {
21
21
  behindProxy: false,
22
22
  allowFrom: [],
23
23
  auth: DEFAULT_AUTH,
24
+ dshUiCompat: { trustE2EEAsLoopback: true },
24
25
  };
25
26
  /** 解析配置文件路径(--config > $RDSH_CONFIG > 默认 host.json)。 */
26
27
  export function resolveConfigPath(cliPath, env = process.env) {
@@ -174,6 +175,19 @@ export function normalizeConfig(raw, source = "config") {
174
175
  assertString(cfg.dshPath, "dshPath", source);
175
176
  out.dshPath = cfg.dshPath;
176
177
  }
178
+ // ---- dshUiCompat(缺省 trustE2EEAsLoopback: true)----
179
+ if (cfg.dshUiCompat !== undefined) {
180
+ if (typeof cfg.dshUiCompat !== "object" || cfg.dshUiCompat === null) {
181
+ throw new Error(`${source}: "dshUiCompat" must be an object`);
182
+ }
183
+ const compat = cfg.dshUiCompat;
184
+ if (compat.trustE2EEAsLoopback !== undefined) {
185
+ if (typeof compat.trustE2EEAsLoopback !== "boolean") {
186
+ throw new Error(`${source}: "dshUiCompat.trustE2EEAsLoopback" must be boolean`);
187
+ }
188
+ out.dshUiCompat = { trustE2EEAsLoopback: compat.trustE2EEAsLoopback };
189
+ }
190
+ }
177
191
  return out;
178
192
  }
179
193
  function assertString(v, field, source) {
package/dist/join.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { IncomingHttpHeaders } from "node:http";
1
2
  import type { ProxyTarget } from "./proxy.ts";
2
3
  import type { JoinLockRole } from "./lock.ts";
3
4
  export interface JoinOptions {
@@ -11,6 +12,10 @@ export interface JoinOptions {
11
12
  insecure?: boolean;
12
13
  /** 主机名(注册命名 / host.json) */
13
14
  name?: string;
15
+ /** DSH UI 兼容(透传 host.json dshUiCompat;缺省 true) */
16
+ dshUiCompat?: {
17
+ trustE2EEAsLoopback?: boolean;
18
+ };
14
19
  }
15
20
  /** 注册/接入结果:解析出的 host token + 是否需 insecure + 生效的主机名(缺省=机器 hostname)。 */
16
21
  export interface RegisterOutcome {
@@ -41,10 +46,16 @@ export interface StartJoinOptions {
41
46
  /** 锁文件路径(缺省 ~/.rdsh/join.lock;测试可注入临时路径) */
42
47
  lockPath?: string;
43
48
  hooks?: JoinHooks;
49
+ /** DSH UI 兼容(缺省 trustE2EEAsLoopback=true;false 关闭 JS patch) */
50
+ dshUiCompat?: {
51
+ trustE2EEAsLoopback?: boolean;
52
+ };
44
53
  }
45
54
  /** 可停止的 join 隧道句柄。 */
46
55
  export interface JoinHandle {
47
56
  stop(): Promise<void>;
57
+ /** 运行中切换 DSH UI 兼容(trustE2EEAsLoopback);下一个请求即生效。 */
58
+ setUiCompat(trustE2EEAsLoopback: boolean): void;
48
59
  }
49
60
  /** 探测 hub 是否需 insecure:以严格校验握手一次;证书错误 → true(需 insecure)。 */
50
61
  export declare function detectInsecure(hubUrl: string): Promise<boolean>;
@@ -56,6 +67,14 @@ export declare function registerJoin(opts: JoinOptions): Promise<RegisterOutcome
56
67
  * 启动 join 隧道(no-spawn):转发到外部 `opts.target`,不 spawn dsh、不 process.exit。
57
68
  * 获取 pid 锁(opts.role);返回 `JoinHandle`,`stop()` 干净停止(关 WS/清 heartbeat/释放锁)。
58
69
  */
70
+ /** JS 响应判定(content-type 含 javascript)。 */
71
+ export declare function isJsContentType(headers: IncomingHttpHeaders): boolean;
72
+ /**
73
+ * 最小 patch:把 DSH 客户端 bundle 里的前端 isLoopback 判定替换为 true
74
+ * (持久设置/API key 输入只对 loopback 开放;E2EE 流上信任基础等同 loopback)。
75
+ * fail-open:未命中目标串 → 返回 null,调用方原样透传(DSH 升级不炸)。
76
+ */
77
+ export declare function patchLoopbackJs(body: Buffer): Buffer | null;
59
78
  export declare function startJoin(opts: StartJoinOptions): JoinHandle;
60
79
  /** `rdsh host serve`(join 模式)的 CLI 封装:spawn dsh + 信号退出 + startJoin(role:cli)。 */
61
80
  export declare function join(opts: JoinOptions): Promise<void>;
package/dist/join.js CHANGED
@@ -128,9 +128,29 @@ async function register(hubUrl, joinToken, name, insecure, e2eePublicKey) {
128
128
  * 启动 join 隧道(no-spawn):转发到外部 `opts.target`,不 spawn dsh、不 process.exit。
129
129
  * 获取 pid 锁(opts.role);返回 `JoinHandle`,`stop()` 干净停止(关 WS/清 heartbeat/释放锁)。
130
130
  */
131
+ /** JS 响应判定(content-type 含 javascript)。 */
132
+ export function isJsContentType(headers) {
133
+ const ct = headers["content-type"];
134
+ const s = Array.isArray(ct) ? ct.join(";") : (ct ?? "");
135
+ return /javascript/i.test(s);
136
+ }
137
+ /**
138
+ * 最小 patch:把 DSH 客户端 bundle 里的前端 isLoopback 判定替换为 true
139
+ * (持久设置/API key 输入只对 loopback 开放;E2EE 流上信任基础等同 loopback)。
140
+ * fail-open:未命中目标串 → 返回 null,调用方原样透传(DSH 升级不炸)。
141
+ */
142
+ export function patchLoopbackJs(body) {
143
+ const src = body.toString("utf8");
144
+ const target = "isLoopbackHostname(pageLocation.hostname)";
145
+ if (!src.includes(target))
146
+ return null;
147
+ return Buffer.from(src.split(target).join("true"), "utf8");
148
+ }
131
149
  export function startJoin(opts) {
132
150
  const hubWsBase = opts.hubUrl.replace(/^https/, "wss").replace(/^http/, "ws");
133
151
  const hooks = opts.hooks ?? {};
152
+ // DSH UI 兼容开关:缺省 true(跟随 E2EE);可变引用 → 运行中可切换(插件面板即时生效)
153
+ const uiCompat = { trustE2EEAsLoopback: opts.dshUiCompat?.trustE2EEAsLoopback !== false };
134
154
  const log = (level, message) => {
135
155
  hooks.onLog?.(level, message);
136
156
  };
@@ -153,7 +173,7 @@ export function startJoin(opts) {
153
173
  }
154
174
  }
155
175
  /** 内层帧分发器(plain 与 raw 共用):OPEN http/ws + DATA → DSH 转发,响应帧经 `send` 回传。 */
156
- function makeInnerDispatcher(send) {
176
+ function makeInnerDispatcher(send, dio) {
157
177
  const httpStreams = new Map();
158
178
  const wsStreams = new Map();
159
179
  function closeStream(streamId) {
@@ -241,6 +261,25 @@ export function startJoin(opts) {
241
261
  reason: upRes.statusMessage,
242
262
  headers: normalizeRespHeaders(upRes.headers),
243
263
  })));
264
+ if (dio?.jsPatch?.() === true && isJsContentType(upRes.headers)) {
265
+ // 最小 patch:E2EE 流上的 JS 响应,把前端 isLoopback 判定替换为 true
266
+ // (fail-open:未命中 → 原样透传;DSH 升级不炸)
267
+ const chunks = [];
268
+ upRes.on("data", (chunk) => chunks.push(chunk));
269
+ upRes.on("end", () => {
270
+ const body = Buffer.concat(chunks);
271
+ const patched = patchLoopbackJs(body);
272
+ // fail-open:未命中也必须原样发 body(否则空响应白屏)
273
+ send(encodeFrame(FRAME_TYPE.DATA, streamId, patched !== null ? patched : body));
274
+ send(encodeFrame(FRAME_TYPE.CLOSE, streamId, jsonPayload({ code: 0 })));
275
+ httpStreams.delete(streamId);
276
+ });
277
+ upRes.on("error", () => {
278
+ send(encodeFrame(FRAME_TYPE.CLOSE, streamId, jsonPayload({ code: 502, message: "upstream error" })));
279
+ httpStreams.delete(streamId);
280
+ });
281
+ return;
282
+ }
244
283
  upRes.on("data", (chunk) => {
245
284
  send(encodeFrame(FRAME_TYPE.DATA, streamId, chunk));
246
285
  });
@@ -310,7 +349,7 @@ export function startJoin(opts) {
310
349
  }
311
350
  return { handleFrame, cleanup };
312
351
  }
313
- const plainDispatcher = makeInnerDispatcher(sendTunnelFrame);
352
+ const plainDispatcher = makeInnerDispatcher(sendTunnelFrame, { jsPatch: () => uiCompat.trustE2EEAsLoopback });
314
353
  // host 端 E2EE 静态密钥对(持久化 ~/.rdsh/e2ee-key.json;join 注册时上送指纹)
315
354
  const hostE2eeKeypair = loadOrCreateE2eeKeyPair();
316
355
  const rawStreams = new Map();
@@ -321,7 +360,7 @@ export function startJoin(opts) {
321
360
  const ct = raw.encryptor.encrypt(frame, Buffer.alloc(0));
322
361
  sendTunnelFrame(encodeFrame(FRAME_TYPE.DATA, streamId, ct, FLAG_E2E));
323
362
  }
324
- });
363
+ }, { jsPatch: () => uiCompat.trustE2EEAsLoopback });
325
364
  rawStreams.set(streamId, {
326
365
  handshakeBuf: Buffer.alloc(0),
327
366
  keys: null,
@@ -498,6 +537,10 @@ export function startJoin(opts) {
498
537
  }
499
538
  connect();
500
539
  return {
540
+ setUiCompat(trustE2EEAsLoopback) {
541
+ uiCompat.trustE2EEAsLoopback = trustE2EEAsLoopback;
542
+ console.log(`rdsh join: dshUiCompat.trustE2EEAsLoopback = ${trustE2EEAsLoopback}(运行中生效)`);
543
+ },
501
544
  async stop() {
502
545
  if (shuttingDown)
503
546
  return;
@@ -534,6 +577,7 @@ export async function join(opts) {
534
577
  insecure,
535
578
  target,
536
579
  role: "cli",
580
+ dshUiCompat: opts.dshUiCompat,
537
581
  hooks: {
538
582
  onLog: (level, message) => {
539
583
  (level === "error" ? console.error : console.log)(`rdsh join: ${message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rdsh-gateway",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "rdsh host-side component: LAN auth gateway + outbound tunnel endpoint (spawns dsh web)",
5
5
  "license": "MIT",
6
6
  "type": "module",