Skip to content

0.8.36 — 自动部署远端 dsh 并验证

Choose a tag to compare

@flymysql flymysql released this 03 Oct 01:50
· 184 commits to main since this release

自动部署远端 dsh 并验证(体检 → 一键装 → 交给 AI 兜底)+ 新增 rw_deploy_probe

接着上面的「远程 DSH 界面」:连不上大多不是插件的问题,而是远端 dsh 版本或环境不对。
最典型的一种是这次真机踩到的——dsh 0.1.0-rc.6 依赖 node-pty@1.1.0,而那个版本发布的包里
没有 linux-x64 预编译产物
,于是 dsh web 在 Linux 上根本起不来
(Failed to load native module: pty.node)。以前用户只会看到「没拿到启动令牌」,无从判断。
现在分三层解决。

1)体检(只读) —— 探测远端平台/架构、node/npm、现有 dsh 及版本、
原生模块能否启动、是否认识 --no-open、代理变量、npm 源,并给出结论与修复建议。
分级是 blocker/warn/info/ok,UI 原样渲染、不做二次判定。严格只读:不写、不装、不改,
所以可以在用户还没同意任何事之前随便跑。刻意不用 ; 拼接语句(launch 命令被 do; 坑过一次),
并由真实 sh -n 的语法闸门兜底 —— 这道闸门当场抓出了一个我自己写的 || 续行语法错。

2)部署并验证 —— 只在体检判定「需要且可以自动装」时出现,点击时明确告知会装到
私有目录(默认 $HOME/.dsh-remote/dsh):不写系统目录、不改 PATH、不覆盖用户在用的版本,
删掉目录即完全回滚。装完逐步校验(二进制 / 原生模块 / web 子命令),成功后按机器
记住这条 dsh 路径,之后连接自动使用。默认不覆盖远端 npm 源,所以内网镜像仍然生效。

3)让 AI 排查 —— 只在失败后出现,且只有点击才创建会话(会消耗模型额度,绝不自动)。
它把体检结论、原始事实、真实报错原样交给内置的 dsh-remote-deploy 技能,覆盖确定性流程
表达不了的长尾:没有 npm、要 sudo、代理、内网镜像源、Windows 远端、报错不匹配任何已知签名。
技能正文把这次实测的坑写在最前面——pty.node 与 --no-open 两个失败签名、
哪些报错不是部署问题(NO_ADAPTER/no API key/QUOTA)、以及一个会误导判断的
符号链接陷阱:dirname(dirname(realpath(.bin/dsh))) 落在包内部、那里没有兄弟 node-pty,
好安装会被判成缺 pty(这个 bug 就是我在真机上撞到并修的)。

其余覆盖面:Windows 远端有 Git Bash 即可部署(pool.exec 会经 bash -s 执行,之前一律判不支持);
代理与自定义 registry 只报告不改;版本可配置(默认 0.1.5-rc.2,第一个 Linux 上能起 web 的版本)。

新增配置:webInstallPrefix / webInstallVersion / webInstallRegistry。
新增工具 rw_deploy_probe(只读环境体检,输出结论 + 原始事实 + 修复建议)——
AI 排查时可以先拿到结构化结果,而不是自己拼命令猜。

真机验证(编译机 9.134.186.191,从「坏的」状态开始):体检精确复现手工定位的结论
(0.1.0-rc.6 + pty=no ⇒ blocker 且给出修复版本)→ 安装 5/5 步通过 → 重新体检转为 ok 且选中新装命令
→ 用自动装出来的 dsh 端到端起 web(token 换取 303、SPA 200 且含 __DSH_BOOT__)。
真实 GUI 里点「体检(只读)」也复现了同样的结论卡片与「部署并验证」按钮。

默认安装前缀必须由远端解析。 修一个只有真机能发现的 bug:调用方在探测前用
resolvePrefix({home:''}) 猜了个 /tmp/...,于是报告的是与真实安装位置无关的目录可写性
(那台机器的 $HOME 还是指向 /data/... 的符号链接)。现在默认前缀由远端 $HOME 解析并回报,
只有用户显式配置 webInstallPrefix 才在本地拼绝对路径。

测试:423 通过(原 316)。新增两处平台无关但必要的测试基建:
sh -n 真实语法闸门(用 stdin 投喂——Windows 下 bash -c 会破坏内嵌双引号、把合法脚本
误报成 EOF),以及 i18n 静态守卫(不允许「声明了却从未渲染」的字符串,
这正是之前 token 警告文案悄悄没显示的原因)。

安全修复 + 新能力:把远程机器的 DSH 网页界面挂到本机(issue #46)

issue #46 问的是「能连接远程的 dsh-web 吗?」,补一句「只当客户端用」—— 这句话决定了方向。
它排除"让远端把界面暴露出来给人连",指向"让本地这个 DSH 主动连出去,把远端那个 dsh web
当目标"。这与插件既有定位完全同构(本来就是把远程机器变成工作区),所以本次把「远程工作区」
扩展成「远程界面」。

顺带发现并修掉一个真实安全缺陷(本身独立成立,且是新端点的前置条件)。
插件自己注册的 /dsh-remote/* 路由完全没有 DSH 的 Host/Origin 栅栏,也没有浏览器认证 ——
DSH 的 requestRejection 只挂在 /api 前缀与 /api/remote.mux 升级路由上。在 0.8.35 的实机
(127.0.0.1:3080)实测:

  • POST /dsh-remote/forwards 带 Origin: https://evil.example.com(且用 text/plain,
    浏览器不会发 preflight)返回 200,而且那个转发定义真的落进了 forwards.json;
  • Host: evil.com 也返回 200(DNS rebinding 根本不需要 Origin);
  • GET /dsh-remote/machines 直接吐出 host/user/port/workspace。

同样的请求打 DSH 自家的 /api 是 403。能触发就能作恶:任何用户访问过的网页都可以借此
建隧道、改机器清单、甚至触发 update-apply(装包 + 重载插件)。

修法是复用 DSH 的权威判定而不是另写一套:lib/http-transport.js 现在用 guardRoute()
包住每一个 webServer 路由,优先问 connection.requestRejection,没有该服务时退回到
fail-closed 的兜底(跨站 / Origin 与 Host 不符 / 非 loopback Host 一律拒绝)。
非浏览器调用方(curl、脚本)与进程内调用方不受影响 —— 与 DSH 自己的语义一致;
/api 那条孪生路由刻意不重复包一层。

新能力:lib/web-attach.js。 SSH 连出去,在选定机器上启动(或复用)一个只监听
127.0.0.1
的 dsh web,再把它的端口经 SSH 隧道搬到本机的 loopback 端口。在一台真实
Linux 主机上验证:一个纯 TCP 转发就够了 —— SPA、静态资源、以及 /api/remote.mux 的
WebSocket 升级(HTTP/1.1 101)全部正常通过,不需要反代、不需要改写 host。原因是 DSH 把
认证 cookie 绑定在请求 authority 上,所以按 127.0.0.1:<本地端口> 签发的 cookie 正好就是
浏览器随后发回同一 authority 的那一个。

启动令牌只存在于 stdout(进程内 randomBytes,不落盘、无环境变量入口),所以解析
dsh web: http://127.0.0.1:<port>/?token=… 这一行拿到它;--port 0 让远端自己挑端口,
而同一行会把挑到的端口报回来,两者从同一行读出因而必然一致。默认不会杀远端进程,
要用「断开并停止远端」,且只作用于本次由插件启动的进程。

设置页新增「远程 DSH 界面」卡片:选机器 → 连接并打开 / 打开 / 断开 / 断开并停止远端,
也可以粘贴远端已在运行的 http://127.0.0.1:<端口>/?token=… 直接接上(不会再启第二个)。

三个只有真机才能发现的缺陷(单测全都"通过"了,实机第一次跑就露出来,各补了一条回归):

  1. 语句用 ; 连接,而 while …; do 被当成独立元素 ⇒ 生成出 do;,POSIX 语法错误。
    每次 attach 都失败在 syntax error near unexpected token ';',而等价的手写脚本却是好的。
    (sh -c 正是 sshd 执行命令的方式,所以必须是合法 POSIX sh。)
  2. stopRemote 实际什么都没做:它 pkill -f 会话日志路径,而那个路径只作为重定向出现、
    从不进入 argv,所以匹配不到任何进程 —— 实机观察到远端 DSH 仍在监听。
  3. 给 (2) 打的第一版补丁改成匹配 argv 里的端口,同样永远不会命中:启动刻意用
    --port 0,真实端口是事后才从启动行读到的。

现在改为向记录的 PID 的进程组发信号(启动走 setsid,子进程即组长,组内正好是我们
启动的那棵树 —— 不会误伤用户自己的 DSH,也不会误伤另一个 attach 会话),发信号前先用
/proc/<pid>/cmdline 复核它仍然像个 dsh 进程,避免 PID 复用误杀;顺带清掉本次的日志文件。

测试:346 通过(原 316)。新增 test/route-fence.test.js(12 例)做了负对照 ——
把栅栏去掉后其中 7 例立刻变红,而 5 例"必须仍然可用"的对照保持绿色。
scripts/integration-web-attach.mjs(10 项断言)与
scripts/integration-route-web-attach.mjs(13 项断言)打真实主机,全绿。