diff --git a/README.en.md b/README.en.md index 66cb4ee..d32d913 100644 --- a/README.en.md +++ b/README.en.md @@ -47,8 +47,8 @@ What it looks like — the phone shows the exact same UI as your computer, live: | 🚪 LAN switch | **Turn LAN access off/on with one click** in Settings (a confirmation dialog shows each time): off kills the LAN QR code and link instantly; public access is unaffected | | 🌐 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 require an **8-character PIN** (rotated on every tunnel start by default; **customizable to a fixed PIN** — custom PINs are not rotated); LAN has its own separate **8-character 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 your own fixed 8-character PIN (letters and digits) in Settings** (custom PINs are never auto-rotated) | +| 🔐 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) | | 🧘 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 | @@ -82,7 +82,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 your own fixed 8 digits) → 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–64-character alphanumeric PIN) → 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. > @@ -94,7 +94,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 8-character PIN** (shown in the settings page's public section; **rotated on every tunnel start by default**, or **Customize** it to a fixed PIN — letters and digits — 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–64-character alphanumeric PIN 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). @@ -113,7 +113,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 is protected by an **8-character PIN**: the link is random, the PIN rotates on every tunnel start by default, and old links die instantly — even a leaked link can't get in. **A custom PIN is never auto-rotated** (your value stays stable; custom PINs may use letters and digits). +- **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**. - 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 01715b6..cbbefdc 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ DSH Pocket 就是干这个的:**装上它,手机扫个码,就能实时看 | 🚪 局域网开关 | 设置页可**一键关闭/开启局域网访问**(切换时弹窗提醒):关闭后局域网二维码/链接立即失效,仅公网可用 | | 🌐 公网扫码(人在外面) | 点「开启公网访问」→ cloudflared 隧道 → 出公网二维码,4G/任何网络都能访问 | | 🏷️ 公网固定域名 | 可选「**命名隧道**」模式:填 Cloudflare Tunnel Token + 自己的域名,公网地址**固定不变**(重启不再变;见下方说明) | -| 🔐 访问密码 | 公网链接需输入 **8 位密码**(默认每次开启公网自动换新;**可自定义固定密码**——自定义后不再换新);局域网有独立 **8 位密码**(默认开启,设置页可**一键关闭**——关闭后局域网扫码直连) | -| 🔑 自定义密码 | 公网/局域网密码都可在设置页**设成自己固定的 8 位密码(英文字母大小写或数字)**(自定义后公网不再自动换新) | +| 🔐 访问密码 | 公网链接默认使用 **8 位随机密码**(每次开启公网自动换新;**可自定义固定密码**——自定义后不再换新);局域网有独立的默认 **8 位随机密码**(默认开启,设置页可**一键关闭**——关闭后局域网扫码直连) | +| 🔑 自定义密码 | 公网/局域网密码都可在设置页**设成自己固定的 8–64 位密码(英文字母大小写或数字)**(自定义后公网不再自动换新) | | 🧘 会话保持 | 手机输一次密码后**长期免输**(登录状态绑定电脑上的 dsh web 进程:只要它不重启,手机不用再输;**dsh web 重启/更新后需重新输入一次**) | | ⚡ 实时同步 | 流式输出走 WebSocket 全透传——**电脑上在输出,手机上同步在滚**,可双向操作;内置心跳保活(防路由器 NAT/省电机制静默断链,断线自动重连) | | 📱 移动端适配 | 窄屏自动变抽屉布局(移植 dsh-web-mobile,MIT):侧栏抽屉、会话全宽、状态栏安全区、触控优化 | @@ -82,7 +82,7 @@ npx @deepseek-ai/dsh web ### 局域网(同一 WiFi) -设置 → **手机访问** → 手机扫「📶 局域网」二维码 → 打开链接**输入局域网密码**(显示在设置页局域网区块,点「刷新」可换新,或点「自定义」设成自己固定的 8 位密码——英文字母大小写或数字)→ 打开的就是电脑上的 DSH,实时同步。 +设置 → **手机访问** → 手机扫「📶 局域网」二维码 → 打开链接**输入局域网密码**(显示在设置页局域网区块,点「刷新」可换新,或点「自定义」设成自己固定的 8–64 位密码——英文字母大小写或数字)→ 打开的就是电脑上的 DSH,实时同步。 > 「**局域网访问**」开关默认**开**:可一键**关闭/开启**(切换时弹窗提醒)——关闭后局域网二维码/链接立即失效(手机打不开),**公网不受影响**;想恢复时再点「开」即可。 > @@ -94,7 +94,7 @@ npx @deepseek-ai/dsh web ### 公网(人在外面) -同一页点「**开启公网访问**」→ **每次都会先弹出安全免责声明**,勾选「我已知情」后才能开启(公司/涉密网络请先确认合规)→ 等隧道建立(首次会下载 cloudflared,macOS/Linux 走清华镜像秒下)→ 手机扫「🌐 公网」二维码 → 打开链接**输入 8 位访问密码**(密码显示在设置页公网区块,默认**每次开启公网变新**,也可点「自定义」设成固定密码——英文字母大小写或数字,自定义后不再换新)→ 人在外面(4G/公司网)也能访问。 +同一页点「**开启公网访问**」→ **每次都会先弹出安全免责声明**,勾选「我已知情」后才能开启(公司/涉密网络请先确认合规)→ 等隧道建立(首次会下载 cloudflared,macOS/Linux 走清华镜像秒下)→ 手机扫「🌐 公网」二维码 → 打开链接**输入访问密码**(密码显示在设置页公网区块,默认是**每次开启公网变新的 8 位随机密码**,也可点「自定义」设成 8–64 位固定密码——英文字母大小写或数字,自定义后不再换新)→ 人在外面(4G/公司网)也能访问。 > 更新到新版本:`dsh plugin --profile web update dsh-pocket --latest -w`(跨大版本时 `--latest` 是必须的,`^0.x` 范围不会自动升到 1.x)。 @@ -113,7 +113,7 @@ npx @deepseek-ai/dsh web - **DSH 能执行你电脑上的代码**。**局域网**二维码/URL 配上独立 **8 位密码**才是钥匙(密码**默认开启**,可关——关闭后局域网扫码直连,仅同一网络设备可访问),**请勿把局域网二维码、URL 或密码发给别人** - **开启公网访问前必须阅读并勾选免责声明**(每次开启都会弹框;服务端强制校验,无法绕过):公网 = 把能执行代码的 DSH 暴露到互联网,请使用强密码、用完即关、涉密网络勿用 -- **公网**有 **8 位密码**保护:链接随机分配、默认每次开启换新密码、旧链接立即作废——泄露了也进不来,改密码/重开即可作废;**自定义密码后不再自动换新**(你设的值即稳定密码,可为英文字母大小写或数字) +- **公网**默认有 **8 位随机密码**保护:链接随机分配、默认每次开启换新密码、旧链接立即作废——泄露了也进不来,改密码/重开即可作废;**自定义密码可设为 8–64 位英文字母或数字,且不再自动换新** - 手机登录状态与电脑上的 dsh web 进程绑定:**电脑 dsh web 一直开着就不用重复输入;重启/更新后需重新输入一次** - **登录限速**(防暴力破解):同一 IP 连续输错 **5 次**锁定 **60 秒**;全局失败超阈值时短暂全锁(防换 IP 分布式扫描);输对密码后计数清零 - 公网 URL 由 cloudflared 随机分配,**每次重启会变化**(旧链接自动失效,相当于天然轮换);**命名隧道固定域名**模式下地址不变、密码不自动轮换,请配合自定义密码管理 diff --git a/client/client.js b/client/client.js index d2eb711..42f1067 100644 --- a/client/client.js +++ b/client/client.js @@ -1799,8 +1799,8 @@ function mobileApply(ctx) { ctx.effect(() => { if (!narrow.matches) return () => { }; - const PHRASES = ["加载提供方目录失败", "Settings are unavailable in this browser"]; - const NOTICE = "手机上不支持模型设置,请去电脑端修改设置"; + const PHRASES = ["\u52A0\u8F7D\u63D0\u4F9B\u65B9\u76EE\u5F55\u5931\u8D25", "Settings are unavailable in this browser"]; + const NOTICE = "\u624B\u673A\u4E0A\u4E0D\u652F\u6301\u6A21\u578B\u8BBE\u7F6E\uFF0C\u8BF7\u53BB\u7535\u8111\u7AEF\u4FEE\u6539\u8BBE\u7F6E"; const findDeepest = (el) => { let deepest = el; for (const child of el.querySelectorAll("*")) { @@ -1821,9 +1821,7 @@ function mobileApply(ctx) { const observer = new MutationObserver(patch); observer.observe(document.body, { childList: true, subtree: true, characterData: true }); patch(); - return () => { - observer.disconnect(); - }; + return () => observer.disconnect(); }, "dsh-mobile-nav: replace model-settings load error with mobile hint"); ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({ name: "conversation.session.header.actions", @@ -1913,10 +1911,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 \u4F4D\uFF0C\u82F1\u6587\u5B57\u6BCD\u6216\u6570\u5B57\uFF09\uFF1A", + "customizing": "\u65B0\u5BC6\u7801\uFF088\u201364 \u4F4D\uFF0C\u82F1\u6587\u5B57\u6BCD\u6216\u6570\u5B57\uFF09\uFF1A", "save": "\u4FDD\u5B58", "cancel": "\u53D6\u6D88", - "pinInvalid": "\u5BC6\u7801\u5FC5\u987B\u662F 8 \u4F4D\u82F1\u6587\u5B57\u6BCD\u6216\u6570\u5B57", + "pinInvalid": "\u5BC6\u7801\u5FC5\u987B\u662F 8\u201364 \u4F4D\u82F1\u6587\u5B57\u6BCD\u6216\u6570\u5B57", "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", @@ -2009,10 +2007,10 @@ var en2 = { "lanPinCustomValue": "\u{1F510} PIN: {pin} (custom; required on the phone)", "refresh": "Refresh", "customize": "Customize", - "customizing": "New PIN (8 chars, letters/digits): ", + "customizing": "New PIN (8\u201364 chars, letters/digits): ", "save": "Save", "cancel": "Cancel", - "pinInvalid": "PIN must be exactly 8 characters (letters and digits only)", + "pinInvalid": "PIN must be 8\u201364 characters (letters and digits only)", "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", @@ -2312,7 +2310,8 @@ 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", - maxLength: 8, + minLength: 8, + maxLength: 64, value: customPin?.value ?? "", autoFocus: true, onChange: (e) => setCustomPin((c) => ({ ...c, value: e.target.value.replace(/[^a-zA-Z0-9]/g, ""), err: null })), diff --git a/client/index.jsx b/client/index.jsx index 99f09ba..37279c9 100644 --- a/client/index.jsx +++ b/client/index.jsx @@ -282,7 +282,7 @@ function PocketSettingsTab({ rpcCall, t }) { } }; - // 自定义访问密码(issue #33):公网/局域网各自设固定 8 位密码(英文字母大小写或数字);自定义后公网不再自动轮换。 + // 自定义访问密码(issue #33):公网/局域网各自设固定 8–64 位密码(英文字母大小写或数字);自定义后公网不再自动轮换。 // customPin: { which: 'public'|'lan', value, err } | null —— 正在输入自定义密码的区块 const [customPin, setCustomPin] = useState(null); const saveCustomPin = async (which) => { @@ -306,7 +306,8 @@ 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', - maxLength: 8, + minLength: 8, + maxLength: 64, value: customPin?.value ?? '', autoFocus: true, onChange: (e) => setCustomPin((c) => ({ ...c, value: e.target.value.replace(/[^a-zA-Z0-9]/g, ''), err: null })), diff --git a/client/pocket-locales.js b/client/pocket-locales.js index 9c34bf8..8e37fbb 100644 --- a/client/pocket-locales.js +++ b/client/pocket-locales.js @@ -59,10 +59,10 @@ export const zh = { 'lanPinCustomValue': '🔐 访问密码:{pin}(自定义;手机打开需输入)', 'refresh': '刷新', 'customize': '自定义', - 'customizing': '新密码(8 位,英文字母或数字):', + 'customizing': '新密码(8–64 位,英文字母或数字):', 'save': '保存', 'cancel': '取消', - 'pinInvalid': '密码必须是 8 位英文字母或数字', + 'pinInvalid': '密码必须是 8–64 位英文字母或数字', 'pinCustomHint': '自定义后开启公网不再自动换新', 'lanPinOff': '🔓 密码已关闭:扫码直连,无需密码(仅同一局域网设备可访问;公网仍要密码)', 'lanStarting': '代理未就绪…', @@ -157,10 +157,10 @@ export const en = { 'lanPinCustomValue': '🔐 PIN: {pin} (custom; required on the phone)', 'refresh': 'Refresh', 'customize': 'Customize', - 'customizing': 'New PIN (8 chars, letters/digits): ', + 'customizing': 'New PIN (8–64 chars, letters/digits): ', 'save': 'Save', 'cancel': 'Cancel', - 'pinInvalid': 'PIN must be exactly 8 characters (letters and digits only)', + 'pinInvalid': 'PIN must be 8–64 characters (letters and digits only)', '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 79f3c05..6f86b9d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -49,8 +49,8 @@ const loadedVersion = currentVersion(); // 局域网密码(token-lan):默认手动刷新(设置页按钮);自定义后刷新会换回随机值。 // 会话保持(issue #33):登录 cookie 绑定进程级 sessionKey(见 apply)—— // dsh web 重启/更新后 sessionKey 变化 → 手机需重新输入。 -// 密码规则:8 位英文字母(大小写)或数字(自动生成的为 8 位数字,自定义可为字母+数字)。 -const PIN_RE = /^[a-zA-Z0-9]{8}$/; +// 默认 PIN 为 8 位数字;自定义 PIN 可使用 8–64 位英文字母(大小写)或数字。 +const PIN_RE = /^[a-zA-Z0-9]{8,64}$/; function writePinToFile(p, fresh) { try { mkdirSync(dirname(p), { recursive: true }); @@ -136,13 +136,13 @@ export function isLanOverrideHost(host) { return override.length > 0 && hostNameOnly(host) === override; } /** - * 用户自定义访问密码(issue #33):公网/局域网各自设一个固定的 8 位密码(英文字母大小写或数字)。 + * 用户自定义访问密码(issue #33):公网/局域网各自设置固定的 8–64 位密码(英文字母大小写或数字)。 * 自定义后公网开启时不再自动轮换(rotateAccessToken 见上)。 - * 非法输入(非 8 位、含字母数字以外字符)抛错,由 RPC 层转成错误响应。 + * 非法输入(少于 8 位、超过 64 位或含字母数字以外字符)抛错,由 RPC 层转成错误响应。 */ export function setCustomPin(which, value) { const v = String(value ?? '').trim(); - if (!PIN_RE.test(v)) throw new Error('密码必须是 8 位英文字母或数字 | PIN must be exactly 8 characters (letters and digits only)'); + if (!PIN_RE.test(v)) throw new Error('密码必须是 8–64 位英文字母或数字 | PIN must be 8–64 characters (letters and digits only)'); if (which === 'public') { writePinToFile(tokenPath(), v); setPinCustom('public', true); diff --git a/lib/proxy.mjs b/lib/proxy.mjs index 740e13e..8e29df1 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}受访问密码保护,请输入 8 位密码(英文字母或数字) | ${whereEn} is password-protected — enter the 8-character PIN (letters/digits)
+${where}受访问密码保护,请输入访问密码 | ${whereEn} is password-protected — enter the access PIN