import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as plugins from '../ts/plugins.js'; import * as http from 'http'; import { SmartProxy, SocketHandlers } from '../ts/index.js'; /** * Helper to make HTTP requests using Node's http module (unlike fetch/undici, * http.request doesn't keep the event loop alive via a connection pool). */ function httpRequest(url: string, options: { method?: string; headers?: Record } = {}): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: string }> { return new Promise((resolve, reject) => { const parsed = new URL(url); const req = http.request({ hostname: parsed.hostname, port: parsed.port, path: parsed.pathname + parsed.search, method: options.method || 'GET', headers: options.headers, }, (res) => { let body = ''; res.on('data', (chunk: Buffer) => { body += chunk.toString(); }); res.on('end', () => resolve({ status: res.statusCode!, headers: res.headers, body })); }); req.on('error', reject); req.end(); }); } tap.test('should handle HTTP requests on port 80 for ACME challenges', async (tools) => { tools.timeout(10000); // Track HTTP requests that are handled const handledRequests: any[] = []; const settings = { routes: [ { name: 'acme-test-route', match: { ports: [18080], // Use high port to avoid permission issues path: '/.well-known/acme-challenge/*' }, action: { type: 'socket-handler' as const, socketHandler: SocketHandlers.httpServer((req, res) => { handledRequests.push({ path: req.url, method: req.method, headers: req.headers }); // Simulate ACME challenge response const token = req.url?.split('/').pop() || ''; res.header('Content-Type', 'text/plain'); res.send(`challenge-response-for-${token}`); }) } } ] }; const proxy = new SmartProxy(settings); await proxy.start(); // Make an HTTP request to the challenge endpoint const response = await httpRequest('http://localhost:18080/.well-known/acme-challenge/test-token'); // Verify response expect(response.status).toEqual(200); expect(response.body).toEqual('challenge-response-for-test-token'); // Verify request was handled expect(handledRequests.length).toEqual(1); expect(handledRequests[0].path).toEqual('/.well-known/acme-challenge/test-token'); expect(handledRequests[0].method).toEqual('GET'); await proxy.stop(); }); tap.test('should parse HTTP headers correctly', async (tools) => { tools.timeout(10000); const capturedContext: any = {}; const settings = { routes: [ { name: 'header-test-route', match: { ports: [18081] }, action: { type: 'socket-handler' as const, socketHandler: SocketHandlers.httpServer((req, res) => { Object.assign(capturedContext, { path: req.url, method: req.method, headers: req.headers }); res.header('Content-Type', 'application/json'); res.send(JSON.stringify({ received: req.headers })); }) } } ] }; const proxy = new SmartProxy(settings); await proxy.start(); // Make request with custom headers const response = await httpRequest('http://localhost:18081/test', { method: 'POST', headers: { 'X-Custom-Header': 'test-value', 'User-Agent': 'test-agent' } }); expect(response.status).toEqual(200); const body = JSON.parse(response.body); // Verify headers were parsed correctly expect(capturedContext.headers['x-custom-header']).toEqual('test-value'); expect(capturedContext.headers['user-agent']).toEqual('test-agent'); expect(capturedContext.method).toEqual('POST'); expect(capturedContext.path).toEqual('/test'); await proxy.stop(); }); export default tap.start();