@yezack/dsh-remote-mobile 1.9.2 → 1.10.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.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * DSH 底座运行模式探测器
3
+ *
4
+ * 负责检测宿主 DSH WebServer 当前的网络暴露姿态:
5
+ * 1. 默认使用代理模式 ('proxy'):宿主仅监听本地回环地址 (127.0.0.1 / localhost),
6
+ * 由插件启动独立安全接入代理 (0.0.0.0:3081);
7
+ * 2. 若宿主已监听 0.0.0.0 或具体的外部网卡 IP,则外部网络可直连,
8
+ * 自动回退直连模式 ('direct'),沿用原生的 Hook 拦截机制,不额外占用端口;
9
+ * 3. 用户可通过 options.mode ('proxy' | 'direct') 显式选择;不再提供 auto 模式。
10
+ */
11
+ export type ProxyMode = 'proxy' | 'direct';
12
+ export interface ServerModeDetectionResult {
13
+ /** 最终生效的运行模式 */
14
+ mode: ProxyMode;
15
+ /** 底座核心监听的 host(未配置时默认 127.0.0.1,在 proxy 模式下已安全规范化为回环 target) */
16
+ coreHost: string;
17
+ /** 底座核心监听的端口(默认 3080) */
18
+ corePort: number;
19
+ /** 底座原始配置是否判定为纯回环监听 */
20
+ isLoopback: boolean;
21
+ /** 原始底座 host */
22
+ rawCoreHost: string;
23
+ }
24
+ /**
25
+ * 判断指定 host 是否属于本地回环地址(支持 localhost, 127.*, ::1, 0:0:0:0:0:0:0:1 等完整形态)
26
+ */
27
+ export declare function isLoopbackHost(host?: string | null): boolean;
28
+ /**
29
+ * 规范化安全接入代理内部转发的目标核心 Host(仅在 proxy 模式下作为 proxy target 使用)
30
+ * 将通配或 localhost 规范化为明确的回环 IP,消除 Node 18+ 的 Happy Eyeballs IPv6 优先解析 502 隐患
31
+ */
32
+ export declare function normalizeGatewayTargetHost(host: string): string;
33
+ /**
34
+ * 探测底座运行姿态并裁决插件的工作模式
35
+ *
36
+ * @param ctx Cordis 上下文(含 webServer / webStartup 等服务)
37
+ * @param configuredMode 用户显式配置的运行模式 ('proxy' | 'direct')
38
+ * @param defaultCorePort 底座默认核心端口(3080)
39
+ */
40
+ export declare function detectServerMode(ctx: any, configuredMode?: 'proxy' | 'direct', defaultCorePort?: number): ServerModeDetectionResult;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * DSH 底座运行模式探测器
3
+ *
4
+ * 负责检测宿主 DSH WebServer 当前的网络暴露姿态:
5
+ * 1. 默认使用代理模式 ('proxy'):宿主仅监听本地回环地址 (127.0.0.1 / localhost),
6
+ * 由插件启动独立安全接入代理 (0.0.0.0:3081);
7
+ * 2. 若宿主已监听 0.0.0.0 或具体的外部网卡 IP,则外部网络可直连,
8
+ * 自动回退直连模式 ('direct'),沿用原生的 Hook 拦截机制,不额外占用端口;
9
+ * 3. 用户可通过 options.mode ('proxy' | 'direct') 显式选择;不再提供 auto 模式。
10
+ */
11
+ /**
12
+ * 判断指定 host 是否属于本地回环地址(支持 localhost, 127.*, ::1, 0:0:0:0:0:0:0:1 等完整形态)
13
+ */
14
+ export function isLoopbackHost(host) {
15
+ if (!host || typeof host !== 'string')
16
+ return true; // 默认回环
17
+ let trimmed = host.trim().toLowerCase();
18
+ if (!trimmed)
19
+ return true;
20
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
21
+ trimmed = trimmed.slice(1, -1);
22
+ }
23
+ if (trimmed === 'localhost' || trimmed === '::1' || trimmed === '0:0:0:0:0:0:0:1')
24
+ return true;
25
+ if (trimmed === '127.0.0.1' || trimmed.startsWith('127.'))
26
+ return true;
27
+ if (trimmed === '::ffff:127.0.0.1' || trimmed.startsWith('::ffff:127.'))
28
+ return true;
29
+ return false;
30
+ }
31
+ /**
32
+ * 规范化安全接入代理内部转发的目标核心 Host(仅在 proxy 模式下作为 proxy target 使用)
33
+ * 将通配或 localhost 规范化为明确的回环 IP,消除 Node 18+ 的 Happy Eyeballs IPv6 优先解析 502 隐患
34
+ */
35
+ export function normalizeGatewayTargetHost(host) {
36
+ const trimmed = host.trim().toLowerCase();
37
+ if (trimmed === '0.0.0.0' || trimmed === 'localhost')
38
+ return '127.0.0.1';
39
+ if (trimmed === '::' || trimmed === '[::]')
40
+ return '::1';
41
+ // Node http.request 的 hostname 不接受 IPv6 方括号;去掉后由 coreTargetAuthority 重新加回
42
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
43
+ return host.trim().slice(1, -1);
44
+ }
45
+ return host.trim();
46
+ }
47
+ /**
48
+ * 探测底座运行姿态并裁决插件的工作模式
49
+ *
50
+ * @param ctx Cordis 上下文(含 webServer / webStartup 等服务)
51
+ * @param configuredMode 用户显式配置的运行模式 ('proxy' | 'direct')
52
+ * @param defaultCorePort 底座默认核心端口(3080)
53
+ */
54
+ export function detectServerMode(ctx, configuredMode = 'proxy', defaultCorePort = 3080) {
55
+ const webServer = (typeof ctx?.get === 'function' ? ctx.get('webServer') : undefined) || ctx?.webServer;
56
+ const webStartup = (typeof ctx?.get === 'function' ? ctx.get('webStartup') : undefined) || ctx?.webStartup;
57
+ // 提取宿主底座的 host 与 port 配置(保持原始字面量用于模式判定)
58
+ const coreHostRaw = webServer?.config?.host ?? webServer?.options?.host ?? webStartup?.host ?? '127.0.0.1';
59
+ const corePortRaw = webServer?.config?.port ?? webServer?.options?.port ?? webStartup?.port ?? webServer?.port ?? defaultCorePort;
60
+ const rawCoreHost = String(coreHostRaw || '127.0.0.1').trim();
61
+ const corePort = Number(corePortRaw) || defaultCorePort;
62
+ const isLoopback = isLoopbackHost(rawCoreHost);
63
+ // proxy 是默认模式,但只在核心回环时成立;核心已对外监听时回退 direct,
64
+ // 以兼容旧版 webserver 配置或用户显式绑定的具体网卡 IP。
65
+ const finalMode = configuredMode === 'direct' || !isLoopback ? 'direct' : 'proxy';
66
+ // 仅对最终生效的 proxy 模式的目标核心地址执行 proxyTarget 规范化
67
+ const coreHost = finalMode === 'proxy' ? normalizeGatewayTargetHost(rawCoreHost) : rawCoreHost;
68
+ return {
69
+ mode: finalMode,
70
+ coreHost,
71
+ corePort,
72
+ isLoopback,
73
+ rawCoreHost,
74
+ };
75
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 生成 32 字节高强度密码学随机密钥
3
+ */
4
+ export declare function generateGatewaySecret(): string;
5
+ /**
6
+ * 设置网关内部共享密钥
7
+ * @param serverOrKey - 关联的 Server 实例(可选)
8
+ * @param secret - 密钥字符串
9
+ */
10
+ export declare function setGatewaySecret(serverOrKey: any, secret: string): void;
11
+ /**
12
+ * 获取当前有效的网关内部共享密钥
13
+ * @param serverOrKey - 关联的 Server 实例(可选)
14
+ */
15
+ export declare function getGatewaySecret(serverOrKey?: any): string;
16
+ /**
17
+ * 清除网关内部密钥
18
+ */
19
+ export declare function clearGatewaySecret(serverOrKey?: any): void;
20
+ /**
21
+ * 恒定时间安全比对两个密钥,防止时序侧信道攻击
22
+ */
23
+ export declare function safeCompareSecret(candidate: string | string[] | undefined, actual: string): boolean;
@@ -0,0 +1,65 @@
1
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
2
+ /**
3
+ * 内部网关私密握手密钥管理器
4
+ *
5
+ * 用于网关与底层 DSH HTTP Server 之间的受信通信鉴权:
6
+ * 仅当底层物理连接为回环 Socket (127.0.0.1 / ::1) 且携带匹配的私密握手密钥时,
7
+ * 底层虚拟化补丁才采纳网关上报的真实客户端 IP (x-dsh-real-ip)。
8
+ *
9
+ * 采用全局活跃单例兼 WeakMap 绑定实例机制,完美兼容 Cordis 插件热重载与多实例隔离场景。
10
+ */
11
+ let globalActiveSecret = '';
12
+ const serverSecretMap = new WeakMap();
13
+ /**
14
+ * 生成 32 字节高强度密码学随机密钥
15
+ */
16
+ export function generateGatewaySecret() {
17
+ return randomBytes(32).toString('hex');
18
+ }
19
+ /**
20
+ * 设置网关内部共享密钥
21
+ * @param serverOrKey - 关联的 Server 实例(可选)
22
+ * @param secret - 密钥字符串
23
+ */
24
+ export function setGatewaySecret(serverOrKey, secret) {
25
+ globalActiveSecret = secret;
26
+ if (serverOrKey && (typeof serverOrKey === 'object' || typeof serverOrKey === 'function')) {
27
+ serverSecretMap.set(serverOrKey, secret);
28
+ }
29
+ }
30
+ /**
31
+ * 获取当前有效的网关内部共享密钥
32
+ * @param serverOrKey - 关联的 Server 实例(可选)
33
+ */
34
+ export function getGatewaySecret(serverOrKey) {
35
+ if (serverOrKey && (typeof serverOrKey === 'object' || typeof serverOrKey === 'function')) {
36
+ const bound = serverSecretMap.get(serverOrKey);
37
+ if (bound)
38
+ return bound;
39
+ }
40
+ return globalActiveSecret;
41
+ }
42
+ /**
43
+ * 清除网关内部密钥
44
+ */
45
+ export function clearGatewaySecret(serverOrKey) {
46
+ if (serverOrKey && (typeof serverOrKey === 'object' || typeof serverOrKey === 'function')) {
47
+ serverSecretMap.delete(serverOrKey);
48
+ }
49
+ globalActiveSecret = '';
50
+ }
51
+ /**
52
+ * 恒定时间安全比对两个密钥,防止时序侧信道攻击
53
+ */
54
+ export function safeCompareSecret(candidate, actual) {
55
+ if (!candidate || !actual)
56
+ return false;
57
+ const candStr = Array.isArray(candidate) ? candidate[0] : candidate;
58
+ if (typeof candStr !== 'string')
59
+ return false;
60
+ const bufCand = Buffer.from(candStr, 'utf8');
61
+ const bufActual = Buffer.from(actual, 'utf8');
62
+ if (bufCand.length !== bufActual.length)
63
+ return false;
64
+ return timingSafeEqual(bufCand, bufActual);
65
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * DSH 移动端远程独立安全接入代理服务 (GatewayServer)
3
+ *
4
+ * 架构定位:
5
+ * 解决 DSH 0.2.1+ 底层强行禁止 0.0.0.0 监听的问题。
6
+ * DSH 官方 WebServer 继续保持纯粹、安全的 127.0.0.1:3080 本地回环监听(零外部暴露风险);
7
+ * 由本服务在 0.0.0.0:3081 启动专用安全接入代理,接纳局域网与 Tailscale 流量,
8
+ * 执行端到端 RSA 加密、扫码配对、慢哈希密码认证与防暴力破解 IP 熔断,
9
+ * 并将合法的流量全双工代理至底座核心服务,彻底解决换 Wi-Fi 崩溃、无法兼顾多网卡与本机回环失效的痛点。
10
+ */
11
+ import http, { type IncomingMessage, type ServerResponse } from 'node:http';
12
+ import type { SessionStore } from '../auth/token.js';
13
+ export interface GatewayServerOptions {
14
+ /** 安全接入代理监听端口,默认 3081 */
15
+ port?: number;
16
+ /** 安全接入代理监听网络地址,默认 0.0.0.0 */
17
+ host?: string;
18
+ /** DSH 宿主核心服务端口,默认 3080 */
19
+ corePort: number;
20
+ /** DSH 宿主核心服务主机,默认 127.0.0.1 */
21
+ coreHost?: string;
22
+ /** 端口冲突时是否允许自动向后递增探测,默认 true */
23
+ autoIncrementPort?: boolean;
24
+ /** WebSocket 升级握手超时(毫秒),默认 10000ms */
25
+ upgradeTimeoutMs?: number;
26
+ }
27
+ export type GatewayServerStatus = 'stopped' | 'starting' | 'running' | 'error';
28
+ export declare class GatewayServer {
29
+ private server;
30
+ private activeSockets;
31
+ private store;
32
+ private gateMiddleware;
33
+ private configuredPort;
34
+ private boundPort;
35
+ private host;
36
+ private corePort;
37
+ private coreHost;
38
+ private coreTargetAuthority;
39
+ private autoIncrementPort;
40
+ private upgradeTimeoutMs;
41
+ private status;
42
+ private lastError;
43
+ private logger?;
44
+ constructor(options: GatewayServerOptions, store: SessionStore, gateMiddleware: (req: IncomingMessage, res: ServerResponse, next?: () => void) => boolean, logger?: any);
45
+ /** 获取安全接入代理当前运行状态契约 */
46
+ getStatus(): GatewayServerStatus;
47
+ /** 获取网关最近发生的异常信息 */
48
+ getLastError(): string | null;
49
+ /** 获取安全接入代理实际生效绑定的端口 */
50
+ getBoundPort(): number;
51
+ /** 获取安全接入代理监听的 host */
52
+ getHost(): string;
53
+ /** 获取内部转发的目标核心端口 */
54
+ getCorePort(): number;
55
+ /** 获取内部底层 Server 实例(供测试或状态检测) */
56
+ getServer(): http.Server | null;
57
+ /**
58
+ * 检验请求的 Host 头是否符合动态白名单(防 DNS-Rebinding 与非法主机注入)
59
+ */
60
+ isHostAllowed(hostHeader?: string): boolean;
61
+ /**
62
+ * 获取外部访问协议(兼容 Nginx / Caddy / Tailscale Serve 等反代的 X-Forwarded-Proto)
63
+ */
64
+ private getExternalProtocol;
65
+ /**
66
+ * 计算转发到底座的真实客户端 IP。
67
+ *
68
+ * 正常 LAN/Tailscale 客户端直接连网关,socket IP 即真实 IP;
69
+ * 同机反向代理从 127.0.0.1 连入时,若门禁已判定为非回环(Host/Forwarded 表明是代理),
70
+ * 则采用 X-Forwarded-For 首个合法 IP,恢复防暴破/审计的真实来源。
71
+ */
72
+ private getEffectiveClientIp;
73
+ /** 请求是否携带反向代理透传头 */
74
+ private hasForwardedHeaders;
75
+ /**
76
+ * 启动安全接入代理服务(带端口冲突自愈探测与状态机落定)
77
+ */
78
+ start(): Promise<{
79
+ port: number;
80
+ host: string;
81
+ }>;
82
+ /**
83
+ * 优雅关闭安全接入代理,立即释放端口与所有客户端 Socket
84
+ */
85
+ close(): Promise<void>;
86
+ /**
87
+ * 处理 HTTP 请求:
88
+ * 1. 严格校验 Host 白名单(阻断 DNS-Rebinding)
89
+ * 2. 执行安全门禁 gateMiddleware 与跨站写拦截
90
+ * 3. 构造请求头:抹除客户端伪造 Secret,注入真实 IP 与内部有效握手密钥
91
+ * 4. 实施成对虚拟化决策矩阵
92
+ * 5. 转发到底座并流式透明 Pipe
93
+ */
94
+ private handleHttpRequest;
95
+ /**
96
+ * 转发已授权的 HTTP 请求至 DSH 本地核心服务
97
+ */
98
+ private proxyHttpRequest;
99
+ /**
100
+ * 处理 WebSocket Upgrade 协议升级代理(支持超时清除、非 101 状态传递与 CSWSH 防御)
101
+ */
102
+ private handleUpgrade;
103
+ /**
104
+ * 改写底座 30x 重定向的目标 Location,将回环地址替换为外部真实访问的 Host
105
+ * 兼容 127.0.0.1, localhost, [::1] 以及 127.* 回环变体
106
+ */
107
+ rewriteRedirectLocation(location: string, originalHost: string, externalProto?: 'http' | 'https'): string;
108
+ }