dsh-pocket 2.9.0 → 2.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/client/client.js CHANGED
@@ -503,85 +503,6 @@ function startFileGuard(readFile) {
503
503
  };
504
504
  }
505
505
 
506
- // client/mobile/sessionGuard.ts
507
- var SESSION_META = "dsh-pocket-session";
508
- function readFingerprint() {
509
- const meta = document.querySelector(`meta[name="${SESSION_META}"]`);
510
- const fp = meta?.content?.trim();
511
- return fp && fp.length > 0 ? fp : null;
512
- }
513
- function mountBadge(fp) {
514
- const badge = document.createElement("div");
515
- badge.setAttribute("data-mobile-nav", "session-fp");
516
- badge.textContent = `\u{1F512} \u4F1A\u8BDD\u6307\u7EB9 ${fp}`;
517
- badge.style.cssText = [
518
- "position:fixed",
519
- "left:50%",
520
- "bottom:8px",
521
- "transform:translateX(-50%)",
522
- "z-index:2147483646",
523
- "padding:4px 10px",
524
- "border-radius:999px",
525
- "background:rgba(17,24,39,.82)",
526
- "color:#fff",
527
- 'font:12px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
528
- "letter-spacing:.5px",
529
- "pointer-events:none",
530
- "user-select:none",
531
- "white-space:nowrap"
532
- ].join(";");
533
- badge.title = "\u672C\u9875\u4F1A\u8BDD\u6307\u7EB9\uFF0C\u5E94\u4E0E\u7535\u8111\u300C\u624B\u673A\u8BBF\u95EE\u300D\u8BBE\u7F6E\u9875\u663E\u793A\u7684\u4E00\u81F4\uFF1B\u4E0D\u4E00\u81F4\u8BF4\u660E\u94FE\u63A5\u5DF2\u88AB\u4ED6\u4EBA\u590D\u7528";
534
- document.body.appendChild(badge);
535
- return () => badge.remove();
536
- }
537
- function mountWarning() {
538
- const overlay = document.createElement("div");
539
- overlay.setAttribute("data-mobile-nav", "session-warn");
540
- overlay.style.cssText = [
541
- "position:fixed",
542
- "inset:0",
543
- "z-index:2147483647",
544
- "display:flex",
545
- "align-items:center",
546
- "justify-content:center",
547
- "padding:24px",
548
- "background:rgba(0,0,0,.72)",
549
- "color:#fff",
550
- 'font:15px/1.7 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
551
- "text-align:center"
552
- ].join(";");
553
- overlay.style.whiteSpace = "pre-line";
554
- overlay.textContent = "\u26A0\uFE0F \u6B64\u9875\u9762\u4E0D\u662F\u4F60\u7684 DSH\uFF1A\u94FE\u63A5\u53EF\u80FD\u5DF2\u88AB\u4ED6\u4EBA\u590D\u7528\u3002\n\u8BF7\u52FF\u5728\u6B64\u8F93\u5165\u4EFB\u4F55\u5BC6\u7801\u3002\n\u8BF7\u91CD\u65B0\u5728\u7535\u8111\u300C\u624B\u673A\u8BBF\u95EE\u300D\u8BBE\u7F6E\u9875\u626B\u5F53\u524D\u4E8C\u7EF4\u7801\u3002";
555
- document.documentElement.appendChild(overlay);
556
- return () => overlay.remove();
557
- }
558
- function startSessionGuard() {
559
- let cleanupBadge = null;
560
- let cleanupWarn = null;
561
- const evaluate = () => {
562
- const fp = readFingerprint();
563
- if (fp) {
564
- cleanupWarn?.();
565
- cleanupWarn = null;
566
- if (!cleanupBadge) cleanupBadge = mountBadge(fp);
567
- } else {
568
- cleanupBadge?.();
569
- cleanupBadge = null;
570
- if (!cleanupWarn) cleanupWarn = mountWarning();
571
- }
572
- };
573
- evaluate();
574
- const timer = window.setTimeout(evaluate, 300);
575
- const observer = new MutationObserver(evaluate);
576
- observer.observe(document.documentElement, { childList: true, subtree: true });
577
- return () => {
578
- window.clearTimeout(timer);
579
- observer.disconnect();
580
- cleanupBadge?.();
581
- cleanupWarn?.();
582
- };
583
- }
584
-
585
506
  // client/mobile/mobile.css.ts
586
507
  var MOBILE_CSS = `
587
508
  /* ---------- base control styles (rendered at any width, hidden where unused) ---------- */
@@ -1740,11 +1661,6 @@ function mobileApply(ctx) {
1740
1661
  );
1741
1662
  return startFileGuard(readFile);
1742
1663
  }, "dsh-mobile-nav: file open guard + copy button + hide add-workspace (issue #17)");
1743
- ctx.effect(() => {
1744
- if (!narrow.matches) return () => {
1745
- };
1746
- return startSessionGuard();
1747
- }, "dsh-mobile-nav: session fingerprint guard (issue #82)");
1748
1664
  ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
1749
1665
  name: "conversation.session.header.actions",
1750
1666
  id: "mobile-nav-toggle",
@@ -1845,7 +1761,6 @@ var zh2 = {
1845
1761
  "wanPin": "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A{pin}\uFF08\u6BCF\u6B21\u5F00\u542F\u516C\u7F51\u53D8\u65B0\uFF1B\u624B\u673A\u6253\u5F00\u94FE\u63A5\u9700\u8F93\u5165\u6B64\u5BC6\u7801\uFF09",
1846
1762
  "wanPinCustom": "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A{pin}\uFF08\u81EA\u5B9A\u4E49\uFF0C\u5F00\u542F\u516C\u7F51\u4E0D\u518D\u81EA\u52A8\u6362\u65B0\uFF09",
1847
1763
  "wanEphemeralWarn": "\u26A0\uFE0F \u516C\u7F51\u94FE\u63A5\u4EC5\u5728\u672C\u6B21\u5F00\u542F\u671F\u95F4\u6709\u6548\uFF1A\u5173\u95ED\u6216\u91CD\u542F\u540E\u5931\u6548\uFF0C\u5E76\u53EF\u80FD\u88AB\u4ED6\u4EBA\u590D\u7528\u4E3A\u964C\u751F\u7F51\u7AD9\u3002\u8BF7\u52FF\u6536\u85CF\uFF0C\u6BCF\u6B21\u4ECE\u672C\u9875\u626B\u300C\u5F53\u524D\u300D\u4E8C\u7EF4\u7801\u3002\u9700\u8981\u56FA\u5B9A\u4E0D\u53D8\u7684\u5730\u5740\u8BF7\u7528\u4E0B\u65B9\u300C\u56FA\u5B9A\u57DF\u540D\u300D\u3002",
1848
- "wanSessionFp": "\u672C\u9875\u4F1A\u8BDD\u6307\u7EB9\uFF1A{fp}\uFF08\u624B\u673A\u767B\u5F55\u9875\u4F1A\u663E\u793A\u540C\u6837\u7684\uFF1B\u4E0D\u4E00\u81F4\u8BF4\u660E\u94FE\u63A5\u5DF2\u88AB\u4ED6\u4EBA\u590D\u7528\uFF0C\u8BF7\u52FF\u8F93\u5165\u5BC6\u7801\uFF09",
1849
1764
  "stopTunnel": "\u5173\u95ED\u516C\u7F51",
1850
1765
  "enable": "\u5F00\u542F\u516C\u7F51\u8BBF\u95EE",
1851
1766
  "opening": "\u5F00\u542F\u4E2D\u2026",
@@ -1942,7 +1857,6 @@ var en2 = {
1942
1857
  "wanPin": "\u{1F510} PIN: {pin} (changes each time the tunnel is enabled; required on the phone)",
1943
1858
  "wanPinCustom": "\u{1F510} PIN: {pin} (custom \u2014 not rotated on tunnel start)",
1944
1859
  "wanEphemeralWarn": '\u26A0\uFE0F The public link is valid only for this session: it stops working after you close or restart, and may be reused by someone else for an unrelated site. Do not bookmark it \u2014 scan the CURRENT QR code from this page each time. For a permanent address use "Fixed domain" below.',
1945
- "wanSessionFp": "Session fingerprint: {fp} (the phone login page shows the same; if it differs, the link has been reused \u2014 do not enter your PIN)",
1946
1860
  "stopTunnel": "Stop",
1947
1861
  "enable": "Enable anywhere",
1948
1862
  "opening": "Enabling\u2026",
@@ -2443,13 +2357,8 @@ function PocketSettingsTab({ rpcCall, t }) {
2443
2357
  "div",
2444
2358
  null,
2445
2359
  qrArea(status.tunnelQr, tunnelUrl, namedMode ? t("namedRunningHint") : t("wanHint")),
2446
- // 防钓鱼 / 别收藏(issue #82):链接 ephemeral 提示 + 本次会话指纹交叉核对
2360
+ // 防钓鱼 / 别收藏(issue #82):公网链接仅本次有效、勿收藏提示
2447
2361
  (0, import_react2.createElement)("div", { style: { marginTop: 8, fontSize: 12, lineHeight: 1.5, borderLeft: "4px solid var(--dsw-alias-state-warn-primary,#b45309)", background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", borderRadius: 8, padding: "8px 10px" } }, t("wanEphemeralWarn")),
2448
- status?.sessionFingerprint ? (0, import_react2.createElement)(
2449
- "div",
2450
- { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
2451
- fmt(t, "wanSessionFp", { fp: status.sessionFingerprint })
2452
- ) : null,
2453
2362
  // 地址模式行(随机/固定;固定域名选中或编辑时高亮)
2454
2363
  row(
2455
2364
  t("modeLabel"),
package/client/index.jsx CHANGED
@@ -487,11 +487,8 @@ function PocketSettingsTab({ rpcCall, t }) {
487
487
  tunnelUrl
488
488
  ? h('div', null,
489
489
  qrArea(status.tunnelQr, tunnelUrl, namedMode ? t('namedRunningHint') : t('wanHint')),
490
- // 防钓鱼 / 别收藏(issue #82):链接 ephemeral 提示 + 本次会话指纹交叉核对
490
+ // 防钓鱼 / 别收藏(issue #82):公网链接仅本次有效、勿收藏提示
491
491
  h('div', { style: { marginTop: 8, fontSize: 12, lineHeight: 1.5, borderLeft: '4px solid var(--dsw-alias-state-warn-primary,#b45309)', background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', borderRadius: 8, padding: '8px 10px' } }, t('wanEphemeralWarn')),
492
- status?.sessionFingerprint ? h('div', { style: { marginTop: 8, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.5 } },
493
- fmt(t, 'wanSessionFp', { fp: status.sessionFingerprint }),
494
- ) : null,
495
492
  // 地址模式行(随机/固定;固定域名选中或编辑时高亮)
496
493
  row(t('modeLabel'),
497
494
  h('span', { style: { display: 'inline-flex', gap: 6 } },
@@ -4,7 +4,6 @@ import { MobileNavToggle } from './MobileNavToggle.tsx'
4
4
  import { MobileNavOverlay } from './MobileNavOverlay.tsx'
5
5
  import { MobileDrawerFooter } from './MobileDrawerFooter.tsx'
6
6
  import { startFileGuard } from './fileGuard.ts'
7
- import { startSessionGuard } from './sessionGuard.ts'
8
7
  import { MOBILE_CSS } from './mobile.css.ts'
9
8
  import { POCKET_RPC_CHANNEL, POCKET_ENDPOINTS } from '../api.js'
10
9
  import { NS, en, zh } from './locales.ts'
@@ -304,14 +303,6 @@ export function mobileApply(ctx): void {
304
303
  return startFileGuard(readFile)
305
304
  }, 'dsh-mobile-nav: file open guard + copy button + hide add-workspace (issue #17)')
306
305
 
307
- // 公网会话指纹校验(issue #82,防密码被钓):页面带指纹 meta 即我们的页面、
308
- // 显示交叉核对徽标;不带则极可能不是我们的页面(链接被复用/克隆登录页钓鱼),
309
- // 弹红屏警告并拦截交互。只挂窄屏(登录页是 server-render、无本脚本,主要靠
310
- // 登录页与设置页的可见指纹做人工交叉核对;此层是应用页的兜底)。
311
- ctx.effect(() => {
312
- if (!narrow.matches) return () => {}
313
- return startSessionGuard()
314
- }, 'dsh-mobile-nav: session fingerprint guard (issue #82)')
315
306
 
316
307
  ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({
317
308
  name: 'conversation.session.header.actions',
@@ -71,7 +71,6 @@ export const zh = {
71
71
  'wanPin': '🔐 访问密码:{pin}(每次开启公网变新;手机打开链接需输入此密码)',
72
72
  'wanPinCustom': '🔐 访问密码:{pin}(自定义,开启公网不再自动换新)',
73
73
  'wanEphemeralWarn': '⚠️ 公网链接仅在本次开启期间有效:关闭或重启后失效,并可能被他人复用为陌生网站。请勿收藏,每次从本页扫「当前」二维码。需要固定不变的地址请用下方「固定域名」。',
74
- 'wanSessionFp': '本页会话指纹:{fp}(手机登录页会显示同样的;不一致说明链接已被他人复用,请勿输入密码)',
75
74
  'stopTunnel': '关闭公网',
76
75
  'enable': '开启公网访问',
77
76
  'opening': '开启中…',
@@ -170,7 +169,6 @@ export const en = {
170
169
  'wanPin': '🔐 PIN: {pin} (changes each time the tunnel is enabled; required on the phone)',
171
170
  'wanPinCustom': '🔐 PIN: {pin} (custom — not rotated on tunnel start)',
172
171
  'wanEphemeralWarn': '⚠️ The public link is valid only for this session: it stops working after you close or restart, and may be reused by someone else for an unrelated site. Do not bookmark it — scan the CURRENT QR code from this page each time. For a permanent address use "Fixed domain" below.',
173
- 'wanSessionFp': 'Session fingerprint: {fp} (the phone login page shows the same; if it differs, the link has been reused — do not enter your PIN)',
174
172
  'stopTunnel': 'Stop',
175
173
  'enable': 'Enable anywhere',
176
174
  'opening': 'Enabling…',
package/lib/proxy.mjs CHANGED
@@ -15,7 +15,6 @@ import { createServer } from 'node:http';
15
15
  import { request as httpRequest } from 'node:http';
16
16
  import { createGzip, createBrotliCompress, constants as zlibConstants } from 'node:zlib';
17
17
  import { createHash } from 'node:crypto';
18
- import { ensureSessionFingerprint } from './session.mjs';
19
18
 
20
19
  const DEFAULT_UPSTREAM = { host: '127.0.0.1', port: 3080 };
21
20
 
@@ -37,8 +36,6 @@ export const RANDOM_UUID_POLYFILL = `<script data-dsh-pocket-polyfill="1">!funct
37
36
  已回退;该问题属 DSH 客户端限制(location.hostname 是 unforgeable 属性)无法安全绕过。*/</script>`;
38
37
 
39
38
  const INJECT_MARK = 'data-dsh-pocket-polyfill="1"';
40
- /** 防密码被钓(issue #82):本代理注入到每个 HTML 的会话指纹标记。 */
41
- const SESSION_MARK = 'name="dsh-pocket-session"';
42
39
 
43
40
  /**
44
41
  * DSH Desktop(桌面版)渲染进程兼容补丁(issue #3/#4,已于 issue #76 停用)。
@@ -168,20 +165,15 @@ function parseCookies(header) {
168
165
  return out;
169
166
  }
170
167
 
171
- /** 登录页:按访问来源显示提示(局域网 / 公网);error: false|true|'locked'(locked 带剩余秒数)。
172
- * fp: 本次公网会话指纹(issue #82 防钓鱼)——与电脑「手机访问」设置页一致才是你的 DSH。 */
173
- function loginPageHtml(error, isPublic, retryAfter = 0, fp = '') {
168
+ /** 登录页:按访问来源显示提示(局域网 / 公网);error: false|true|'locked'(locked 带剩余秒数)。 */
169
+ function loginPageHtml(error, isPublic, retryAfter = 0) {
174
170
  const where = isPublic ? '此公网地址' : '此局域网地址';
175
171
  const whereEn = isPublic ? 'This public address' : 'This LAN address';
176
172
  const errMsg = error === 'locked'
177
173
  ? `尝试次数过多,请 ${retryAfter} 秒后再试 | Too many attempts — try again in ${retryAfter}s`
178
174
  : error ? '密码错误,请重试 | Wrong PIN, try again' : '';
179
- const fpLine = fp
180
- ? `<p style="margin:14px 0 0;font-size:12px;line-height:1.6;color:#6b7280">本页会话指纹:<b style="font-family:ui-monospace,Menlo,monospace;letter-spacing:1px;color:#111827">${fp}</b><br>与电脑「手机访问」设置页显示的<b>一致</b>,才是你的 DSH;不一致请勿输入密码。<br><span style="color:#9ca3af">Session fingerprint — only enter your PIN if it matches the one shown on your computer's "Phone access" settings.</span></p>`
181
- : '';
182
175
  return `<!doctype html><html lang="zh"><head><meta charset="utf-8">
183
176
  <meta name="viewport" content="width=device-width,initial-scale=1">
184
- <meta name="dsh-pocket-session" content="${fp}">
185
177
  <title>DSH Pocket · 访问验证</title>
186
178
  <style>
187
179
  body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#f3f4f6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
@@ -199,7 +191,7 @@ button{width:100%;padding:10px;font-size:15px;background:#4f6ef7;color:#fff;bord
199
191
  <form method="post" action="/pocket-login">
200
192
  <input name="token" type="password" maxlength="8" autocomplete="one-time-code" autofocus required>
201
193
  <button type="submit">进入 | Enter</button>
202
- </form>${fpLine}
194
+ </form>
203
195
  </div></body></html>`;
204
196
  }
205
197
 
@@ -488,9 +480,6 @@ function attachWebSocketHeartbeat(socket, { intervalMs = 30_000, missLimit = 2 }
488
480
  */
489
481
  export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DEFAULT_UPSTREAM, log = null, injectHtml = DEFAULT_INJECT, auth = null, rateLimit = null, heartbeat = {}, lanAccessEnabled = () => true, launchToken = () => '' } = {}) {
490
482
  const limiter = auth ? createRateLimiter(rateLimit ?? {}) : null;
491
- // 防密码被钓(issue #82):本次进程会话指纹,注入到所有经代理伺服的 HTML
492
- const sessionFingerprint = ensureSessionFingerprint();
493
- const sessionMeta = `<meta name="dsh-pocket-session" content="${sessionFingerprint}">`;
494
483
  const server = createServer((req, res) => {
495
484
  const host = String(req.headers.host ?? '');
496
485
  const isPublic = classifyHost(host) === 'public';
@@ -526,7 +515,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
526
515
  'cache-control': 'no-store',
527
516
  'retry-after': String(rl.retryAfter),
528
517
  });
529
- res.end(loginPageHtml('locked', isPublic, rl.retryAfter, sessionFingerprint));
518
+ res.end(loginPageHtml('locked', isPublic, rl.retryAfter));
530
519
  return;
531
520
  }
532
521
  let body = '';
@@ -547,7 +536,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
547
536
  limiter?.record(ip);
548
537
  log?.(`dsh-pocket: login failed from ${ip} | 登录失败 IP: ${ip}`);
549
538
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
550
- res.end(loginPageHtml(true, isPublic, 0, sessionFingerprint));
539
+ res.end(loginPageHtml(true, isPublic, 0));
551
540
  }
552
541
  });
553
542
  return;
@@ -558,7 +547,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
558
547
  // 锁定期间打开登录页也给提示(HTTP 200 + 锁定文案;429 语义留给 POST 拒绝)
559
548
  const rl = limiter?.status(ip) ?? { locked: false, retryAfter: 0 };
560
549
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
561
- res.end(loginPageHtml(rl.locked ? 'locked' : false, isPublic, rl.retryAfter, sessionFingerprint));
550
+ res.end(loginPageHtml(rl.locked ? 'locked' : false, isPublic, rl.retryAfter));
562
551
  } else {
563
552
  res.writeHead(401, { 'content-type': 'application/json', 'cache-control': 'no-store' });
564
553
  res.end('{"error":"unauthorized"}');
@@ -628,11 +617,6 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
628
617
  proxyRes.on('data', (c) => chunks.push(c));
629
618
  proxyRes.on('end', () => {
630
619
  let html = Buffer.concat(chunks).toString('utf8');
631
- // 防密码被钓(issue #82):注入会话指纹 meta(与登录页同源)。
632
- // 仅当页面尚无该标记时注入,避免重复;不存在 <head> 时跳过(dsh web 必有)。
633
- if (!html.includes(SESSION_MARK)) {
634
- html = html.replace(/<head[^>]*>/i, (m) => `${m}${sessionMeta}`);
635
- }
636
620
  if (!html.includes(INJECT_MARK)) {
637
621
  html = html.replace(/<head[^>]*>/i, (m) => `${m}${injectHtml}`);
638
622
  }
package/lib/service.mjs CHANGED
@@ -15,7 +15,6 @@ import { join, dirname } from 'node:path';
15
15
  import { createPocketProxy } from './proxy.mjs';
16
16
  import { startQuickTunnel, startNamedTunnel } from './tunnel.mjs';
17
17
  import { isValidIpv4 } from './ip.mjs';
18
- import { ensureSessionFingerprint } from './session.mjs';
19
18
 
20
19
  const require = createRequire(import.meta.url);
21
20
 
@@ -400,8 +399,6 @@ export function createPocketService({
400
399
  };
401
400
  })(),
402
401
  dshPort,
403
- // 防密码被钓(issue #82):本次进程的会话指纹,设置页展示供用户与手机登录页交叉核对
404
- sessionFingerprint: ensureSessionFingerprint(),
405
402
  };
406
403
  },
407
404
 
package/lib/tunnel.mjs CHANGED
@@ -27,15 +27,15 @@ export const QUICK_TUNNEL_URL_RE = /https:\/\/(?!api\.)[a-z0-9-]+\.trycloudflare
27
27
  *
28
28
  * cloudflared 参数错误(如 "Incorrect Usage: flag provided but not defined")的
29
29
  * 关键信息在输出**开头**,尾部整段都是 usage 帮助文本(对用户没用);而运行期错误
30
- * (403 / 协议 / 网络)的关键信息在**尾部**。所以:参数错误取开头片段,其它仍取尾部。
30
+ * (403 / 协议 / 网络)的关键信息在**尾部**。所以:参数错误取该行,其它仍取尾部。
31
31
  * 最多 500 字符,与历史上限一致。
32
32
  */
33
33
  export function firstMeaningfulErrorLine(buf) {
34
- const text = String(buf ?? '').trim();
35
- if (/^(Incorrect Usage|flag provided but not defined|unknown flag|unknown command)/im.test(text)) {
36
- return text.split(/\r?\n/)[0].trim().slice(0, 500);
37
- }
38
- return text.split(/\r?\n/).slice(-4).join('\n').trim().slice(0, 500);
34
+ const lines = String(buf ?? '').trim().split(/\r?\n/);
35
+ // 命中哪行就返回哪行:cloudflared 先打版本横幅时,参数错误未必是首行
36
+ const usageIdx = lines.findIndex((l) => /^(?:Incorrect Usage|flag provided but not defined|unknown flag|unknown command)/i.test(l.trim()));
37
+ if (usageIdx >= 0) return lines[usageIdx].trim().slice(0, 500);
38
+ return lines.slice(-4).join('\n').trim().slice(0, 500);
39
39
  }
40
40
 
41
41
  function platformBinary() {
package/package.json CHANGED
@@ -80,5 +80,5 @@
80
80
  "access": "public",
81
81
  "registry": "https://registry.npmjs.org/"
82
82
  },
83
- "version": "2.9.0"
83
+ "version": "2.10.0"
84
84
  }
@@ -1,80 +0,0 @@
1
- // 公网会话指纹校验(issue #82,防密码被钓)。
2
- //
3
- // 本代理会给每个经它伺服的 HTML(应用页 + 登录页)注入
4
- // <meta name="dsh-pocket-session" content="<本次进程随机指纹>">。该指纹只在本机
5
- // 代理内生成、只出现在我们伺服的页面,陌生人复用被回收的快速隧道子域时(其站点
6
- // 不经我们的代理)拿不到它、也猜不出。
7
- //
8
- // 校验逻辑(纯前端、不依赖任何外部请求,避免被钓鱼站点误导):
9
- // - 页面带指纹 meta → 这是我们的页面,显示一个小徽标供用户与电脑设置页交叉核对
10
- // (不一致即说明链接被复用);
11
- // - 页面不带指纹 meta → 很可能不是我们的页面(链接已被他人复用,或克隆登录页钓鱼),
12
- // 弹红色全屏警告并拦截交互,防止用户在被钓鱼页输入 8 位密码。
13
-
14
- const SESSION_META = 'dsh-pocket-session'
15
-
16
- function readFingerprint(): string | null {
17
- const meta = document.querySelector<HTMLMetaElement>(`meta[name="${SESSION_META}"]`)
18
- const fp = meta?.content?.trim()
19
- return fp && fp.length > 0 ? fp : null
20
- }
21
-
22
- function mountBadge(fp: string): () => void {
23
- const badge = document.createElement('div')
24
- badge.setAttribute('data-mobile-nav', 'session-fp')
25
- badge.textContent = `🔒 会话指纹 ${fp}`
26
- // 固定在底部居中、低存在感、仅供交叉核对
27
- badge.style.cssText = [
28
- 'position:fixed', 'left:50%', 'bottom:8px', 'transform:translateX(-50%)',
29
- 'z-index:2147483646', 'padding:4px 10px', 'border-radius:999px',
30
- 'background:rgba(17,24,39,.82)', 'color:#fff',
31
- 'font:12px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
32
- 'letter-spacing:.5px', 'pointer-events:none', 'user-select:none', 'white-space:nowrap',
33
- ].join(';')
34
- badge.title = '本页会话指纹,应与电脑「手机访问」设置页显示的一致;不一致说明链接已被他人复用'
35
- document.body.appendChild(badge)
36
- return () => badge.remove()
37
- }
38
-
39
- function mountWarning(): () => void {
40
- const overlay = document.createElement('div')
41
- overlay.setAttribute('data-mobile-nav', 'session-warn')
42
- overlay.style.cssText = [
43
- 'position:fixed', 'inset:0', 'z-index:2147483647',
44
- 'display:flex', 'align-items:center', 'justify-content:center', 'padding:24px',
45
- 'background:rgba(0,0,0,.72)', 'color:#fff',
46
- 'font:15px/1.7 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif', 'text-align:center',
47
- ].join(';')
48
- overlay.style.whiteSpace = 'pre-line'
49
- overlay.textContent = '⚠️ 此页面不是你的 DSH:链接可能已被他人复用。\n请勿在此输入任何密码。\n请重新在电脑「手机访问」设置页扫当前二维码。'
50
- document.documentElement.appendChild(overlay)
51
- return () => overlay.remove()
52
- }
53
-
54
- export function startSessionGuard(): () => void {
55
- let cleanupBadge: (() => void) | null = null
56
- let cleanupWarn: (() => void) | null = null
57
- const evaluate = (): void => {
58
- const fp = readFingerprint()
59
- if (fp) {
60
- cleanupWarn?.()
61
- cleanupWarn = null
62
- if (!cleanupBadge) cleanupBadge = mountBadge(fp)
63
- } else {
64
- cleanupBadge?.()
65
- cleanupBadge = null
66
- if (!cleanupWarn) cleanupWarn = mountWarning()
67
- }
68
- }
69
- evaluate()
70
- // 容错:极少数情况下 meta 在 effect 运行时尚未解析,稍后复查一次
71
- const timer = window.setTimeout(evaluate, 300)
72
- const observer = new MutationObserver(evaluate)
73
- observer.observe(document.documentElement, { childList: true, subtree: true })
74
- return () => {
75
- window.clearTimeout(timer)
76
- observer.disconnect()
77
- cleanupBadge?.()
78
- cleanupWarn?.()
79
- }
80
- }
package/lib/session.mjs DELETED
@@ -1,35 +0,0 @@
1
- // 公网会话指纹(防密码被钓,issue #82)。
2
- //
3
- // 每次 dsh web 进程启动生成一次,随机、不可预测。只经本代理注入到我们伺服的
4
- // HTML(应用页 + 登录页),并显示在电脑「手机访问」设置页——永不落盘、不进日志、
5
- // 不出现在任何静态产物里。
6
- //
7
- // 快速隧道子域被 Cloudflare 回收、分给陌生人后,陌生人的站点不经我们的代理,
8
- // 拿不到这个指纹、也猜不出。于是其克隆的登录页要么没有指纹、要么指纹对不上:
9
- // 用户在手机登录页看到的指纹若与电脑设置页不一致,即说明链接已被他人复用,
10
- // 不应输入密码。这是"密码不防回收"之外,真正能拦住密码钓鱼的一层。
11
-
12
- import { randomBytes } from 'node:crypto'
13
-
14
- let fingerprint = null
15
-
16
- /**
17
- * 惰性生成并返回本次进程的会话指纹(多次调用返回同一值)。
18
- * @returns {string} 6 位大写字母/数字指纹
19
- */
20
- export function ensureSessionFingerprint() {
21
- if (!fingerprint) {
22
- let s = ''
23
- while (s.length < 6) {
24
- // base64 含 + / 等非字母数字字符,只保留 A-Z0-9 并转大写
25
- s += randomBytes(6).toString('base64').replace(/[^A-Z0-9]/gi, '').toUpperCase()
26
- }
27
- fingerprint = s.slice(0, 6)
28
- }
29
- return fingerprint
30
- }
31
-
32
- /** 读取已生成的指纹(代理尚未启动时返回 null)。 */
33
- export function getSessionFingerprint() {
34
- return fingerprint
35
- }