Runner: Proxy härten (R13 Listen-Adressen, Hop-by-hop, Upgrade-Race, Touch, Timeout)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4afe7eb5cd
commit
e358514240
6 files changed
+173
-15
No files matched your search
+30
-8
@@ -22,7 +22,21 @@ function html(res, status, body, extra = {}) {
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
export function createProxy({ config, dev, request = http.request }) {
|
||||
const HOP = ['connection', 'keep-alive', 'proxy-authorization', 'proxy-connection', 'te', 'trailer', 'transfer-encoding', 'upgrade'];
|
||||
// allowHalfOpen: ein vom Client geschlossener Socket ist zwar nicht 'destroyed', aber nicht mehr lesbar
|
||||
const gone = (s) => s.destroyed || !s.readable;
|
||||
const TOUCH_MS = 60e3;
|
||||
|
||||
// Hop-by-hop-Header entfernen (samt der in Connection genannten Tokens). Auf dem Upgrade-Pfad bleiben Upgrade/Connection: Upgrade.
|
||||
export function stripHop(headers, { upgrade = false } = {}) {
|
||||
const out = { ...headers };
|
||||
const listed = String(headers.connection || '').split(',').map((t) => t.trim().toLowerCase()).filter(Boolean);
|
||||
for (const k of Object.keys(out)) if (HOP.includes(k.toLowerCase()) || listed.includes(k.toLowerCase())) delete out[k];
|
||||
if (upgrade) { out.connection = 'Upgrade'; out.upgrade = headers.upgrade; }
|
||||
return out;
|
||||
}
|
||||
|
||||
export function createProxy({ config, dev, request = http.request, upstreamTimeoutMs = 120e3, now = Date.now }) {
|
||||
// Sub aus dem Host-Header: exakt <sub>.<baseHost>, klein geschrieben, Port entfernt.
|
||||
function subOf(hostHeader) {
|
||||
const host = String(hostHeader || '').replace(/:\d+$/, '');
|
||||
@@ -32,8 +46,8 @@ export function createProxy({ config, dev, request = http.request }) {
|
||||
return isValidSub(sub) ? sub : null;
|
||||
}
|
||||
|
||||
function fwdHeaders(req) {
|
||||
const h = { ...req.headers };
|
||||
function fwdHeaders(req, upgrade = false) {
|
||||
const h = stripHop(req.headers, { upgrade });
|
||||
const prior = req.headers['x-forwarded-for'];
|
||||
const ip = req.socket.remoteAddress || '';
|
||||
h['x-forwarded-for'] = prior ? `${prior}, ${ip}` : ip;
|
||||
@@ -45,7 +59,7 @@ export function createProxy({ config, dev, request = http.request }) {
|
||||
// Ergebnis: { target } wenn online, sonst { state }.
|
||||
async function resolve(sub) {
|
||||
let r;
|
||||
try { r = await dev.wake(sub); } catch (e) { return { state: e?.status === 503 ? 'busy' : 'unknown' }; }
|
||||
try { r = await dev.wake(sub); } catch (e) { if (e?.status !== 503) console.error('Proxy wake', sub, e); return { state: e?.status === 503 ? 'busy' : 'unknown' }; }
|
||||
if (r.state === 'online' && r.target) return { target: new URL(r.target) };
|
||||
return { state: r.state };
|
||||
}
|
||||
@@ -63,14 +77,16 @@ export function createProxy({ config, dev, request = http.request }) {
|
||||
const up = request({
|
||||
hostname: r.target.hostname, port: r.target.port, method: req.method, path: req.url, headers: fwdHeaders(req),
|
||||
}, (ures) => {
|
||||
res.writeHead(ures.statusCode, ures.headers);
|
||||
res.writeHead(ures.statusCode, stripHop(ures.headers));
|
||||
ures.pipe(res);
|
||||
ures.on('error', () => res.destroy());
|
||||
});
|
||||
let timedOut = false;
|
||||
up.setTimeout(upstreamTimeoutMs, () => { timedOut = true; up.destroy(); });
|
||||
up.on('error', () => {
|
||||
if (!res.headersSent) {
|
||||
const body = 'Die Preview ist gerade nicht erreichbar (502).';
|
||||
res.writeHead(502, { 'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': Buffer.byteLength(body) });
|
||||
const body = timedOut ? 'Die Preview antwortet nicht rechtzeitig (504).' : 'Die Preview ist gerade nicht erreichbar (502).';
|
||||
res.writeHead(timedOut ? 504 : 502, { 'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': Buffer.byteLength(body) });
|
||||
res.end(body);
|
||||
} else res.destroy();
|
||||
});
|
||||
@@ -89,10 +105,11 @@ export function createProxy({ config, dev, request = http.request }) {
|
||||
const sub = subOf(req.headers.host);
|
||||
if (!sub) return socket.end('HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n');
|
||||
const r = await resolve(sub);
|
||||
if (gone(socket)) return socket.destroy(); // Client hat sich während des Wartens verabschiedet
|
||||
if (!r.target) return socket.end('HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n');
|
||||
dev.touch(sub);
|
||||
const up = request({
|
||||
hostname: r.target.hostname, port: r.target.port, method: req.method, path: req.url, headers: fwdHeaders(req),
|
||||
hostname: r.target.hostname, port: r.target.port, method: req.method, path: req.url, headers: fwdHeaders(req, true),
|
||||
});
|
||||
up.on('response', (ures) => { // Upstream lehnt das Upgrade ab: Antwort durchreichen und schließen
|
||||
let raw = `HTTP/1.1 ${ures.statusCode} ${ures.statusMessage}\r\n`;
|
||||
@@ -101,6 +118,7 @@ export function createProxy({ config, dev, request = http.request }) {
|
||||
ures.pipe(socket);
|
||||
});
|
||||
up.on('upgrade', (ures, usock, uhead) => {
|
||||
if (gone(socket)) { usock.destroy(); up.destroy(); return; }
|
||||
usock.on('error', () => { usock.destroy(); socket.destroy(); });
|
||||
// allowHalfOpen-Sockets bleiben nach 'end' offen: bei Ende oder Schließen einer Seite beide zerstören
|
||||
const both = () => { usock.destroy(); socket.destroy(); };
|
||||
@@ -110,6 +128,10 @@ export function createProxy({ config, dev, request = http.request }) {
|
||||
socket.write(`${raw}\r\n`);
|
||||
if (uhead?.length) socket.write(uhead);
|
||||
if (head?.length) usock.write(head);
|
||||
// Offene Verbindung (z. B. HMR) hält den Dev-Server wach: höchstens einmal pro Minute touch
|
||||
let last = now();
|
||||
const keep = () => { const t = now(); if (t - last >= TOUCH_MS) { last = t; dev.touch(sub); } };
|
||||
usock.on('data', keep); socket.on('data', keep);
|
||||
usock.pipe(socket);
|
||||
socket.pipe(usock);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user