From 9a39bbb3606a656132bc5682c2617a08af72c83b Mon Sep 17 00:00:00 2001 From: luxiuzhe <3434055686@qq.com> Date: Mon, 28 Sep 2026 17:02:13 +0800 Subject: [PATCH 1/2] feat(auth): support stronger custom access passwords --- README.en.md | 8 ++++---- README.md | 8 ++++---- client/client.js | 13 ++++++------- client/index.jsx | 7 +++---- client/pocket-locales.js | 8 ++++---- lib/index.js | 13 +++++++------ lib/proxy.mjs | 4 ++-- lib/settings.mjs | 2 +- test/proxy.test.js | 2 +- test/settings.test.js | 20 ++++++++++++-------- 10 files changed, 44 insertions(+), 41 deletions(-) diff --git a/README.en.md b/README.en.md index c5a23f8..c0285ac 100644 --- a/README.en.md +++ b/README.en.md @@ -48,7 +48,7 @@ What it looks like — the phone shows the exact same UI as your computer, live: | 🌐 Public QR (from anywhere) | Click "Enable anywhere" → cloudflared tunnel → scan the public QR over 4G / any network | | 🏷️ Fixed public hostname | Optional "**Named tunnel**" mode: paste a Cloudflare Tunnel Token + your own domain — the public address stays **fixed across restarts** (see below) | | 🔐 Access PIN | Public links use an **8-digit random PIN** by default (rotated on every tunnel start; **customizable to a fixed PIN** — custom PINs are not rotated); LAN has its own separate **8-digit random PIN** (on by default; switchable off in Settings — then LAN scans connect directly) | -| 🔑 Custom PINs | Both the public and LAN PINs can be **set to a fixed 8–64-character PIN using letters and digits in Settings** (custom PINs are never auto-rotated) | +| 🔑 Custom PINs | Both the public and LAN PINs can be **set to a fixed 8-128-character PIN using non-whitespace characters (including symbols) in Settings** (custom PINs are never auto-rotated) | | 🧘 Session persistence | Enter the PIN once and you're set for a long time (login is tied to the computer's dsh web process: as long as it stays up, the phone won't ask again; **after a dsh web restart/update, enter it once more**) | | ⚡ Real-time sync | Streaming output passes through WebSocket untouched — what the computer renders, the phone renders live; fully interactive both ways; built-in WS heartbeat keep-alive (defeats silent NAT/battery link drops with auto-reconnect) | | 📱 Mobile-adaptive layout | Narrow screens get a drawer layout automatically (ported from dsh-web-mobile, MIT): sidebar drawer, full-width conversation, safe-area insets, touch optimizations | @@ -83,7 +83,7 @@ npx @deepseek-ai/dsh web ### LAN (same Wi-Fi) -Settings → **Phone access** → scan the "📶 LAN" QR code → enter the **LAN PIN** (shown in the LAN block; hit **Refresh** to roll a new one, or **Customize** to set an 8–64-character alphanumeric PIN) → the phone opens the exact same DSH, in real time. +Settings → **Phone access** → scan the "📶 LAN" QR code → enter the **LAN PIN** (shown in the LAN block; hit **Refresh** to roll a new one, or **Customize** to set an 8-128-character PIN using non-whitespace characters, including symbols) → the phone opens the exact same DSH, in real time. > The "**LAN access**" switch is **on by default** and can be **turned off/on with one click** (a confirmation dialog shows each time). Off kills the LAN QR code and link instantly (phones can't open them); **public access is unaffected**. Tap "On" to restore it. > @@ -95,7 +95,7 @@ Settings → **Phone access** → scan the "📶 LAN" QR code → enter the **LA ### Public (from anywhere) -On the same page click "**Enable anywhere**" → **a security disclaimer pops up every time — check "I understand and agree" to proceed** (on a corporate/classified network, confirm compliance first) → wait for the tunnel (first run downloads cloudflared; macOS/Linux use the Tsinghua mirror, seconds) → scan the "🌐 Public" QR code → the phone opens the link and **enters the access PIN** (shown in the settings page's public section; the default is an **8-digit random PIN rotated on every tunnel start**, or use **Customize** for a fixed 8–64-character alphanumeric PIN that is never rotated) → works from outside (4G / office network). +On the same page click "**Enable anywhere**" → **a security disclaimer pops up every time — check "I understand and agree" to proceed** (on a corporate/classified network, confirm compliance first) → wait for the tunnel (first run downloads cloudflared; macOS/Linux use the Tsinghua mirror, seconds) → scan the "🌐 Public" QR code → the phone opens the link and **enters the access PIN** (shown in the settings page's public section; the default is an **8-digit random PIN rotated on every tunnel start**, or use **Customize** for a fixed 8-128-character PIN using non-whitespace characters, including symbols, that is never rotated) → works from outside (4G / office network). > Upgrading: `dsh plugin --profile web update dsh-pocket --latest -w` (`--latest` is required across major versions — a `^0.x` range won't auto-jump to 1.x). @@ -114,7 +114,7 @@ Note: in named-tunnel mode the public PIN is **not auto-rotated** (the address i - **DSH can execute code on your computer.** **LAN** QR/URL plus its own **8-character PIN** is the key (PIN **on by default**, switchable off — then LAN scans connect directly, same-network devices only) — **never share the LAN QR, URL or PIN**. - **Read and accept the security disclaimer before enabling public access** (the dialog shows on every enable; the server enforces it, so it can't be bypassed): public = exposing a code-executing DSH to the internet — use a strong PIN, turn it off when done, never on classified networks. -- **Public** access uses an **8-digit random PIN** by default: the link is random, the PIN rotates on every tunnel start, and old links die instantly — even a leaked link can't get in. **A custom PIN may contain 8–64 letters and digits and is never auto-rotated**. +- **Public** access uses an **8-digit random PIN** by default: the link is random, the PIN rotates on every tunnel start, and old links die instantly — even a leaked link can't get in. **A custom PIN may contain 8-128 non-whitespace characters, including symbols, and is never auto-rotated**. - Phone login state is tied to the computer's dsh web process: **no re-entry while dsh web stays up; one re-entry after a restart/update**. - **Login rate limiting** (anti brute-force): **5** consecutive wrong PINs from the same IP lock it for **60s**; a global failure threshold briefly locks everyone (blocks distributed IP-rotation scans); a successful login resets the counter. - The public URL is randomly assigned by cloudflared and **changes on every restart** (old links die automatically — a natural key rotation); in **named-tunnel fixed-hostname** mode the address stays and the PIN is not auto-rotated — manage it with a custom PIN. diff --git a/README.md b/README.md index 470dc21..7a70dc9 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ DSH Pocket 就是干这个的:**装上它,手机扫个码,就能实时看 | 🌐 公网扫码(人在外面) | 点「开启公网访问」→ cloudflared 隧道 → 出公网二维码,4G/任何网络都能访问 | | 🏷️ 公网固定域名 | 可选「**命名隧道**」模式:填 Cloudflare Tunnel Token + 自己的域名,公网地址**固定不变**(重启不再变;见下方说明) | | 🔐 访问密码 | 公网链接默认使用 **8 位随机密码**(每次开启公网自动换新;**可自定义固定密码**——自定义后不再换新);局域网有独立的默认 **8 位随机密码**(默认开启,设置页可**一键关闭**——关闭后局域网扫码直连) | -| 🔑 自定义密码 | 公网/局域网密码都可在设置页**设成自己固定的 8–64 位密码(英文字母大小写或数字)**(自定义后公网不再自动换新) | +| 🔑 自定义密码 | 公网/局域网密码都可在设置页**设成自己固定的 8-128 位密码(允许特殊字符,不含空白或控制字符)**(自定义后公网不再自动换新) | | 🧘 会话保持 | 手机输一次密码后**长期免输**(登录状态绑定电脑上的 dsh web 进程:只要它不重启,手机不用再输;**dsh web 重启/更新后需重新输入一次**) | | ⚡ 实时同步 | 流式输出走 WebSocket 全透传——**电脑上在输出,手机上同步在滚**,可双向操作;内置心跳保活(防路由器 NAT/省电机制静默断链,断线自动重连) | | 📱 移动端适配 | 窄屏自动变抽屉布局(移植 dsh-web-mobile,MIT):侧栏抽屉、会话全宽、状态栏安全区、触控优化 | @@ -83,7 +83,7 @@ npx @deepseek-ai/dsh web ### 局域网(同一 WiFi) -设置 → **手机访问** → 手机扫「📶 局域网」二维码 → 打开链接**输入局域网密码**(显示在设置页局域网区块,点「刷新」可换新,或点「自定义」设成自己固定的 8–64 位密码——英文字母大小写或数字)→ 打开的就是电脑上的 DSH,实时同步。 +设置 → **手机访问** → 手机扫「📶 局域网」二维码 → 打开链接**输入局域网密码**(显示在设置页局域网区块,点「刷新」可换新,或点「自定义」设成自己固定的 8-128 位密码——允许特殊字符,但不能含空白或控制字符)→ 打开的就是电脑上的 DSH,实时同步。 > 「**局域网访问**」开关默认**开**:可一键**关闭/开启**(切换时弹窗提醒)——关闭后局域网二维码/链接立即失效(手机打不开),**公网不受影响**;想恢复时再点「开」即可。 > @@ -95,7 +95,7 @@ npx @deepseek-ai/dsh web ### 公网(人在外面) -同一页点「**开启公网访问**」→ **每次都会先弹出安全免责声明**,勾选「我已知情」后才能开启(公司/涉密网络请先确认合规)→ 等隧道建立(首次会下载 cloudflared,macOS/Linux 走清华镜像秒下)→ 手机扫「🌐 公网」二维码 → 打开链接**输入访问密码**(密码显示在设置页公网区块,默认是**每次开启公网变新的 8 位随机密码**,也可点「自定义」设成 8–64 位固定密码——英文字母大小写或数字,自定义后不再换新)→ 人在外面(4G/公司网)也能访问。 +同一页点「**开启公网访问**」→ **每次都会先弹出安全免责声明**,勾选「我已知情」后才能开启(公司/涉密网络请先确认合规)→ 等隧道建立(首次会下载 cloudflared,macOS/Linux 走清华镜像秒下)→ 手机扫「🌐 公网」二维码 → 打开链接**输入访问密码**(密码显示在设置页公网区块,默认是**每次开启公网变新的 8 位随机密码**,也可点「自定义」设成 8-128 位固定密码——允许特殊字符,但不能含空白或控制字符,自定义后不再换新)→ 人在外面(4G/公司网)也能访问。 > 更新到新版本:`dsh plugin --profile web update dsh-pocket --latest -w`(跨大版本时 `--latest` 是必须的,`^0.x` 范围不会自动升到 1.x)。 @@ -114,7 +114,7 @@ npx @deepseek-ai/dsh web - **DSH 能执行你电脑上的代码**。**局域网**二维码/URL 配上独立 **8 位密码**才是钥匙(密码**默认开启**,可关——关闭后局域网扫码直连,仅同一网络设备可访问),**请勿把局域网二维码、URL 或密码发给别人** - **开启公网访问前必须阅读并勾选免责声明**(每次开启都会弹框;服务端强制校验,无法绕过):公网 = 把能执行代码的 DSH 暴露到互联网,请使用强密码、用完即关、涉密网络勿用 -- **公网**默认有 **8 位随机密码**保护:链接随机分配、默认每次开启换新密码、旧链接立即作废——泄露了也进不来,改密码/重开即可作废;**自定义密码可设为 8–64 位英文字母或数字,且不再自动换新** +- **公网**默认有 **8 位随机密码**保护:链接随机分配、默认每次开启换新密码、旧链接立即作废——泄露了也进不来,改密码/重开即可作废;**自定义密码可设为 8-128 位非空白字符(包括特殊字符),且不再自动换新** - 手机登录状态与电脑上的 dsh web 进程绑定:**电脑 dsh web 一直开着就不用重复输入;重启/更新后需重新输入一次** - **登录限速**(防暴力破解):同一 IP 连续输错 **5 次**锁定 **60 秒**;全局失败超阈值时短暂全锁(防换 IP 分布式扫描);输对密码后计数清零 - 公网 URL 由 cloudflared 随机分配,**每次重启会变化**(旧链接自动失效,相当于天然轮换);**命名隧道固定域名**模式下地址不变、密码不自动轮换,请配合自定义密码管理 diff --git a/client/client.js b/client/client.js index d614db0..59bf2e6 100644 --- a/client/client.js +++ b/client/client.js @@ -1937,10 +1937,10 @@ var zh2 = { "lanPinCustomValue": "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A{pin}\uFF08\u81EA\u5B9A\u4E49\uFF1B\u624B\u673A\u6253\u5F00\u9700\u8F93\u5165\uFF09", "refresh": "\u5237\u65B0", "customize": "\u81EA\u5B9A\u4E49", - "customizing": "\u65B0\u5BC6\u7801\uFF088\u201364 \u4F4D\uFF0C\u82F1\u6587\u5B57\u6BCD\u6216\u6570\u5B57\uFF09\uFF1A", + "customizing": "\u65B0\u5BC6\u7801\uFF088-128 \u4F4D\uFF0C\u53EF\u542B\u7279\u6B8A\u5B57\u7B26\uFF09\uFF1A", "save": "\u4FDD\u5B58", "cancel": "\u53D6\u6D88", - "pinInvalid": "\u5BC6\u7801\u5FC5\u987B\u662F 8\u201364 \u4F4D\u82F1\u6587\u5B57\u6BCD\u6216\u6570\u5B57", + "pinInvalid": "\u5BC6\u7801\u9700\u4E3A 8-128 \u4F4D\u4E14\u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u6216\u63A7\u5236\u5B57\u7B26", "pinCustomHint": "\u81EA\u5B9A\u4E49\u540E\u5F00\u542F\u516C\u7F51\u4E0D\u518D\u81EA\u52A8\u6362\u65B0", "lanPinOff": "\u{1F513} \u5BC6\u7801\u5DF2\u5173\u95ED\uFF1A\u626B\u7801\u76F4\u8FDE\uFF0C\u65E0\u9700\u5BC6\u7801\uFF08\u4EC5\u540C\u4E00\u5C40\u57DF\u7F51\u8BBE\u5907\u53EF\u8BBF\u95EE\uFF1B\u516C\u7F51\u4ECD\u8981\u5BC6\u7801\uFF09", "lanStarting": "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026", @@ -2035,10 +2035,10 @@ var en2 = { "lanPinCustomValue": "\u{1F510} PIN: {pin} (custom; required on the phone)", "refresh": "Refresh", "customize": "Customize", - "customizing": "New PIN (8\u201364 chars, letters/digits): ", + "customizing": "New PIN (8-128 chars; special characters allowed): ", "save": "Save", "cancel": "Cancel", - "pinInvalid": "PIN must be 8\u201364 characters (letters and digits only)", + "pinInvalid": "PIN must be 8-128 characters with no whitespace or control characters", "pinCustomHint": "custom PINs are not rotated on tunnel start", "lanPinOff": "\u{1F513} PIN off \u2014 scan & go, no PIN (LAN devices only; public still requires PIN)", "lanStarting": "Proxy starting\u2026", @@ -2358,11 +2358,10 @@ function PocketSettingsTab({ rpcCall, t }) { (0, import_react2.createElement)("input", { style: { width: 130, margin: "0 6px", padding: "4px 8px", fontSize: 14, letterSpacing: 1, textAlign: "center", border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", borderRadius: 6, outline: "none" }, type: "password", - minLength: 8, - maxLength: 64, + maxLength: 128, value: customPin?.value ?? "", autoFocus: true, - onChange: (e) => setCustomPin((c) => ({ ...c, value: e.target.value.replace(/[^a-zA-Z0-9]/g, ""), err: null })), + onChange: (e) => setCustomPin((c) => ({ ...c, value: e.target.value.replace(/[\s\x00-\x1f\x7f]/g, ""), err: null })), onKeyDown: (e) => { if (e.key === "Enter") saveCustomPin(which); if (e.key === "Escape") setCustomPin(null); diff --git a/client/index.jsx b/client/index.jsx index 1227511..6cdeff4 100644 --- a/client/index.jsx +++ b/client/index.jsx @@ -302,7 +302,7 @@ function PocketSettingsTab({ rpcCall, t }) { } }; - // 自定义访问密码(issue #33):公网/局域网各自设固定 8–64 位密码(英文字母大小写或数字);自定义后公网不再自动轮换。 + // 自定义访问密码(issue #33):公网/局域网各自设 8-128 位密码(允许特殊字符);自定义后公网不再自动轮换。 // customPin: { which: 'public'|'lan', value, err } | null —— 正在输入自定义密码的区块 const [customPin, setCustomPin] = useState(null); const saveCustomPin = async (which) => { @@ -326,11 +326,10 @@ function PocketSettingsTab({ rpcCall, t }) { h('input', { style: { width: 130, margin: '0 6px', padding: '4px 8px', fontSize: 14, letterSpacing: 1, textAlign: 'center', border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', borderRadius: 6, outline: 'none' }, type: 'password', - minLength: 8, - maxLength: 64, + maxLength: 128, value: customPin?.value ?? '', autoFocus: true, - onChange: (e) => setCustomPin((c) => ({ ...c, value: e.target.value.replace(/[^a-zA-Z0-9]/g, ''), err: null })), + onChange: (e) => setCustomPin((c) => ({ ...c, value: e.target.value.replace(/[\s\x00-\x1f\x7f]/g, ''), err: null })), onKeyDown: (e) => { if (e.key === 'Enter') saveCustomPin(which); if (e.key === 'Escape') setCustomPin(null); }, }), h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 2 }, onClick: () => saveCustomPin(which) }, t('save')), diff --git a/client/pocket-locales.js b/client/pocket-locales.js index effee90..a6fd16d 100644 --- a/client/pocket-locales.js +++ b/client/pocket-locales.js @@ -59,10 +59,10 @@ export const zh = { 'lanPinCustomValue': '🔐 访问密码:{pin}(自定义;手机打开需输入)', 'refresh': '刷新', 'customize': '自定义', - 'customizing': '新密码(8–64 位,英文字母或数字):', + 'customizing': '新密码(8-128 位,可含特殊字符):', 'save': '保存', 'cancel': '取消', - 'pinInvalid': '密码必须是 8–64 位英文字母或数字', + 'pinInvalid': '密码需为 8-128 位且不能包含空白或控制字符', 'pinCustomHint': '自定义后开启公网不再自动换新', 'lanPinOff': '🔓 密码已关闭:扫码直连,无需密码(仅同一局域网设备可访问;公网仍要密码)', 'lanStarting': '代理未就绪…', @@ -159,10 +159,10 @@ export const en = { 'lanPinCustomValue': '🔐 PIN: {pin} (custom; required on the phone)', 'refresh': 'Refresh', 'customize': 'Customize', - 'customizing': 'New PIN (8–64 chars, letters/digits): ', + 'customizing': 'New PIN (8-128 chars; special characters allowed): ', 'save': 'Save', 'cancel': 'Cancel', - 'pinInvalid': 'PIN must be 8–64 characters (letters and digits only)', + 'pinInvalid': 'PIN must be 8-128 characters with no whitespace or control characters', 'pinCustomHint': 'custom PINs are not rotated on tunnel start', 'lanPinOff': '🔓 PIN off — scan & go, no PIN (LAN devices only; public still requires PIN)', 'lanStarting': 'Proxy starting…', diff --git a/lib/index.js b/lib/index.js index bcfe433..fbb1ec8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -49,8 +49,9 @@ const loadedVersion = currentVersion(); // 局域网密码(token-lan):默认手动刷新(设置页按钮);自定义后刷新会换回随机值。 // 会话保持(issue #33):登录 cookie 绑定进程级 sessionKey(见 apply)—— // dsh web 重启/更新后 sessionKey 变化 → 手机需重新输入。 -// 默认 PIN 为 8 位数字;自定义 PIN 可使用 8–64 位英文字母(大小写)或数字。 -const PIN_RE = /^[a-zA-Z0-9]{8,64}$/; +// 自动生成的 PIN 仍为 8 位数字;自定义 PIN 支持 8-128 个非空白字符, +// 同时拒绝控制字符,避免表单、URL 和日志注入歧义。 +const PIN_RE = /^(?=.{8,128}$)[^\s\x00-\x1f\x7f]+$/u; function writePinToFile(p, fresh) { try { mkdirSync(dirname(p), { recursive: true }); @@ -136,13 +137,13 @@ export function isLanOverrideHost(host) { return override.length > 0 && hostNameOnly(host) === override; } /** - * 用户自定义访问密码(issue #33):公网/局域网各自设置固定的 8–64 位密码(英文字母大小写或数字)。 + * 用户自定义访问密码(issue #33):公网/局域网各自设置固定的 8-128 位非空白密码。 * 自定义后公网开启时不再自动轮换(rotateAccessToken 见上)。 - * 非法输入(少于 8 位、超过 64 位或含字母数字以外字符)抛错,由 RPC 层转成错误响应。 + * 非法输入(长度不在 8-128 位、含空白或控制字符)抛错,由 RPC 层转成错误响应。 */ export function setCustomPin(which, value) { - const v = String(value ?? '').trim(); - if (!PIN_RE.test(v)) throw new Error('密码必须是 8–64 位英文字母或数字 | PIN must be 8–64 characters (letters and digits only)'); + const v = String(value ?? ''); + if (!PIN_RE.test(v)) throw new Error('密码需为 8-128 位且不能包含空白或控制字符 | PIN must be 8-128 characters and contain no whitespace or control characters'); if (which === 'public') { writePinToFile(tokenPath(), v); setPinCustom('public', true); diff --git a/lib/proxy.mjs b/lib/proxy.mjs index 8e29df1..cd62758 100644 --- a/lib/proxy.mjs +++ b/lib/proxy.mjs @@ -216,10 +216,10 @@ button{width:100%;padding:10px;font-size:15px;background:#4f6ef7;color:#fff;bord .err{color:#dc2626;font-size:12px;margin-bottom:10px;min-height:16px}
${where}受访问密码保护,请输入访问密码 | ${whereEn} is password-protected — enter the access PIN
+${where}受访问密码保护,请输入 8-128 位密码 | ${whereEn} is password-protected — enter your 8-128 character PIN