dsh-remote-mobile 1.8.0 → 1.9.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/CHANGELOG.md +28 -0
- package/README.md +24 -11
- package/README_EN.md +24 -11
- package/lib/auth/token.d.ts +55 -3
- package/lib/auth/token.js +202 -76
- package/lib/client.js +12 -12
- package/lib/index.d.ts +2 -1
- package/lib/index.js +10 -5
- package/lib/routes/login-page.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.9.1] - 2026-10-17
|
|
9
|
+
|
|
10
|
+
### Fixed (解决 Issue #7 · 修复 /auth 登录页内联脚本语法错误导致局域网密码登录完全失效)
|
|
11
|
+
|
|
12
|
+
- **修复登录页内联脚本模板转义缺陷**:在 1.8.0 引入子路径/网关前缀自适应时,`src/routes/login-page.ts` 内使用 ES6 模板字符串拼接登录页内联 `<script>`,其中第 344/356/365 行的正则字面量写为 `window.location.pathname.replace(/\/auth\/?$/, ...)`。外层模板字符串反引号在求值时将 `\/` 折叠为 `/`,导致下发给客户端浏览器的内联脚本退化为 `replace(//auth/?$, ...)`,被 JS 词法解析器识别为单行注释吞掉后续代码并抛出 `SyntaxError: Unexpected token 'return'`:
|
|
13
|
+
- 内联脚本因此整体中断执行,表单 submit 监听器、`doVerify()`、RSA 密钥提取与 `/api/remote-mobile/verify` 接口调用均未注册,表现为局域网设备输入正确长期密码点击「立即授权连接」无任何反应,输入错误密码也无错误提示并退化为原生 GET 刷新;
|
|
14
|
+
- 现改用 `new RegExp('/auth/?$')` 显式构造正则对象,彻底消除模板字符串与正则字面量转义规则的歧义与折叠风险。
|
|
15
|
+
- **测试覆盖与语法保障**:
|
|
16
|
+
- 在 `test/routes.test.js` 中新增针对中英双语 `/auth` 页面内联 `<script>` 的自动化测试,通过 `new Function(...)` 语法解析并断言无 `SyntaxError`,且严禁出现未转义退化的 `//auth` 注释;
|
|
17
|
+
- 在 `test/compat.test.js` 中新增对所有注入到客户端的 5 个 Snippet 脚本的 `new Function(...)` 语法合法性全量断言。
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## [1.9.0] - 2026-10-09
|
|
22
|
+
|
|
23
|
+
### Fixed (适配 DSH 0.1.7+ 配置存储变更,修复插件配置无法持久化)
|
|
24
|
+
|
|
25
|
+
- **配置持久化迁移**:DSH 自 **0.1.7** 起(最早见于 v0.1.7-alpha.1 的 release notes,0.2.x 延续)官方已弃用 `~/.dsh/settings.yaml` 作为实时配置源(启动时一次性迁移进当前 profile 的 `cordis.patch.yml` 并改名为 `settings.yaml.imported`)。原实现持续写回该文件,导致免密开关、长期密码哈希、限频/锁定阈值在升级后无法持久化(重启即回落默认):
|
|
26
|
+
- 新增插件自有配置 `~/.dsh/remote-mobile/settings.json`(JSON、临时文件原子替换、权限 `0o600`),由 `SessionStore` 读写;
|
|
27
|
+
- 首次启动自动从旧 `settings.yaml` / `settings.yaml.imported` 一次性迁移,救回既有长期密码与限频配置;
|
|
28
|
+
- 保留旧版 `ctx.settings.register` 集成作为旧版 DSH 的尽力兼容(0.1.7+ 的 `SettingsForms` 无该 API,会安全跳过)。
|
|
29
|
+
- **防止已清空的密码复活**:配置迁移仅在自有 `settings.json` 不存在时执行一次;一旦自有配置存在,旧 `settings.yaml.imported` 不再参与合并,避免用户清空长期密码后重启又被旧值救回。决策逻辑抽为纯函数 `resolveInitialStoreOptions()` 并由单测锁定。
|
|
30
|
+
- **语言偏好读取适配**:`readGlobalLocale()` 改为优先读取当前 profile 的 `cordis.patch.yml`(locale entry),回退旧 `settings.yaml` / `settings.yaml.imported`,修复升级后登录页与接口语言恒为中文的问题。
|
|
31
|
+
- **测试**:新增 `test/settings-persistence.test.js` 覆盖自有配置读写、旧版迁移合并与 locale 解析。
|
|
32
|
+
- **文档**:README(中 / 英)更新配置存储位置与新增 FAQ Q8,说明 DSH 0.1.7+ 的 settings.yaml 迁移行为。
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
8
36
|
## [1.8.0] - 2026-10-08
|
|
9
37
|
|
|
10
38
|
### Fixed & Security (适配 NAS / 网关子路径前缀反代,修复静默失败与开放重定向)
|
package/README.md
CHANGED
|
@@ -166,7 +166,7 @@ dsh web --no-open
|
|
|
166
166
|
- 访问审计与锁定状态持久化落盘,服务重启后自动恢复;
|
|
167
167
|
- 支持管理员在管理面板中一键解锁指定 IP。
|
|
168
168
|
- **智能静态资源放行机制**:安全门禁内置智能前端静态扩展名识别(放行 `.js`、`.css`、`.png`、`.svg`、`.woff2` 等 20 种合法资源),拦截无扩展名或动态管理 API 请求(如 `/plugins/xxx/admin`),确保第三方生态插件前端正常展示的同时严密保护后台动态接口,无需用户在设置界面手动维护白名单。(DSH 核心静态目录 `/assets/` 属官方资源,单独无条件放行。)
|
|
169
|
-
- **高可靠原子持久化与防抖**:采用毫秒级写盘防抖节流保护磁盘 I/O(会话数据 500ms、样式片段 300ms),配合 `beforeExit` 进程退出 Flush 保证数据不丢失;`settings.
|
|
169
|
+
- **高可靠原子持久化与防抖**:采用毫秒级写盘防抖节流保护磁盘 I/O(会话数据 500ms、样式片段 300ms),配合 `beforeExit` 进程退出 Flush 保证数据不丢失;`settings.json` 与 `style-snippets.json` 采用临时文件原子替换(`renameSync`),防止异常中断损坏数据。
|
|
170
170
|
- **真实 IP 安全提取**:仅信任底层 Socket 真实连接地址,防范伪造的 `X-Forwarded-For` 欺骗攻击。
|
|
171
171
|
- **本机回环 CSRF 防御**:插件的变更类 API 会校验浏览器同源信号(`Origin` / `Sec-Fetch-Site`),恶意网页驱使浏览器向 `127.0.0.1` 发起的跨站写请求将被直接拒绝;curl / 本机脚本等无浏览器信号的客户端不受影响。
|
|
172
172
|
- **本机回环请求不做上下文改写**:上下文虚拟化仅作用于外部来源流量;本机回环请求保留原始 Host / Origin(不"洗白"),使 DSH 自带的 DNS-Rebinding 与同源校验对外部域名继续生效。SSE 事件流仅限同源连接,不开放跨域读取。
|
|
@@ -283,18 +283,21 @@ dsh plugin --profile web remove dsh-remote-mobile
|
|
|
283
283
|
<span id="advanced"></span>
|
|
284
284
|
## ⚙️ 高级配置
|
|
285
285
|
|
|
286
|
-
|
|
286
|
+
配置持久化于插件自有文件 `~/.dsh/remote-mobile/settings.json`,可在 Web 界面中直观调整,也可直接编辑该文件:
|
|
287
287
|
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
allowTailscale: false
|
|
291
|
-
allowLan: false
|
|
292
|
-
secretHash: ""
|
|
293
|
-
maxVisitsPerMinute: 60
|
|
294
|
-
maxFailedAttempts: 5
|
|
295
|
-
lockDurationMs: 900000
|
|
288
|
+
```jsonc
|
|
289
|
+
{
|
|
290
|
+
"allowTailscale": false, // boolean,默认 false:是否允许 Tailscale 虚拟私网免密访问
|
|
291
|
+
"allowLan": false, // boolean,默认 false:是否允许局域网免密访问(高危警示)
|
|
292
|
+
"secretHash": "", // string,默认空:长期访问密码的 scrypt 加盐哈希值
|
|
293
|
+
"maxVisitsPerMinute": 60, // number,默认 60:单 IP 每分钟最大访问登录页次数
|
|
294
|
+
"maxFailedAttempts": 5, // number,默认 5:触发封禁的连续认证失败最大次数
|
|
295
|
+
"lockDurationMs": 900000 // number,默认 900000 (15分钟):IP 锁定持续时间(毫秒)
|
|
296
|
+
}
|
|
296
297
|
```
|
|
297
298
|
|
|
299
|
+
> **关于配置存储**:DSH 自 **0.1.7** 起(最早见于 v0.1.7-alpha.1 的 release notes,0.2.x 延续)官方已弃用 `~/.dsh/settings.yaml` 作为**实时配置源**——该文件会在启动时被一次性迁移进当前 profile 的 `cordis.patch.yml`,并改名为 `settings.yaml.imported`(详见 [Q8](#faq))。因此本插件不再写入 settings.yaml,而是持久化到上述自有 `settings.json`;首次启动时会自动从旧 `settings.yaml` / `settings.yaml.imported` 迁移一次(把旧长期密码、限频等配置救回)。
|
|
300
|
+
|
|
298
301
|
### 🎨 移动端样式片段(可选)
|
|
299
302
|
|
|
300
303
|
自定义样式片段(样式小插件)与启停状态持久化于 `~/.dsh/remote-mobile/style-snippets.json`,可在设置面板图形化管理,也可直接编辑该文件:
|
|
@@ -331,7 +334,7 @@ dsh-remote-mobile:
|
|
|
331
334
|
|
|
332
335
|
| 文件路径 | 说明 | 安全级别 |
|
|
333
336
|
|---|---|---|
|
|
334
|
-
| `~/.dsh/settings.
|
|
337
|
+
| `~/.dsh/remote-mobile/settings.json` | 插件配置:免密开关、长期密码哈希与限频/锁定策略 | 本地落盘,权限 `0o600`(仅当前用户可读写) |
|
|
335
338
|
| `~/.dsh/remote-mobile/devices.json` | 已授权设备会话、IP 访问计数与安全审计数据(内含长效 Token,权限 `0o600`) | 本地落盘,仅当前用户可读写 |
|
|
336
339
|
| `~/.dsh/remote-mobile/rsa-keys.json` | 服务端 RSA 密钥对文件(含公钥与私钥) | 本地落盘,权限 `0o600`(仅当前用户可读写) |
|
|
337
340
|
| `~/.dsh/remote-mobile/style-snippets.json` | 移动端样式片段(内置预设启停状态 + 用户自定义 CSS 小插件) | 本地落盘持久化 |
|
|
@@ -435,6 +438,16 @@ dsh-remote-mobile:
|
|
|
435
438
|
* 若你希望通过外部统一域名/网关访问时仍强制要求输入本插件的动态配对码或长期访问密码,建议配置反向代理透明透传客户端源 IP,或直接通过 DSH 开放的外部端口(如默认的 `3080` 或自定义映射端口)直连访问。
|
|
436
439
|
</details>
|
|
437
440
|
|
|
441
|
+
<details>
|
|
442
|
+
<summary><b>Q8: 升级 DSH 0.1.7+(含 0.2.x)后 <code>~/.dsh/settings.yaml</code> 不见了 / 我的配置丢了?</b></summary>
|
|
443
|
+
|
|
444
|
+
**答**:这是 **DSH 官方自 0.1.7 起的行为变更**(最早见于 v0.1.7-alpha.1 的 release notes,0.2.x 延续),与本插件无关:
|
|
445
|
+
|
|
446
|
+
- DSH 自 **0.1.7** 起已**不再把 `~/.dsh/settings.yaml` 当作实时配置源**。启动时它由官方 `dsh-settings` 的 `importLegacyDocument()` **一次性导入**到当前 profile 的 `~/.dsh/profiles/<profile>/cordis.patch.yml`,随后原文件被**改名为 `settings.yaml.imported`**(先改名、再导入;若某个 section 找不到对应 entry,则会跳过,仅保留在该文件中,且不会重试)。官方 release notes 原文:「设置改由当前 Profile 的插件配置保存,支持声明过的实时更新字段;旧 settings.yaml 仅尝试导入一次,自定义设置插件需适配。」
|
|
447
|
+
- 因此升级后 `settings.yaml` 消失、只剩 `settings.yaml.imported` 属**正常现象**;模型 provider、locale 等应改为在 profile 的 `cordis.patch.yml` 中查看/编辑。
|
|
448
|
+
- **本插件自 v1.9.0 起**已改为把自身配置持久化到 `~/.dsh/remote-mobile/settings.json`,并在首次启动时**自动从旧 `settings.yaml` / `settings.yaml.imported` 迁移一次**(把长期密码哈希、免密开关、限频阈值等救回),不再依赖 DSH 的 settings.yaml。若你曾发现"长期密码失效/限频重置",升级到 v1.9.0 后会自动恢复。
|
|
449
|
+
</details>
|
|
450
|
+
|
|
438
451
|
---
|
|
439
452
|
|
|
440
453
|
## 📄 开源许可证
|
package/README_EN.md
CHANGED
|
@@ -165,7 +165,7 @@ Open the DSH Web Console in your browser, navigate to **Settings ⚙️ -> Remot
|
|
|
165
165
|
- Audit logs and lockout states persist to disk and restore across restarts;
|
|
166
166
|
- Administrators can manually unlock blocked IPs with one click in the panel.
|
|
167
167
|
- **Intelligent Static Asset Whitelisting**: The authentication gate automatically allows verified frontend asset extensions (`.js`, `.css`, `.png`, `.svg`, `.woff2`, etc., 20 types) while blocking extensionless or dynamic API routes (e.g. `/plugins/xxx/admin`), ensuring smooth third-party plugin rendering without manual whitelist configuration. (The DSH core static directory `/assets/` is an official resource and is allowed unconditionally.)
|
|
168
|
-
- **Resilient Atomic Persistence & Debounce**: Millisecond-level debounce write-throttling to safeguard disk I/O (500ms for session data, 300ms for style snippets), coupled with `beforeExit` process flush hooks and atomic temporary file replacement (`renameSync`) for `settings.
|
|
168
|
+
- **Resilient Atomic Persistence & Debounce**: Millisecond-level debounce write-throttling to safeguard disk I/O (500ms for session data, 300ms for style snippets), coupled with `beforeExit` process flush hooks and atomic temporary file replacement (`renameSync`) for `settings.json` and `style-snippets.json`.
|
|
169
169
|
- **Real Socket IP Extraction**: Relies strictly on underlying Socket connection addresses, preventing spoofed `X-Forwarded-For` header attacks.
|
|
170
170
|
- **Loopback CSRF Defense**: Mutating plugin APIs validate browser same-origin signals (`Origin` / `Sec-Fetch-Site`). Cross-site write requests to `127.0.0.1` driven by malicious webpages are rejected outright; non-browser clients (curl / local scripts) are unaffected.
|
|
171
171
|
- **No Context Rewriting for Loopback Requests**: Context virtualization applies only to external traffic. Loopback requests keep their original Host / Origin (no "laundering"), so DSH's built-in DNS-rebinding and same-origin checks keep working against external domains. The SSE event stream accepts same-origin connections only, with no cross-origin reads.
|
|
@@ -282,18 +282,21 @@ dsh plugin --profile web remove dsh-remote-mobile
|
|
|
282
282
|
<span id="advanced"></span>
|
|
283
283
|
## ⚙️ Advanced Configuration
|
|
284
284
|
|
|
285
|
-
|
|
285
|
+
Configurations persist in the plugin-owned file `~/.dsh/remote-mobile/settings.json`; adjust them in the Web UI or edit the file directly:
|
|
286
286
|
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
allowTailscale: false
|
|
290
|
-
allowLan: false
|
|
291
|
-
secretHash: ""
|
|
292
|
-
maxVisitsPerMinute: 60
|
|
293
|
-
maxFailedAttempts: 5
|
|
294
|
-
lockDurationMs: 900000
|
|
287
|
+
```jsonc
|
|
288
|
+
{
|
|
289
|
+
"allowTailscale": false, // boolean, default false: allow passwordless access via Tailscale
|
|
290
|
+
"allowLan": false, // boolean, default false: allow passwordless access via LAN (High Risk)
|
|
291
|
+
"secretHash": "", // string, default empty: scrypt salted hash of the persistent password
|
|
292
|
+
"maxVisitsPerMinute": 60, // number, default 60: max login page visits per minute per IP
|
|
293
|
+
"maxFailedAttempts": 5, // number, default 5: max consecutive failed attempts before IP lockout
|
|
294
|
+
"lockDurationMs": 900000 // number, default 900000 (15 mins): IP lockout duration in ms
|
|
295
|
+
}
|
|
295
296
|
```
|
|
296
297
|
|
|
298
|
+
> **About config storage**: Since DSH **0.1.7** (first noted in the v0.1.7-alpha.1 release notes; continued in 0.2.x), `~/.dsh/settings.yaml` is no longer a live configuration source — on boot it is imported once into the active profile's `cordis.patch.yml` and renamed to `settings.yaml.imported` (see [Q8](#faq)). This plugin therefore no longer writes to settings.yaml and persists to its own `settings.json` above; on first launch it automatically migrates once from the legacy `settings.yaml` / `settings.yaml.imported` (recovering your old persistent password, rate limits, etc.).
|
|
299
|
+
|
|
297
300
|
### 🎨 Mobile Style Snippets (optional)
|
|
298
301
|
|
|
299
302
|
Custom snippets (style mini-plugins) and their enabled states are persisted in `~/.dsh/remote-mobile/style-snippets.json`. Manage them from the settings panel or edit the file directly:
|
|
@@ -330,7 +333,7 @@ Custom snippets (style mini-plugins) and their enabled states are persisted in `
|
|
|
330
333
|
|
|
331
334
|
| Path | Description | Security Level |
|
|
332
335
|
|---|---|---|
|
|
333
|
-
| `~/.dsh/settings.
|
|
336
|
+
| `~/.dsh/remote-mobile/settings.json` | Plugin config: bypass toggles, persistent-password hash, rate-limit & lockout policy | Local storage, `0o600` (Restricted to current user) |
|
|
334
337
|
| `~/.dsh/remote-mobile/devices.json` | Authorized devices, sessions & IP audit statistics (contains long-lived tokens, `0o600`) | Local storage, restricted to current user |
|
|
335
338
|
| `~/.dsh/remote-mobile/rsa-keys.json` | Server RSA keypair (public & private) | Local storage, `0o600` (Restricted to current user) |
|
|
336
339
|
| `~/.dsh/remote-mobile/style-snippets.json` | Mobile style snippets (built-in toggles + custom CSS mini-plugins) | Local persistent storage |
|
|
@@ -434,6 +437,16 @@ If your log instead shows `service "remoteWebUiPairing" has been registered`, pl
|
|
|
434
437
|
* If you wish to enforce the plugin's pairing code or persistent password even when entering through an external reverse proxy, configure the proxy to transparently pass client IPs or connect directly via the exposed DSH listening port (e.g. default port `3080` or custom mapped ports).
|
|
435
438
|
</details>
|
|
436
439
|
|
|
440
|
+
<details>
|
|
441
|
+
<summary><b>Q8: After upgrading to DSH 0.1.7+ (including 0.2.x), <code>~/.dsh/settings.yaml</code> is gone / my settings were lost?</b></summary>
|
|
442
|
+
|
|
443
|
+
**Answer**: This is an **official DSH behavior change introduced in 0.1.7** (first noted in the v0.1.7-alpha.1 release notes; continued in 0.2.x), unrelated to this plugin:
|
|
444
|
+
|
|
445
|
+
- Since DSH **0.1.7**, `~/.dsh/settings.yaml` is **no longer treated as a live configuration source**. On boot it is imported **once** by the official `dsh-settings` `importLegacyDocument()` into the active profile's `~/.dsh/profiles/<profile>/cordis.patch.yml`, then the original file is **renamed to `settings.yaml.imported`** (it renames first, then imports; a section with no matching entry is skipped, kept only in that file, and never retried). The official v0.1.7-alpha.1 release notes state that settings are now persisted as the current profile's plugin configuration (with declared live-update fields), and the legacy `settings.yaml` is imported only once — custom settings plugins must adapt.
|
|
446
|
+
- So after upgrading, `settings.yaml` disappearing and only `settings.yaml.imported` remaining is **expected**; model providers, locale, etc. should now be viewed/edited in the profile's `cordis.patch.yml`.
|
|
447
|
+
- **As of v1.9.0**, this plugin persists its own config to `~/.dsh/remote-mobile/settings.json` and, on first launch, **migrates once from the legacy `settings.yaml` / `settings.yaml.imported`** (recovering the persistent-password hash, bypass toggles, rate limits, etc.). It no longer depends on DSH's settings.yaml. If you saw a "persistent password stopped working / limits reset" symptom, upgrading to v1.9.0 restores it automatically.
|
|
448
|
+
</details>
|
|
449
|
+
|
|
437
450
|
---
|
|
438
451
|
|
|
439
452
|
## 📄 License
|
package/lib/auth/token.d.ts
CHANGED
|
@@ -1,20 +1,72 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import type { IncomingMessage } from 'node:http';
|
|
3
3
|
export declare const AUTH_COOKIE_NAME = "dsh_mobile_token";
|
|
4
|
+
/** DSH_HOME 目录(支持 DSH_HOME 覆盖) */
|
|
5
|
+
export declare const DSH_HOME: string;
|
|
6
|
+
/** 插件自有数据目录 */
|
|
7
|
+
export declare const REMOTE_MOBILE_DIR: string;
|
|
4
8
|
export declare const DEFAULT_PERSIST_FILE: string;
|
|
9
|
+
/** 插件自有配置持久化文件(DSH 0.1.7+ 起官方弃用 settings.yaml,插件改为自持存储) */
|
|
10
|
+
export declare const SETTINGS_FILE: string;
|
|
11
|
+
/** DSH 旧版全局设置文件(0.1.7+ 起仅作一次性迁移来源,启动时会被改名) */
|
|
5
12
|
export declare const GLOBAL_SETTINGS_FILE: string;
|
|
13
|
+
/** DSH 迁移后遗留的旧设置存档(legacy settings.yaml → settings.yaml.imported) */
|
|
14
|
+
export declare const LEGACY_SETTINGS_IMPORTED_FILE: string;
|
|
6
15
|
/**
|
|
7
16
|
* 从 ~/.dsh/settings.yaml 中安全读取 dsh-remote-mobile 命名空间的最新配置
|
|
8
17
|
*/
|
|
9
18
|
export declare function readFromSettingsYaml(filePath?: string): Partial<SessionStoreOptions>;
|
|
10
19
|
/**
|
|
11
|
-
*
|
|
20
|
+
* 从旧版 settings.yaml 文本内容中解析 locale.preference(返回 null 表示未命中)
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseLocaleFromSettingsYaml(content: string): 'zh' | 'en' | null;
|
|
23
|
+
/**
|
|
24
|
+
* 从 cordis.patch.yml 文本内容中解析 locale entry 的 config.preference(返回 null 表示未命中)
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseLocaleFromPatch(content: string): 'zh' | 'en' | null;
|
|
27
|
+
/**
|
|
28
|
+
* 读取 DSH 全局语言偏好。
|
|
29
|
+
*
|
|
30
|
+
* DSH 0.1.7+ 起配置真实落点为当前 profile 的 cordis.patch.yml(旧版 settings.yaml
|
|
31
|
+
* 已被一次性迁移并改名为 settings.yaml.imported),因此优先读取 profile patch,
|
|
32
|
+
* 再回退旧 settings.yaml / settings.yaml.imported。
|
|
33
|
+
*
|
|
34
|
+
* @param filePath 显式传入时按旧版 settings.yaml 格式解析(向后兼容与测试)。
|
|
12
35
|
*/
|
|
13
36
|
export declare function readGlobalLocale(filePath?: string): 'zh' | 'en';
|
|
14
37
|
/**
|
|
15
|
-
*
|
|
38
|
+
* 读取插件自有持久化配置文件 ~/.dsh/remote-mobile/settings.json
|
|
39
|
+
*
|
|
40
|
+
* 注:若文件中 secretHash 为空串 ""(表示用户清空了密码),此处会跳过赋值以归一化为 undefined。
|
|
41
|
+
* 上层 resolveInitialStoreOptions 必须根据文件存在性(own !== null)做出决策,严禁在自有配置存在时用 legacy 兜底回退。
|
|
42
|
+
*/
|
|
43
|
+
export declare function readPluginSettingsJson(filePath?: string): Partial<SessionStoreOptions>;
|
|
44
|
+
/**
|
|
45
|
+
* 将插件配置原子写回自有配置文件 ~/.dsh/remote-mobile/settings.json(权限 0600)。
|
|
46
|
+
*
|
|
47
|
+
* DSH 0.1.7+ 起官方已弃用 ~/.dsh/settings.yaml 作为实时配置源(启动时一次性迁移为
|
|
48
|
+
* settings.yaml.imported),因此插件不再依赖该文件,改为自持存储,避免升级后配置丢失。
|
|
49
|
+
*/
|
|
50
|
+
export declare function writePluginSettingsJson(options: SessionStoreOptions, filePath?: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* 兼容读取 DSH 旧版 settings.yaml / 迁移后的 settings.yaml.imported 中的
|
|
53
|
+
* dsh-remote-mobile 段,用于首次启动时把旧配置迁移到自有 settings.json。
|
|
54
|
+
*/
|
|
55
|
+
export declare function readLegacyDshSettings(liveFile?: string, importedFile?: string): Partial<SessionStoreOptions>;
|
|
56
|
+
/**
|
|
57
|
+
* 解析 SessionStore 的初始运行配置(纯函数决策逻辑)。
|
|
58
|
+
*
|
|
59
|
+
* 优先级与防反噬规则:
|
|
60
|
+
* 1. 显式传入的 explicit 参数优先级最高;
|
|
61
|
+
* 2. 若自有配置文件已存在(own !== null),则以自有配置 own 为准,此时 legacy 配置被彻底忽略(防止已清空的密码死灰复燃);
|
|
62
|
+
* 3. 仅当自有配置文件尚不存在(own === null,即首次安装或旧版升级)时,才回退读取 legacy 配置进行一次性迁移;
|
|
63
|
+
* 4. 最终缺失项采用系统安全默认值。
|
|
64
|
+
*
|
|
65
|
+
* @param explicit 构造函数传入的显式参数
|
|
66
|
+
* @param own 插件自有 settings.json 内容;null 表示文件尚不存在(首次启动迁移状态)
|
|
67
|
+
* @param legacy 旧版 settings.yaml / settings.yaml.imported 内容
|
|
16
68
|
*/
|
|
17
|
-
export declare function
|
|
69
|
+
export declare function resolveInitialStoreOptions(explicit?: SessionStoreOptions, own?: Partial<SessionStoreOptions> | null, legacy?: Partial<SessionStoreOptions>): Required<SessionStoreOptions>;
|
|
18
70
|
export interface SessionRecord {
|
|
19
71
|
token: string;
|
|
20
72
|
createdAt: number;
|
package/lib/auth/token.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { randomBytes, randomInt } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync, chmodSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { EventEmitter } from 'node:events';
|
|
6
6
|
import { hashSecret, verifySecretHash } from './crypto.js';
|
|
7
7
|
import { isTailscaleIp, isLanIp } from './tailscale.js';
|
|
8
8
|
export const AUTH_COOKIE_NAME = 'dsh_mobile_token';
|
|
9
|
-
|
|
10
|
-
export const
|
|
9
|
+
/** DSH_HOME 目录(支持 DSH_HOME 覆盖) */
|
|
10
|
+
export const DSH_HOME = process.env.DSH_HOME || join(homedir(), '.dsh');
|
|
11
|
+
/** 插件自有数据目录 */
|
|
12
|
+
export const REMOTE_MOBILE_DIR = join(DSH_HOME, 'remote-mobile');
|
|
13
|
+
export const DEFAULT_PERSIST_FILE = join(REMOTE_MOBILE_DIR, 'devices.json');
|
|
14
|
+
/** 插件自有配置持久化文件(DSH 0.1.7+ 起官方弃用 settings.yaml,插件改为自持存储) */
|
|
15
|
+
export const SETTINGS_FILE = join(REMOTE_MOBILE_DIR, 'settings.json');
|
|
16
|
+
/** DSH 旧版全局设置文件(0.1.7+ 起仅作一次性迁移来源,启动时会被改名) */
|
|
17
|
+
export const GLOBAL_SETTINGS_FILE = join(DSH_HOME, 'settings.yaml');
|
|
18
|
+
/** DSH 迁移后遗留的旧设置存档(legacy settings.yaml → settings.yaml.imported) */
|
|
19
|
+
export const LEGACY_SETTINGS_IMPORTED_FILE = join(DSH_HOME, 'settings.yaml.imported');
|
|
11
20
|
/**
|
|
12
21
|
* 从 ~/.dsh/settings.yaml 中安全读取 dsh-remote-mobile 命名空间的最新配置
|
|
13
22
|
*/
|
|
@@ -72,63 +81,143 @@ export function readFromSettingsYaml(filePath = GLOBAL_SETTINGS_FILE) {
|
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
83
|
/**
|
|
75
|
-
*
|
|
84
|
+
* 从旧版 settings.yaml 文本内容中解析 locale.preference(返回 null 表示未命中)
|
|
76
85
|
*/
|
|
77
|
-
export function
|
|
86
|
+
export function parseLocaleFromSettingsYaml(content) {
|
|
87
|
+
const match = content.match(/^locale:\s*\r?\n\s+preference:\s*['"]?([a-zA-Z_-]+)['"]?/m);
|
|
88
|
+
if (match && match[1])
|
|
89
|
+
return match[1].toLowerCase().startsWith('en') ? 'en' : 'zh';
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 从 cordis.patch.yml 文本内容中解析 locale entry 的 config.preference(返回 null 表示未命中)
|
|
94
|
+
*/
|
|
95
|
+
export function parseLocaleFromPatch(content) {
|
|
96
|
+
const lines = content.split(/\r?\n/);
|
|
97
|
+
for (let i = 0; i < lines.length; i++) {
|
|
98
|
+
// 精确匹配 `- id: locale`(避免误命中 locale-xxx 等其它 entry)
|
|
99
|
+
if (!/^\s*-\s*id:\s*['"]?locale['"]?\s*$/.test(lines[i]))
|
|
100
|
+
continue;
|
|
101
|
+
for (let j = i + 1; j < Math.min(i + 40, lines.length); j++) {
|
|
102
|
+
// 进入下一个 patch entry,说明本条已结束
|
|
103
|
+
if (/^\s*-\s*id:/.test(lines[j]))
|
|
104
|
+
break;
|
|
105
|
+
const m = lines[j].match(/^\s*preference:\s*['"]?([a-zA-Z_-]+)['"]?/);
|
|
106
|
+
if (m && m[1])
|
|
107
|
+
return m[1].toLowerCase().startsWith('en') ? 'en' : 'zh';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
function readLocaleFromFile(filePath, parser) {
|
|
78
113
|
try {
|
|
79
114
|
if (!existsSync(filePath))
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
const match = content.match(/^locale:\s*\r?\n\s+preference:\s*['"]?([a-zA-Z_-]+)['"]?/m);
|
|
83
|
-
if (match && match[1]) {
|
|
84
|
-
return match[1].toLowerCase().startsWith('en') ? 'en' : 'zh';
|
|
85
|
-
}
|
|
86
|
-
return 'zh';
|
|
115
|
+
return null;
|
|
116
|
+
return parser(readFileSync(filePath, 'utf8'));
|
|
87
117
|
}
|
|
88
118
|
catch {
|
|
89
|
-
return
|
|
119
|
+
return null;
|
|
90
120
|
}
|
|
91
121
|
}
|
|
92
122
|
/**
|
|
93
|
-
*
|
|
123
|
+
* 读取 DSH 全局语言偏好。
|
|
124
|
+
*
|
|
125
|
+
* DSH 0.1.7+ 起配置真实落点为当前 profile 的 cordis.patch.yml(旧版 settings.yaml
|
|
126
|
+
* 已被一次性迁移并改名为 settings.yaml.imported),因此优先读取 profile patch,
|
|
127
|
+
* 再回退旧 settings.yaml / settings.yaml.imported。
|
|
128
|
+
*
|
|
129
|
+
* @param filePath 显式传入时按旧版 settings.yaml 格式解析(向后兼容与测试)。
|
|
94
130
|
*/
|
|
95
|
-
export function
|
|
96
|
-
|
|
131
|
+
export function readGlobalLocale(filePath) {
|
|
132
|
+
if (filePath) {
|
|
133
|
+
return readLocaleFromFile(filePath, parseLocaleFromSettingsYaml) ?? 'zh';
|
|
134
|
+
}
|
|
135
|
+
// 1. 当前 profile 的 cordis.patch.yml(web 优先,其余按目录名排序兜底)
|
|
97
136
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
` allowLan: ${Boolean(options.allowLan)}`,
|
|
106
|
-
` secretHash: ${options.secretHash ? `'${options.secretHash}'` : `''`}`,
|
|
107
|
-
` maxFailedAttempts: ${options.maxFailedAttempts || 5}`,
|
|
108
|
-
` lockDurationMs: ${options.lockDurationMs || 900000}`,
|
|
109
|
-
` maxVisitsPerMinute: ${options.maxVisitsPerMinute || 60}`,
|
|
110
|
-
];
|
|
111
|
-
const newBlock = fields.join('\n');
|
|
112
|
-
const lines = content.split(/\r?\n/);
|
|
113
|
-
const startIdx = lines.findIndex((l) => l.trim() === ns);
|
|
114
|
-
let updatedContent = '';
|
|
115
|
-
if (startIdx === -1) {
|
|
116
|
-
updatedContent = content.trimEnd() + '\n\n' + newBlock + '\n';
|
|
137
|
+
const profilesDir = join(DSH_HOME, 'profiles');
|
|
138
|
+
let names = [];
|
|
139
|
+
try {
|
|
140
|
+
names = readdirSync(profilesDir, { withFileTypes: true })
|
|
141
|
+
.filter((e) => e.isDirectory())
|
|
142
|
+
.map((e) => e.name)
|
|
143
|
+
.sort((a, b) => (a === 'web' ? -1 : b === 'web' ? 1 : a.localeCompare(b)));
|
|
117
144
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
lines.splice(startIdx, endIdx - startIdx, newBlock);
|
|
128
|
-
updatedContent = lines.join('\n');
|
|
145
|
+
catch { }
|
|
146
|
+
const candidates = [
|
|
147
|
+
...names.map((n) => join(profilesDir, n, 'cordis.patch.yml')),
|
|
148
|
+
join(DSH_HOME, 'cordis.patch.yml'),
|
|
149
|
+
];
|
|
150
|
+
for (const c of candidates) {
|
|
151
|
+
const loc = readLocaleFromFile(c, parseLocaleFromPatch);
|
|
152
|
+
if (loc)
|
|
153
|
+
return loc;
|
|
129
154
|
}
|
|
130
|
-
|
|
155
|
+
}
|
|
156
|
+
catch { }
|
|
157
|
+
// 2. 回退旧版 settings.yaml / settings.yaml.imported
|
|
158
|
+
for (const p of [GLOBAL_SETTINGS_FILE, LEGACY_SETTINGS_IMPORTED_FILE]) {
|
|
159
|
+
const loc = readLocaleFromFile(p, parseLocaleFromSettingsYaml);
|
|
160
|
+
if (loc)
|
|
161
|
+
return loc;
|
|
162
|
+
}
|
|
163
|
+
return 'zh';
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* 读取插件自有持久化配置文件 ~/.dsh/remote-mobile/settings.json
|
|
167
|
+
*
|
|
168
|
+
* 注:若文件中 secretHash 为空串 ""(表示用户清空了密码),此处会跳过赋值以归一化为 undefined。
|
|
169
|
+
* 上层 resolveInitialStoreOptions 必须根据文件存在性(own !== null)做出决策,严禁在自有配置存在时用 legacy 兜底回退。
|
|
170
|
+
*/
|
|
171
|
+
export function readPluginSettingsJson(filePath = SETTINGS_FILE) {
|
|
172
|
+
try {
|
|
173
|
+
if (!existsSync(filePath))
|
|
174
|
+
return {};
|
|
175
|
+
const data = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
176
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
177
|
+
return {};
|
|
178
|
+
const result = {};
|
|
179
|
+
if (typeof data.allowTailscale === 'boolean')
|
|
180
|
+
result.allowTailscale = data.allowTailscale;
|
|
181
|
+
if (typeof data.allowLan === 'boolean')
|
|
182
|
+
result.allowLan = data.allowLan;
|
|
183
|
+
if (typeof data.secretHash === 'string' && data.secretHash)
|
|
184
|
+
result.secretHash = data.secretHash;
|
|
185
|
+
if (typeof data.maxFailedAttempts === 'number' && data.maxFailedAttempts > 0)
|
|
186
|
+
result.maxFailedAttempts = data.maxFailedAttempts;
|
|
187
|
+
if (typeof data.lockDurationMs === 'number' && data.lockDurationMs > 0)
|
|
188
|
+
result.lockDurationMs = data.lockDurationMs;
|
|
189
|
+
if (typeof data.maxVisitsPerMinute === 'number' && data.maxVisitsPerMinute > 0)
|
|
190
|
+
result.maxVisitsPerMinute = data.maxVisitsPerMinute;
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return {};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 将插件配置原子写回自有配置文件 ~/.dsh/remote-mobile/settings.json(权限 0600)。
|
|
199
|
+
*
|
|
200
|
+
* DSH 0.1.7+ 起官方已弃用 ~/.dsh/settings.yaml 作为实时配置源(启动时一次性迁移为
|
|
201
|
+
* settings.yaml.imported),因此插件不再依赖该文件,改为自持存储,避免升级后配置丢失。
|
|
202
|
+
*/
|
|
203
|
+
export function writePluginSettingsJson(options, filePath = SETTINGS_FILE) {
|
|
204
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
205
|
+
try {
|
|
206
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
207
|
+
const payload = {
|
|
208
|
+
allowTailscale: Boolean(options.allowTailscale),
|
|
209
|
+
allowLan: Boolean(options.allowLan),
|
|
210
|
+
secretHash: options.secretHash || '',
|
|
211
|
+
maxFailedAttempts: options.maxFailedAttempts || 5,
|
|
212
|
+
lockDurationMs: options.lockDurationMs || 900000,
|
|
213
|
+
maxVisitsPerMinute: options.maxVisitsPerMinute || 60,
|
|
214
|
+
};
|
|
215
|
+
writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
131
216
|
renameSync(tmpPath, filePath);
|
|
217
|
+
try {
|
|
218
|
+
chmodSync(filePath, 0o600);
|
|
219
|
+
}
|
|
220
|
+
catch { }
|
|
132
221
|
return true;
|
|
133
222
|
}
|
|
134
223
|
catch {
|
|
@@ -140,6 +229,56 @@ export function writeBackToSettingsYaml(options, filePath = GLOBAL_SETTINGS_FILE
|
|
|
140
229
|
return false;
|
|
141
230
|
}
|
|
142
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* 兼容读取 DSH 旧版 settings.yaml / 迁移后的 settings.yaml.imported 中的
|
|
234
|
+
* dsh-remote-mobile 段,用于首次启动时把旧配置迁移到自有 settings.json。
|
|
235
|
+
*/
|
|
236
|
+
export function readLegacyDshSettings(liveFile = GLOBAL_SETTINGS_FILE, importedFile = LEGACY_SETTINGS_IMPORTED_FILE) {
|
|
237
|
+
return { ...readFromSettingsYaml(importedFile), ...readFromSettingsYaml(liveFile) };
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* 解析 SessionStore 的初始运行配置(纯函数决策逻辑)。
|
|
241
|
+
*
|
|
242
|
+
* 优先级与防反噬规则:
|
|
243
|
+
* 1. 显式传入的 explicit 参数优先级最高;
|
|
244
|
+
* 2. 若自有配置文件已存在(own !== null),则以自有配置 own 为准,此时 legacy 配置被彻底忽略(防止已清空的密码死灰复燃);
|
|
245
|
+
* 3. 仅当自有配置文件尚不存在(own === null,即首次安装或旧版升级)时,才回退读取 legacy 配置进行一次性迁移;
|
|
246
|
+
* 4. 最终缺失项采用系统安全默认值。
|
|
247
|
+
*
|
|
248
|
+
* @param explicit 构造函数传入的显式参数
|
|
249
|
+
* @param own 插件自有 settings.json 内容;null 表示文件尚不存在(首次启动迁移状态)
|
|
250
|
+
* @param legacy 旧版 settings.yaml / settings.yaml.imported 内容
|
|
251
|
+
*/
|
|
252
|
+
export function resolveInitialStoreOptions(explicit = {}, own = null, legacy = {}) {
|
|
253
|
+
// 当自有配置文件已存在时,legacy 彻底失效;仅在 own 为 null 时继承 legacy
|
|
254
|
+
const base = own !== null ? own : legacy;
|
|
255
|
+
const merged = { ...base, ...explicit };
|
|
256
|
+
// 密码优先级:explicit > base(base 即 own 或 legacy,已由上面的门控决定;
|
|
257
|
+
// 自有配置存在但未设密码时 base 无密码,保持为空,绝不回退 legacy)
|
|
258
|
+
let secretHash = '';
|
|
259
|
+
if (typeof explicit.secretHash === 'string' && explicit.secretHash) {
|
|
260
|
+
secretHash = explicit.secretHash;
|
|
261
|
+
}
|
|
262
|
+
else if (typeof explicit.secret === 'string' && explicit.secret) {
|
|
263
|
+
secretHash = hashSecret(explicit.secret);
|
|
264
|
+
}
|
|
265
|
+
else if (typeof base.secretHash === 'string' && base.secretHash) {
|
|
266
|
+
secretHash = base.secretHash;
|
|
267
|
+
}
|
|
268
|
+
else if (typeof base.secret === 'string' && base.secret) {
|
|
269
|
+
secretHash = hashSecret(base.secret);
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
allowTailscale: merged.allowTailscale ?? false,
|
|
273
|
+
allowLan: merged.allowLan ?? false,
|
|
274
|
+
secret: '',
|
|
275
|
+
secretHash,
|
|
276
|
+
devicesFile: merged.devicesFile ?? DEFAULT_PERSIST_FILE,
|
|
277
|
+
maxFailedAttempts: merged.maxFailedAttempts ?? 5,
|
|
278
|
+
lockDurationMs: merged.lockDurationMs ?? 15 * 60 * 1000,
|
|
279
|
+
maxVisitsPerMinute: merged.maxVisitsPerMinute ?? 60,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
143
282
|
/**
|
|
144
283
|
* 将 User-Agent 字符串解析为人类友好的设备名称
|
|
145
284
|
*/
|
|
@@ -198,37 +337,23 @@ export class SessionStore extends EventEmitter {
|
|
|
198
337
|
constructor(options = {}) {
|
|
199
338
|
super();
|
|
200
339
|
this.setMaxListeners(100);
|
|
201
|
-
// 严密隔离单测环境:当处于 node --test
|
|
340
|
+
// 严密隔离单测环境:当处于 node --test 环境或使用了自定义设备文件时,严禁读写全局配置文件
|
|
202
341
|
const isTestEnv = Boolean(process.env.NODE_TEST_CONTEXT ||
|
|
203
342
|
process.argv.some((arg) => arg.includes('test')) ||
|
|
204
343
|
(options.devicesFile && options.devicesFile !== DEFAULT_PERSIST_FILE));
|
|
205
|
-
this.settingsFilePath = isTestEnv ? null :
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
else if (typeof options.secret === 'string' && options.secret) {
|
|
213
|
-
secretHash = hashSecret(options.secret);
|
|
214
|
-
}
|
|
215
|
-
else if (typeof yamlOpts.secretHash === 'string' && yamlOpts.secretHash) {
|
|
216
|
-
secretHash = yamlOpts.secretHash;
|
|
217
|
-
}
|
|
218
|
-
else if (typeof yamlOpts.secret === 'string' && yamlOpts.secret) {
|
|
219
|
-
secretHash = hashSecret(yamlOpts.secret);
|
|
220
|
-
}
|
|
221
|
-
this.options = {
|
|
222
|
-
allowTailscale: mergedOpts.allowTailscale ?? false,
|
|
223
|
-
allowLan: mergedOpts.allowLan ?? false,
|
|
224
|
-
secret: '',
|
|
225
|
-
secretHash,
|
|
226
|
-
devicesFile: mergedOpts.devicesFile ?? DEFAULT_PERSIST_FILE,
|
|
227
|
-
maxFailedAttempts: mergedOpts.maxFailedAttempts ?? 5,
|
|
228
|
-
lockDurationMs: mergedOpts.lockDurationMs ?? 15 * 60 * 1000,
|
|
229
|
-
maxVisitsPerMinute: mergedOpts.maxVisitsPerMinute ?? 60,
|
|
230
|
-
};
|
|
344
|
+
this.settingsFilePath = isTestEnv ? null : SETTINGS_FILE;
|
|
345
|
+
// 仅在自有 settings.json 尚不存在时,才尝试读取旧版 settings.yaml / settings.yaml.imported 进行一次性兼容迁移;
|
|
346
|
+
// 一旦 settings.json 存在,即以自有配置为准,杜绝旧文件中已废弃的密码或参数在重启后反向覆盖当前配置。
|
|
347
|
+
const hasOwnSettings = !isTestEnv && existsSync(SETTINGS_FILE);
|
|
348
|
+
const ownOpts = isTestEnv ? {} : (hasOwnSettings ? readPluginSettingsJson(SETTINGS_FILE) : null);
|
|
349
|
+
const legacyOpts = isTestEnv || hasOwnSettings ? {} : readLegacyDshSettings();
|
|
350
|
+
this.options = resolveInitialStoreOptions(options, ownOpts, legacyOpts);
|
|
231
351
|
this.persistPath = this.options.devicesFile;
|
|
352
|
+
// 首次启动迁移:自有配置不存在但旧 settings.yaml / settings.yaml.imported 中仍有配置时,
|
|
353
|
+
// 落盘一次到自有 settings.json,把旧配置(如长期密码哈希、限频)救回。
|
|
354
|
+
if (!isTestEnv && !hasOwnSettings && Object.keys(legacyOpts).length > 0) {
|
|
355
|
+
writePluginSettingsJson(this.options, SETTINGS_FILE);
|
|
356
|
+
}
|
|
232
357
|
this.loadPersistedData();
|
|
233
358
|
activeStores.add(this);
|
|
234
359
|
}
|
|
@@ -275,9 +400,10 @@ export class SessionStore extends EventEmitter {
|
|
|
275
400
|
}
|
|
276
401
|
catch { }
|
|
277
402
|
}
|
|
278
|
-
//
|
|
403
|
+
// 同步持久化到插件自有配置文件 ~/.dsh/remote-mobile/settings.json
|
|
404
|
+
// (DSH 0.1.7+ 起官方已弃用 ~/.dsh/settings.yaml 作为实时配置源)
|
|
279
405
|
if (this.settingsFilePath) {
|
|
280
|
-
|
|
406
|
+
writePluginSettingsJson(this.options, this.settingsFilePath);
|
|
281
407
|
}
|
|
282
408
|
}
|
|
283
409
|
// 1. 如果关闭了 Tailscale 免密直连,自动清理所有 Tailscale 免密设备,并将审计状态重置为 ⚪ 待认证
|
package/lib/client.js
CHANGED
|
@@ -782,8 +782,8 @@ var translations = {
|
|
|
782
782
|
storageStylesFile: "\u79FB\u52A8\u7AEF\u6837\u5F0F\u7247\u6BB5\u6587\u4EF6 (\u53EF\u72EC\u7ACB\u542F\u505C\u7684 CSS \u5C0F\u63D2\u4EF6)\uFF1A",
|
|
783
783
|
// 卡片 7: 全局高级配置
|
|
784
784
|
configCardTitle: "\u5168\u5C40\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u914D\u7F6E",
|
|
785
|
-
configCardDesc: "\u8C03\u6574\u5355 IP \u8BBF\u95EE\u9891\u7387\u9650\u5236\u3001\u8F93\u9519\u5C01\u7981\u9608\u503C\u4E0E\u9501\u5B9A\u6301\u7EED\u65F6\u95F4\u3002\u70B9\u51FB\u4FDD\u5B58\u5C06\u7ACB\u5373\u751F\u6548\u5E76\
|
|
786
|
-
copySettingsPathBtn: "\u{1F4CB} \u590D\u5236 settings.
|
|
785
|
+
configCardDesc: "\u8C03\u6574\u5355 IP \u8BBF\u95EE\u9891\u7387\u9650\u5236\u3001\u8F93\u9519\u5C01\u7981\u9608\u503C\u4E0E\u9501\u5B9A\u6301\u7EED\u65F6\u95F4\u3002\u70B9\u51FB\u4FDD\u5B58\u5C06\u7ACB\u5373\u751F\u6548\u5E76\u4FDD\u5B58\u5230 ~/.dsh/remote-mobile/settings.json\u3002",
|
|
786
|
+
copySettingsPathBtn: "\u{1F4CB} \u590D\u5236 settings.json \u8DEF\u5F84",
|
|
787
787
|
fieldVisitsLimit: "\u23F1\uFE0F \u6BCF\u5206\u949F\u8BBF\u95EE\u4E0A\u9650 (\u6B21/\u5206)",
|
|
788
788
|
fieldVisitsLimitHint: "\u5355 IP 1\u5206\u949F\u5185\u6253\u5F00\u767B\u5F55\u9875\u8D85\u8FC7\u6B64\u503C\u5C06\u89E6\u53D1\u9650\u9891",
|
|
789
789
|
fieldFailedLimit: "\u{1F6E1}\uFE0F \u8F93\u9519\u5BC6\u7801\u5C01\u7981\u9608\u503C (\u6B21)",
|
|
@@ -793,9 +793,9 @@ var translations = {
|
|
|
793
793
|
restoreDefaultsBtn: "\u{1F504} \u6062\u590D\u9ED8\u8BA4\u53C2\u6570",
|
|
794
794
|
saveConfigBtn: "\u{1F4BE} \u4FDD\u5B58\u53C2\u6570\u914D\u7F6E",
|
|
795
795
|
savingConfigBtn: "\u6B63\u5728\u4FDD\u5B58...",
|
|
796
|
-
saveConfigConfirm: "\u786E\u5B9A\u8981\u4FDD\u5B58\u5F53\u524D\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u914D\u7F6E\u5417\uFF1F\u4FDD\u5B58\u540E\u5C06\u7ACB\u5373\u751F\u6548\u5E76\
|
|
796
|
+
saveConfigConfirm: "\u786E\u5B9A\u8981\u4FDD\u5B58\u5F53\u524D\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u914D\u7F6E\u5417\uFF1F\u4FDD\u5B58\u540E\u5C06\u7ACB\u5373\u751F\u6548\u5E76\u4FDD\u5B58\u5230 ~/.dsh/remote-mobile/settings.json\u3002",
|
|
797
797
|
restoreDefaultsConfirm: "\u786E\u5B9A\u8981\u5C06\u5B89\u5168\u53C2\u6570\u6062\u590D\u4E3A\u7CFB\u7EDF\u9ED8\u8BA4\u503C\uFF0860\u6B21/\u5206\u30015\u6B21\u5931\u8D25\u3001\u9501\u5B9A15\u5206\u949F\uFF09\u5417\uFF1F\u786E\u8BA4\u540E\u5C06\u76F4\u63A5\u751F\u6548\u5E76\u81EA\u52A8\u540C\u6B65\u3002",
|
|
798
|
-
saveConfigSuccessToast: "\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u5DF2\u6210\u529F\u4FDD\u5B58\u5E76\u540C\u6B65\u5230 settings.
|
|
798
|
+
saveConfigSuccessToast: "\u9AD8\u7EA7\u5B89\u5168\u53C2\u6570\u5DF2\u6210\u529F\u4FDD\u5B58\u5E76\u540C\u6B65\u5230 settings.json\uFF01",
|
|
799
799
|
restoreDefaultsSuccessToast: "\u5DF2\u6210\u529F\u6062\u590D\u9ED8\u8BA4\u5B89\u5168\u53C2\u6570\u5E76\u7ACB\u5373\u751F\u6548\uFF01",
|
|
800
800
|
saveConfigFailTip: "\u53C2\u6570\u5FC5\u987B\u4E3A\u5927\u4E8E 0 \u7684\u6709\u6548\u6574\u6570",
|
|
801
801
|
// 卡片 8: 移动端样式片段(样式小插件)
|
|
@@ -936,8 +936,8 @@ var translations = {
|
|
|
936
936
|
storageStylesFile: "Mobile style snippets file (toggleable CSS mini-plugins):",
|
|
937
937
|
// Card 7: Advanced Security Config
|
|
938
938
|
configCardTitle: "Global Security Policy Configuration",
|
|
939
|
-
configCardDesc: "Configure per-IP rate limits, brute-force failure thresholds, and lockout durations. Changes take effect immediately and
|
|
940
|
-
copySettingsPathBtn: "\u{1F4CB} Copy settings.
|
|
939
|
+
configCardDesc: "Configure per-IP rate limits, brute-force failure thresholds, and lockout durations. Changes take effect immediately and are saved to ~/.dsh/remote-mobile/settings.json.",
|
|
940
|
+
copySettingsPathBtn: "\u{1F4CB} Copy settings.json Path",
|
|
941
941
|
fieldVisitsLimit: "\u23F1\uFE0F Max Visits per Minute (visits/min)",
|
|
942
942
|
fieldVisitsLimitHint: "Rate limit triggered when a single IP opens login page more than this value",
|
|
943
943
|
fieldFailedLimit: "\u{1F6E1}\uFE0F Max Failed Password Attempts (times)",
|
|
@@ -947,9 +947,9 @@ var translations = {
|
|
|
947
947
|
restoreDefaultsBtn: "\u{1F504} Restore Defaults",
|
|
948
948
|
saveConfigBtn: "\u{1F4BE} Save Configuration",
|
|
949
949
|
savingConfigBtn: "Saving...",
|
|
950
|
-
saveConfigConfirm: "Are you sure you want to save the current security policy? It will take effect immediately and
|
|
950
|
+
saveConfigConfirm: "Are you sure you want to save the current security policy? It will take effect immediately and be saved to ~/.dsh/remote-mobile/settings.json.",
|
|
951
951
|
restoreDefaultsConfirm: "Are you sure you want to restore security policies to default values (60 visits/min, 5 failed attempts, 15m lockout)? It will take effect immediately.",
|
|
952
|
-
saveConfigSuccessToast: "Security policy successfully saved and synced to settings.
|
|
952
|
+
saveConfigSuccessToast: "Security policy successfully saved and synced to settings.json!",
|
|
953
953
|
restoreDefaultsSuccessToast: "Default security policies restored and applied immediately!",
|
|
954
954
|
saveConfigFailTip: "Parameters must be valid integers greater than 0",
|
|
955
955
|
// Card 8: Mobile Style Snippets (style mini-plugin)
|
|
@@ -2783,8 +2783,8 @@ function ConfigCard({
|
|
|
2783
2783
|
onClick: () => {
|
|
2784
2784
|
if (typeof copyText === "function") {
|
|
2785
2785
|
copyText(
|
|
2786
|
-
"~/.dsh/settings.
|
|
2787
|
-
lang === "en" ? "Settings path copied (~/.dsh/settings.
|
|
2786
|
+
"~/.dsh/remote-mobile/settings.json",
|
|
2787
|
+
lang === "en" ? "Settings path copied (~/.dsh/remote-mobile/settings.json)" : "\u5DF2\u590D\u5236\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84\uFF08~/.dsh/remote-mobile/settings.json\uFF09"
|
|
2788
2788
|
);
|
|
2789
2789
|
}
|
|
2790
2790
|
},
|
|
@@ -3538,8 +3538,8 @@ function StylesCard({ lang, showToast }) {
|
|
|
3538
3538
|
}
|
|
3539
3539
|
|
|
3540
3540
|
// src/client/version.js
|
|
3541
|
-
var PLUGIN_VERSION = "v1.
|
|
3542
|
-
var BUILD_TIME = "2026-10-
|
|
3541
|
+
var PLUGIN_VERSION = "v1.9.1";
|
|
3542
|
+
var BUILD_TIME = "2026-10-17 19:11:57";
|
|
3543
3543
|
|
|
3544
3544
|
// src/client/components/MainSection.jsx
|
|
3545
3545
|
var import_jsx_runtime9 = require("react/jsx-runtime");
|
package/lib/index.d.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* 4. Tailscale 虚拟私网与局域网 LAN 智能免密直连切换;
|
|
10
10
|
* 5. 防暴力破解限流与单 IP 异常访问熔断审计;
|
|
11
11
|
* 6. 全链路网络上下文虚拟化桥接与 @linxin666 生态无缝兼容。
|
|
12
|
-
* 7.
|
|
12
|
+
* 7. 配置持久化于插件自有文件 ~/.dsh/remote-mobile/settings.json(DSH 0.1.7+ 起官方已弃用 settings.yaml),
|
|
13
|
+
* 启动时一次性兼容迁移旧 settings.yaml / settings.yaml.imported 中的配置。
|
|
13
14
|
*/
|
|
14
15
|
import { type SessionStoreOptions } from './auth/token.js';
|
|
15
16
|
/** Cordis 插件名称标识 */
|
package/lib/index.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* 4. Tailscale 虚拟私网与局域网 LAN 智能免密直连切换;
|
|
10
10
|
* 5. 防暴力破解限流与单 IP 异常访问熔断审计;
|
|
11
11
|
* 6. 全链路网络上下文虚拟化桥接与 @linxin666 生态无缝兼容。
|
|
12
|
-
* 7.
|
|
12
|
+
* 7. 配置持久化于插件自有文件 ~/.dsh/remote-mobile/settings.json(DSH 0.1.7+ 起官方已弃用 settings.yaml),
|
|
13
|
+
* 启动时一次性兼容迁移旧 settings.yaml / settings.yaml.imported 中的配置。
|
|
13
14
|
*/
|
|
14
15
|
import { SessionStore } from './auth/token.js';
|
|
15
16
|
import { createGlobalAuthGate } from './auth/gate.js';
|
|
@@ -24,7 +25,11 @@ export const REMOTE_MOBILE_SETTINGS_NAMESPACE = 'dsh-remote-mobile';
|
|
|
24
25
|
/** Cordis 依赖注入声明:声明注入底座内置的 webServer 服务 */
|
|
25
26
|
export const inject = ['webServer'];
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
+
* 尽力接入 DSH Settings 服务(若宿主提供旧版 register API)。
|
|
29
|
+
*
|
|
30
|
+
* 说明:DSH 0.1.7+ 起官方 dsh-settings 已改为基于 profile cordis.patch.yml 的
|
|
31
|
+
* Config 表单模型(无 register 方法),本函数在该版本下会安全跳过;插件的配置
|
|
32
|
+
* 持久化改由自有 settings.json 承担(见 token.ts)。
|
|
28
33
|
*/
|
|
29
34
|
function bindDshSettings(ctx, store) {
|
|
30
35
|
try {
|
|
@@ -39,19 +44,19 @@ function bindDshSettings(ctx, store) {
|
|
|
39
44
|
base: store.getOptions(),
|
|
40
45
|
});
|
|
41
46
|
if (scope) {
|
|
42
|
-
// 1.
|
|
47
|
+
// 1. 同步宿主 Settings 中的最新配置到 SessionStore
|
|
43
48
|
const initialVal = scope.get?.();
|
|
44
49
|
if (initialVal && typeof initialVal === 'object') {
|
|
45
50
|
store.updateOptions(initialVal, false);
|
|
46
51
|
}
|
|
47
|
-
// 2.
|
|
52
|
+
// 2. 监听宿主 Settings 或 Web UI 配置变更
|
|
48
53
|
scope.watch?.(() => {
|
|
49
54
|
const updated = scope.get?.();
|
|
50
55
|
if (updated && typeof updated === 'object') {
|
|
51
56
|
store.updateOptions(updated, false);
|
|
52
57
|
}
|
|
53
58
|
});
|
|
54
|
-
// 3.
|
|
59
|
+
// 3. 绑定反向写入钩子:插件 API 修改配置时同步写回宿主 Settings(旧版 API)
|
|
55
60
|
store.setSettingsMutator((patch) => {
|
|
56
61
|
try {
|
|
57
62
|
const ops = Object.entries(patch).map(([field, value]) => ({
|
package/lib/routes/login-page.js
CHANGED
|
@@ -338,7 +338,7 @@ export function getLoginPageHtml(publicKeyPem, lang = 'zh') {
|
|
|
338
338
|
if (document.baseURI && document.querySelector('base')) {
|
|
339
339
|
return new URL('api/remote-mobile/verify', document.baseURI).toString();
|
|
340
340
|
}
|
|
341
|
-
const pathname = window.location.pathname.replace(
|
|
341
|
+
const pathname = window.location.pathname.replace(new RegExp('/auth/?$'), '/');
|
|
342
342
|
return new URL('api/remote-mobile/verify', window.location.origin + pathname).toString();
|
|
343
343
|
} catch (e) {
|
|
344
344
|
return '/api/remote-mobile/verify';
|
|
@@ -350,7 +350,7 @@ export function getLoginPageHtml(publicKeyPem, lang = 'zh') {
|
|
|
350
350
|
// 预留参数能力:支持通过 ?redirect=/path 携带登录后重定向目标
|
|
351
351
|
const redirectParam = urlParams.get('redirect');
|
|
352
352
|
if (redirectParam && redirectParam.startsWith('/') && !redirectParam.startsWith('//')) {
|
|
353
|
-
const appPrefix = window.location.pathname.replace(
|
|
353
|
+
const appPrefix = window.location.pathname.replace(new RegExp('/auth/?$'), '');
|
|
354
354
|
if (appPrefix && redirectParam.startsWith(appPrefix)) {
|
|
355
355
|
return window.location.origin + redirectParam;
|
|
356
356
|
}
|
|
@@ -359,7 +359,7 @@ export function getLoginPageHtml(publicKeyPem, lang = 'zh') {
|
|
|
359
359
|
if (document.baseURI && document.querySelector('base')) {
|
|
360
360
|
return new URL('./', document.baseURI).toString();
|
|
361
361
|
}
|
|
362
|
-
const appPrefix = window.location.pathname.replace(
|
|
362
|
+
const appPrefix = window.location.pathname.replace(new RegExp('/auth/?$'), '/');
|
|
363
363
|
return window.location.origin + appPrefix;
|
|
364
364
|
} catch (e) {
|
|
365
365
|
return '/';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-mobile",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "DeepSeek Harness 远程与移动端安全网关插件:零修改 DSH 底层代码安全开放局域网与 Tailscale 连接 | DeepSeek Harness (DSH) Remote & Mobile Security Guard: safely opens Tailscale/LAN with zero core modifications, QR scan auth, RSA encryption & brute-force defense.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|