@iasiv5/dsh-skip-browser-auth 0.1.0
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/LICENSE +27 -0
- package/README.md +193 -0
- package/cordis.patch.yml +20 -0
- package/lib/bridge.js +83 -0
- package/lib/client.js +4836 -0
- package/lib/gate.js +25 -0
- package/lib/host.js +129 -0
- package/lib/trust-fence.js +135 -0
- package/lib/trusted-auth.js +57 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
Copyright (c) 2026 iasi (@iasiv5/dsh-skip-browser-auth 适配与新增部分)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
本插件包含或移植 DeepSeek Harness(@deepseek-ai/dsh-client-connection 等)的
|
|
17
|
+
MIT 许可代码:lib/client.js 为官方发布产物的不可变 re-ID 副本,src/trust-fence.ts
|
|
18
|
+
与 src/bridge.ts 移植自官方源码,src/index.ts、src/trusted-auth.ts 基于官方
|
|
19
|
+
Connection 实现语义。上游版权与许可声明如上保留。
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# @iasiv5/dsh-skip-browser-auth
|
|
2
|
+
|
|
3
|
+
个人自用的 DSH Web 插件:在兼容版本上替换官方 Connection 提供方,跳过 DSH 官方
|
|
4
|
+
BrowserAuth 浏览器身份层,同时完整保留请求信任栅栏(request trust fence)。
|
|
5
|
+
不修改 DSH 本体,不创建任何新的监听服务或端口。
|
|
6
|
+
|
|
7
|
+
## 这是什么
|
|
8
|
+
|
|
9
|
+
DSH 官方的 BrowserAuth 在进程启动 token 与浏览器之间换取 authority 绑定的签名
|
|
10
|
+
cookie,保护全部 Host API 与 Remote WebSocket。本插件安装后在任意 DSH 版本上都能
|
|
11
|
+
被 `dsh plugin add` 接受;但只有当运行环境中
|
|
12
|
+
`@deepseek-ai/dsh-client-connection` 的**精确版本为 `0.1.2-rc.1`** 时才激活:
|
|
13
|
+
|
|
14
|
+
- 组合期(Loader 装配阶段)由 bundle patch(`cordis.patch.yml`)自门控:
|
|
15
|
+
- 第一行把官方 `connection` 行的 `disabled` 绑定到版本探针(`!!js` 表达式,
|
|
16
|
+
从 pnpm store 路径段提取精确版本并与白名单比对);
|
|
17
|
+
- 第二行插入本插件的 `trusted-connection` 行,其 `disabled` 为行绑定表达式的
|
|
18
|
+
否定(自身探针为真 + 同组官方行存在、名字匹配、确被禁用)。
|
|
19
|
+
- 激活后由本插件提供 trusted-network(可信网络)等效 Connection:复用官方导出的
|
|
20
|
+
`HostConnectionService`,以三方法 stub(trusted-network 语义)替代 BrowserAuth,
|
|
21
|
+
`/api` 路由、请求体上限、image capacity 断言照抄官方 apply 语义。
|
|
22
|
+
- 任一门控条件不成立(探针为假、行不存在、名字漂移、官方行未禁用)→ 插件保持
|
|
23
|
+
dormant(休眠):官方行为分毫不变,插件不注册路由、不提供服务、不加载浏览器
|
|
24
|
+
bundle。注意:在探针为假(非白名单版本)时,即使后续 patch 层把官方行强制设为
|
|
25
|
+
`disabled: true`,行绑定内嵌的版本探针仍会阻止 Replacement 激活。
|
|
26
|
+
- `apply()` 内另有 backstop(最后防线):读官方 manifest 比对白名单、校验行绑定
|
|
27
|
+
状态,任一不满足即 fail loud 拒绝运行;产品上下文中 loader / internal /
|
|
28
|
+
当前 entry 任一缺失(最后防线无法执行)同样 fail loud。
|
|
29
|
+
|
|
30
|
+
本插件包含或移植 DeepSeek Harness 的 MIT 许可代码,详见 LICENSE。
|
|
31
|
+
|
|
32
|
+
## 安全警告
|
|
33
|
+
|
|
34
|
+
Replacement 激活时进程会输出以下固定警告(逐字):
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
@iasiv5/dsh-skip-browser-auth: BrowserAuth has been skipped. DSH Web is using the trusted-network behavior; this plugin does not verify any upstream proxy.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
trusted-network mode 的判据只有一个:**请求来源是否通过信任栅栏**
|
|
41
|
+
(Host/Origin/Fetch Metadata/trustedHosts)。**网络可达即身份**——能通过栅栏的
|
|
42
|
+
请求一律被接受,不存在身份层,本插件也不校验任何上游反代。仅在与外界隔离、或由
|
|
43
|
+
可信网络层(如具备身份认证的反代)完全包住的网络中使用;`trustedHosts` 只声明
|
|
44
|
+
「这些主机名解析到的请求被接受」,不构成认证。
|
|
45
|
+
|
|
46
|
+
## 安装与卸载
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
# 从本地 checkout 安装(scope 是包名的一部分,不是目录路径)
|
|
50
|
+
dsh plugin --profile web add /path/to/dsh-skip-browser-auth
|
|
51
|
+
|
|
52
|
+
# 发布 npm 后,从 registry 安装
|
|
53
|
+
dsh plugin --profile web add @iasiv5/dsh-skip-browser-auth
|
|
54
|
+
|
|
55
|
+
sudo systemctl restart deepseek-harness.service
|
|
56
|
+
|
|
57
|
+
# 卸载(恢复官方行为;统一使用 manifest 包名)
|
|
58
|
+
dsh plugin --profile web remove @iasiv5/dsh-skip-browser-auth
|
|
59
|
+
sudo systemctl restart deepseek-harness.service
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
没有 `skipBrowserAuth` 之类的配置项——始终 skip;卸载或禁用插件并重启即恢复官方
|
|
63
|
+
行为。激活状态由「行为 + 警告」共同证明(见下方升级清单),配置证据
|
|
64
|
+
(dump-config 出现 `trusted-connection` 行)仅作安装证据。
|
|
65
|
+
|
|
66
|
+
## 版本白名单与升级策略
|
|
67
|
+
|
|
68
|
+
- 唯一白名单版本:字符串精确值 `'0.1.2-rc.1'`。禁止 `>=`、范围、`latest` 判断;
|
|
69
|
+
探测失败一律 fail-closed 到 dormant。
|
|
70
|
+
- 升级后 `@deepseek-ai/dsh-client-connection` 版本不在精确白名单内时,本插件
|
|
71
|
+
自动回到 dormant(官方行为分毫不变),不影响使用;升级到白名单版本
|
|
72
|
+
(`0.1.2-rc.1`)则会激活。
|
|
73
|
+
- 新版本需要逐个测试验证后把精确版本加入白名单(`src/gate.ts` 的
|
|
74
|
+
`GATE_VERSION` 与白名单判断、`src/index.ts` backstop 的白名单文案);
|
|
75
|
+
无法安全跟随时对插件做版本分叉。
|
|
76
|
+
|
|
77
|
+
## 行为矩阵
|
|
78
|
+
|
|
79
|
+
| 组合 | 官方 `connection` 行 | 本插件 `trusted-connection` 行 | 行为 |
|
|
80
|
+
| --- | --- | --- | --- |
|
|
81
|
+
| rc.1 + 探针真 + 绑定完好 | `disabled: true`(被 patch 禁用,无 fiber) | `disabled: false`(活跃 fiber) | Replacement 激活:trusted-network 行为 + 固定警告 |
|
|
82
|
+
| rc.1 + 探针假(如 9.9.9 段) | 活跃 | dormant | 官方 BrowserAuth 照常(401/cookie 换取) |
|
|
83
|
+
| rc.2(0.1.1-rc.2,真实产物) | 活跃(rc.2 无 BrowserAuth) | dormant | 官方 rc.2 基线:`GET /` 200、`/api` 无认证层 |
|
|
84
|
+
| 官方行名字漂移 | 活跃(patch 被 name-guard 跳过) | dormant | 官方行为,无冲突 |
|
|
85
|
+
| 非白名单版本 + 后续 patch 层强制禁用官方行 | `disabled: true`(探针仍为假) | dormant(行绑定内嵌探针拦截) | 无 Connection 服务(无身份层也无替代) |
|
|
86
|
+
| 白名单 rc.1 + 后续 patch 层强制禁用官方行 | `disabled: true`(与本插件自身禁用一致) | 激活(行绑定要求的正是官方行已禁用) | Replacement 照常激活 |
|
|
87
|
+
| 路径段真、manifest 版本漂移 | 禁用 | 进入 apply 后被 backstop 拒绝 | fail loud:whitelist 诊断,`loader.await()` reject |
|
|
88
|
+
|
|
89
|
+
## 测试说明
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
npm run test:all
|
|
93
|
+
# 等价于:npm run test:rc2-fixture && npm run build && npm run typecheck && npm test
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- `test:rc2-fixture` 需要 npm 网络访问:以别名安装 rc.2 dormant 组合所需产物。
|
|
97
|
+
`--no-save` 会改动 `node_modules`(以及为满足 rc.2 peer 链所需的嵌套安装),
|
|
98
|
+
但不改 package manifest 与 lockfile(脚本内以 `git diff` 证明)。
|
|
99
|
+
- 本机 npm 11.19 的 allow-scripts 策略与 project-scoped 安装存在三处适配
|
|
100
|
+
(`--ignore-scripts`、`--force` 取代 `--legacy-peer-deps`、仅剥离
|
|
101
|
+
`npm_config_allow_scripts` / `NPM_CONFIG_ALLOW_SCRIPTS` 两个确证冲突键),
|
|
102
|
+
详见 `scripts/install-rc2-fixture.mjs` 头注释。
|
|
103
|
+
- 组合测试(`tests/composition/`)用真实 Loader + Include + 真实官方产物端到端
|
|
104
|
+
覆盖 active / gate-false / 真实 rc.2 dormant / gate 绑定负向四条路径;探针
|
|
105
|
+
fixture 是真实文件(临时目录内构造 pnpm 风格路径段 + manifest)。
|
|
106
|
+
- 测试使用 Node 内置 test runner(无 vitest)。
|
|
107
|
+
|
|
108
|
+
## 已知限制
|
|
109
|
+
|
|
110
|
+
- npm 布局(`node_modules/@deepseek-ai/dsh-client-connection`,路径无版本段)下
|
|
111
|
+
探针无法确认版本 → 不激活(dormant)。pnpm 布局与真实部署(pnpm store 路径段)
|
|
112
|
+
可确认版本。
|
|
113
|
+
- 组合测试的探针 fixture 是受控构造(真实文件但路径段版本可指定),真实布局的
|
|
114
|
+
版本提取由 Task 11 式实机验证与 Task 5 的 pnpm 真实路径断言兜底。
|
|
115
|
+
- 完整浏览器组图 e2e(client-modules 组图、浏览器半区真实加载)未覆盖;
|
|
116
|
+
由 client bundle materialization 测试(单次注册、factory 可执行、external
|
|
117
|
+
全满足)与下方升级 active 清单兜底。
|
|
118
|
+
- 激活要求 DSH 升级到 `0.1.2-rc.1` 后执行实机清单验证。
|
|
119
|
+
|
|
120
|
+
## 实机 dormant 前后对照记录
|
|
121
|
+
|
|
122
|
+
本机 DSH runtime 为 `0.1.1-rc.2`(2026-09-05 实测)。验证协议:安装前基线 →
|
|
123
|
+
安装 + 重启 → 行为逐项对照 → 卸载 + 重启 → 复验基线;journal 检查统一使用
|
|
124
|
+
安装前固化的时间锚;任何失败立即回滚(本次回滚未被触发)。
|
|
125
|
+
|
|
126
|
+
| 检查项 | 安装前基线 | 安装后(dormant) | 卸载恢复后 |
|
|
127
|
+
| --- | --- | --- | --- |
|
|
128
|
+
| `GET /` | `200` `text/html; charset=utf-8` | `200` `text/html; charset=utf-8` | `200` `text/html; charset=utf-8` |
|
|
129
|
+
| `POST /api/session.list`(未认证) | `200` `application/json` | `200` `application/json` | `200` `application/json` |
|
|
130
|
+
| envelope `type` / `rpcId` / `resultOk` | `server-response` / `probe` / `true` | `server-response` / `probe` / `true` | `server-response` / `probe` / `true` |
|
|
131
|
+
| 首页静态 shell marker(首个 `<script src=`) | 在 | 在 | 在 |
|
|
132
|
+
| 首页认证文案 `dsh web authentication required` | 0 次出现 | 0 次出现 | 0 次出现 |
|
|
133
|
+
|
|
134
|
+
三组行为文件逐字节 diff 均为空;安装后与卸载后 journal(同一时间锚窗口内)
|
|
135
|
+
均无固定警告 `BrowserAuth has been skipped`、无 err 级日志。配置证据:安装后
|
|
136
|
+
`dsh --profile web --dump-config` 出现 `trusted-connection` 行(仅作安装证据,
|
|
137
|
+
不作为未激活证明);卸载后该行消失、bundles 中无本插件。回滚未被触发。
|
|
138
|
+
|
|
139
|
+
### scoped rename 后重新验证(2026-09-05,`@iasiv5/dsh-skip-browser-auth@0.1.0`)
|
|
140
|
+
|
|
141
|
+
包名改为 scoped 后按同一协议重新执行(安装 `/home/ubuntu/workspace/dsh-skip-browser-auth`
|
|
142
|
+
→ 重启 → 对照 → `dsh plugin --profile web remove @iasiv5/dsh-skip-browser-auth`
|
|
143
|
+
→ 重启 → 复验):安装证据为 bundles 出现 `@iasiv5/dsh-skip-browser-auth`、
|
|
144
|
+
dump-config 第 612 行 `trusted-connection` 行(行名 `@iasiv5/dsh-skip-browser-auth`);
|
|
145
|
+
三组行为文件逐字节 diff 均为空(`root 200 text/html`、`api 200 application/json`、
|
|
146
|
+
`type=server-response` / `rpcId=probe` / `resultOk=true`、authCount=0);
|
|
147
|
+
安装后与卸载后 journal 均无固定警告、无 err 级日志;scoped 卸载无残留;
|
|
148
|
+
回滚未被触发。本节替代首次记录,作为 scoped 包身份的实机 dormant 结论。
|
|
149
|
+
|
|
150
|
+
## 升级到 0.1.2-rc.1 后的 active 验证清单
|
|
151
|
+
|
|
152
|
+
DSH 升级到 `0.1.2-rc.1` 并安装本插件、重启后,依次执行:
|
|
153
|
+
|
|
154
|
+
1. 配置证据(仅安装证据,不证明激活):
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
dsh --profile web --dump-config | grep -n -A2 'id: trusted-connection'
|
|
158
|
+
# 预期:出现 trusted-connection 行
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
2. 行为验证(与固定警告共同证明激活):
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
# GET / 免 token → 200(无 401、无认证文案)
|
|
165
|
+
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3080/
|
|
166
|
+
# 预期:200
|
|
167
|
+
|
|
168
|
+
# GET /?token=x → 303 location: /(token 清理)
|
|
169
|
+
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' 'http://127.0.0.1:3080/?token=x'
|
|
170
|
+
# 预期:303 http://127.0.0.1:3080/
|
|
171
|
+
|
|
172
|
+
# 无 cookie POST /api → 非 401/403(无认证层;无 api-gateway 挂载时 404)
|
|
173
|
+
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
|
|
174
|
+
-H 'content-type: application/json' \
|
|
175
|
+
-d '{"type":"client-request","rpcId":"probe","method":"session.list","payload":{}}' \
|
|
176
|
+
http://127.0.0.1:3080/api/session.list
|
|
177
|
+
# 预期:非 401/403
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
3. journal 固定警告:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
sudo journalctl -u deepseek-harness.service --since '<重启时间点>' \
|
|
184
|
+
| grep -F 'BrowserAuth has been skipped'
|
|
185
|
+
# 预期:出现固定警告文案
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
4. 回滚路径(任何时候行为异常):
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
dsh plugin --profile web remove @iasiv5/dsh-skip-browser-auth
|
|
192
|
+
sudo systemctl restart deepseek-harness.service
|
|
193
|
+
```
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 本文件由 scripts/write-patch.mjs 从 lib/gate.js 常量机械生成,请勿手改。
|
|
2
|
+
# 自门控 bundle patch:第一行把官方 connection 行的 disabled 绑定到版本探针
|
|
3
|
+
# (仅 @deepseek-ai/dsh-client-connection@0.1.2-rc.1 为真);第二行插入本插件的
|
|
4
|
+
# trusted-connection 行,其 disabled 为行绑定表达式的否定(行绑定内嵌同一探针)。
|
|
5
|
+
# 任何门控条件不成立(探针为假、行不存在、名字漂移、官方行未禁用),
|
|
6
|
+
# Replacement 一律保持 dormant(fail-closed)。探针为假时,即使后续 patch 层
|
|
7
|
+
# 把官方行强制 disabled,行绑定内嵌探针仍保持 dormant。
|
|
8
|
+
- id: connection
|
|
9
|
+
name: '@deepseek-ai/dsh-client-connection'
|
|
10
|
+
disabled: !!js '(() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== ''function'') return false; let url; try { url = internal.version === ''v2'' ? internal.resolveSync(ctx.baseUrl, { specifier: ''@deepseek-ai/dsh-client-connection/package.json'', attributes: {} }).url : internal.resolveSync(''@deepseek-ai/dsh-client-connection/package.json'', ctx.baseUrl, {}).url; } catch (gateError) { return false; } if (typeof url !== ''string'') return false; const match = decodeURIComponent(url).match(/[+]dsh-client-connection@([^/_]+)[_/]/); return match !== null && match[1] === ''0.1.2-rc.1''; } catch (gateError) { return false; } })()'
|
|
11
|
+
- insert:
|
|
12
|
+
- id: trusted-connection
|
|
13
|
+
name: '@iasiv5/dsh-skip-browser-auth'
|
|
14
|
+
inject:
|
|
15
|
+
- webRuntime
|
|
16
|
+
config:
|
|
17
|
+
trustedHosts: !!js ctx.webRuntime.trustedHosts
|
|
18
|
+
disabled: !!js >-
|
|
19
|
+
!((() => { try { const self = ctx[Symbol.for('cordis.entry')]; if (!self || !self.parent || !self.parent.tree) return false; const official = self.parent.tree.resolve('connection'); if (!official || official.parent !== self.parent) return false; if (official.options.id !== 'connection' || official.options.name !== '@deepseek-ai/dsh-client-connection') return false; const probeAllowed = ((() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return false; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier: '@deepseek-ai/dsh-client-connection/package.json', attributes: {} }).url : internal.resolveSync('@deepseek-ai/dsh-client-connection/package.json', ctx.baseUrl, {}).url; } catch (gateError) { return false; } if (typeof url !== 'string') return false; const match = decodeURIComponent(url).match(/[+]dsh-client-connection@([^/_]+)[_/]/); return match !== null &&
|
|
20
|
+
match[1] === '0.1.2-rc.1'; } catch (gateError) { return false; } })()); if (probeAllowed !== true) return false; return official.disabled === true; } catch (gateError) { return false; } })())
|
package/lib/bridge.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node:http ↔ WHATWG fetch bridge for the /api transport (host side of the
|
|
3
|
+
* web carrier; the fetch-shaped handler itself is transport-agnostic).
|
|
4
|
+
*
|
|
5
|
+
* 移植自官方 packages/client/connection/src/http-bridge.ts(rc.1),语义照抄。
|
|
6
|
+
*/
|
|
7
|
+
/** Default carrier cap for all HTTP RPC bodies: sized for the default
|
|
8
|
+
* aggregate image limit (200 MiB) after base64 expansion plus envelope
|
|
9
|
+
* headroom (~267.7 MiB required), rounded up for slack. The bridge buffers
|
|
10
|
+
* each body in memory, so this cap is also the per-request resident bound. */
|
|
11
|
+
export const DEFAULT_MAX_REQUEST_BODY_BYTES = 300 * 1024 * 1024;
|
|
12
|
+
/**
|
|
13
|
+
* Bridge one node:http request to the fetch-shaped handler (client close
|
|
14
|
+
* aborts; response bodies stream out chunk by chunk).
|
|
15
|
+
* @param req - incoming node:http request (fully read before dispatch).
|
|
16
|
+
* @param res - node:http response the bridge writes and owns to completion.
|
|
17
|
+
* @param apiHandler - fetch-shaped API carrier the request is dispatched to.
|
|
18
|
+
* @param maxRequestBodyBytes - maximum body bytes buffered before dispatch.
|
|
19
|
+
*/
|
|
20
|
+
export async function bridge(req, res, apiHandler, maxRequestBodyBytes = DEFAULT_MAX_REQUEST_BODY_BYTES) {
|
|
21
|
+
const abort = new AbortController();
|
|
22
|
+
// Client-disconnect detection MUST hang off the response, not the request:
|
|
23
|
+
// since Node 16, IncomingMessage 'close' fires as soon as the request body is
|
|
24
|
+
// fully consumed (immediately for a bodyless GET), which would abort a
|
|
25
|
+
// streaming response right after open. ServerResponse 'close' fires on connection teardown;
|
|
26
|
+
// writableEnded distinguishes a normal end() from the client going away.
|
|
27
|
+
res.on('close', () => {
|
|
28
|
+
if (!res.writableEnded)
|
|
29
|
+
abort.abort();
|
|
30
|
+
});
|
|
31
|
+
const declaredLength = req.headers['content-length'];
|
|
32
|
+
if (declaredLength !== undefined && Number(declaredLength) > maxRequestBodyBytes) {
|
|
33
|
+
res.writeHead(413, { connection: 'close' });
|
|
34
|
+
res.end();
|
|
35
|
+
req.destroy();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const chunks = [];
|
|
39
|
+
let received = 0;
|
|
40
|
+
for await (const chunk of req) {
|
|
41
|
+
const buffer = chunk;
|
|
42
|
+
received += buffer.byteLength;
|
|
43
|
+
if (received > maxRequestBodyBytes) {
|
|
44
|
+
res.writeHead(413, { connection: 'close' });
|
|
45
|
+
res.end();
|
|
46
|
+
req.destroy();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
chunks.push(buffer);
|
|
50
|
+
}
|
|
51
|
+
/* v8 ignore next 3 -- `??` arms: node:http always sets url/method on server
|
|
52
|
+
requests; the fields are only optional on the client-side IncomingMessage type */
|
|
53
|
+
const request = new Request(new URL(req.url ?? '/', 'http://dsh.internal'), {
|
|
54
|
+
method: req.method ?? 'GET',
|
|
55
|
+
headers: Object.fromEntries(Object.entries(req.headers).filter(([, v]) => typeof v === 'string')),
|
|
56
|
+
...chunks.length > 0 ? { body: Buffer.concat(chunks) } : {},
|
|
57
|
+
signal: abort.signal,
|
|
58
|
+
});
|
|
59
|
+
const response = await apiHandler.fetch(request);
|
|
60
|
+
res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
|
|
61
|
+
if (response.body === null) {
|
|
62
|
+
res.end();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for await (const chunk of response.body) {
|
|
66
|
+
// Backpressure: a false return means the socket buffer is full — wait for drain
|
|
67
|
+
// instead of buffering unboundedly (slow or suspended consumers). 'close' also
|
|
68
|
+
// resolves so a mid-wait disconnect can't park this loop forever; the close
|
|
69
|
+
// handler above aborts the handler stream, which then ends the iteration.
|
|
70
|
+
if (!res.write(chunk)) {
|
|
71
|
+
await new Promise((resolve) => {
|
|
72
|
+
const done = () => {
|
|
73
|
+
res.off('drain', done);
|
|
74
|
+
res.off('close', done);
|
|
75
|
+
resolve();
|
|
76
|
+
};
|
|
77
|
+
res.once('drain', done);
|
|
78
|
+
res.once('close', done);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
res.end();
|
|
83
|
+
}
|