import { expect, tap } from '@git.zone/tstest/tapbundle'; import { SmartProxy } from '../ts/index.js'; import * as http from 'http'; import * as net from 'net'; import * as tls from 'tls'; import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { assertPortsFree, findFreePorts } from './helpers/port-allocator.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const CERT_PEM = fs.readFileSync(path.join(__dirname, '..', 'assets', 'certs', 'cert.pem'), 'utf8'); const KEY_PEM = fs.readFileSync(path.join(__dirname, '..', 'assets', 'certs', 'key.pem'), 'utf8'); let httpBackendPort: number; let tlsBackendPort: number; let httpProxyPort: number; let tlsProxyPort: number; let httpBackend: http.Server; let tlsBackend: tls.Server; let proxy: SmartProxy; async function pollMetrics(proxyToPoll: SmartProxy): Promise { await (proxyToPoll as any).metricsAdapter.poll(); } async function waitForCondition( callback: () => Promise, timeoutMs: number = 5000, stepMs: number = 100, ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (await callback()) { return; } await new Promise((resolve) => setTimeout(resolve, stepMs)); } throw new Error(`Condition not met within ${timeoutMs}ms`); } function hasIpDomainRequest(domain: string): boolean { const byIp = proxy.getMetrics().connections.domainRequestsByIP(); for (const domainMap of byIp.values()) { if (domainMap.has(domain)) { return true; } } return false; } tap.test('setup - backend servers for HTTP domain rate metrics', async () => { [httpBackendPort, tlsBackendPort, httpProxyPort, tlsProxyPort] = await findFreePorts(4); httpBackend = http.createServer((req, res) => { let body = ''; req.on('data', (chunk) => { body += chunk; }); req.on('end', () => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(`ok:${body}`); }); }); await new Promise((resolve) => { httpBackend.listen(httpBackendPort, () => resolve()); }); tlsBackend = tls.createServer({ cert: CERT_PEM, key: KEY_PEM }, (socket) => { socket.on('data', (data) => { socket.write(data); }); socket.on('error', () => {}); }); await new Promise((resolve) => { tlsBackend.listen(tlsBackendPort, () => resolve()); }); }); tap.test('setup - start proxy with HTTP and TLS passthrough routes', async () => { proxy = new SmartProxy({ routes: [ { id: 'http-domain-rates', name: 'http-domain-rates', match: { ports: httpProxyPort, domains: 'example.com' }, action: { type: 'forward', targets: [{ host: 'localhost', port: httpBackendPort }], }, }, { id: 'tls-passthrough-domain-rates', name: 'tls-passthrough-domain-rates', match: { ports: tlsProxyPort, domains: 'passthrough.example.com' }, action: { type: 'forward', tls: { mode: 'passthrough' }, targets: [{ host: 'localhost', port: tlsBackendPort }], }, }, ], metrics: { enabled: true, sampleIntervalMs: 100, retentionSeconds: 60 }, }); await proxy.start(); await new Promise((resolve) => setTimeout(resolve, 300)); }); tap.test('HTTP requests populate per-domain HTTP request rates', async () => { for (let i = 0; i < 3; i++) { await new Promise((resolve, reject) => { const body = `payload-${i}`; const req = http.request( { hostname: 'localhost', port: httpProxyPort, path: '/echo', method: 'POST', headers: { Host: 'Example.COM', 'Content-Type': 'text/plain', 'Content-Length': String(body.length), }, }, (res) => { res.resume(); res.on('end', () => resolve()); }, ); req.on('error', reject); req.end(body); }); } await waitForCondition(async () => { await pollMetrics(proxy); const domainMetrics = proxy.getMetrics().requests.byDomain().get('example.com'); return (domainMetrics?.lastMinute ?? 0) >= 3 && (domainMetrics?.perSecond ?? 0) > 0; }); const exampleMetrics = proxy.getMetrics().requests.byDomain().get('example.com'); expect(exampleMetrics).toBeTruthy(); expect(exampleMetrics?.lastMinute).toEqual(3); expect(exampleMetrics?.perSecond).toBeGreaterThan(0); }); tap.test('TLS passthrough SNI does not inflate HTTP domain request rates', async () => { const tlsClient = tls.connect({ host: 'localhost', port: tlsProxyPort, servername: 'passthrough.example.com', rejectUnauthorized: false, }); await new Promise((resolve, reject) => { tlsClient.once('secureConnect', () => resolve()); tlsClient.once('error', reject); }); const echoPromise = new Promise((resolve, reject) => { tlsClient.once('data', () => resolve()); tlsClient.once('error', reject); }); tlsClient.write(Buffer.from('hello over tls passthrough')); await echoPromise; await waitForCondition(async () => { await pollMetrics(proxy); return hasIpDomainRequest('passthrough.example.com'); }); const requestRates = proxy.getMetrics().requests.byDomain(); expect(requestRates.has('passthrough.example.com')).toBeFalse(); expect(requestRates.get('example.com')?.lastMinute).toEqual(3); expect(hasIpDomainRequest('passthrough.example.com')).toBeTrue(); tlsClient.destroy(); }); tap.test('cleanup - stop proxy and close backend servers', async () => { await proxy.stop(); await new Promise((resolve) => httpBackend.close(() => resolve())); await new Promise((resolve) => tlsBackend.close(() => resolve())); await assertPortsFree([httpBackendPort, tlsBackendPort, httpProxyPort, tlsProxyPort]); }); export default tap.start()