@lvxianchao/dsh-web-guard 3.0.0 → 3.0.1
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/CHANGELOG.md +12 -0
- package/lib/index.js +3 -1
- package/lib/proxy.js +17 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.1 — 2026-11-10
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **The proxy no longer leaks upstream sockets when a browser disconnects.** Closing a tab, reloading or dropping the link mid-response left the upstream request alive; for a response that never ends — an SSE stream, a long poll — the socket stayed allocated in the shared keep-alive agent forever. Once `maxSockets` (128) of them had accumulated, every later request through the proxy queued until the reverse proxy in front gave up with `504` — while `/__guard/session` kept answering 200, which made it look like the harness was up. The guard now destroys the upstream request when the client closes and treats the resulting error as expected instead of logging it.
|
|
8
|
+
- **The launch-token exchange is bounded to 10 seconds.** An unanswered exchange used to stay `inflight` forever, stalling every request that awaited it.
|
|
9
|
+
|
|
10
|
+
### Upgrade
|
|
11
|
+
|
|
12
|
+
1. `dsh plugin --profile web add @lvxianchao/dsh-web-guard@3.0.1` (or reinstall from the same source you use).
|
|
13
|
+
2. `sudo systemctl restart dsh-web` (or however the harness is supervised). Sessions survive the restart; open pages reconnect on their own.
|
|
14
|
+
|
|
3
15
|
## 3.0.0 — 2026-11-06
|
|
4
16
|
|
|
5
17
|
### Breaking
|
package/lib/index.js
CHANGED
|
@@ -235,7 +235,9 @@ export function apply(ctx, config = {}) {
|
|
|
235
235
|
const pending = (async () => {
|
|
236
236
|
try {
|
|
237
237
|
const url = ctx.connection.authenticatedUrl(`${targetOrigin}/`);
|
|
238
|
-
|
|
238
|
+
// Bound the exchange: an unanswered fetch would otherwise stay
|
|
239
|
+
// `inflight` forever and stall every request that awaits it.
|
|
240
|
+
const res = await fetch(url, { redirect: 'manual', signal: AbortSignal.timeout(10_000) });
|
|
239
241
|
const parts = res.headers.getSetCookie();
|
|
240
242
|
const value = parts.map((line) => line.split(';')[0]).filter(Boolean).join('; ');
|
|
241
243
|
if (!value)
|
package/lib/proxy.js
CHANGED
|
@@ -110,7 +110,19 @@ export function forward(req, res, options) {
|
|
|
110
110
|
const proxyReq = secure
|
|
111
111
|
? https.request(requestOptions, (proxyRes) => forwardResponse(proxyRes, res, req, started, log))
|
|
112
112
|
: http.request(requestOptions, (proxyRes) => forwardResponse(proxyRes, res, req, started, log));
|
|
113
|
+
// A browser can walk away mid-response (tab closed, reload, flaky link).
|
|
114
|
+
// Without this the upstream request survives the client, and a response that
|
|
115
|
+
// never ends — an SSE stream, a long poll — keeps its socket allocated in the
|
|
116
|
+
// shared keep-alive agent forever. Once `maxSockets` of those pile up every
|
|
117
|
+
// later request queues until the proxy in front gives up (504).
|
|
118
|
+
res.on('close', () => {
|
|
119
|
+
if (!res.writableEnded)
|
|
120
|
+
proxyReq.destroy();
|
|
121
|
+
});
|
|
113
122
|
proxyReq.on('error', (err) => {
|
|
123
|
+
// Destroying the upstream is the expected outcome of a client hangup.
|
|
124
|
+
if (res.destroyed || res.writableEnded)
|
|
125
|
+
return;
|
|
114
126
|
log(`upstream error ${req.method} ${req.url}: ${err.message}`);
|
|
115
127
|
if (!res.headersSent) {
|
|
116
128
|
res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
|
|
@@ -123,10 +135,15 @@ export function forward(req, res, options) {
|
|
|
123
135
|
req.pipe(proxyReq);
|
|
124
136
|
}
|
|
125
137
|
function forwardResponse(proxyRes, res, req, started, log) {
|
|
138
|
+
if (res.destroyed) {
|
|
139
|
+
proxyRes.destroy();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
126
142
|
const headers = { ...proxyRes.headers };
|
|
127
143
|
// Never leak the harness cookie to the browser: the guard owns it.
|
|
128
144
|
delete headers['set-cookie'];
|
|
129
145
|
res.writeHead(proxyRes.statusCode ?? 502, headers);
|
|
146
|
+
proxyRes.on('error', () => res.destroy());
|
|
130
147
|
proxyRes.pipe(res);
|
|
131
148
|
// Quiet by default: the Web UI polls constantly. Log failures and slow
|
|
132
149
|
// requests only, so the journal does not fill with 200s.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lvxianchao/dsh-web-guard",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Password login for the DeepSeek Harness Web UI, so it can be exposed on the internet: sign in from anywhere instead of chasing one-off tokens, with reverse-proxy quirks handled. Ships a native DSH settings page.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|