dsh-cookie-janitor:清掉过期 dsh-auth-* cookie,避免 DSH 请求头溢出 431

前言

DSH 的认证依赖浏览器 cookie。每次启动时 DSH 会按监听地址签一条新 cookie,名字里含端口;端口是随机的,cookie 归属范围却只到主机、不含端口。于是浏览器每次请求都会把历史上所有 dsh-auth-* 一起带上,攒到约 70 条后 Cookie 头接近 Node.js 默认的 16 KiB上限,服务端在应用代码执行前就返回 431 Request Header Fields Too Large,表现是白屏、加载卡住或认证失败。

下面介绍 yan3536 维护的 dsh-cookie-janitor 插件(分类 admin-security,GitHub 1 stars,MIT)。它挂在 Host 的 HTTP 服务上,在请求进入应用之前把属于已消失端口的旧 cookie 裁掉,并对已经溢出的请求做一次 302 兜底。README 里给出的离线自测覆盖 24 个用例。

它解决什么问题

DSH 的浏览器认证 cookie 名由地址推导:

cookie 名 = "dsh-auth-" + base64url(sha256("host:port"))

名字里带了端口,而端口并不固定:

  • 桌面版启动时写死 --port 0,让操作系统挑空闲端口;
  • 命令行 dsh --profile web --port 0 同理。

于是 30 天内启动 N 次就堆 N 条无用 cookie,且有效期不因使用而续期。单条约 221 字节,README 实测 73 条时 Cookie 头 16277 B 仍能通过,74 条时 16500 B 拿到 431;因为 Host、User-Agent 等头同样占用额度,实际触顶会略早,按 70 条左右估算。

已有做法是手动在浏览器里清掉 127.0.0.1 的站点数据,能解一次急,但下次启动仍然开始重新堆。这个插件把清理放进请求链路,不再需要人工介入。

三层行为

插件按请求发生顺序做三件事:

  1. 请求入口:从 Cookie 头里摘掉属于其他端口的 dsh-auth-*,应用看不到这些 cookie;
  2. 响应出口:对被摘掉的那些下发 Max-Age=0,让浏览器从 cookie 库里删除;
  3. 溢出兜底:如果请求头已经大到 Node 拒绝解析,就把 431 换成一次 302 回到原地址并附清除指令,用户看到的是刷新一次,而不是崩溃。

当前正在使用的那条 cookie 永远不动,删掉它会把用户从正在使用的实例里登出。清理完成后第二次请求的 Cookie 头就只剩一条,之后不再增长。

安装

从 Git 仓库装:

dsh plugin --profile web add github:yan3536/dsh-cookie-janitor

离线调试源码时,先克隆再用绝对路径装:

git clone https://github.com/yan3536/dsh-cookie-janitor
dsh plugin --profile web add /绝对路径/dsh-cookie-janitor

装完必须完全退出 DSH 再启动。插件挂在 Host 的 HTTP 服务上,只有在启动时才能装上,刷新页面不生效。

桌面版用户把 --profile web 换成 --profile desktop。桌面版 profile 被 Electron 独占,必须先完全退出 DSH 再执行命令,否则会报 Open DeepSeek Harness Desktop once to initialize its profile, then fully quit it before running dsh plugin --profile desktop.

卸载:

dsh plugin --profile web remove dsh-cookie-janitor

配置

全部可选,默认值即推荐值。需要改时写进 profile 的 cordis.patch.yml:

- id: cookie-janitor
  name: dsh-cookie-janitor
  config:
    keepForeign: 0        # 允许保留几条非当前端口的 cookie;0 = 全清
    pruneRequests: true   # 关掉就只剩溢出兜底
    rescueOverflow: true  # 关掉则 431 原样返回
    maxHeaderBytes: 0     # >0 时把 Node 的头上限抬到这个字节数
    attachTimeoutMs: 15000

maxHeaderBytes 只是把症状往后推,真正止血的是 keepForeign: 0。同一浏览器需要同时连多个 DSH 实例时,把 keepForeign 设成实际并用的实例数,插件按浏览器发送顺序保留最新的那几条。

会不会误删

不会。判定完全按 DSH 自己的算法来:

  1. 只碰 dsh-auth- 开头的 cookie,主题、语言、第三方 cookie 一律不动;
  2. 当前 Host 对应的那条永远保留;
  3. 被删的那些对应的监听端口在当前进程里不存在,留着没有用处。

如果哪天 DSH 换了 cookie 命名规则,插件识别不到任何 dsh-auth-*,于是什么都不做,最坏情况是退回原状。

离线验证

node test/smoke.mjs

24 个用例,不依赖 DSH、不联网:16 个纯函数(cookie 名推导、authority 归一化、Cookie 头解析、清理策略的六种情形、过期指令拼装、原始报文与 target 抓取);6 个端到端(真实 node:http 服务器加裸 socket,覆盖正常透传、干净请求逐字节不变、20 KiB 头被救成 302、关掉兜底后恢复原生 431、放宽上限生效、关掉清理后原样透传);2 个健壮性(没有 web 服务时静默、畸形配置不抛)。

要求与边界

插件版本 0.1.0,在 DSH 0.2.0-rc.2 上实测通过,要求 Node ≥ 20(用到 Buffer.toString('base64url') 与 replaceAll)。插件没有声明 engines.dsh:DSH 的兼容检查一旦看到这个键就按独占判定,而预发布版低于 0.2.0,写上反而把当前能用的版本挡在外面。

插件只用 webServer 服务的两个公开成员:server 与 protocol;不复用未导出的内部符号,cookie 名是自己按 sha256(authority) + base64url 复算的,因为 BrowserAuth、COOKIE_PREFIX、cookieName 没有从 @deepseek-ai/dsh-client-connection 导出。

需要注意的边界:

  1. 只覆盖 Host 自己的 HTTP 监听。桌面版主窗口走 dsh-app:// 自定义协议,那条路径的认证 cookie 由主进程内存变量持有,不写浏览器 cookie 库,本来也不会堆积。
  2. 清理发生在下次请求时,不是安装的那一瞬间。装完第一次打开页面,插件才看到并清掉旧 cookie;如果此刻请求头已超限,走溢出兜底。
  3. 不改 DSH 的端口策略。--port 0 是给远程访问留的正规能力,插件只在 cookie 侧做减法。

适用场景与注意

适合用 DSH web profile 且频繁重启、出现过白屏或 431 的本地开发者;也适合在本地排查请求头问题的维护者。插件以当前 dsh 进程的权限运行,装的是仓库里的 cordis.patch.yml;安装前应自己读一遍源码,确认许可证是 MIT 再启用。

手动清理可以解一次急:打开 http://127.0.0.1:<端口>,从地址栏左侧的站点信息里删除 Cookie 和网站数据,或在开发者工具 Application → Cookies 里逐个删掉 dsh-auth-*。代价是下次启动重新开始堆。

小结

dsh-cookie-janitor 的位置很窄:解决的是「端口变化导致历史认证 cookie 无限累积、最终撞上请求头上限」这一个具体问题,做法是在请求进入应用前裁掉旧 cookie 并让浏览器删除它们,同时对已溢出的请求做 302 兜底。它不改变 DSH 的端口策略,也不碰与 dsh-auth- 无关的 cookie。

目录页:https://www.skillhub.cn/plugins/yan3536/dsh-cookie-janitor
源码:https://github.com/yan3536/dsh-cookie-janitor

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜