@mrrisega/dsh-remote 0.6.7-beta.2 → 0.6.7-beta.4
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/README.md +15 -1
- package/clients/dsh-remote/dsh-bridge.mjs +47 -1
- package/clients/dsh-remote/e2ee-shim-script.js +18 -1
- package/clients/dsh-remote/mobile-adapter.mjs +485 -170
- package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +91 -34
- package/dsh-setup.mjs +137 -12
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +59 -8
- package/packages/dsh-remote-web/lib/index.js +419 -63
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/test/account-switch-device.test.mjs +307 -0
- package/packages/dsh-remote-web/test/self-manage.test.mjs +16 -7
- package/packages/dsh-remote-web/test/uninstall-runtime.test.mjs +44 -33
- package/packages/dsh-remote-web/test/uninstall-safety.test.mjs +442 -0
package/README.md
CHANGED
|
@@ -30,6 +30,12 @@ dsh-remote 是一个轻量的**隧道模式**远程控制方案:电脑端运
|
|
|
30
30
|
|
|
31
31
|
## 0.6.7 速览(Windows 可用版)
|
|
32
32
|
|
|
33
|
+
> **预发中**:先以 `0.6.7-beta.x` 发布在 npm `beta` 通道(`latest` 仍是 0.6.6),
|
|
34
|
+
> 待 Windows 真机验证通过后转正式版。安装预发版:
|
|
35
|
+
> `npx @mrrisega/dsh-remote@0.6.7-beta.3`;若 dsh web 已起不来,用
|
|
36
|
+
> `npx @mrrisega/dsh-remote@0.6.7-beta.3 repair`(只修 profile,不联网)。
|
|
37
|
+
|
|
38
|
+
|
|
33
39
|
0.6.6 及更早的插件半整套「服务状态 / 启停 / 重启」是按 macOS/Linux 写死的
|
|
34
40
|
(launchctl / systemd / pgrep / ps / /bin/sh),**在 Windows 上安装一切正常、运行期必死**:
|
|
35
41
|
面板红字「读取状态失败: process.getuid is not a function」、状态永远停在「查询中…」、
|
|
@@ -154,8 +160,16 @@ npx @mrrisega/dsh-remote setup --server wss://<你的域名>:端口 --key <访
|
|
|
154
160
|
校验值用于登录,与加密密钥域分离(详见 [docs/e2ee-protocol.md](docs/e2ee-protocol.md))。
|
|
155
161
|
- 请**牢记账号密码**:修改/重置密码会使全部旧设备会话失效,且服务端不保存你的内容、
|
|
156
162
|
无法代为解密旧会话;改密后需在电脑端面板重新登录并重启 bridge,手机端重新解锁。
|
|
157
|
-
- 电脑端本机配置(`.dsh-config.json
|
|
163
|
+
- 电脑端本机配置(`.dsh-config.json`)会保存账号密码用于自动登录,等于该账号
|
|
158
164
|
内容的“解密权”,请妥善保护电脑;电脑被他人使用期间请退出登录。
|
|
165
|
+
- macOS / Linux:文件权限 **0600**(仅本人可读写)。
|
|
166
|
+
- Windows:**`0600` 在 Windows 上无效**(Windows 用 ACL,不是 POSIX 权限位)——
|
|
167
|
+
早期版本只写了 `mode: 0o600`,实测该文件拿到的仍是用户目录的**默认继承 ACL**,
|
|
168
|
+
等于没有这层保护。0.6.7-beta.3 起安装器/插件/bridge 都会显式收紧:`icacls` 断开继承、
|
|
169
|
+
只授予当前用户。若收紧失败(无 icacls / 权限异常),安装日志会明确告警,不会假装成功。
|
|
170
|
+
- 无论哪个平台:**不要把 `.dsh-remote` 目录交给备份/同步盘/他人排查**——
|
|
171
|
+
里面的 `.dsh-config.json`(明文账号密码)与 `.harness-cookie.json`(dsh web 会话 Cookie,
|
|
172
|
+
等于该会话的完整访问权)被复制走就等于把钥匙一起给了对方。发支持包前请先删除这两项。
|
|
159
173
|
|
|
160
174
|
**边界与建议(如实告知)**
|
|
161
175
|
|
|
@@ -65,7 +65,7 @@ import WebSocket from "ws";
|
|
|
65
65
|
import fs from "node:fs";
|
|
66
66
|
import os from "node:os";
|
|
67
67
|
import path from "node:path";
|
|
68
|
-
import { execSync } from "node:child_process";
|
|
68
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
69
69
|
import { randomBytes, generateKeyPairSync, createHash } from "node:crypto";
|
|
70
70
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
71
71
|
import { promisify } from "node:util";
|
|
@@ -200,10 +200,40 @@ function loadLocalConfig() {
|
|
|
200
200
|
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")); } catch { return {}; }
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* 把文件权限收紧到「只有本人可读写」。
|
|
205
|
+
*
|
|
206
|
+
* ⚠️ Windows 上 `{ mode: 0o600 }` 是**空操作**(Windows 用 ACL,不是 POSIX mode 位):
|
|
207
|
+
* 实测 .dsh-config.json 拿到的是用户目录的默认**继承 ACL**(AreAccessRulesProtected=False),
|
|
208
|
+
* 也就是说代码里那句 0600 对 Windows 用户完全没效果 —— 而文件里是**明文账号密码**。
|
|
209
|
+
* 风险不在"同机他人可读"(默认 ACL 下其实读不到),而在**任何按文件复制的场景**
|
|
210
|
+
* (备份/同步盘/杀软上报/崩溃转储/用户发给作者的支持包)都会连钥匙一起被带走,
|
|
211
|
+
* 而文档里的"0600 请妥善保护"会让用户误以为 Windows 上已有这层保护。
|
|
212
|
+
* 这里显式断开继承、只授予当前用户;失败只警告一次,绝不影响主流程。
|
|
213
|
+
*/
|
|
214
|
+
let hardenWarned = false;
|
|
215
|
+
function hardenFile(file) {
|
|
216
|
+
try { fs.chmodSync(file, 0o600); } catch { /* POSIX 上失败不致命 */ }
|
|
217
|
+
if (process.platform !== "win32") return;
|
|
218
|
+
const who = [process.env.USERDOMAIN, process.env.USERNAME].filter(Boolean).join("\\");
|
|
219
|
+
if (!who) return;
|
|
220
|
+
let r;
|
|
221
|
+
try {
|
|
222
|
+
r = spawnSync("icacls", [file, "/inheritance:r", "/grant:r", `${who}:F`],
|
|
223
|
+
{ windowsHide: true, encoding: "utf8", timeout: 8000 });
|
|
224
|
+
} catch (e) { r = { status: -1, stderr: e.message }; }
|
|
225
|
+
if (r.status !== 0 && !hardenWarned) {
|
|
226
|
+
hardenWarned = true;
|
|
227
|
+
console.warn(`⚠️ 收紧文件权限失败(${who}):${String(r.stderr || "").trim() || `icacls 退出码 ${r.status}`}`);
|
|
228
|
+
console.warn(" 配置文件可能仍可被其它账户/备份工具读取,请自行确认其存放位置。");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
203
232
|
function saveLocalConfig(cfg) {
|
|
204
233
|
try {
|
|
205
234
|
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
206
235
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), { mode: 0o600 });
|
|
236
|
+
hardenFile(CONFIG_PATH); // Windows 上 mode 是空操作 → 显式收紧 ACL(内含明文账号密码)
|
|
207
237
|
} catch (e) {
|
|
208
238
|
console.warn(`[bridge] 无法写配置 ${CONFIG_PATH}: ${e.message}`);
|
|
209
239
|
}
|
|
@@ -943,8 +973,24 @@ async function resolveToken(refresh = false) {
|
|
|
943
973
|
const d = await r.json();
|
|
944
974
|
if (r.status === 200 && d.token) {
|
|
945
975
|
console.log("[bridge] 登录成功,已获取 JWT");
|
|
976
|
+
// 登录成功 = 限流窗口已结束(服务端成功即清零),把退避提示删掉让 watcher 恢复正常节奏
|
|
977
|
+
try { fs.rmSync(path.join(path.dirname(CONFIG_PATH), ".dsh-login-ratelimited"), { force: true }); } catch { /* 非关键 */ }
|
|
946
978
|
return d.token;
|
|
947
979
|
}
|
|
980
|
+
if (r.status === 429) {
|
|
981
|
+
// 服务端登录限流(每 IP 5 次失败 / 15 分钟)。**这不是密码错**:
|
|
982
|
+
// 旧文案一律提示"请检查 DSH_BRIDGE_EMAIL / DSH_BRIDGE_PASSWORD",把限流误报成凭据问题,
|
|
983
|
+
// 用户会去反复改密码、反而把窗口拖长(2026-09-19 实测)。
|
|
984
|
+
const waitMs = Number(d.retry_after_ms) > 0 ? Number(d.retry_after_ms) : 0;
|
|
985
|
+
console.error(`[bridge] 登录被服务端限流(429)${waitMs ? `,约 ${Math.ceil(waitMs / 1000)} 秒后自动重试` : ",请稍后自动重试"}`);
|
|
986
|
+
console.error("[bridge] 凭据没错,是这台机器的出口 IP 短时间登录失败过多被临时限制;无需改密码。");
|
|
987
|
+
// 把"最早可重试时刻"落盘:watcher 据此退避,避免 10 秒一次空撞限流
|
|
988
|
+
try {
|
|
989
|
+
const until = Date.now() + (waitMs || 5 * 60 * 1000);
|
|
990
|
+
fs.writeFileSync(path.join(path.dirname(CONFIG_PATH), ".dsh-login-ratelimited"), String(until));
|
|
991
|
+
} catch { /* 非关键 */ }
|
|
992
|
+
process.exit(3);
|
|
993
|
+
}
|
|
948
994
|
console.error(`[bridge] 登录失败(${r.status}): ${d.error?.message || "未知错误"}`);
|
|
949
995
|
console.error("[bridge] 请检查 DSH_BRIDGE_EMAIL / DSH_BRIDGE_PASSWORD,或直接设 DSH_BRIDGE_TOKEN");
|
|
950
996
|
process.exit(1);
|
|
@@ -599,6 +599,14 @@
|
|
|
599
599
|
|
|
600
600
|
/* ---- 状态徽标 ---- */
|
|
601
601
|
var _badgeCollapseTimer = null; // 自动收缩(仅 ok 态):展示几秒后缩成仅 🔒,不挡界面
|
|
602
|
+
var _badgeState = null; // 最近一次状态 {mode,text,note} —— 供合并后的悬浮菜单读取
|
|
603
|
+
/** 对外只读接口:让统一的「远程控制」悬浮按钮显示加密状态(不暴露任何密钥/会话材料)。 */
|
|
604
|
+
try {
|
|
605
|
+
window.__dshE2eeBadge = {
|
|
606
|
+
state: function () { return _badgeState; },
|
|
607
|
+
open: function () { var el = document.getElementById("dsh-e2ee-badge"); if (el && el.click) el.click(); return !!el; }
|
|
608
|
+
};
|
|
609
|
+
} catch (e) { /* ignore */ }
|
|
602
610
|
function _badgeClearCollapse() {
|
|
603
611
|
try { if (_badgeCollapseTimer) { clearTimeout(_badgeCollapseTimer); _badgeCollapseTimer = null; } } catch (e) {}
|
|
604
612
|
}
|
|
@@ -643,8 +651,17 @@
|
|
|
643
651
|
}
|
|
644
652
|
function _badge(mode, text, note) {
|
|
645
653
|
try {
|
|
646
|
-
//
|
|
654
|
+
// 合并给「远程控制悬浮菜单」(2026-09-19):状态挂到 window.__dshE2eeBadge 并广播事件,
|
|
655
|
+
// 让 mobile-adapter 的统一悬浮按钮显示加密状态、并把独立药丸收起来(只读、不碰加解密逻辑)。
|
|
656
|
+
_badgeState = { mode: String(mode), text: String(text == null ? "" : text), note: String(note == null ? "" : note) };
|
|
657
|
+
try {
|
|
658
|
+
var ev = new CustomEvent("dsh-e2ee-badge", { detail: _badgeState });
|
|
659
|
+
document.dispatchEvent(ev);
|
|
660
|
+
if (window !== document) window.dispatchEvent(ev);
|
|
661
|
+
} catch (e0) { /* 老浏览器:忽略 */ }
|
|
662
|
+
// 桌面宽屏(>820)明文/告警态不加**独立**徽标 —— 与 mobile-adapter 的“桌面零打扰”契约一致;
|
|
647
663
|
// 加密态(ok)或窄屏始终给出明确状态(协议 §2.4“不静默”)。
|
|
664
|
+
// 状态本身照常发布,合并后的悬浮菜单无论宽窄都能看到它。
|
|
648
665
|
if (mode !== "ok" && window.innerWidth > 820) return;
|
|
649
666
|
var el = _badgeEl();
|
|
650
667
|
if (!el) return;
|