import { tap, expect } from '@git.zone/tstest/tapbundle'; import { SmartProxy } from '../ts/index.js'; import * as plugins from '../ts/plugins.js'; import * as net from 'net'; import * as http from 'http'; /** * This test verifies our improved port binding intelligence for ACME challenges. * It specifically tests: * 1. Using port 8080 instead of 80 for ACME HTTP challenges * 2. Correctly handling shared port bindings between regular routes and challenge routes * 3. Avoiding port conflicts when updating routes */ tap.test('should handle ACME challenges on port 8080 with improved port binding intelligence', async (tapTest) => { // Create a simple echo server to act as our target const targetPort = 9001; let receivedData = ''; const targetServer = net.createServer((socket) => { console.log('Target server received connection'); socket.on('data', (data) => { receivedData += data.toString(); console.log('Target server received data:', data.toString().split('\n')[0]); // Send a simple HTTP response const response = 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!'; socket.write(response); }); }); await new Promise((resolve) => { targetServer.listen(targetPort, () => { console.log(`Target server listening on port ${targetPort}`); resolve(); }); }); // In this test we will NOT create a mock ACME server on the same port // as SmartProxy will use, instead we'll let SmartProxy handle it const acmeServerPort = 9009; const acmeRequests: string[] = []; let acmeServer: http.Server | null = null; // We'll assume the ACME port is available for SmartProxy let acmePortAvailable = true; // Create SmartProxy with ACME configured to use port 8080 console.log('Creating SmartProxy with ACME port 8080...'); const tempCertDir = './temp-certs'; try { await plugins.smartfile.SmartFile.createDirectory(tempCertDir); } catch (error) { // Directory may already exist, that's ok } const proxy = new SmartProxy({ enableDetailedLogging: true, routes: [ { name: 'test-route', match: { ports: [9003], domains: ['test.example.com'] }, action: { type: 'forward', target: { host: 'localhost', port: targetPort }, tls: { mode: 'terminate', certificate: 'auto' // Use ACME for certificate } } }, // Also add a route for port 8080 to test port sharing { name: 'http-route', match: { ports: [9009], domains: ['test.example.com'] }, action: { type: 'forward', target: { host: 'localhost', port: targetPort } } } ], acme: { email: 'test@example.com', useProduction: false, port: 9009, // Use 9009 instead of default 80 certificateStore: tempCertDir } }); // Mock the certificate manager to avoid actual ACME operations console.log('Mocking certificate manager...'); const createCertManager = (proxy as any).createCertificateManager; (proxy as any).createCertificateManager = async function(...args: any[]) { // Create a completely mocked certificate manager that doesn't use ACME at all return { initialize: async () => {}, getCertPair: async () => { return { publicKey: 'MOCK CERTIFICATE', privateKey: 'MOCK PRIVATE KEY' }; }, getAcmeOptions: () => { return { port: 9009 }; }, getState: () => { return { initializing: false, ready: true, port: 9009 }; }, provisionAllCertificates: async () => { console.log('Mock: Provisioning certificates'); return []; }, stop: async () => {}, smartAcme: { getCertificateForDomain: async () => { // Return a mock certificate return { publicKey: 'MOCK CERTIFICATE', privateKey: 'MOCK PRIVATE KEY', validUntil: Date.now() + 90 * 24 * 60 * 60 * 1000, created: Date.now() }; }, start: async () => {}, stop: async () => {} } }; }; // Track port binding attempts to verify intelligence const portBindAttempts: number[] = []; const originalAddPort = (proxy as any).portManager.addPort; (proxy as any).portManager.addPort = async function(port: number) { portBindAttempts.push(port); return originalAddPort.call(this, port); }; try { console.log('Starting SmartProxy...'); await proxy.start(); console.log('Port binding attempts:', portBindAttempts); // Check that we tried to bind to port 9009 expect(portBindAttempts.includes(9009)).toEqual(true, 'Should attempt to bind to port 9009'); expect(portBindAttempts.includes(9003)).toEqual(true, 'Should attempt to bind to port 9003'); // Get actual bound ports const boundPorts = proxy.getListeningPorts(); console.log('Actually bound ports:', boundPorts); // If port 9009 was available, we should be bound to it if (acmePortAvailable) { expect(boundPorts.includes(9009)).toEqual(true, 'Should be bound to port 9009 if available'); } expect(boundPorts.includes(9003)).toEqual(true, 'Should be bound to port 9003'); // Test adding a new route on port 8080 console.log('Testing route update with port reuse...'); // Reset tracking portBindAttempts.length = 0; // Add a new route on port 8080 const newRoutes = [ ...proxy.settings.routes, { name: 'additional-route', match: { ports: [9009], path: '/additional' }, action: { type: 'forward', target: { host: 'localhost', port: targetPort } } } ]; // Update routes - this should NOT try to rebind port 8080 await proxy.updateRoutes(newRoutes); console.log('Port binding attempts after update:', portBindAttempts); // We should not try to rebind port 9009 since it's already bound expect(portBindAttempts.includes(9009)).toEqual(false, 'Should not attempt to rebind port 9009'); // We should still be listening on both ports const portsAfterUpdate = proxy.getListeningPorts(); console.log('Bound ports after update:', portsAfterUpdate); if (acmePortAvailable) { expect(portsAfterUpdate.includes(9009)).toEqual(true, 'Should still be bound to port 9009'); } expect(portsAfterUpdate.includes(9003)).toEqual(true, 'Should still be bound to port 9003'); // The test is successful at this point - we've verified the port binding intelligence console.log('Port binding intelligence verified successfully!'); // We'll skip the actual connection test to avoid timeouts } finally { // Clean up console.log('Cleaning up...'); await proxy.stop(); if (targetServer) { await new Promise((resolve) => { targetServer.close(() => resolve()); }); } // No acmeServer to close in this test // Clean up temp directory try { // Try different removal methods if (typeof plugins.smartfile.fs.removeManySync === 'function') { plugins.smartfile.fs.removeManySync([tempCertDir]); } else if (typeof plugins.smartfile.fs.removeDirectory === 'function') { await plugins.smartfile.fs.removeDirectory(tempCertDir); } else if (typeof plugins.smartfile.removeDirectory === 'function') { await plugins.smartfile.removeDirectory(tempCertDir); } else { console.log('Unable to find appropriate directory removal method'); } } catch (error) { console.error('Failed to remove temp directory:', error); } } }); tap.start();