import { expect, tap } from '@push.rocks/tapbundle'; import * as net from 'net'; import { PortProxy } from '../ts/classes.portproxy.js'; let testServer: net.Server; let portProxy: PortProxy; const TEST_SERVER_PORT = 4000; const PROXY_PORT = 4001; const TEST_DATA = 'Hello through port proxy!'; // Helper function to create a test TCP server function createTestServer(port: number): Promise { return new Promise((resolve) => { const server = net.createServer((socket) => { socket.on('data', (data) => { // Echo the received data back socket.write(`Echo: ${data.toString()}`); }); socket.on('error', (error) => { console.error('[Test Server] Socket error:', error); }); }); server.listen(port, () => { console.log(`[Test Server] Listening on port ${port}`); resolve(server); }); }); } // Helper function to create a test client connection function createTestClient(port: number, data: string): Promise { return new Promise((resolve, reject) => { const client = new net.Socket(); let response = ''; client.connect(port, 'localhost', () => { console.log('[Test Client] Connected to server'); client.write(data); }); client.on('data', (chunk) => { response += chunk.toString(); client.end(); }); client.on('end', () => { resolve(response); }); client.on('error', (error) => { reject(error); }); }); } // Setup test environment tap.test('setup port proxy test environment', async () => { testServer = await createTestServer(TEST_SERVER_PORT); portProxy = new PortProxy({ fromPort: PROXY_PORT, toPort: TEST_SERVER_PORT, targetIP: 'localhost', domains: [], sniEnabled: false, defaultAllowedIPs: ['127.0.0.1'], globalPortRanges: [] }); }); tap.test('should start port proxy', async () => { await portProxy.start(); // Since netServers is private, we cast to any to verify that all created servers are listening. expect((portProxy as any).netServers.every((server: net.Server) => server.listening)).toBeTrue(); }); tap.test('should forward TCP connections and data to localhost', async () => { const response = await createTestClient(PROXY_PORT, TEST_DATA); expect(response).toEqual(`Echo: ${TEST_DATA}`); }); tap.test('should forward TCP connections to custom host', async () => { // Create a new proxy instance with a custom host (targetIP) const customHostProxy = new PortProxy({ fromPort: PROXY_PORT + 1, toPort: TEST_SERVER_PORT, targetIP: '127.0.0.1', domains: [], sniEnabled: false, defaultAllowedIPs: ['127.0.0.1'], globalPortRanges: [] }); await customHostProxy.start(); const response = await createTestClient(PROXY_PORT + 1, TEST_DATA); expect(response).toEqual(`Echo: ${TEST_DATA}`); await customHostProxy.stop(); }); tap.test('should forward connections based on domain-specific target IP', async () => { // Create a second test server on a different port const TEST_SERVER_PORT_2 = TEST_SERVER_PORT + 100; const testServer2 = await createTestServer(TEST_SERVER_PORT_2); // Create a proxy with domain-specific target IPs const domainProxy = new PortProxy({ fromPort: PROXY_PORT + 2, toPort: TEST_SERVER_PORT, // default port (for non-port-range handling) targetIP: 'localhost', // default target IP domains: [{ domain: 'domain1.test', allowedIPs: ['127.0.0.1'], targetIP: '127.0.0.1' }, { domain: 'domain2.test', allowedIPs: ['127.0.0.1'], targetIP: 'localhost' }], sniEnabled: false, defaultAllowedIPs: ['127.0.0.1'], globalPortRanges: [] }); await domainProxy.start(); // Test default connection (should use default targetIP) const response1 = await createTestClient(PROXY_PORT + 2, TEST_DATA); expect(response1).toEqual(`Echo: ${TEST_DATA}`); // Create another proxy with a different default targetIP const domainProxy2 = new PortProxy({ fromPort: PROXY_PORT + 3, toPort: TEST_SERVER_PORT, targetIP: '127.0.0.1', domains: [], sniEnabled: false, defaultAllowedIPs: ['127.0.0.1'], globalPortRanges: [] }); await domainProxy2.start(); const response2 = await createTestClient(PROXY_PORT + 3, TEST_DATA); expect(response2).toEqual(`Echo: ${TEST_DATA}`); await domainProxy.stop(); await domainProxy2.stop(); await new Promise((resolve) => testServer2.close(() => resolve())); }); tap.test('should handle multiple concurrent connections', async () => { const concurrentRequests = 5; const requests = Array(concurrentRequests).fill(null).map((_, i) => createTestClient(PROXY_PORT, `${TEST_DATA} ${i + 1}`) ); const responses = await Promise.all(requests); responses.forEach((response, i) => { expect(response).toEqual(`Echo: ${TEST_DATA} ${i + 1}`); }); }); tap.test('should handle connection timeouts', async () => { const client = new net.Socket(); await new Promise((resolve) => { client.connect(PROXY_PORT, 'localhost', () => { // Don't send any data, just wait for timeout client.on('close', () => { resolve(); }); }); }); }); tap.test('should stop port proxy', async () => { await portProxy.stop(); expect((portProxy as any).netServers.every((server: net.Server) => !server.listening)).toBeTrue(); }); // Cleanup chained proxies tests tap.test('should support optional source IP preservation in chained proxies', async () => { // Test 1: Without IP preservation (default behavior) const firstProxyDefault = new PortProxy({ fromPort: PROXY_PORT + 4, toPort: PROXY_PORT + 5, targetIP: 'localhost', domains: [], sniEnabled: false, defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'], globalPortRanges: [] }); const secondProxyDefault = new PortProxy({ fromPort: PROXY_PORT + 5, toPort: TEST_SERVER_PORT, targetIP: 'localhost', domains: [], sniEnabled: false, defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'], globalPortRanges: [] }); await secondProxyDefault.start(); await firstProxyDefault.start(); // This should work because we explicitly allow both IPv4 and IPv6 formats const response1 = await createTestClient(PROXY_PORT + 4, TEST_DATA); expect(response1).toEqual(`Echo: ${TEST_DATA}`); await firstProxyDefault.stop(); await secondProxyDefault.stop(); // Test 2: With IP preservation const firstProxyPreserved = new PortProxy({ fromPort: PROXY_PORT + 6, toPort: PROXY_PORT + 7, targetIP: 'localhost', domains: [], sniEnabled: false, defaultAllowedIPs: ['127.0.0.1'], preserveSourceIP: true, globalPortRanges: [] }); const secondProxyPreserved = new PortProxy({ fromPort: PROXY_PORT + 7, toPort: TEST_SERVER_PORT, targetIP: 'localhost', domains: [], sniEnabled: false, defaultAllowedIPs: ['127.0.0.1'], preserveSourceIP: true, globalPortRanges: [] }); await secondProxyPreserved.start(); await firstProxyPreserved.start(); // This should work with just IPv4 because source IP is preserved const response2 = await createTestClient(PROXY_PORT + 6, TEST_DATA); expect(response2).toEqual(`Echo: ${TEST_DATA}`); await firstProxyPreserved.stop(); await secondProxyPreserved.stop(); }); tap.test('cleanup port proxy test environment', async () => { await new Promise((resolve) => testServer.close(() => resolve())); }); process.on('exit', () => { if (testServer) { testServer.close(); } // Use a cast to access the private property for cleanup. if (portProxy && (portProxy as any).netServers) { portProxy.stop(); } }); export default tap.start();