import { tap, expect } from '@git.zone/tstest/tapbundle'; import { SmartProxy } from '../ts/index.js'; import * as plugins from '../ts/plugins.js'; /** * Test that verifies ACME challenge routes are properly created */ tap.test('should create ACME challenge route with high ports', async (tools) => { tools.timeout(5000); const capturedRoutes: any[] = []; const settings = { routes: [ { name: 'secure-route', match: { ports: [18443], // High port to avoid permission issues domains: 'test.local' }, action: { type: 'forward' as const, target: { host: 'localhost', port: 8080 }, tls: { mode: 'terminate' as const, certificate: 'auto' as const } } } ], acme: { email: 'test@acmetest.local', // Use a non-forbidden domain port: 18080, // High port for ACME challenges useProduction: false // Use staging environment } }; const proxy = new SmartProxy(settings); // Mock certificate manager to avoid ACME account creation (proxy as any).createCertificateManager = async function() { const mockCertManager = { updateRoutesCallback: null as any, setUpdateRoutesCallback: function(cb: any) { this.updateRoutesCallback = cb; // Simulate adding the ACME challenge route immediately const challengeRoute = { name: 'acme-challenge', priority: 1000, match: { ports: 18080, path: '/.well-known/acme-challenge/*' }, action: { type: 'socket-handler', socketHandler: () => {} } }; const updatedRoutes = [...proxy.settings.routes, challengeRoute]; capturedRoutes.push(updatedRoutes); }, setHttpProxy: () => {}, setGlobalAcmeDefaults: () => {}, setAcmeStateManager: () => {}, initialize: async () => {}, provisionAllCertificates: async () => {}, stop: async () => {}, getAcmeOptions: () => settings.acme, getState: () => ({ challengeRouteActive: false }) }; return mockCertManager; }; // Also mock initializeCertificateManager to avoid real initialization (proxy as any).initializeCertificateManager = async function() { this.certManager = await this.createCertificateManager(); }; await proxy.start(); // Check that ACME challenge route was added const finalRoutes = capturedRoutes[capturedRoutes.length - 1]; const challengeRoute = finalRoutes.find((r: any) => r.name === 'acme-challenge'); expect(challengeRoute).toBeDefined(); expect(challengeRoute.match.path).toEqual('/.well-known/acme-challenge/*'); expect(challengeRoute.match.ports).toEqual(18080); expect(challengeRoute.action.type).toEqual('socket-handler'); expect(challengeRoute.priority).toEqual(1000); await proxy.stop(); }); tap.test('should handle HTTP request parsing correctly', async (tools) => { tools.timeout(5000); let handlerCalled = false; let receivedContext: any; let parsedRequest: any = {}; const settings = { routes: [ { name: 'test-static', match: { ports: [18090], path: '/test/*' }, action: { type: 'socket-handler' as const, socketHandler: (socket, context) => { handlerCalled = true; receivedContext = context; // Parse HTTP request from socket socket.once('data', (data) => { const request = data.toString(); const lines = request.split('\r\n'); const [method, path, protocol] = lines[0].split(' '); // Parse headers const headers: any = {}; for (let i = 1; i < lines.length; i++) { if (lines[i] === '') break; const [key, value] = lines[i].split(': '); if (key && value) { headers[key.toLowerCase()] = value; } } // Store parsed request data parsedRequest = { method, path, headers }; // Send HTTP response const response = [ 'HTTP/1.1 200 OK', 'Content-Type: text/plain', 'Content-Length: 2', 'Connection: close', '', 'OK' ].join('\r\n'); socket.write(response); socket.end(); }); } } } ] }; const proxy = new SmartProxy(settings); // Mock NFTables manager (proxy as any).nftablesManager = { ensureNFTablesSetup: async () => {}, stop: async () => {} }; await proxy.start(); // Create a simple HTTP request const client = new plugins.net.Socket(); await new Promise((resolve, reject) => { client.connect(18090, 'localhost', () => { // Send HTTP request const request = [ 'GET /test/example HTTP/1.1', 'Host: localhost:18090', 'User-Agent: test-client', '', '' ].join('\r\n'); client.write(request); // Wait for response client.on('data', (data) => { const response = data.toString(); expect(response).toContain('HTTP/1.1 200'); expect(response).toContain('OK'); client.end(); resolve(); }); }); client.on('error', reject); }); // Verify handler was called expect(handlerCalled).toBeTrue(); expect(receivedContext).toBeDefined(); // The context passed to socket handlers is IRouteContext, not HTTP request data expect(receivedContext.port).toEqual(18090); expect(receivedContext.routeName).toEqual('test-static'); // Verify the parsed HTTP request data expect(parsedRequest.path).toEqual('/test/example'); expect(parsedRequest.method).toEqual('GET'); expect(parsedRequest.headers.host).toEqual('localhost:18090'); await proxy.stop(); }); tap.start();