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
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
||||
import http from 'node:http';
|
||||
import net from 'node:net';
|
||||
import crypto from 'node:crypto';
|
||||
import { createProxy } from '../src/proxy.js';
|
||||
import { createProxy, stripHop } from '../src/proxy.js';
|
||||
|
||||
const config = { baseHost: 'project.webklar.com' };
|
||||
|
||||
@@ -141,3 +141,83 @@ test('Upstream-Fehler ⇒ 502 deutscher Text', async () => {
|
||||
assert.match(text, /nicht erreichbar/);
|
||||
});
|
||||
});
|
||||
|
||||
test('stripHop: Hop-by-hop und in Connection genannte Header fallen weg; Upgrade-Pfad behält Upgrade', () => {
|
||||
const h = { connection: 'keep-alive, X-Geheim', 'keep-alive': 'timeout=5', 'x-geheim': '1', te: 'trailers', upgrade: 'websocket', 'proxy-authorization': 'x', 'x-ok': '2' };
|
||||
assert.deepEqual(stripHop(h), { 'x-ok': '2' });
|
||||
assert.deepEqual(stripHop(h, { upgrade: true }), { connection: 'Upgrade', upgrade: 'websocket', 'x-ok': '2' });
|
||||
});
|
||||
|
||||
test('Hop-by-hop-Header werden in beide Richtungen nicht durchgereicht', async () => {
|
||||
const srv = http.createServer((req, res) => {
|
||||
res.writeHead(200, { Connection: 'close, X-Weg', 'X-Weg': 'a', 'Keep-Alive': 'timeout=1', 'X-Bleibt': 'b' });
|
||||
res.end(JSON.stringify(req.headers));
|
||||
});
|
||||
await new Promise((r) => srv.listen(0, '127.0.0.1', r));
|
||||
try {
|
||||
await withProxy({ wake: async () => ({ state: 'online', target: `http://127.0.0.1:${srv.address().port}` }) }, async ({ port }) => {
|
||||
const { res, text } = await new Promise((resolve, reject) => {
|
||||
const rq = http.request({ port, host: '127.0.0.1', headers: { Host: 'demo.project.webklar.com', Connection: 'keep-alive, X-Hop', 'X-Hop': '1', 'Proxy-Authorization': 'Basic x', TE: 'trailers', 'X-Normal': 'ja' } }, (rs) => { const c = []; rs.on('data', (d) => c.push(d)); rs.on('end', () => resolve({ res: rs, text: Buffer.concat(c).toString() })); });
|
||||
rq.on('error', reject); rq.end();
|
||||
});
|
||||
const seen = JSON.parse(text);
|
||||
assert.equal(seen['x-hop'], undefined); assert.equal(seen['proxy-authorization'], undefined); assert.equal(seen.te, undefined);
|
||||
assert.equal(seen['x-normal'], 'ja');
|
||||
assert.equal(res.headers['x-weg'], undefined); assert.notEqual(res.headers['keep-alive'], 'timeout=1');
|
||||
assert.equal(res.headers['x-bleibt'], 'b');
|
||||
});
|
||||
} finally { srv.closeAllConnections?.(); await new Promise((r) => srv.close(r)); }
|
||||
});
|
||||
|
||||
test('Upstream antwortet nicht ⇒ 504', async () => {
|
||||
const srv = http.createServer(() => {}); // antwortet nie
|
||||
await new Promise((r) => srv.listen(0, '127.0.0.1', r));
|
||||
try {
|
||||
await withProxy({ wake: async () => ({ state: 'online', target: `http://127.0.0.1:${srv.address().port}` }) }, async ({ port }) => {
|
||||
const { res, text } = await get(port, 'demo.project.webklar.com');
|
||||
assert.equal(res.statusCode, 504);
|
||||
assert.match(text, /nicht rechtzeitig/);
|
||||
}, { upstreamTimeoutMs: 100 });
|
||||
} finally { srv.closeAllConnections?.(); await new Promise((r) => srv.close(r)); }
|
||||
});
|
||||
|
||||
test('Upgrade-Race: Client weg während wake ⇒ kein Upstream-Request', async () => {
|
||||
let release; const gate = new Promise((r) => { release = r; });
|
||||
let upstreamCalls = 0;
|
||||
const request = () => { upstreamCalls++; throw new Error('darf nicht aufgerufen werden'); };
|
||||
await withProxy({ wake: async () => { await gate; return { state: 'online', target: 'http://127.0.0.1:1' }; } }, async ({ port, touched }) => {
|
||||
const sock = net.connect(port, '127.0.0.1');
|
||||
sock.on('error', () => {});
|
||||
sock.write('GET /ws HTTP/1.1\r\nHost: demo.project.webklar.com\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n');
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
sock.destroy();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
release();
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
assert.equal(upstreamCalls, 0);
|
||||
assert.deepEqual(touched, []);
|
||||
}, { request });
|
||||
});
|
||||
|
||||
test('WebSocket-Daten touchen höchstens einmal pro Minute', async () => {
|
||||
const t = await startTarget();
|
||||
const clock = { t: 1000 };
|
||||
try {
|
||||
await withProxy({ wake: async () => ({ state: 'online', target: `http://127.0.0.1:${t.port}` }) }, async ({ port, touched }) => {
|
||||
const sock = net.connect(port, '127.0.0.1'); sock.on('error', () => {});
|
||||
let buf = '';
|
||||
sock.on('data', (d) => { buf += d.toString('latin1'); });
|
||||
const waitFor = async (f) => { for (let i = 0; i < 200 && !f(); i++) await new Promise((r) => setTimeout(r, 5)); assert.ok(f()); };
|
||||
try {
|
||||
sock.write('GET /ws HTTP/1.1\r\nHost: demo.project.webklar.com\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n');
|
||||
await waitFor(() => buf.includes('\r\n\r\n'));
|
||||
assert.deepEqual(touched, ['demo']);
|
||||
sock.write('a'); await waitFor(() => buf.endsWith('a'));
|
||||
assert.deepEqual(touched, ['demo']); // innerhalb der Minute: kein weiteres touch
|
||||
clock.t += 61e3;
|
||||
sock.write('b'); await waitFor(() => buf.endsWith('b'));
|
||||
assert.deepEqual(touched, ['demo', 'demo']);
|
||||
} finally { sock.destroy(); }
|
||||
}, { now: () => clock.t });
|
||||
} finally { t.srv.closeAllConnections?.(); await new Promise((r) => t.srv.close(r)); }
|
||||
});
|
||||
Reference in new issue
Block a user