Repository navigation
Description
环境 / Environment
- DSH Desktop 0.13.1(Windows 10 19045 x64;Electron 44.7.0 / Chromium 152.0.7977.130 / node 24.21.0)
- dshmarket:尚未安装(问题发生在点击「安装插件市场」装它之前;按钮目标 ^1.66.47)
- harness
@deepseek-ai/dsh0.2.2-alpha.2 - 界面加载方式:桌面窗口从
dsh-desktop://harness/自定义协议加载
现象 / What happened
设置 → 插件市场 → 点击「安装插件市场」:
- 「正在安装插件市场…」一闪而过,同一帧还出现「重试」按钮;
- 随后 UI 静默复位回未安装状态,没有任何错误提示停留;
- 安装从未开始:
harness.log在点击时刻零记录、profilenode_modules无变化、无 pnpm 进程。
重装应用、清空全部配置、更换网络均无效——这不是环境问题(见根因)。
根因 / Root cause(已定位,并有对照实验证实)
桌面窗口的 UI 运行在自定义协议 dsh-desktop://harness 源上,而「安装」是 mutation POST,必须通过一条要求 http: Origin 的信任链;自定义协议的 POST 拿不到合格的 Origin,请求在中途即被 403,永远到不了后台。GET(状态轮询)不做 mutation 校验,所以页面渲染、状态刷新全部正常——只有 POST 必死。
代码链条(0.13.1 app.asar):
-
主窗口加载
HARNESS_SHELL_URL = "dsh-desktop://harness/",页面所有 API 请求经主进程HarnessShell.handle()代理转发到http://127.0.0.1:<port>; -
shell 代理的信任规则(
out/main/index.js,handle()入口):const origin = request.headers.get("origin"); if (origin !== null && origin !== SHELL_ORIGIN) return new Response("Forbidden", { status: 403 }); // …转发前: if (origin === SHELL_ORIGIN) headers.set("origin", backend.origin); // 仅当 Origin 恰好等于 "dsh-desktop://harness"
-
dsh-desktop-market-installer后端的isTrustedRequest(req, mutation=true)要求:Origin 头存在、可解析为http:协议、且与 Host 一致,否则 403{"error":"Request rejected."}; -
Chromium 对非 http(s) scheme 的 fetch POST 无法给出
dsh-desktop://harness形态的 Origin(序列化为null或不发)。于是 POST 要么在代理处被 403(纯文本 "Forbidden"),要么转发后因 Origin 不合格被后端 403——两条路都到不了runInstall(),所以后台零日志; -
UI 侧行为完全吻合(
dsh-desktop-market-installer/client.js):install()先本地置phase='installing',随后 fetch 失败(403 纯文本还会让response.json()抛 SyntaxError)→ error 态与本地 installing 残留并存 → 「正在安装…」与「重试」同框;850ms 状态轮询把页面拉回absent→ 「下载显示一下就直接消失」。
对照实验 / Evidence(同一按钮、同一后端实例,仅请求来源不同)
| 场景 | Origin 路径 | 结果 |
|---|---|---|
| 桌面端点击按钮 | dsh-desktop://harness 自定义协议代理 |
失败:POST 未抵达后端(后台零日志),UI 静默复位,多次复现 |
curl -X POST -H "Origin: http://127.0.0.1:43129" .../install |
直接 http | 成功:202,约 12s 装完 dshmarket 1.73.9 |
浏览器打开 http://127.0.0.1:43129/?token=… 后点同一个按钮 |
页面同源 http | 成功:页面内请求监视录得 POST → 202,约 20s 装完 1.73.9 |
唯一变量是 Origin 的可达形态,结果翻转——后台安装链路本身完全正常。
复现步骤 / Steps to reproduce
- Windows 全新安装 DSH Desktop 0.13.1(或卸载 dshmarket 回到占位按钮状态);
- 设置 → 插件市场 → 点击「安装插件市场」;
- 观察上述静默失败;后台
harness.log无任何安装记录。
修复建议 / Suggested fixes(任一即可)
HarnessShell.handle:转发到 loopback 后端时,对 Origin 为null/缺失的 mutation 请求也补写origin: backend.origin(目前只有恰好等于SHELL_ORIGIN才改写);- 或
isTrustedRequest:对已携带有效会话 cookie(backend.cookie)的 loopback 请求放行 mutation,不依赖 http Origin; - 或与 dsh-market#1031 的方向合并:Origin 校验改为「scheme 白名单(
http:+ 应用自有 scheme)+ host 匹配」,而非严格要求http:。
临时绕过 / Workaround(均已验证)
- 每次启动后从
logs/harness.log取dsh web: http://127.0.0.1:<port>/?token=…,在任意浏览器打开 → 设置 → 插件市场 → 安装(202 成功); - 或
curl -X POST -H "Origin: http://127.0.0.1:43129" http://127.0.0.1:43129/dsh-desktop/market-installer/install
相关 / Related
- dsh-market#1031「安装插件失败:untrusted origin」——同属「Origin 信任链 vs 安装操作」家族,但代码路径不同:那是 dshmarket 已装后市场内
sameOrigin()被 Chrome 151+ 无端口 Origin 绊倒;本 issue 是桌面自带 market-installer 在dsh-desktop://scheme 下被外壳信任链拒绝,且完全静默。 - [Bug] DSH Desktop 0.11.0:升级后 profile 迁移必然冻结 —— 闭包目录不存在 + 迁移路径未传
dshEntryPath,宿主单例 peer 校验必失败,插件市场无法安装 #654(profile 迁移冻结)同为「市场装不上」的表亲,但根因不同。 - 仓库里已有
fix(market): never leave the installer section silently dead相关提交,但未覆盖本问题——本问题在请求层,不在 UI 层。
Activity
eatmoreduck commentedon Nov 3, 2026
Read the whole chain in source on current main — the report's mechanism checks out, and there's one more fact worth surfacing before a fix is chosen: the strict Origin posture is a pinned, unit-tested invariant, not an accident.
HarnessShell.handle()(harness-shell.ts) translates the Origin only when it equalsdsh-desktop://harnessexactly, andtest/harness-shell.test.tshas a dedicated case — "translates only a trusted Origin and never upgrades absent or foreign origins" — which asserts that an absent Origin stays absent on forward and that the literalOrigin: nullis rejected with 403 alongside genuinely foreign origins. So all three suggested fixes (rewrite null/absent at the proxy, cookie-based trust inisTrustedRequest, or the scheme whitelist) reverse a deliberately tested security decision rather than repairing an oversight — they need a maintainer call, not a drive-by patch.One direction that changes no posture: make the shell page emit a real Origin. The scheme is already registered with
standard + secure + supportFetchAPI + corsEnabled(desktop-protocol.ts), so adsh-desktop://harnesspage fetching a same-origin relative URL should serializeOrigin: dsh-desktop://harness— the exact value the gate wants. The report's on-machine evidence (Origin arrives null/absent on Electron 44.7/Windows) then points at Electron's network stack not populating Origin for same-scheme fetches on custom standard schemes, which would make this an upstream quirk to work around rather than a trust-rule to relax. Two cheap discriminating probes on the affected machine:fetch('/dsh-desktop/market-installer/status', {method:'POST', credentials:'same-origin'})and inspect the request's Origin in the network tab — does Chromium even generate the header for same-scheme POSTs here?navigator.origin/self.originin the same console — confirm the page itself reports a real origin (dsh-desktop://harness) rather thannull.If (1) shows the header genuinely never leaves the renderer, the fix has to be main-side after all — and the least-bad shape I see is keeping the 403 for foreign origins but treating absent (not
"null"-string) as shell-originated for mutations only, justified by the channel being app-private (protocol.handleonly receives this app's renderer traffic; the session cookie is already injected unconditionally on forward, so the absent-Origin strictness isn't what guards the credential). That's still a conscious amendment to the pinned test and should be the maintainer's pick between it and dsh-market#1031's whitelist direction.UI-side note: the "installing… + retry flash, then silent reset" the report describes is the same state machine #831/#833 addresses — once a fix lands here, #833's checking/failed states would make whatever the backend answers visible instead of flashing.
momolee-gg commentedon Nov 4, 2026
两个判别实验都做了,用的是与 DSH Desktop 0.13.1 完全相同的 Electron 44.7.0 最小复现(同样的 scheme 注册参数
standard + secure + supportFetchAPI + corsEnabled + stream + codeCache,protocol.handle直收,无任何代理层):①
self.origin正常:页面报告dsh-desktop://harness—— 页面持有正确的 origin。② Origin 头从未生成:页面内
fetch('/echo', { method: 'POST', credentials: 'same-origin' })发出的 POST 到达protocol.handle时,request.headers.has('origin') === false。GET(页面导航)、/echoPOST、/reportPOST 全部无 Origin 头。实测 main 侧收到的原始数据:
{ "mainSide": [ { "url": "dsh-desktop://harness/", "method": "GET", "originHeader": null, "hasOriginHeader": false }, { "url": "dsh-desktop://harness/echo", "method": "POST", "originHeader": null, "hasOriginHeader": false }, { "url": "dsh-desktop://harness/report", "method": "POST", "originHeader": null, "hasOriginHeader": false } ], "pageReport": "{\"selfOrigin\":\"dsh-desktop://harness\",\"echo\":{\"seenOriginHeader\":null,\"hasOriginHeader\":false}}" }结论(按你的推理链收口):
Origin)。所以修复只能在 main 侧兜底,同时值得上报 Electron 上游;"null"字符串)时,在 mutation 场景视为 shell 来源放行。理由如你所列——protocol.handle只接收本应用渲染进程的流量、session cookie 本来就无条件注入,"缺失即拒"并没有保护任何东西,只是把上游怪癖变成了功能门禁。这是对已钉死测试的有意识修订,由维护者拍板,同时建议上游跟进 Electron。最小复现(Electron 44.7.0,Win10 19045 x64,main.js 全文):
运行后
result.json如上——协议处理器收到的 POST 永远没有 Origin 头,而页面self.origin是完整合法的dsh-desktop://harness。任何人装同版本 Electron 就能复现。eatmoreduck commentedon Nov 4, 2026
两个判别实验做得很扎实,结果把链条完全钉死了:页面持有正确的 origin(
self.origin正常),但 Electron 44.7 的网络栈对自定义 standard scheme 的同 scheme POST 就是不生成 Origin 头。这意味着:HarnessShell.handle()里origin === SHELL_ORIGIN的精确匹配分支对壳页面自己发出的请求永远命不中(头根本不存在),等于死代码。最小复现(同样的 scheme 注册参数、protocol.handle直收)同时也排除了本仓代理层引入差异的可能。protocol.handle只收本 app 渲染进程的请求,外部进程进不来)+ 会话 cookie 本就在转发时由主进程无条件注入,Origin 门禁并非凭据防线。最小改法=mutation 且 Origin 缺席 → 转发时补写origin: backend.origin,同时保留'null'字面量与外来 Origin 的 403(单测里那条 foreign 名单原样保留,只改 absent 分支的语义);对应的translates only a trusted Origin用例需要同步改两条断言。方向定了的话我可以直接出补丁(含 harness-shell 单测的 absent-mutation 用例修订),维护者拍板即可。