dsh-multi-chat 0.5.1 → 0.5.2
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/package.json
CHANGED
|
@@ -18,6 +18,9 @@ import { connect, createServer } from "node:net";
|
|
|
18
18
|
* `127.0.0.1:<target-port>`, rewriting Host/Origin so the official `/api`
|
|
19
19
|
* browser-trust fence treats it as a local request. WebSocket upgrades pass
|
|
20
20
|
* through untouched because the socket is piped byte-for-byte after the head.
|
|
21
|
+
* The HTML document response is buffered once so a `crypto.randomUUID`
|
|
22
|
+
* polyfill can be injected for phones (insecure-origin HTTP); all other
|
|
23
|
+
* responses stream through unchanged.
|
|
21
24
|
*
|
|
22
25
|
* Zero dependencies: node:net + node:crypto only.
|
|
23
26
|
* @module @deepseek-ai/dsh-client-ui-multi-wall/gateway
|
|
@@ -140,6 +143,29 @@ cache-control: no-store\r
|
|
|
140
143
|
socket.end();
|
|
141
144
|
}
|
|
142
145
|
/**
|
|
146
|
+
* `crypto.randomUUID` exists only in a *secure context* (HTTPS or localhost).
|
|
147
|
+
* A phone reaching the instance via `http://<LAN-IP>:<gateway-port>` is an
|
|
148
|
+
* insecure origin, so the official DSH client throws
|
|
149
|
+
* `crypto.randomUUID is not a function` on its first RPC and renders blank.
|
|
150
|
+
* This polyfill supplies the same RFC-4122 v4 form using
|
|
151
|
+
* `crypto.getRandomValues`, which insecure origins still expose.
|
|
152
|
+
*/
|
|
153
|
+
const POLYFILL_SCRIPT = "<script>(function(){if(typeof crypto!==\"undefined\"&&typeof crypto.randomUUID===\"undefined\"&&typeof crypto.getRandomValues===\"function\"){crypto.randomUUID=function(){var b=crypto.getRandomValues(new Uint8Array(16));b[6]=(b[6]&15)|64;b[8]=(b[8]&63)|128;var h=Array.prototype.map.call(b,function(x){return x.toString(16).padStart(2,\"0\")}).join(\"\");return h.slice(0,8)+\"-\"+h.slice(8,12)+\"-\"+h.slice(12,16)+\"-\"+h.slice(16,20)+\"-\"+h.slice(20);};}})();<\/script>";
|
|
154
|
+
/**
|
|
155
|
+
* Rewrite a buffered HTML response body to inject the `randomUUID` polyfill
|
|
156
|
+
* into the top of `<head>` (before the DSH bootstrap script runs). Returns the
|
|
157
|
+
* new body, or `null` when the polyfill is not needed (already present, or the
|
|
158
|
+
* response carries no `<head>` to inject into).
|
|
159
|
+
*/
|
|
160
|
+
function injectPolyfill(body) {
|
|
161
|
+
const html = body.toString("utf8");
|
|
162
|
+
if (html.includes("randomUUID")) return null;
|
|
163
|
+
const headMatch = /<head[^>]*>/i.exec(html);
|
|
164
|
+
if (headMatch === null) return null;
|
|
165
|
+
const insertAt = headMatch.index + headMatch[0].length;
|
|
166
|
+
return Buffer.from(html.slice(0, insertAt) + POLYFILL_SCRIPT + html.slice(insertAt), "utf8");
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
143
169
|
* Rebuild the request head for the loopback target. Host and Origin point at
|
|
144
170
|
* `127.0.0.1:<target-port>` so the DSH `/api` browser-trust fence sees a local
|
|
145
171
|
* request. Non-upgrade requests get `connection: close` (fresh connection per
|
|
@@ -258,6 +284,110 @@ connection: close\r
|
|
|
258
284
|
socket.once("end", done);
|
|
259
285
|
}
|
|
260
286
|
};
|
|
287
|
+
/**
|
|
288
|
+
* Decode an HTTP/1.1 `transfer-encoding: chunked` body back to its raw
|
|
289
|
+
* bytes. Returns `null` on malformed framing (caller then relays verbatim).
|
|
290
|
+
*/
|
|
291
|
+
const decodeChunked = (raw) => {
|
|
292
|
+
const out = [];
|
|
293
|
+
let i = 0;
|
|
294
|
+
const text = raw.toString("latin1");
|
|
295
|
+
while (true) {
|
|
296
|
+
const crlf = text.indexOf("\r\n", i);
|
|
297
|
+
if (crlf === -1) return null;
|
|
298
|
+
const sizeHex = text.slice(i, crlf).trim();
|
|
299
|
+
const size = parseInt(sizeHex, 16);
|
|
300
|
+
if (!Number.isFinite(size) || size < 0) return null;
|
|
301
|
+
i = crlf + 2;
|
|
302
|
+
if (size === 0) return Buffer.concat(out);
|
|
303
|
+
if (i + size > raw.length) return null;
|
|
304
|
+
out.push(raw.subarray(i, i + size));
|
|
305
|
+
i += size + 2;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* Relay the DSH target's response to the phone client. HTML document
|
|
310
|
+
* responses (the DSH shell) are buffered so the `randomUUID` polyfill can be
|
|
311
|
+
* injected before the bootstrap script runs; every other response
|
|
312
|
+
* (assets, API JSON, SSE streams, WebSocket upgrades) is piped byte-for-byte
|
|
313
|
+
* so streaming and framing stay untouched.
|
|
314
|
+
*/
|
|
315
|
+
const relayTarget = (socket, target) => {
|
|
316
|
+
let head = Buffer.alloc(0);
|
|
317
|
+
let bodyParts = null;
|
|
318
|
+
let relayed = false;
|
|
319
|
+
const pipeThrough = () => {
|
|
320
|
+
if (relayed) return;
|
|
321
|
+
relayed = true;
|
|
322
|
+
if (head.length > 0) socket.write(head);
|
|
323
|
+
if (bodyParts !== null && bodyParts.length > 0) {
|
|
324
|
+
socket.write(Buffer.concat(bodyParts));
|
|
325
|
+
bodyParts = null;
|
|
326
|
+
}
|
|
327
|
+
target.pipe(socket);
|
|
328
|
+
};
|
|
329
|
+
const finishInjection = (fullHead, fullBody) => {
|
|
330
|
+
const rewritten = injectPolyfill(fullBody);
|
|
331
|
+
if (rewritten === null) {
|
|
332
|
+
socket.write(fullHead);
|
|
333
|
+
socket.write(fullBody);
|
|
334
|
+
socket.end();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const lines = fullHead.toString("latin1").split("\r\n");
|
|
338
|
+
const filtered = [];
|
|
339
|
+
for (const line of lines) {
|
|
340
|
+
const lower = line.toLowerCase();
|
|
341
|
+
if (lower.startsWith("transfer-encoding:")) continue;
|
|
342
|
+
if (lower.startsWith("content-length:")) continue;
|
|
343
|
+
if (lower.startsWith("connection:")) continue;
|
|
344
|
+
filtered.push(line);
|
|
345
|
+
}
|
|
346
|
+
while (filtered.length > 0 && filtered[filtered.length - 1] === "") filtered.pop();
|
|
347
|
+
filtered.push(`content-length: ${rewritten.length}`, "connection: close", "", "");
|
|
348
|
+
socket.write(Buffer.from(filtered.join("\r\n"), "latin1"));
|
|
349
|
+
socket.write(rewritten);
|
|
350
|
+
socket.end();
|
|
351
|
+
};
|
|
352
|
+
target.on("data", (chunk) => {
|
|
353
|
+
if (relayed) return;
|
|
354
|
+
if (head.indexOf("\r\n\r\n") === -1) {
|
|
355
|
+
head = head.length === 0 ? chunk : Buffer.concat([head, chunk]);
|
|
356
|
+
const newIdx = head.indexOf("\r\n\r\n");
|
|
357
|
+
if (newIdx === -1) {
|
|
358
|
+
if (head.length > 65536) pipeThrough();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const headBuf = head.subarray(0, newIdx + 4);
|
|
362
|
+
const bodyStart = head.subarray(newIdx + 4);
|
|
363
|
+
const headText = headBuf.toString("latin1");
|
|
364
|
+
const status = /^HTTP\/\d\.\d (\d{3})/.exec(headText)?.[1] ?? "";
|
|
365
|
+
const contentType = /^content-type:\s*([^\r\n]+)/im.exec(headText)?.[1] ?? "";
|
|
366
|
+
const ce = /^content-encoding:\s*([^\r\n]+)/im.exec(headText)?.[1] ?? "";
|
|
367
|
+
const isHtml = status === "200" && /text\/html/i.test(contentType);
|
|
368
|
+
const compressed = ce !== "" && !/identity/i.test(ce);
|
|
369
|
+
if (!isHtml || compressed) {
|
|
370
|
+
pipeThrough();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
bodyParts = bodyStart.length > 0 ? [bodyStart] : [];
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (bodyParts !== null) bodyParts.push(chunk);
|
|
377
|
+
});
|
|
378
|
+
target.once("end", () => {
|
|
379
|
+
if (relayed || bodyParts === null) {
|
|
380
|
+
if (!relayed) socket.end();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const headBuf = head.subarray(0, head.indexOf("\r\n\r\n") + 4);
|
|
384
|
+
const headText = headBuf.toString("latin1");
|
|
385
|
+
const rawBody = Buffer.concat(bodyParts);
|
|
386
|
+
const te = /^transfer-encoding:\s*([^\r\n]+)/im.exec(headText)?.[1] ?? "";
|
|
387
|
+
finishInjection(headBuf, /chunked/i.test(te) ? decodeChunked(rawBody) ?? rawBody : rawBody);
|
|
388
|
+
});
|
|
389
|
+
target.once("error", () => socket.destroy());
|
|
390
|
+
};
|
|
261
391
|
const handleSocket = (socket) => {
|
|
262
392
|
let buffer = Buffer.alloc(0);
|
|
263
393
|
let headDone = false;
|
|
@@ -300,7 +430,7 @@ connection: close\r
|
|
|
300
430
|
if (rest.length > 0) target.write(rest);
|
|
301
431
|
proxying = true;
|
|
302
432
|
socket.pipe(target);
|
|
303
|
-
|
|
433
|
+
relayTarget(socket, target);
|
|
304
434
|
});
|
|
305
435
|
target.on("error", (error) => {
|
|
306
436
|
if (!connected) respondPlain(socket, "502", `cannot reach target 127.0.0.1:${targetPort}: ${error.message}`);
|