@yezack/dsh-remote-mobile 1.10.0 → 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.
- package/CHANGELOG.md +54 -42
- package/LICENSE +21 -21
- package/README.md +203 -61
- package/README_EN.md +205 -57
- package/lib/auth/gate.js +1 -1
- package/lib/auth/token.d.ts +10 -10
- package/lib/auth/token.js +18 -15
- package/lib/bridge/compat.js +260 -260
- package/lib/bridge/directory-picker.d.ts +118 -0
- package/lib/bridge/directory-picker.js +617 -0
- package/lib/client.js +1010 -178
- package/lib/gateway/detector.d.ts +10 -12
- package/lib/gateway/detector.js +12 -25
- package/lib/gateway/server.d.ts +9 -9
- package/lib/gateway/server.js +10 -10
- package/lib/index.d.ts +1 -0
- package/lib/index.js +86 -35
- package/lib/routes/api.d.ts +15 -12
- package/lib/routes/api.js +113 -4
- package/lib/routes/login-page.js +9 -7
- package/lib/routes/register.d.ts +9 -0
- package/lib/routes/register.js +53 -0
- package/lib/startup-hint.d.ts +5 -0
- package/lib/startup-hint.js +72 -0
- package/lib/styles/style-snippets.js +639 -639
- package/package.json +3 -3
|
@@ -2,19 +2,17 @@
|
|
|
2
2
|
* DSH 底座运行模式探测器
|
|
3
3
|
*
|
|
4
4
|
* 负责检测宿主 DSH WebServer 当前的网络暴露姿态:
|
|
5
|
-
* 1.
|
|
6
|
-
*
|
|
5
|
+
* 1. 默认使用代理模式 ('proxy'):宿主仅监听本地回环地址 (127.0.0.1 / localhost),
|
|
6
|
+
* 由插件启动独立安全接入代理 (0.0.0.0:3081);
|
|
7
7
|
* 2. 若宿主已监听 0.0.0.0 或具体的外部网卡 IP,则外部网络可直连,
|
|
8
|
-
*
|
|
9
|
-
* 3.
|
|
8
|
+
* 自动回退直连模式 ('direct'),沿用原生的 Hook 拦截机制,不额外占用端口;
|
|
9
|
+
* 3. 用户可通过 options.mode ('proxy' | 'direct') 显式选择;不再提供 auto 模式。
|
|
10
10
|
*/
|
|
11
|
-
export type
|
|
11
|
+
export type ProxyMode = 'proxy' | 'direct';
|
|
12
12
|
export interface ServerModeDetectionResult {
|
|
13
13
|
/** 最终生效的运行模式 */
|
|
14
|
-
mode:
|
|
15
|
-
/**
|
|
16
|
-
isAuto: boolean;
|
|
17
|
-
/** 底座核心监听的 host(未配置时默认 127.0.0.1,在 gateway 模式下已安全规范化为回环 target) */
|
|
14
|
+
mode: ProxyMode;
|
|
15
|
+
/** 底座核心监听的 host(未配置时默认 127.0.0.1,在 proxy 模式下已安全规范化为回环 target) */
|
|
18
16
|
coreHost: string;
|
|
19
17
|
/** 底座核心监听的端口(默认 3080) */
|
|
20
18
|
corePort: number;
|
|
@@ -28,7 +26,7 @@ export interface ServerModeDetectionResult {
|
|
|
28
26
|
*/
|
|
29
27
|
export declare function isLoopbackHost(host?: string | null): boolean;
|
|
30
28
|
/**
|
|
31
|
-
*
|
|
29
|
+
* 规范化安全接入代理内部转发的目标核心 Host(仅在 proxy 模式下作为 proxy target 使用)
|
|
32
30
|
* 将通配或 localhost 规范化为明确的回环 IP,消除 Node 18+ 的 Happy Eyeballs IPv6 优先解析 502 隐患
|
|
33
31
|
*/
|
|
34
32
|
export declare function normalizeGatewayTargetHost(host: string): string;
|
|
@@ -36,7 +34,7 @@ export declare function normalizeGatewayTargetHost(host: string): string;
|
|
|
36
34
|
* 探测底座运行姿态并裁决插件的工作模式
|
|
37
35
|
*
|
|
38
36
|
* @param ctx Cordis 上下文(含 webServer / webStartup 等服务)
|
|
39
|
-
* @param configuredMode 用户显式配置的运行模式 ('
|
|
37
|
+
* @param configuredMode 用户显式配置的运行模式 ('proxy' | 'direct')
|
|
40
38
|
* @param defaultCorePort 底座默认核心端口(3080)
|
|
41
39
|
*/
|
|
42
|
-
export declare function detectServerMode(ctx: any, configuredMode?: '
|
|
40
|
+
export declare function detectServerMode(ctx: any, configuredMode?: 'proxy' | 'direct', defaultCorePort?: number): ServerModeDetectionResult;
|
package/lib/gateway/detector.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* DSH 底座运行模式探测器
|
|
3
3
|
*
|
|
4
4
|
* 负责检测宿主 DSH WebServer 当前的网络暴露姿态:
|
|
5
|
-
* 1.
|
|
6
|
-
*
|
|
5
|
+
* 1. 默认使用代理模式 ('proxy'):宿主仅监听本地回环地址 (127.0.0.1 / localhost),
|
|
6
|
+
* 由插件启动独立安全接入代理 (0.0.0.0:3081);
|
|
7
7
|
* 2. 若宿主已监听 0.0.0.0 或具体的外部网卡 IP,则外部网络可直连,
|
|
8
|
-
*
|
|
9
|
-
* 3.
|
|
8
|
+
* 自动回退直连模式 ('direct'),沿用原生的 Hook 拦截机制,不额外占用端口;
|
|
9
|
+
* 3. 用户可通过 options.mode ('proxy' | 'direct') 显式选择;不再提供 auto 模式。
|
|
10
10
|
*/
|
|
11
11
|
/**
|
|
12
12
|
* 判断指定 host 是否属于本地回环地址(支持 localhost, 127.*, ::1, 0:0:0:0:0:0:0:1 等完整形态)
|
|
@@ -29,7 +29,7 @@ export function isLoopbackHost(host) {
|
|
|
29
29
|
return false;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* 规范化安全接入代理内部转发的目标核心 Host(仅在 proxy 模式下作为 proxy target 使用)
|
|
33
33
|
* 将通配或 localhost 规范化为明确的回环 IP,消除 Node 18+ 的 Happy Eyeballs IPv6 优先解析 502 隐患
|
|
34
34
|
*/
|
|
35
35
|
export function normalizeGatewayTargetHost(host) {
|
|
@@ -48,10 +48,10 @@ export function normalizeGatewayTargetHost(host) {
|
|
|
48
48
|
* 探测底座运行姿态并裁决插件的工作模式
|
|
49
49
|
*
|
|
50
50
|
* @param ctx Cordis 上下文(含 webServer / webStartup 等服务)
|
|
51
|
-
* @param configuredMode 用户显式配置的运行模式 ('
|
|
51
|
+
* @param configuredMode 用户显式配置的运行模式 ('proxy' | 'direct')
|
|
52
52
|
* @param defaultCorePort 底座默认核心端口(3080)
|
|
53
53
|
*/
|
|
54
|
-
export function detectServerMode(ctx, configuredMode = '
|
|
54
|
+
export function detectServerMode(ctx, configuredMode = 'proxy', defaultCorePort = 3080) {
|
|
55
55
|
const webServer = (typeof ctx?.get === 'function' ? ctx.get('webServer') : undefined) || ctx?.webServer;
|
|
56
56
|
const webStartup = (typeof ctx?.get === 'function' ? ctx.get('webStartup') : undefined) || ctx?.webStartup;
|
|
57
57
|
// 提取宿主底座的 host 与 port 配置(保持原始字面量用于模式判定)
|
|
@@ -60,26 +60,13 @@ export function detectServerMode(ctx, configuredMode = 'auto', defaultCorePort =
|
|
|
60
60
|
const rawCoreHost = String(coreHostRaw || '127.0.0.1').trim();
|
|
61
61
|
const corePort = Number(corePortRaw) || defaultCorePort;
|
|
62
62
|
const isLoopback = isLoopbackHost(rawCoreHost);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
finalMode = 'gateway';
|
|
70
|
-
}
|
|
71
|
-
else if (configuredMode === 'direct') {
|
|
72
|
-
finalMode = 'direct';
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
// auto 自适应模式:底座为回环则启用网关,底座已对外开放(0.0.0.0 / 具体外部IP)则直连
|
|
76
|
-
finalMode = isLoopback ? 'gateway' : 'direct';
|
|
77
|
-
}
|
|
78
|
-
// 仅对最终生效的 gateway 模式的目标核心地址执行 proxyTarget 规范化
|
|
79
|
-
const coreHost = finalMode === 'gateway' ? normalizeGatewayTargetHost(rawCoreHost) : 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;
|
|
80
68
|
return {
|
|
81
69
|
mode: finalMode,
|
|
82
|
-
isAuto,
|
|
83
70
|
coreHost,
|
|
84
71
|
corePort,
|
|
85
72
|
isLoopback,
|
package/lib/gateway/server.d.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DSH
|
|
2
|
+
* DSH 移动端远程独立安全接入代理服务 (GatewayServer)
|
|
3
3
|
*
|
|
4
4
|
* 架构定位:
|
|
5
5
|
* 解决 DSH 0.2.1+ 底层强行禁止 0.0.0.0 监听的问题。
|
|
6
6
|
* DSH 官方 WebServer 继续保持纯粹、安全的 127.0.0.1:3080 本地回环监听(零外部暴露风险);
|
|
7
|
-
* 由本服务在 0.0.0.0:3081
|
|
7
|
+
* 由本服务在 0.0.0.0:3081 启动专用安全接入代理,接纳局域网与 Tailscale 流量,
|
|
8
8
|
* 执行端到端 RSA 加密、扫码配对、慢哈希密码认证与防暴力破解 IP 熔断,
|
|
9
9
|
* 并将合法的流量全双工代理至底座核心服务,彻底解决换 Wi-Fi 崩溃、无法兼顾多网卡与本机回环失效的痛点。
|
|
10
10
|
*/
|
|
11
11
|
import http, { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
12
12
|
import type { SessionStore } from '../auth/token.js';
|
|
13
13
|
export interface GatewayServerOptions {
|
|
14
|
-
/**
|
|
14
|
+
/** 安全接入代理监听端口,默认 3081 */
|
|
15
15
|
port?: number;
|
|
16
|
-
/**
|
|
16
|
+
/** 安全接入代理监听网络地址,默认 0.0.0.0 */
|
|
17
17
|
host?: string;
|
|
18
18
|
/** DSH 宿主核心服务端口,默认 3080 */
|
|
19
19
|
corePort: number;
|
|
@@ -42,13 +42,13 @@ export declare class GatewayServer {
|
|
|
42
42
|
private lastError;
|
|
43
43
|
private logger?;
|
|
44
44
|
constructor(options: GatewayServerOptions, store: SessionStore, gateMiddleware: (req: IncomingMessage, res: ServerResponse, next?: () => void) => boolean, logger?: any);
|
|
45
|
-
/**
|
|
45
|
+
/** 获取安全接入代理当前运行状态契约 */
|
|
46
46
|
getStatus(): GatewayServerStatus;
|
|
47
47
|
/** 获取网关最近发生的异常信息 */
|
|
48
48
|
getLastError(): string | null;
|
|
49
|
-
/**
|
|
49
|
+
/** 获取安全接入代理实际生效绑定的端口 */
|
|
50
50
|
getBoundPort(): number;
|
|
51
|
-
/**
|
|
51
|
+
/** 获取安全接入代理监听的 host */
|
|
52
52
|
getHost(): string;
|
|
53
53
|
/** 获取内部转发的目标核心端口 */
|
|
54
54
|
getCorePort(): number;
|
|
@@ -73,14 +73,14 @@ export declare class GatewayServer {
|
|
|
73
73
|
/** 请求是否携带反向代理透传头 */
|
|
74
74
|
private hasForwardedHeaders;
|
|
75
75
|
/**
|
|
76
|
-
*
|
|
76
|
+
* 启动安全接入代理服务(带端口冲突自愈探测与状态机落定)
|
|
77
77
|
*/
|
|
78
78
|
start(): Promise<{
|
|
79
79
|
port: number;
|
|
80
80
|
host: string;
|
|
81
81
|
}>;
|
|
82
82
|
/**
|
|
83
|
-
*
|
|
83
|
+
* 优雅关闭安全接入代理,立即释放端口与所有客户端 Socket
|
|
84
84
|
*/
|
|
85
85
|
close(): Promise<void>;
|
|
86
86
|
/**
|
package/lib/gateway/server.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DSH
|
|
2
|
+
* DSH 移动端远程独立安全接入代理服务 (GatewayServer)
|
|
3
3
|
*
|
|
4
4
|
* 架构定位:
|
|
5
5
|
* 解决 DSH 0.2.1+ 底层强行禁止 0.0.0.0 监听的问题。
|
|
6
6
|
* DSH 官方 WebServer 继续保持纯粹、安全的 127.0.0.1:3080 本地回环监听(零外部暴露风险);
|
|
7
|
-
* 由本服务在 0.0.0.0:3081
|
|
7
|
+
* 由本服务在 0.0.0.0:3081 启动专用安全接入代理,接纳局域网与 Tailscale 流量,
|
|
8
8
|
* 执行端到端 RSA 加密、扫码配对、慢哈希密码认证与防暴力破解 IP 熔断,
|
|
9
9
|
* 并将合法的流量全双工代理至底座核心服务,彻底解决换 Wi-Fi 崩溃、无法兼顾多网卡与本机回环失效的痛点。
|
|
10
10
|
*/
|
|
@@ -46,7 +46,7 @@ export class GatewayServer {
|
|
|
46
46
|
this.gateMiddleware = gateMiddleware;
|
|
47
47
|
this.logger = logger;
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
49
|
+
/** 获取安全接入代理当前运行状态契约 */
|
|
50
50
|
getStatus() {
|
|
51
51
|
return this.status;
|
|
52
52
|
}
|
|
@@ -54,11 +54,11 @@ export class GatewayServer {
|
|
|
54
54
|
getLastError() {
|
|
55
55
|
return this.lastError;
|
|
56
56
|
}
|
|
57
|
-
/**
|
|
57
|
+
/** 获取安全接入代理实际生效绑定的端口 */
|
|
58
58
|
getBoundPort() {
|
|
59
59
|
return this.boundPort;
|
|
60
60
|
}
|
|
61
|
-
/**
|
|
61
|
+
/** 获取安全接入代理监听的 host */
|
|
62
62
|
getHost() {
|
|
63
63
|
return this.host;
|
|
64
64
|
}
|
|
@@ -113,7 +113,7 @@ export class GatewayServer {
|
|
|
113
113
|
req.headers['x-real-ip']);
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
|
-
*
|
|
116
|
+
* 启动安全接入代理服务(带端口冲突自愈探测与状态机落定)
|
|
117
117
|
*/
|
|
118
118
|
async start() {
|
|
119
119
|
if (this.server && this.status === 'running') {
|
|
@@ -149,7 +149,7 @@ export class GatewayServer {
|
|
|
149
149
|
const addr = server.address();
|
|
150
150
|
this.boundPort = addr && typeof addr === 'object' ? addr.port : currentPort;
|
|
151
151
|
this.status = 'running';
|
|
152
|
-
this.logger?.info?.(`[dsh-remote-mobile]
|
|
152
|
+
this.logger?.info?.(`[dsh-remote-mobile] 安全接入代理已就绪: http://${this.host}:${this.boundPort} -> ${this.coreHost}:${this.corePort}`);
|
|
153
153
|
return { port: this.boundPort, host: this.host };
|
|
154
154
|
}
|
|
155
155
|
catch (err) {
|
|
@@ -166,10 +166,10 @@ export class GatewayServer {
|
|
|
166
166
|
}
|
|
167
167
|
this.status = 'error';
|
|
168
168
|
this.lastError = `尝试端口均被占用 (${this.configuredPort} ~ ${currentPort})`;
|
|
169
|
-
throw new Error(`[dsh-remote-mobile]
|
|
169
|
+
throw new Error(`[dsh-remote-mobile] 启动安全接入代理失败:${this.lastError}`);
|
|
170
170
|
}
|
|
171
171
|
/**
|
|
172
|
-
*
|
|
172
|
+
* 优雅关闭安全接入代理,立即释放端口与所有客户端 Socket
|
|
173
173
|
*/
|
|
174
174
|
async close() {
|
|
175
175
|
if (!this.server) {
|
|
@@ -215,7 +215,7 @@ export class GatewayServer {
|
|
|
215
215
|
return;
|
|
216
216
|
}
|
|
217
217
|
// 网关已完成 Host 白名单校验,通知共享门禁跳过重复 Host 校验
|
|
218
|
-
// (
|
|
218
|
+
// (proxyHost 可能是自定义域名,不属于核心的 trustedHosts / 网卡 IP)
|
|
219
219
|
;
|
|
220
220
|
req.__dsh_rm_host_allowed__ = true;
|
|
221
221
|
// 2. 门禁中间件校验与原因标记获取
|
package/lib/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export * from './auth/token.js';
|
|
|
31
31
|
export * from './auth/gate.js';
|
|
32
32
|
export * from './routes/api.js';
|
|
33
33
|
export * from './bridge/compat.js';
|
|
34
|
+
export * from './bridge/directory-picker.js';
|
|
34
35
|
export * from './styles/style-snippets.js';
|
|
35
36
|
export * from './gateway/detector.js';
|
|
36
37
|
export * from './gateway/server.js';
|
package/lib/index.js
CHANGED
|
@@ -12,17 +12,30 @@
|
|
|
12
12
|
* 7. 配置持久化于插件自有文件 ~/.dsh/remote-mobile/settings.json(DSH 0.1.7+ 起官方已弃用 settings.yaml),
|
|
13
13
|
* 启动时一次性兼容迁移旧 settings.yaml / settings.yaml.imported 中的配置。
|
|
14
14
|
*/
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { installStartupFailureHint } from './startup-hint.js';
|
|
15
17
|
import { SessionStore } from './auth/token.js';
|
|
16
18
|
import { createGlobalAuthGate } from './auth/gate.js';
|
|
17
19
|
import { createRoutes } from './routes/api.js';
|
|
20
|
+
import { registerWebRoutes } from './routes/register.js';
|
|
18
21
|
import { StyleSnippetStore } from './styles/style-snippets.js';
|
|
19
22
|
import { getLocalTailscaleIp, getLocalLanIp } from './auth/tailscale.js';
|
|
20
23
|
import { patchHttpServerWithVirtualizer, mountIndexInjections, registerRemoteWebUiPairingBridge, takeOverConnectionAuth, injectTrustedHosts, } from './bridge/compat.js';
|
|
24
|
+
import { bindDirectoryPickerRuntime, getDirectoryPickerStatus } from './bridge/directory-picker.js';
|
|
21
25
|
import { detectServerMode } from './gateway/detector.js';
|
|
22
26
|
import { GatewayServer } from './gateway/server.js';
|
|
23
27
|
import { generateGatewaySecret, setGatewaySecret, clearGatewaySecret } from './gateway/secret.js';
|
|
28
|
+
// 该 hook 必须在模块顶层注册,才能捕获 webserver 在插件 apply 前的启动失败。
|
|
29
|
+
installStartupFailureHint();
|
|
24
30
|
/** Cordis 插件名称标识 */
|
|
25
31
|
export const name = 'dsh-remote-mobile';
|
|
32
|
+
/** 服务端 Banner 使用的插件版本(从安装包 package.json 读取,避免与客户端版本常量重复维护) */
|
|
33
|
+
let pluginVersion = '';
|
|
34
|
+
try {
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
pluginVersion = String(require('../package.json').version || '');
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
26
39
|
/** DSH 设置命名空间 */
|
|
27
40
|
export const REMOTE_MOBILE_SETTINGS_NAMESPACE = 'dsh-remote-mobile';
|
|
28
41
|
/** Cordis 依赖注入声明:声明注入底座内置的 webServer 服务 */
|
|
@@ -100,12 +113,10 @@ export function apply(ctx, config = {}) {
|
|
|
100
113
|
}
|
|
101
114
|
// 挂载 HTML 模板层的样式与 Polyfill 注入钩子(在 Gzip 压缩前注入)
|
|
102
115
|
mountIndexInjections(ctx, styleStore);
|
|
103
|
-
// 注册插件专属的 HTTP & SSE API
|
|
116
|
+
// 注册插件专属的 HTTP & SSE API 路由。registerWebRoutes 会通过 ctx.effect
|
|
117
|
+
// 收集 DSH WebServer 返回的撤销函数,保证插件重新激活/热重载时不残留路由。
|
|
104
118
|
if (typeof ctx.webServer.register === 'function') {
|
|
105
|
-
|
|
106
|
-
for (const route of routes) {
|
|
107
|
-
ctx.webServer.register(route);
|
|
108
|
-
}
|
|
119
|
+
registerWebRoutes(ctx, createRoutes(store, styleStore));
|
|
109
120
|
}
|
|
110
121
|
}
|
|
111
122
|
// 4. 向 Cordis 容器桥接注册 remoteWebUiPairing 服务(延迟裁决:与其他远程接入
|
|
@@ -121,10 +132,18 @@ export function apply(ctx, config = {}) {
|
|
|
121
132
|
* takeOverConnectionAuth 内部通过 __dsh_rm_auth_takeover__ 标记保证幂等,
|
|
122
133
|
* 同一 connection 实例不会被重复接管。
|
|
123
134
|
*/
|
|
135
|
+
let connectionAuthAttached = false;
|
|
124
136
|
const attachConnectionAuth = (conn) => {
|
|
125
|
-
if (conn)
|
|
137
|
+
if (!conn)
|
|
138
|
+
return false;
|
|
139
|
+
try {
|
|
126
140
|
takeOverConnectionAuth(conn, store);
|
|
141
|
+
connectionAuthAttached = Boolean(conn.__dsh_rm_auth_takeover__);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
connectionAuthAttached = false;
|
|
127
145
|
}
|
|
146
|
+
return connectionAuthAttached;
|
|
128
147
|
};
|
|
129
148
|
// 1) 优先通过 ctx.get 安全获取(未注入时不抛错返回 undefined)
|
|
130
149
|
try {
|
|
@@ -170,9 +189,8 @@ export function apply(ctx, config = {}) {
|
|
|
170
189
|
}
|
|
171
190
|
catch { }
|
|
172
191
|
}
|
|
173
|
-
// 6. 探测 DSH
|
|
174
|
-
//
|
|
175
|
-
// 捕获后回退到 direct 模式,让用户仍可通过 3080 本机/局域网访问并修改配置。
|
|
192
|
+
// 6. 探测 DSH 底座网络暴露姿态:默认 proxy;核心已对外监听时回退 direct。
|
|
193
|
+
// 检测逻辑本身不抛错,异常时仍保留兜底,避免插件 apply 崩溃。
|
|
176
194
|
let modeFallbackError = '';
|
|
177
195
|
let detection;
|
|
178
196
|
try {
|
|
@@ -180,37 +198,44 @@ export function apply(ctx, config = {}) {
|
|
|
180
198
|
}
|
|
181
199
|
catch (err) {
|
|
182
200
|
modeFallbackError = err?.message || String(err);
|
|
183
|
-
ctx.logger?.error?.('[dsh-remote-mobile]
|
|
201
|
+
ctx.logger?.error?.('[dsh-remote-mobile] 安全接入代理配置不可用,已回退 direct 模式:', err);
|
|
184
202
|
detection = detectServerMode(ctx, 'direct', ctx.webServer?.port ?? 3080);
|
|
185
203
|
}
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
// proxy 是默认模式,但核心已对外监听时会回退 direct:
|
|
205
|
+
// - 旧版 webserver 配置(0.0.0.0 / 具体网卡 IP)继续可用;
|
|
206
|
+
// - direct + 回环核心时,外部手机无法直连,仅适合同机反向代理。
|
|
207
|
+
bindDirectoryPickerRuntime(ctx, () => detection.isLoopback);
|
|
208
|
+
const requestedMode = store.getOptions().mode ?? 'proxy';
|
|
209
|
+
const legacyDirectFallback = requestedMode === 'proxy' && detection.mode === 'direct' && !detection.isLoopback;
|
|
210
|
+
const directLoopbackWarning = detection.mode === 'direct' && detection.isLoopback;
|
|
211
|
+
const modeWarning = legacyDirectFallback
|
|
212
|
+
? `检测到底座监听 ${detection.rawCoreHost},已自动使用 direct 模式;3080 直接对外,如需 proxy 请恢复 127.0.0.1 监听。`
|
|
213
|
+
: directLoopbackWarning
|
|
214
|
+
? 'direct 模式 + 回环核心:外部手机无法直连,仅适合同机反向代理。'
|
|
215
|
+
: '';
|
|
191
216
|
let gatewayServer = null;
|
|
192
217
|
let gatewayReadyPromise = null;
|
|
193
|
-
if (detection.mode === '
|
|
218
|
+
if (detection.mode === 'proxy') {
|
|
194
219
|
const opts = store.getOptions();
|
|
195
220
|
gatewayServer = new GatewayServer({
|
|
196
|
-
port: opts.
|
|
197
|
-
host: opts.
|
|
221
|
+
port: opts.proxyPort ?? 3081,
|
|
222
|
+
host: opts.proxyHost ?? '0.0.0.0',
|
|
198
223
|
corePort: detection.corePort,
|
|
199
224
|
coreHost: detection.coreHost,
|
|
200
225
|
}, store, gateMiddleware, ctx.logger);
|
|
201
226
|
// 初始标记为正在启动中
|
|
202
|
-
store.setActiveGatewayInfo('
|
|
227
|
+
store.setActiveGatewayInfo('proxy', opts.proxyPort ?? 3081, detection.corePort, false);
|
|
203
228
|
gatewayReadyPromise = gatewayServer.start().then(({ port }) => {
|
|
204
229
|
if (gatewayServer?.getServer()) {
|
|
205
230
|
setGatewaySecret(gatewayServer.getServer(), gatewaySecret);
|
|
206
231
|
}
|
|
207
|
-
store.setActiveGatewayInfo('
|
|
232
|
+
store.setActiveGatewayInfo('proxy', port, detection.corePort, true);
|
|
208
233
|
syncTrustedHosts();
|
|
209
234
|
return port;
|
|
210
235
|
}).catch((err) => {
|
|
211
|
-
store.setActiveGatewayInfo('
|
|
212
|
-
ctx.logger?.error?.('[dsh-remote-mobile]
|
|
213
|
-
return opts.
|
|
236
|
+
store.setActiveGatewayInfo('proxy', opts.proxyPort ?? 3081, detection.corePort, false, err?.message || String(err));
|
|
237
|
+
ctx.logger?.error?.('[dsh-remote-mobile] 安全接入代理启动失败:', err);
|
|
238
|
+
return opts.proxyPort ?? 3081;
|
|
214
239
|
});
|
|
215
240
|
}
|
|
216
241
|
else {
|
|
@@ -245,7 +270,7 @@ export function apply(ctx, config = {}) {
|
|
|
245
270
|
if (disposed || printed)
|
|
246
271
|
return;
|
|
247
272
|
printed = true;
|
|
248
|
-
//
|
|
273
|
+
// 等待安全接入代理就绪以获得准确的对外端口(避免端口自增探测竞态)
|
|
249
274
|
try {
|
|
250
275
|
if (gatewayReadyPromise) {
|
|
251
276
|
await Promise.race([
|
|
@@ -279,21 +304,34 @@ export function apply(ctx, config = {}) {
|
|
|
279
304
|
const activePort = store.getActivePort();
|
|
280
305
|
const activeMode = store.getActiveMode();
|
|
281
306
|
const corePort = store.getCorePort();
|
|
307
|
+
const coreHost = detection.coreHost;
|
|
282
308
|
const opts = store.getOptions();
|
|
283
|
-
const
|
|
309
|
+
const proxyHost = opts.proxyHost ?? '0.0.0.0';
|
|
310
|
+
const proxyStatus = store.getGatewayStatus();
|
|
311
|
+
const versionLabel = pluginVersion ? `v${pluginVersion}` : 'unknown';
|
|
312
|
+
const coreLabel = detection.isLoopback ? '回环' : '对外监听';
|
|
313
|
+
const modeLabel = activeMode === 'proxy'
|
|
314
|
+
? 'proxy(默认)'
|
|
315
|
+
: legacyDirectFallback
|
|
316
|
+
? 'direct(旧配置回退)'
|
|
317
|
+
: 'direct(显式配置)';
|
|
284
318
|
const divider = '─'.repeat(72);
|
|
285
319
|
console.log(`\n┌${divider}`);
|
|
286
|
-
console.log(
|
|
320
|
+
console.log(connectionAuthAttached
|
|
321
|
+
? `│ [dsh-remote-mobile] ${versionLabel} 官方临时 Token 鉴权已接管,PC/移动端访问无需携带官方token`
|
|
322
|
+
: `│ [dsh-remote-mobile] ${versionLabel} ⚠️ 官方临时 Token 鉴权接管未完成,请检查 DSH 版本或启动日志`);
|
|
323
|
+
console.log(`│ DSH 核心: ${detection.rawCoreHost}:${corePort} (${coreLabel})`);
|
|
324
|
+
console.log(`│ 运行模式: ${modeLabel}`);
|
|
287
325
|
if (modeFallbackError) {
|
|
288
|
-
console.log(`│ ⚠️
|
|
326
|
+
console.log(`│ ⚠️ 安全接入代理配置不可用,已回退 direct 模式: ${modeFallbackError}`);
|
|
289
327
|
}
|
|
290
328
|
if (modeWarning) {
|
|
291
329
|
console.log(`│ ⚠️ ${modeWarning}`);
|
|
292
330
|
}
|
|
293
|
-
if (activeMode === '
|
|
294
|
-
if (
|
|
295
|
-
console.log(`│
|
|
296
|
-
console.log(`│
|
|
331
|
+
if (activeMode === 'proxy') {
|
|
332
|
+
if (proxyStatus.running) {
|
|
333
|
+
console.log(`│ 安全接入代理已激活 (${proxyHost}:${activePort} -> ${coreHost}:${corePort})`);
|
|
334
|
+
console.log(`│ 手机/远程设备请访问安全接入代理地址:`);
|
|
297
335
|
if (lanIp && lanIp !== '127.0.0.1') {
|
|
298
336
|
console.log(`│ • 局域网: http://${lanIp}:${activePort} (同 Wi-Fi 局域网直连)`);
|
|
299
337
|
}
|
|
@@ -301,17 +339,22 @@ export function apply(ctx, config = {}) {
|
|
|
301
339
|
console.log(`│ • 虚拟私网: http://${tsIp}:${activePort} (Tailscale 私网异地直连)`);
|
|
302
340
|
}
|
|
303
341
|
console.log(`│ 电脑本机浏览器访问控制台:`);
|
|
304
|
-
console.log(`│ •
|
|
342
|
+
console.log(`│ • 核心直连: http://127.0.0.1:${corePort}`);
|
|
343
|
+
if (proxyHost === '0.0.0.0' || proxyHost === '::' || proxyHost === '127.0.0.1' || proxyHost === 'localhost') {
|
|
344
|
+
const localProxyHost = proxyHost === '::' ? '[::1]' : '127.0.0.1';
|
|
345
|
+
console.log(`│ • 代理入口: http://${localProxyHost}:${activePort}`);
|
|
346
|
+
console.log(`│ 本机直连不校验配对码/长期密码`);
|
|
347
|
+
}
|
|
305
348
|
}
|
|
306
349
|
else {
|
|
307
|
-
console.log(`│ ⚠️
|
|
308
|
-
console.log(`│ 外部手机设备当前暂时无法连接,请检查 3081 端口占用`);
|
|
350
|
+
console.log(`│ ⚠️ 安全接入代理未成功启动: ${proxyStatus.error || '端口占用'}`);
|
|
351
|
+
console.log(`│ 外部手机设备当前暂时无法连接,请检查 ${opts.proxyPort ?? 3081} 端口占用`);
|
|
309
352
|
console.log(`│ 电脑本机浏览器仍可直接访问:`);
|
|
310
353
|
console.log(`│ • 本机回环: http://127.0.0.1:${corePort}`);
|
|
311
354
|
}
|
|
312
355
|
}
|
|
313
356
|
else {
|
|
314
|
-
console.log(`│
|
|
357
|
+
console.log(`│ 直连模式已启用(无独立安全接入代理),支持手机浏览器访问:`);
|
|
315
358
|
console.log(`│ • 本机回环: http://127.0.0.1:${corePort}`);
|
|
316
359
|
if (lanIp && lanIp !== '127.0.0.1') {
|
|
317
360
|
console.log(`│ • 局域网: http://${lanIp}:${corePort} (同 Wi-Fi 局域网直连)`);
|
|
@@ -320,10 +363,17 @@ export function apply(ctx, config = {}) {
|
|
|
320
363
|
console.log(`│ • 虚拟私网: http://${tsIp}:${corePort} (Tailscale 私网异地直连)`);
|
|
321
364
|
}
|
|
322
365
|
}
|
|
366
|
+
const dirPickerStatus = getDirectoryPickerStatus();
|
|
367
|
+
if (dirPickerStatus.needsWarning) {
|
|
368
|
+
console.log(`│ ⚠️ 移动端选目录提示: 当前未开启网页内目录选择器,手机点「选择文件夹」会在电脑弹窗`);
|
|
369
|
+
console.log(`│ 可在 Web「设置」->「远程与移动端」顶部一键开启网页目录选择器`);
|
|
370
|
+
}
|
|
323
371
|
console.log(`│ 访问与配置说明:`);
|
|
324
372
|
console.log(`│ - 手机/远程设备打开对应地址即可直接访问`);
|
|
325
373
|
console.log(`│ - 在本机打开 Web 界面 ->「设置」->「远程与移动端」进行安全管理:`);
|
|
326
374
|
console.log(`│ 可开启免密直连(局域网/Tailscale)、查看 6 位临时配对码或设置长期密码`);
|
|
375
|
+
console.log(`│ - 高级接入参数: settings.json 中的 mode / proxyPort / proxyHost / trustedHosts`);
|
|
376
|
+
console.log(`│ 修改后需要重启 DSH`);
|
|
327
377
|
console.log(`│ - 项目文档: https://github.com/IceApriler/dsh-remote-mobile`);
|
|
328
378
|
console.log(`└${divider}\n`);
|
|
329
379
|
ctx.logger?.info?.(`[dsh-remote-mobile] 移动端远程控制与安全门禁已激活 (运行模式: ${activeMode}, 对外端口: ${activePort})`);
|
|
@@ -359,6 +409,7 @@ export * from './auth/token.js';
|
|
|
359
409
|
export * from './auth/gate.js';
|
|
360
410
|
export * from './routes/api.js';
|
|
361
411
|
export * from './bridge/compat.js';
|
|
412
|
+
export * from './bridge/directory-picker.js';
|
|
362
413
|
export * from './styles/style-snippets.js';
|
|
363
414
|
export * from './gateway/detector.js';
|
|
364
415
|
export * from './gateway/server.js';
|
package/lib/routes/api.d.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import { type SessionStore } from '../auth/token.js';
|
|
3
3
|
import { type StyleSnippetStore } from '../styles/style-snippets.js';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* DSH 0.2.1+ WebServer 的路由契约:`kind` 决定注册到 exact 还是 prefixes 表。
|
|
6
|
+
* 当前所有接口都是字面路径,因此统一使用 exact;`method` 保留给旧宿主兼容。
|
|
7
|
+
*/
|
|
8
|
+
export interface WebRoute {
|
|
9
|
+
kind: 'exact' | 'prefix';
|
|
10
|
+
/** 旧版 DSH 使用;DSH 0.2.1+ 的 WebServer 不按 method 分派。 */
|
|
11
|
+
method?: 'GET' | 'POST';
|
|
6
12
|
path: string;
|
|
7
|
-
handler: (
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
path: string;
|
|
15
|
-
handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
16
|
-
})[];
|
|
13
|
+
handler: (req: IncomingMessage, res: ServerResponse) => unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface CreateRoutesOptions {
|
|
16
|
+
/** 测试环境注入的自定义 cordis.patch.yml 路径(避免污染宿主配置) */
|
|
17
|
+
patchFilePath?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function createRoutes(store: SessionStore, styleStore?: StyleSnippetStore, routeOptions?: CreateRoutesOptions): WebRoute[];
|