feat(deploy): ausgehender HTTP(S)-Proxy für Build und Laufzeit
- Compose reicht HTTP_PROXY/HTTPS_PROXY/NO_PROXY als Build-Argumente an beide Images weiter (npm ci, Google Fonts) und zur Laufzeit an api und worker; api, db, web stehen immer in NO_PROXY - Spielerköpfe werden über undici EnvHttpProxyAgent geladen (Node-fetch ignoriert Proxy-Variablen) - Doku: Proxy-Abschnitt mit Whitelist und Fehlerbildern - geprüft mit lokalem Squid: Build nur über registry.npmjs.org, fonts.googleapis.com, fonts.gstatic.com; zur Laufzeit nur mc-heads.net Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e66f89f49e
commit
d45d1821df
10 files changed
+179
-4
No files matched your search
@@ -0,0 +1,75 @@
|
||||
import { createServer, type Server } from 'node:http';
|
||||
import { connect, type AddressInfo, type Socket } from 'node:net';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { buildApp } from '../src/app.ts';
|
||||
import { loadConfig } from '../src/config.ts';
|
||||
import { getTestPool } from './helpers/db.ts';
|
||||
import { TEST_SECRET } from './helpers/sign.ts';
|
||||
|
||||
const PNG = Buffer.from('89504e470d0a1a0a0000000d4948445200000001000000010806000000', 'hex');
|
||||
const UUID = '308a693c-2bd8-4d6b-8790-670aef5be601';
|
||||
|
||||
let upstream: Server;
|
||||
let proxy: Server;
|
||||
const proxied: string[] = [];
|
||||
const port = (s: Server) => (s.address() as AddressInfo).port;
|
||||
const listen = (s: Server) => new Promise<void>((r) => s.listen(0, '127.0.0.1', () => r()));
|
||||
|
||||
beforeAll(async () => {
|
||||
upstream = createServer((_req, res) => res.writeHead(200, { 'content-type': 'image/png' }).end(PNG));
|
||||
// Minimal forward proxy like Squid: undici tunnels every request with CONNECT host:port.
|
||||
// "upstream.test" only exists for the proxy, which relays to the local upstream server.
|
||||
proxy = createServer((_req, res) => res.writeHead(405).end());
|
||||
proxy.on('connect', (req, client: Socket, head) => {
|
||||
proxied.push(req.url ?? '');
|
||||
const upstreamSocket = connect(Number(new URL(`http://${req.url}`).port), '127.0.0.1', () => {
|
||||
client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
||||
upstreamSocket.write(head);
|
||||
upstreamSocket.pipe(client);
|
||||
client.pipe(upstreamSocket);
|
||||
});
|
||||
});
|
||||
await listen(upstream);
|
||||
await listen(proxy);
|
||||
});
|
||||
afterAll(() => {
|
||||
upstream.close();
|
||||
proxy.close();
|
||||
});
|
||||
|
||||
async function avatarVia(env: Record<string, string>) {
|
||||
const config = loadConfig({
|
||||
NODE_ENV: 'test',
|
||||
DATABASE_URL: process.env.TEST_DATABASE_URL,
|
||||
INGEST_HMAC_SECRET: TEST_SECRET,
|
||||
LOG_LEVEL: 'silent',
|
||||
AVATAR_SOURCE: `http://upstream.test:${port(upstream)}/avatar/{uuid}`,
|
||||
...env,
|
||||
});
|
||||
const app = await buildApp({ pool: await getTestPool(), config });
|
||||
try {
|
||||
return await app.inject({ method: 'GET', url: `/api/avatar/${UUID}` });
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
}
|
||||
|
||||
describe('outbound proxy', () => {
|
||||
it('fetches player heads through HTTP_PROXY', async () => {
|
||||
proxied.length = 0;
|
||||
const res = await avatarVia({ HTTP_PROXY: `http://127.0.0.1:${port(proxy)}` });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.rawPayload.equals(PNG)).toBe(true);
|
||||
expect(proxied).toEqual([`upstream.test:${port(upstream)}`]);
|
||||
});
|
||||
|
||||
it('accepts lower-case proxy variables', () => {
|
||||
const c = loadConfig({ DATABASE_URL: 'postgres://x', INGEST_HMAC_SECRET: 'x'.repeat(32), https_proxy: 'http://p:3128', no_proxy: '.local' });
|
||||
expect(c.proxy).toEqual({ httpProxy: undefined, httpsProxy: 'http://p:3128', noProxy: '.local' });
|
||||
});
|
||||
|
||||
it('uses no proxy when none is configured', () => {
|
||||
const c = loadConfig({ DATABASE_URL: 'postgres://x', INGEST_HMAC_SECRET: 'x'.repeat(32) });
|
||||
expect(c.proxy).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user