@yezack/dsh-remote-mobile 1.9.2 → 1.10.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/CHANGELOG.md +64 -2
- package/README.md +109 -34
- package/README_EN.md +115 -40
- package/lib/auth/crypto.js +3 -1
- package/lib/auth/gate.d.ts +39 -9
- package/lib/auth/gate.js +231 -33
- package/lib/auth/tailscale.d.ts +24 -2
- package/lib/auth/tailscale.js +38 -3
- package/lib/auth/token.d.ts +22 -0
- package/lib/auth/token.js +58 -2
- package/lib/bridge/compat.d.ts +22 -2
- package/lib/bridge/compat.js +579 -336
- package/lib/client.js +67 -14
- package/lib/gateway/detector.d.ts +42 -0
- package/lib/gateway/detector.js +88 -0
- package/lib/gateway/secret.d.ts +23 -0
- package/lib/gateway/secret.js +65 -0
- package/lib/gateway/server.d.ts +108 -0
- package/lib/gateway/server.js +568 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +143 -15
- package/lib/routes/api.js +16 -10
- package/lib/routes/login-page.js +399 -398
- package/lib/styles/style-snippets.js +639 -639
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -17,7 +17,10 @@ import { createGlobalAuthGate } from './auth/gate.js';
|
|
|
17
17
|
import { createRoutes } from './routes/api.js';
|
|
18
18
|
import { StyleSnippetStore } from './styles/style-snippets.js';
|
|
19
19
|
import { getLocalTailscaleIp, getLocalLanIp } from './auth/tailscale.js';
|
|
20
|
-
import { patchHttpServerWithVirtualizer, mountIndexInjections, registerRemoteWebUiPairingBridge, takeOverConnectionAuth, } from './bridge/compat.js';
|
|
20
|
+
import { patchHttpServerWithVirtualizer, mountIndexInjections, registerRemoteWebUiPairingBridge, takeOverConnectionAuth, injectTrustedHosts, } from './bridge/compat.js';
|
|
21
|
+
import { detectServerMode } from './gateway/detector.js';
|
|
22
|
+
import { GatewayServer } from './gateway/server.js';
|
|
23
|
+
import { generateGatewaySecret, setGatewaySecret, clearGatewaySecret } from './gateway/secret.js';
|
|
21
24
|
/** Cordis 插件名称标识 */
|
|
22
25
|
export const name = 'dsh-remote-mobile';
|
|
23
26
|
/** DSH 设置命名空间 */
|
|
@@ -79,10 +82,15 @@ function bindDshSettings(ctx, store) {
|
|
|
79
82
|
* @param config - 插件持久化配置选项 (SessionStoreOptions)
|
|
80
83
|
*/
|
|
81
84
|
export function apply(ctx, config = {}) {
|
|
82
|
-
// 1.
|
|
85
|
+
// 1. 初始化会话与令牌持久化管理器及内部握手密钥
|
|
83
86
|
const store = new SessionStore(config);
|
|
84
87
|
const styleStore = new StyleSnippetStore();
|
|
85
88
|
const gateMiddleware = createGlobalAuthGate(store);
|
|
89
|
+
// 为本次插件生命周期生成高强度握手 Secret,并绑定到底座 Server
|
|
90
|
+
const gatewaySecret = generateGatewaySecret();
|
|
91
|
+
if (ctx.webServer?.server) {
|
|
92
|
+
setGatewaySecret(ctx.webServer.server, gatewaySecret);
|
|
93
|
+
}
|
|
86
94
|
// 2. 接入 DSH Settings 服务 (命名空间: dsh-remote-mobile)
|
|
87
95
|
bindDshSettings(ctx, store);
|
|
88
96
|
// 3. 挂载底座 WebServer 相关的门禁拦截与路由注册
|
|
@@ -90,8 +98,8 @@ export function apply(ctx, config = {}) {
|
|
|
90
98
|
if (ctx.webServer.server) {
|
|
91
99
|
patchHttpServerWithVirtualizer(ctx.webServer.server, gateMiddleware, store, styleStore);
|
|
92
100
|
}
|
|
93
|
-
// 挂载 HTML
|
|
94
|
-
mountIndexInjections(ctx);
|
|
101
|
+
// 挂载 HTML 模板层的样式与 Polyfill 注入钩子(在 Gzip 压缩前注入)
|
|
102
|
+
mountIndexInjections(ctx, styleStore);
|
|
95
103
|
// 注册插件专属的 HTTP & SSE API 路由
|
|
96
104
|
if (typeof ctx.webServer.register === 'function') {
|
|
97
105
|
const routes = createRoutes(store, styleStore);
|
|
@@ -146,7 +154,70 @@ export function apply(ctx, config = {}) {
|
|
|
146
154
|
ctx.logger?.debug?.('[dsh-remote-mobile] 注册 connection 动态注入失败:', e);
|
|
147
155
|
}
|
|
148
156
|
}
|
|
149
|
-
//
|
|
157
|
+
// 动态同步受信任主机到所有底层运行时(webRuntime、connection、webStartup)
|
|
158
|
+
const syncTrustedHosts = (target) => {
|
|
159
|
+
try {
|
|
160
|
+
injectTrustedHosts(target || ctx, [store.getActivePort(), 3080, 3081], store.getOptions().trustedHosts);
|
|
161
|
+
}
|
|
162
|
+
catch { }
|
|
163
|
+
};
|
|
164
|
+
syncTrustedHosts();
|
|
165
|
+
if (typeof ctx.inject === 'function') {
|
|
166
|
+
try {
|
|
167
|
+
ctx.inject(['webRuntime'], (runtimeCtx) => {
|
|
168
|
+
syncTrustedHosts(runtimeCtx);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch { }
|
|
172
|
+
}
|
|
173
|
+
// 6. 探测 DSH 底座网络暴露姿态,自适应裁决是否拉起独立安全反向代理网关
|
|
174
|
+
// 配置错误(如 mode: 'gateway' + 具体 LAN IP)不能把整个插件 apply 抛崩;
|
|
175
|
+
// 捕获后回退到 direct 模式,让用户仍可通过 3080 本机/局域网访问并修改配置。
|
|
176
|
+
let modeFallbackError = '';
|
|
177
|
+
let detection;
|
|
178
|
+
try {
|
|
179
|
+
detection = detectServerMode(ctx, store.getOptions().mode, ctx.webServer?.port ?? 3080);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
modeFallbackError = err?.message || String(err);
|
|
183
|
+
ctx.logger?.error?.('[dsh-remote-mobile] 网关模式不可用,已回退 direct 模式:', err);
|
|
184
|
+
detection = detectServerMode(ctx, 'direct', ctx.webServer?.port ?? 3080);
|
|
185
|
+
}
|
|
186
|
+
// 强制 gateway 且底座已监听通配地址时,输出安全提醒:核心 3080 仍暴露,
|
|
187
|
+
// 网关只是附加入口,不能替代核心侧的 Host/Origin 防护。
|
|
188
|
+
const modeWarning = detection.mode === 'gateway' && !detection.isLoopback
|
|
189
|
+
? `底座本身已监听 ${detection.rawCoreHost},3080 仍是对外入口;网关 3081 为附加入口,建议优先恢复 127.0.0.1 监听。`
|
|
190
|
+
: '';
|
|
191
|
+
let gatewayServer = null;
|
|
192
|
+
let gatewayReadyPromise = null;
|
|
193
|
+
if (detection.mode === 'gateway') {
|
|
194
|
+
const opts = store.getOptions();
|
|
195
|
+
gatewayServer = new GatewayServer({
|
|
196
|
+
port: opts.gatewayPort ?? 3081,
|
|
197
|
+
host: opts.gatewayHost ?? '0.0.0.0',
|
|
198
|
+
corePort: detection.corePort,
|
|
199
|
+
coreHost: detection.coreHost,
|
|
200
|
+
}, store, gateMiddleware, ctx.logger);
|
|
201
|
+
// 初始标记为正在启动中
|
|
202
|
+
store.setActiveGatewayInfo('gateway', opts.gatewayPort ?? 3081, detection.corePort, false);
|
|
203
|
+
gatewayReadyPromise = gatewayServer.start().then(({ port }) => {
|
|
204
|
+
if (gatewayServer?.getServer()) {
|
|
205
|
+
setGatewaySecret(gatewayServer.getServer(), gatewaySecret);
|
|
206
|
+
}
|
|
207
|
+
store.setActiveGatewayInfo('gateway', port, detection.corePort, true);
|
|
208
|
+
syncTrustedHosts();
|
|
209
|
+
return port;
|
|
210
|
+
}).catch((err) => {
|
|
211
|
+
store.setActiveGatewayInfo('gateway', opts.gatewayPort ?? 3081, detection.corePort, false, err?.message || String(err));
|
|
212
|
+
ctx.logger?.error?.('[dsh-remote-mobile] 独立安全反向代理网关启动失败:', err);
|
|
213
|
+
return opts.gatewayPort ?? 3081;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
store.setActiveGatewayInfo('direct', detection.corePort, detection.corePort, true);
|
|
218
|
+
gatewayReadyPromise = Promise.resolve(detection.corePort);
|
|
219
|
+
}
|
|
220
|
+
// 7. 服务就绪时输出提示横幅与接入地址
|
|
150
221
|
let printed = false;
|
|
151
222
|
let disposed = false;
|
|
152
223
|
let timer1 = null;
|
|
@@ -158,18 +229,42 @@ export function apply(ctx, config = {}) {
|
|
|
158
229
|
clearTimeout(timer1);
|
|
159
230
|
if (timer2)
|
|
160
231
|
clearTimeout(timer2);
|
|
232
|
+
if (gatewayServer) {
|
|
233
|
+
clearGatewaySecret(gatewayServer.getServer());
|
|
234
|
+
gatewayServer.close().catch(() => { });
|
|
235
|
+
}
|
|
236
|
+
const webServer = (typeof ctx.get === 'function' ? ctx.get('webServer') : undefined) || ctx.webServer;
|
|
237
|
+
if (webServer?.server) {
|
|
238
|
+
clearGatewaySecret(webServer.server);
|
|
239
|
+
}
|
|
161
240
|
});
|
|
162
241
|
}
|
|
163
242
|
catch { }
|
|
164
|
-
const printReadyBanner = () => {
|
|
243
|
+
const printReadyBanner = async () => {
|
|
165
244
|
// 生命周期守卫:若上下文已废弃/进入 dispose,放弃回调
|
|
166
245
|
if (disposed || printed)
|
|
167
246
|
return;
|
|
168
247
|
printed = true;
|
|
248
|
+
// 等待网关就绪以获得准确的对外端口(避免端口自增探测竞态)
|
|
249
|
+
try {
|
|
250
|
+
if (gatewayReadyPromise) {
|
|
251
|
+
await Promise.race([
|
|
252
|
+
gatewayReadyPromise,
|
|
253
|
+
new Promise((r) => setTimeout(r, 2000)),
|
|
254
|
+
]);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch { }
|
|
258
|
+
// 等待期间插件可能已被 dispose / 热重载;此时绝不能再 setGatewaySecret / patch / mount
|
|
259
|
+
if (disposed)
|
|
260
|
+
return;
|
|
169
261
|
try {
|
|
262
|
+
syncTrustedHosts();
|
|
170
263
|
const webServer = (typeof ctx.get === 'function' ? ctx.get('webServer') : undefined) || ctx.webServer;
|
|
171
264
|
if (webServer?.server) {
|
|
265
|
+
setGatewaySecret(webServer.server, gatewaySecret);
|
|
172
266
|
patchHttpServerWithVirtualizer(webServer.server, gateMiddleware, store, styleStore);
|
|
267
|
+
mountIndexInjections(ctx, styleStore);
|
|
173
268
|
}
|
|
174
269
|
try {
|
|
175
270
|
const conn = typeof ctx.get === 'function' ? ctx.get('connection') : undefined;
|
|
@@ -181,18 +276,49 @@ export function apply(ctx, config = {}) {
|
|
|
181
276
|
}
|
|
182
277
|
const tsIp = getLocalTailscaleIp();
|
|
183
278
|
const lanIp = getLocalLanIp();
|
|
184
|
-
const
|
|
279
|
+
const activePort = store.getActivePort();
|
|
280
|
+
const activeMode = store.getActiveMode();
|
|
281
|
+
const corePort = store.getCorePort();
|
|
185
282
|
const opts = store.getOptions();
|
|
283
|
+
const gwStatus = store.getGatewayStatus();
|
|
186
284
|
const divider = '─'.repeat(72);
|
|
187
285
|
console.log(`\n┌${divider}`);
|
|
188
286
|
console.log(`│ [dsh-remote-mobile] 官方临时 Token 鉴权已接管,PC/移动端访问无需携带官方token`);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (lanIp && lanIp !== '127.0.0.1') {
|
|
192
|
-
console.log(`│ • 局域网: http://${lanIp}:${port} (同 Wi-Fi 局域网直连)`);
|
|
287
|
+
if (modeFallbackError) {
|
|
288
|
+
console.log(`│ ⚠️ 网关配置不可用,已回退 direct 模式: ${modeFallbackError}`);
|
|
193
289
|
}
|
|
194
|
-
if (
|
|
195
|
-
console.log(`│
|
|
290
|
+
if (modeWarning) {
|
|
291
|
+
console.log(`│ ⚠️ ${modeWarning}`);
|
|
292
|
+
}
|
|
293
|
+
if (activeMode === 'gateway') {
|
|
294
|
+
if (gwStatus.running) {
|
|
295
|
+
console.log(`│ 独立安全反代网关已激活 (0.0.0.0:${activePort} -> 127.0.0.1:${corePort})`);
|
|
296
|
+
console.log(`│ 手机/远程设备请访问安全网关地址:`);
|
|
297
|
+
if (lanIp && lanIp !== '127.0.0.1') {
|
|
298
|
+
console.log(`│ • 局域网: http://${lanIp}:${activePort} (同 Wi-Fi 局域网直连)`);
|
|
299
|
+
}
|
|
300
|
+
if (tsIp) {
|
|
301
|
+
console.log(`│ • 虚拟私网: http://${tsIp}:${activePort} (Tailscale 私网异地直连)`);
|
|
302
|
+
}
|
|
303
|
+
console.log(`│ 电脑本机浏览器访问控制台:`);
|
|
304
|
+
console.log(`│ • 本机回环: http://127.0.0.1:${corePort}`);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
console.log(`│ ⚠️ 独立安全反代网关未成功启动: ${gwStatus.error || '端口占用'}`);
|
|
308
|
+
console.log(`│ 外部手机设备当前暂时无法连接,请检查 3081 端口占用`);
|
|
309
|
+
console.log(`│ 电脑本机浏览器仍可直接访问:`);
|
|
310
|
+
console.log(`│ • 本机回环: http://127.0.0.1:${corePort}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
console.log(`│ 远程安全网关已就绪,支持手机浏览器访问:`);
|
|
315
|
+
console.log(`│ • 本机回环: http://127.0.0.1:${corePort}`);
|
|
316
|
+
if (lanIp && lanIp !== '127.0.0.1') {
|
|
317
|
+
console.log(`│ • 局域网: http://${lanIp}:${corePort} (同 Wi-Fi 局域网直连)`);
|
|
318
|
+
}
|
|
319
|
+
if (tsIp) {
|
|
320
|
+
console.log(`│ • 虚拟私网: http://${tsIp}:${corePort} (Tailscale 私网异地直连)`);
|
|
321
|
+
}
|
|
196
322
|
}
|
|
197
323
|
console.log(`│ 访问与配置说明:`);
|
|
198
324
|
console.log(`│ - 手机/远程设备打开对应地址即可直接访问`);
|
|
@@ -200,9 +326,9 @@ export function apply(ctx, config = {}) {
|
|
|
200
326
|
console.log(`│ 可开启免密直连(局域网/Tailscale)、查看 6 位临时配对码或设置长期密码`);
|
|
201
327
|
console.log(`│ - 项目文档: https://github.com/IceApriler/dsh-remote-mobile`);
|
|
202
328
|
console.log(`└${divider}\n`);
|
|
203
|
-
ctx.logger?.info?.(`[dsh-remote-mobile] 移动端远程控制与安全门禁已激活 (
|
|
329
|
+
ctx.logger?.info?.(`[dsh-remote-mobile] 移动端远程控制与安全门禁已激活 (运行模式: ${activeMode}, 对外端口: ${activePort})`);
|
|
204
330
|
if (tsIp) {
|
|
205
|
-
ctx.logger?.info?.(`[dsh-remote-mobile] Tailscale 地址: http://${tsIp}:${
|
|
331
|
+
ctx.logger?.info?.(`[dsh-remote-mobile] Tailscale 地址: http://${tsIp}:${activePort} (免密直连: ${opts.allowTailscale ? '已开启' : '已关闭'})`);
|
|
206
332
|
}
|
|
207
333
|
}
|
|
208
334
|
catch (e) {
|
|
@@ -234,3 +360,5 @@ export * from './auth/gate.js';
|
|
|
234
360
|
export * from './routes/api.js';
|
|
235
361
|
export * from './bridge/compat.js';
|
|
236
362
|
export * from './styles/style-snippets.js';
|
|
363
|
+
export * from './gateway/detector.js';
|
|
364
|
+
export * from './gateway/server.js';
|
package/lib/routes/api.js
CHANGED
|
@@ -86,6 +86,12 @@ export function createRoutes(store, styleStore) {
|
|
|
86
86
|
pairingBridgeMode: bridgeState.mode,
|
|
87
87
|
pairingBridgeConflict: bridgeState.conflictWith || '',
|
|
88
88
|
pairingBridgeConflictId: bridgeState.conflictEntryId || '',
|
|
89
|
+
// 独立安全网关运行状态
|
|
90
|
+
gateway: {
|
|
91
|
+
...store.getGatewayStatus(),
|
|
92
|
+
activePort: store.getActivePort(),
|
|
93
|
+
gatewayPort: opts.gatewayPort || 3081,
|
|
94
|
+
},
|
|
89
95
|
});
|
|
90
96
|
},
|
|
91
97
|
},
|
|
@@ -298,16 +304,16 @@ export function createRoutes(store, styleStore) {
|
|
|
298
304
|
const visitCheck = store.recordAuthVisit(clientIp, ua);
|
|
299
305
|
if (!visitCheck.allowed) {
|
|
300
306
|
res.writeHead(429, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
301
|
-
return res.end(`
|
|
302
|
-
<!DOCTYPE html>
|
|
303
|
-
<html>
|
|
304
|
-
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>安全限制 - DSH</title>
|
|
305
|
-
<style>body{background:#0f172a;color:#fff;font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;padding:20px;box-sizing:border-box;text-align:center;}
|
|
306
|
-
.card{background:rgba(255,255,255,0.06);border:1px solid rgba(239,68,68,0.4);border-radius:16px;padding:28px 20px;max-width:380px;box-shadow:0 8px 32px rgba(0,0,0,0.4);}
|
|
307
|
-
h2{color:#ef4444;margin:0 0 10px 0;font-size:20px;}p{color:#94a3b8;font-size:14px;line-height:1.6;margin:0;}</style>
|
|
308
|
-
</head>
|
|
309
|
-
<body><div class="card"><h2>⚠️ 访问限制 (429)</h2><p>${visitCheck.reason || '当前 IP 请求过于频繁或已被临时锁定,请稍后刷新重试。'}</p></div></body>
|
|
310
|
-
</html>
|
|
307
|
+
return res.end(`
|
|
308
|
+
<!DOCTYPE html>
|
|
309
|
+
<html>
|
|
310
|
+
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>安全限制 - DSH</title>
|
|
311
|
+
<style>body{background:#0f172a;color:#fff;font-family:-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;padding:20px;box-sizing:border-box;text-align:center;}
|
|
312
|
+
.card{background:rgba(255,255,255,0.06);border:1px solid rgba(239,68,68,0.4);border-radius:16px;padding:28px 20px;max-width:380px;box-shadow:0 8px 32px rgba(0,0,0,0.4);}
|
|
313
|
+
h2{color:#ef4444;margin:0 0 10px 0;font-size:20px;}p{color:#94a3b8;font-size:14px;line-height:1.6;margin:0;}</style>
|
|
314
|
+
</head>
|
|
315
|
+
<body><div class="card"><h2>⚠️ 访问限制 (429)</h2><p>${visitCheck.reason || '当前 IP 请求过于频繁或已被临时锁定,请稍后刷新重试。'}</p></div></body>
|
|
316
|
+
</html>
|
|
311
317
|
`);
|
|
312
318
|
}
|
|
313
319
|
const acceptLang = req.headers['accept-language'] || '';
|