dsh-pocket 1.7.5 → 1.8.1

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/client/api.js CHANGED
@@ -8,6 +8,7 @@ export const POCKET_ENDPOINTS = Object.freeze({
8
8
  version: 'pocket.version',
9
9
  update: 'pocket.update',
10
10
  restart: 'pocket.restart',
11
+ lanTokenRefresh: 'token.lanRefresh',
11
12
  });
12
13
 
13
14
  /** 语义化版本比较:a > b 返回正数,相等 0,a < b 负数(数字段 + 预发布后缀)。 */
package/client/client.js CHANGED
@@ -47,7 +47,8 @@ var POCKET_ENDPOINTS = Object.freeze({
47
47
  tunnelStop: "tunnel.stop",
48
48
  version: "pocket.version",
49
49
  update: "pocket.update",
50
- restart: "pocket.restart"
50
+ restart: "pocket.restart",
51
+ lanTokenRefresh: "token.lanRefresh"
51
52
  });
52
53
  function compareVersions(a, b) {
53
54
  const pa = String(a).replace(/^[vV]/, "").split(".");
@@ -1429,6 +1430,13 @@ function PocketSettingsTab({ rpcCall }) {
1429
1430
  } catch {
1430
1431
  }
1431
1432
  };
1433
+ const refreshLanPin = async () => {
1434
+ try {
1435
+ const r = await call(POCKET_ENDPOINTS.lanTokenRefresh, {});
1436
+ setStatus((s) => ({ ...s, lanToken: r.lanToken }));
1437
+ } catch {
1438
+ }
1439
+ };
1432
1440
  const lanUrl = status?.lanUrl;
1433
1441
  const tunnelUrl = status?.tunnelUrl;
1434
1442
  const tunnelPhase = tunnelState?.phase ?? "idle";
@@ -1503,7 +1511,15 @@ function PocketSettingsTab({ rpcCall }) {
1503
1511
  null,
1504
1512
  (0, import_react2.createElement)("img", { src: status.lanQr, alt: "LAN QR", style: styles.qr }),
1505
1513
  (0, import_react2.createElement)("div", { style: styles.code }, lanUrl),
1506
- (0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00")
1514
+ (0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00"),
1515
+ status.lanToken ? (0, import_react2.createElement)(
1516
+ "div",
1517
+ { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
1518
+ "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A",
1519
+ status.lanToken,
1520
+ "\uFF08\u624B\u673A\u6253\u5F00\u9700\u8F93\u5165\uFF1B\u4E0E\u516C\u7F51\u5BC6\u7801\u5206\u5F00\uFF09",
1521
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, "\u5237\u65B0 | Refresh")
1522
+ ) : null
1507
1523
  ) : (0, import_react2.createElement)("div", { style: styles.muted }, "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026 | proxy starting\u2026")
1508
1524
  ),
1509
1525
  // 公网
package/client/index.jsx CHANGED
@@ -167,6 +167,14 @@ function PocketSettingsTab({ rpcCall }) {
167
167
  try { setStatus(await call(POCKET_ENDPOINTS.tunnelStop, {})); } catch { /* 忽略 */ }
168
168
  };
169
169
 
170
+ // 刷新局域网访问密码(旧密码立即作废)
171
+ const refreshLanPin = async () => {
172
+ try {
173
+ const r = await call(POCKET_ENDPOINTS.lanTokenRefresh, {});
174
+ setStatus((s) => ({ ...s, lanToken: r.lanToken }));
175
+ } catch { /* 忽略 */ }
176
+ };
177
+
170
178
  const lanUrl = status?.lanUrl;
171
179
  const tunnelUrl = status?.tunnelUrl;
172
180
  const tunnelPhase = tunnelState?.phase ?? 'idle';
@@ -235,6 +243,14 @@ function PocketSettingsTab({ rpcCall }) {
235
243
  h('img', { src: status.lanQr, alt: 'LAN QR', style: styles.qr }),
236
244
  h('div', { style: styles.code }, lanUrl),
237
245
  h('div', { style: styles.muted }, '手机连接同一 WiFi 后扫码即可打开'),
246
+ status.lanToken
247
+ ? h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.5 } },
248
+ '🔐 访问密码:',
249
+ status.lanToken,
250
+ '(手机打开需输入;与公网密码分开)',
251
+ h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, '刷新 | Refresh'),
252
+ )
253
+ : null,
238
254
  )
239
255
  : h('div', { style: styles.muted }, '代理未就绪… | proxy starting…'),
240
256
  ),
package/lib/index.js CHANGED
@@ -41,33 +41,58 @@ function currentVersion() {
41
41
  /** 进程启动时加载的版本(模块加载瞬间固化;用于识别「磁盘已更新但进程还是旧代码」)。 */
42
42
  const loadedVersion = currentVersion();
43
43
 
44
- // ---------- 公网访问密码(issue #13) ----------
45
- // 8 位数字,存本机 $DSH_HOME/dsh-pocket/token,每次开启公网时轮换(旧链接作废)。
46
- // 局域网 IP 直连免密码;仅公网隧道 Host 强制。
47
- const tokenRel = join('dsh-pocket', 'token');
48
- function tokenPath() {
49
- return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), tokenRel);
50
- }
51
- /** 生成 8 位数字密码并写文件(同步,保证后续读取一致)。 */
52
- function writeNewToken() {
53
- const fresh = String(Math.floor(10000000 + Math.random() * 90000000));
44
+ // ---------- 访问密码(issue #13 + #18) ----------
45
+ // 公网与局域网**分开**:各自 8 位数字,存本机 $DSH_HOME/dsh-pocket/。
46
+ // 公网密码(token):每次开启公网时轮换(旧链接作废);
47
+ // 局域网密码(token-lan):手动刷新(设置页按钮)。
48
+ function writePinToFile(p, fresh) {
54
49
  try {
55
- mkdirSync(dirname(tokenPath()), { recursive: true });
56
- writeFileSync(tokenPath(), fresh, { mode: 0o600 });
50
+ mkdirSync(dirname(p), { recursive: true });
51
+ writeFileSync(p, fresh, { mode: 0o600 });
57
52
  } catch { /* 忽略 */ }
58
53
  return fresh;
59
54
  }
60
- /** 当前公网访问密码;文件不存在时生成一个。 */
61
- function getAccessToken() {
55
+ function readPin(p) {
62
56
  try {
63
- const existing = readFileSync(tokenPath(), 'utf8').trim();
57
+ const existing = readFileSync(p, 'utf8').trim();
64
58
  if (/^\d{8}$/.test(existing)) return existing;
65
59
  } catch { /* 无文件 */ }
66
- return writeNewToken();
60
+ return null;
61
+ }
62
+ function newPin() {
63
+ return String(Math.floor(10000000 + Math.random() * 90000000));
64
+ }
65
+
66
+ // --- 公网密码 ---
67
+ const tokenRel = join('dsh-pocket', 'token');
68
+ function tokenPath() {
69
+ return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), tokenRel);
70
+ }
71
+ /** 当前公网访问密码;文件不存在时生成一个。 */
72
+ function getAccessToken() {
73
+ return readPin(tokenPath()) ?? writePinToFile(tokenPath(), newPin());
67
74
  }
68
- /** 轮换:生成新的 8 位数字密码(开启公网时调用,旧密码/链接立即作废)。 */
75
+ /** 轮换公网密码(开启公网时调用,旧密码/链接立即作废)。 */
69
76
  function rotateAccessToken() {
70
- return writeNewToken();
77
+ return writePinToFile(tokenPath(), newPin());
78
+ }
79
+
80
+ // --- 局域网密码(issue #18:局域网与公网分开,手动刷新) ---
81
+ const lanTokenRel = join('dsh-pocket', 'token-lan');
82
+ function lanTokenPath() {
83
+ return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), lanTokenRel);
84
+ }
85
+ /** 当前局域网访问密码;文件不存在时生成一个。 */
86
+ function getLanToken() {
87
+ return readPin(lanTokenPath()) ?? writePinToFile(lanTokenPath(), newPin());
88
+ }
89
+ /** 刷新局域网密码(设置页按钮触发,旧密码立即作废)。 */
90
+ function refreshLanToken() {
91
+ return writePinToFile(lanTokenPath(), newPin());
92
+ }
93
+ /** 按 Host 分发的访问密码:公网隧道用公网密码,局域网用局域网密码。 */
94
+ function tokenForHost(host) {
95
+ return /trycloudflare\.com$/i.test(String(host ?? '')) ? getAccessToken() : getLanToken();
71
96
  }
72
97
 
73
98
  const restartNoticeRel = join('dsh-pocket', 'restarted.json');
@@ -177,10 +202,10 @@ export function apply(ctx, config = {}, internals = {}) {
177
202
  injectHtml: isDesktop
178
203
  ? DEFAULT_INJECT + desktopEnvPatchScript(process.platform) + (desktopAdvanced ? advancedNoticeScript() : '')
179
204
  : undefined,
180
- // 公网访问密码(issue #13):仅公网隧道 Host 强制;局域网免密码
205
+ // 访问密码(issue #13 + #18):局域网与公网**分开**,所有代理访问都要验证
181
206
  auth: {
182
- getToken: () => getAccessToken(),
183
- isProtected: (host) => /trycloudflare\.com$/i.test(String(host ?? '')),
207
+ getToken: (host) => tokenForHost(host),
208
+ isProtected: () => true,
184
209
  },
185
210
  // 每次公网隧道就绪 → 轮换 8 位密码(旧密码/旧链接立即作废)
186
211
  onTunnelReady: () => {
@@ -195,6 +220,8 @@ export function apply(ctx, config = {}, internals = {}) {
195
220
  service,
196
221
  desktop: isDesktop,
197
222
  getToken: () => getAccessToken(),
223
+ getLanToken: () => getLanToken(),
224
+ refreshLanToken: () => refreshLanToken(),
198
225
  runUpdate: internals.runUpdate ?? { currentVersion, perform: performUpdate, loadedVersion: () => loadedVersion },
199
226
  restart: internals.restart ?? (() => pocketRestart(service)),
200
227
  restartNotice: internals.restartNotice ?? consumeRestartNotice,
package/lib/proxy.mjs CHANGED
@@ -76,7 +76,10 @@ function parseCookies(header) {
76
76
  return out;
77
77
  }
78
78
 
79
- function loginPageHtml(error) {
79
+ /** 登录页:按访问来源显示提示(局域网 / 公网)。 */
80
+ function loginPageHtml(error, isPublic) {
81
+ const where = isPublic ? '此公网地址' : '此局域网地址';
82
+ const whereEn = isPublic ? 'This public address' : 'This LAN address';
80
83
  return `<!doctype html><html lang="zh"><head><meta charset="utf-8">
81
84
  <meta name="viewport" content="width=device-width,initial-scale=1">
82
85
  <title>DSH Pocket · 访问验证</title>
@@ -91,7 +94,7 @@ button{width:100%;padding:10px;font-size:15px;background:#4f6ef7;color:#fff;bord
91
94
  .err{color:#dc2626;font-size:12px;margin-bottom:10px;min-height:16px}
92
95
  </style></head><body><div class="card">
93
96
  <h1>🔐 DSH Pocket</h1>
94
- <p>此公网地址受访问密码保护,请输入 8 位数字密码</p>
97
+ <p>${where}受访问密码保护,请输入 8 位数字密码 | ${whereEn} is password-protected — enter the 8-digit PIN</p>
95
98
  <div class="err">${error ? '密码错误,请重试' : ''}</div>
96
99
  <form method="post" action="/pocket-login">
97
100
  <input name="token" type="password" inputmode="numeric" maxlength="8" autocomplete="one-time-code" autofocus required>
@@ -141,11 +144,12 @@ function loopbackAuthority(headers, upstream) {
141
144
  */
142
145
  export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DEFAULT_UPSTREAM, log = null, injectHtml = DEFAULT_INJECT, auth = null } = {}) {
143
146
  const server = createServer((req, res) => {
144
- // 访问令牌认证(issue #13):仅公网隧道 Host 强制;局域网 IP 直连免密码
147
+ // 访问令牌认证(issue #13 + #18):局域网与公网都要求密码
145
148
  if (auth) {
146
149
  const host = String(req.headers.host ?? '');
150
+ const isPublic = /trycloudflare\.com$/i.test(host);
147
151
  const protectedHost = isProtectedHost(host, auth.isProtected);
148
- const token = protectedHost ? (auth.getToken?.() ?? null) : null;
152
+ const token = protectedHost ? (auth.getToken?.(host) ?? null) : null;
149
153
  if (protectedHost && token) {
150
154
  // 登录提交:校验密码 → 种 HttpOnly 会话 cookie → 回首页
151
155
  if (req.method === 'POST' && req.url?.startsWith('/pocket-login')) {
@@ -162,7 +166,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
162
166
  res.end();
163
167
  } else {
164
168
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
165
- res.end(loginPageHtml(true));
169
+ res.end(loginPageHtml(true, isPublic));
166
170
  }
167
171
  });
168
172
  return;
@@ -170,7 +174,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
170
174
  if (!authCheck(req, token)) {
171
175
  if (isHtmlRequest(req)) {
172
176
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
173
- res.end(loginPageHtml(false));
177
+ res.end(loginPageHtml(false, isPublic));
174
178
  } else {
175
179
  res.writeHead(401, { 'content-type': 'application/json', 'cache-control': 'no-store' });
176
180
  res.end('{"error":"unauthorized"}');
@@ -258,7 +262,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
258
262
  // WebSocket 同样校验(防止绕过 HTTP 认证从 WS 进入)
259
263
  if (auth) {
260
264
  const host = String(req.headers.host ?? '');
261
- const token = isProtectedHost(host, auth.isProtected) ? (auth.getToken?.() ?? null) : null;
265
+ const token = isProtectedHost(host, auth.isProtected) ? (auth.getToken?.(host) ?? null) : null;
262
266
  if (token && !authCheck(req, token)) {
263
267
  socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
264
268
  socket.destroy();
package/lib/web-rpc.js CHANGED
@@ -25,7 +25,7 @@ export function killHint(port) {
25
25
  }
26
26
 
27
27
  /** 注册 /dsh-pocket 逻辑通道(仅本机 loopback 可调)。 */
28
- export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null }) {
28
+ export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null, getLanToken = null, refreshLanToken = null }) {
29
29
  if (!ctx?.connection?.rpc?.handle) {
30
30
  log.warn?.('dsh-pocket: DSH Host Connection RPC unavailable — settings tab disabled | 无 Connection RPC,设置页不可用');
31
31
  return () => {};
@@ -33,18 +33,30 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false,
33
33
  return ctx.connection.rpc.handle(POCKET_RPC_CHANNEL, async (endpoint, payload = {}, signal) => {
34
34
  if (signal?.aborted) return fail('cancelled', 'The request was cancelled.');
35
35
 
36
- // status 响应:redact 后的服务状态 + 重启提示(重启后 30 分钟内有效)+ 停止命令(按平台)+ 桌面端标志 + 公网访问密码
36
+ // status 响应:服务状态 + 重启提示 + 停止命令 + 桌面端标志 + 公网/局域网访问密码
37
37
  const statusPayload = async () => {
38
38
  let notice = null;
39
39
  try { notice = (await restartNotice?.()) ?? null; } catch { notice = null; }
40
40
  const s = await service.status();
41
- return ok({ ...redactStatus(s), desktop, restartNotice: notice, killHint: killHint(s.dshPort ?? 3080), accessToken: getToken?.() ?? null });
41
+ return ok({
42
+ ...redactStatus(s),
43
+ desktop,
44
+ restartNotice: notice,
45
+ killHint: killHint(s.dshPort ?? 3080),
46
+ accessToken: getToken?.() ?? null,
47
+ lanToken: getLanToken?.() ?? null,
48
+ });
42
49
  };
43
50
 
44
51
  try {
45
52
  if (endpoint === POCKET_ENDPOINTS.status) {
46
53
  return await statusPayload();
47
54
  }
55
+ if (endpoint === POCKET_ENDPOINTS.lanTokenRefresh) {
56
+ const fresh = refreshLanToken?.() ?? null;
57
+ if (!fresh) return fail('bad-request', '局域网密码刷新不可用 | LAN PIN refresh unavailable');
58
+ return ok({ lanToken: fresh });
59
+ }
48
60
  if (endpoint === POCKET_ENDPOINTS.tunnelStart) {
49
61
  await service.startTunnel();
50
62
  return await statusPayload();
package/package.json CHANGED
@@ -76,5 +76,5 @@
76
76
  "access": "public",
77
77
  "registry": "https://registry.npmjs.org/"
78
78
  },
79
- "version": "1.7.5"
79
+ "version": "1.8.1"
80
80
  }