dsh-mobile-plugin 0.5.11 → 0.5.12

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.
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  const fs = require('node:fs');
20
+ const net = require('node:net');
20
21
  const path = require('node:path');
21
22
  const QRCode = require('qrcode');
22
23
  const { randomBytes } = require('node:crypto');
@@ -32,7 +33,7 @@ const { canListen } = require('../relay/port-allocator.cjs');
32
33
  // ⚠️ 面板里**能被选中的**那份仍然只能是 `lan`(没有私网时才退 `public`)——它与
33
34
  // `advertisedCandidates` 同口径。IPv6 那份只**显示**、不进那个选择:它由手机侧实测择优
34
35
  // (和公网中转比速度),做成可选中等于骗人(同 A3-40 的刻意设计)。
35
- const { lanCandidates, publicCandidates, ipv6Candidates, withManualIpv6Candidate } = require('../relay/lan-network.cjs');
36
+ const { lanCandidates, publicCandidates, ipv6Candidates, withManualIpv6Candidate, isPrivateLanAddress } = require('../relay/lan-network.cjs');
36
37
  // 「手机连不上时哪一环断了」这份连接自检:本机只读、不改任何东西(口径见
37
38
  // connection-check.js 与 firewall-probe.js 的文件头)。
38
39
  const { createConnectionCheck } = require('../relay/connection-check.js');
@@ -204,6 +205,42 @@ function panelUrlOf(req) {
204
205
  return `${forwarded || 'http'}://${host}/api/relay/panel`;
205
206
  }
206
207
 
208
+ /**
209
+ * 面板地址落在哪一类网络上:`loopback` / `private` / `public`(判不出来时算 `public`)。
210
+ *
211
+ * 侧边栏那颗手机图标(现在它是进入网页的唯一入口)据此决定点下去要不要先开两小时窗口:
212
+ *
213
+ * - `loopback`:人就在这台机器的 DSH 设置页里,控制面本来就认它,直接跳转;
214
+ * - `private`:同一个局域网里的另一台设备 —— 控制面不认它,得先开窗口,但用户点的就是
215
+ * 自己眼前的界面,不再多弹一次确认;
216
+ * - `public`:公网部署,地址谁都能拨,所以先弹一句确认再开。
217
+ *
218
+ * **域名一律按 `public` 算**:内网域名、内网穿透域名长得跟公网域名一样,这里分不出来。
219
+ * 猜错的代价是「多弹一次确认」,反过来猜错就是「不知不觉把公网管理口开了两小时」。
220
+ */
221
+ function panelScopeOf(req) {
222
+ const host = typeof req?.headers?.host === 'string' ? req.headers.host : '';
223
+ if (!host) return 'public';
224
+ let hostname = '';
225
+ try {
226
+ hostname = new URL(`http://${host}`).hostname.toLowerCase().replace(/^\[|\]$/g, '');
227
+ } catch {
228
+ return 'public';
229
+ }
230
+ if (hostname === 'localhost') return 'loopback';
231
+ if (net.isIP(hostname) === 4) {
232
+ if (/^127\./.test(hostname)) return 'loopback';
233
+ return isPrivateLanAddress(hostname) ? 'private' : 'public';
234
+ }
235
+ if (net.isIP(hostname) === 6) {
236
+ if (hostname === '::1') return 'loopback';
237
+ // fc00::/7 是唯一本地地址(私网),fe80::/10 是链路本地:都只在一个局域网里有意义。
238
+ if (/^f[cd][0-9a-f]{2}:/.test(hostname) || /^fe[89ab][0-9a-f]:/.test(hostname)) return 'private';
239
+ return 'public';
240
+ }
241
+ return 'public';
242
+ }
243
+
207
244
  /**
208
245
  * @param {object} deps 见 `lib/impl.js` 里 registerControlPlaneRoutes({...}) 的调用点
209
246
  */
@@ -431,7 +468,13 @@ function registerControlPlaneRoutes(deps) {
431
468
  }) })
432
469
  // 只有可信本机设置入口可以开 / 关窗口;窗口状态仅驻留进程内,DSH 重启后必定关闭。
433
470
  registerRoute({ kind: 'exact', path: '/api/relay/web-access', handler: localOnly('POST', async (req, res) => {
434
- if (typeof trustedLocalUi !== 'function' || !trustedLocalUi(req)) return sendJson(res, 403, { ok: false, error: { code: 'local_ui_only', message: '请从 DSH 本机设置页管理网页访问' } });
471
+ // 谁来按这个开关(2026-11-02 用户拍板放宽,取代原先「只认可信本机设置页」)。
472
+ //
473
+ // 侧边栏那颗手机图标成为进网页的唯一入口之后,公网 / 局域网部署的人就是在自己的浏览器里
474
+ // 点它 —— 那时 socket 对端与 Host 都不是回环,按老判据会 403,于是谁也没法把窗口打开,
475
+ // 面板永远回 423。现在只要求 `localOnly` 已经保证的那一层:同源、不带中转凭据、不经本机
476
+ // 代理转发。能发出这条请求的只有 DSH 自己页面里的代码,而「人在 DSH 界面里按下了这个
477
+ // 按钮」正是这个窗口存在的理由。跨站页面、中转与局域网代理转进来的请求仍在 localOnly 挡掉。
435
478
  const body = await readJson(req);
436
479
  if (typeof body?.enabled !== 'boolean') {
437
480
  return sendJson(res, 400, { ok: false, error: { code: 'bad-request', message: 'enabled 必须是布尔值' } });
@@ -439,7 +482,7 @@ function registerControlPlaneRoutes(deps) {
439
482
  const webAccess = body.enabled ? webAccessWindow.enable() : webAccessWindow.disable();
440
483
  return sendJson(res, 200, { ok: true, webAccess });
441
484
  }) });
442
- registerRoute({ kind: 'exact', path: '/api/relay/status', handler: localOnly('GET', async (req, res) => sendJson(res, 200, { ...status(), panelUrl: panelUrlOf(req), webAccess: webAccessWindow.status(), ...mobileActivity(), selfCheck: selfCheck.snapshot() })) })
485
+ registerRoute({ kind: 'exact', path: '/api/relay/status', handler: localOnly('GET', async (req, res) => sendJson(res, 200, { ...status(), panelUrl: panelUrlOf(req), panelScope: panelScopeOf(req), webAccess: webAccessWindow.status(), ...mobileActivity(), selfCheck: selfCheck.snapshot() })) })
443
486
  // 面板页本体:设置界面从客户端半边搬到这里,浏览器(或桌面端里的「打开配置页面」)
444
487
  // 直接打开它。三条地址吐的是同一份 HTML,分流在 React 侧(见 React/src/Root.tsx):
445
488
  // /api/relay/panel 看宿主记的「配没配过」,自己决定先走引导还是直接进主页
@@ -1028,6 +1071,8 @@ const WEB_ACCESS_ROUTES = new Set([
1028
1071
  module.exports = {
1029
1072
  registerControlPlaneRoutes,
1030
1073
  panelDiagnosis,
1074
+ panelUrlOf,
1075
+ panelScopeOf,
1031
1076
  WEB_ACCESS_ROUTES,
1032
1077
  WEB_ACCESS_WINDOW_MS,
1033
1078
  createWebAccessWindow,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mobile-plugin",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "description": "Mobile API (/api/mobile/v1) and relay client for the DSH mobile app — desktop profile build (dsh 0.2.x, peers resolved by the host)",
5
5
  "license": "MIT",
6
6
  "icon": "./icon.png",