import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import { SmartProxy } from '../ts/proxies/smart-proxy/index.js'; import { createPortMappingRoute, createOffsetPortMappingRoute, createDynamicRoute, createSmartLoadBalancer, createPortOffset } from '../ts/proxies/smart-proxy/utils/route-helpers.js'; import type { IRouteConfig, IRouteContext } from '../ts/proxies/smart-proxy/models/route-types.js'; // Test server and client utilities let testServers: Array<{ server: net.Server; port: number }> = []; let smartProxy: SmartProxy; const TEST_PORT_START = 4000; const PROXY_PORT_START = 5000; const TEST_DATA = 'Hello through dynamic port mapper!'; // Cleanup function to close all servers and proxies function cleanup() { return Promise.all([ ...testServers.map(({ server }) => new Promise(resolve => { server.close(() => resolve()); })), smartProxy ? smartProxy.stop() : Promise.resolve() ]); } // Helper: Creates a test TCP server that listens on a given port function createTestServer(port: number): Promise { return new Promise((resolve) => { const server = net.createServer((socket) => { socket.on('data', (data) => { // Echo the received data back with a server identifier socket.write(`Server ${port} says: ${data.toString()}`); }); socket.on('error', (error) => { console.error(`[Test Server] Socket error on port ${port}:`, error); }); }); server.listen(port, () => { console.log(`[Test Server] Listening on port ${port}`); testServers.push({ server, port }); resolve(server); }); }); } // Helper: Creates a test client connection with timeout function createTestClient(port: number, data: string): Promise { return new Promise((resolve, reject) => { const client = new net.Socket(); let response = ''; const timeout = setTimeout(() => { client.destroy(); reject(new Error(`Client connection timeout to port ${port}`)); }, 5000); client.connect(port, 'localhost', () => { console.log(`[Test Client] Connected to server on port ${port}`); client.write(data); }); client.on('data', (chunk) => { response += chunk.toString(); client.end(); }); client.on('end', () => { clearTimeout(timeout); resolve(response); }); client.on('error', (error) => { clearTimeout(timeout); reject(error); }); }); } // Set up test environment tap.test('setup port mapping test environment', async () => { // Create multiple test servers on different ports await Promise.all([ createTestServer(TEST_PORT_START), // Server on port 4000 createTestServer(TEST_PORT_START + 1), // Server on port 4001 createTestServer(TEST_PORT_START + 2), // Server on port 4002 ]); // Create a SmartProxy with dynamic port mapping routes smartProxy = new SmartProxy({ routes: [ // Simple function that returns the same port (identity mapping) createPortMappingRoute({ sourcePortRange: PROXY_PORT_START, targetHost: 'localhost', portMapper: (context) => TEST_PORT_START, name: 'Identity Port Mapping' }), // Offset port mapping from 5001 to 4001 (offset -1000) createOffsetPortMappingRoute({ ports: PROXY_PORT_START + 1, targetHost: 'localhost', offset: -1000, name: 'Offset Port Mapping (-1000)' }), // Dynamic route with conditional port mapping createDynamicRoute({ ports: [PROXY_PORT_START + 2, PROXY_PORT_START + 3], targetHost: (context) => { // Dynamic host selection based on port return context.port === PROXY_PORT_START + 2 ? 'localhost' : '127.0.0.1'; }, portMapper: (context) => { // Port mapping logic based on incoming port if (context.port === PROXY_PORT_START + 2) { return TEST_PORT_START; } else { return TEST_PORT_START + 2; } }, name: 'Dynamic Host and Port Mapping' }), // Smart load balancer for domain-based routing createSmartLoadBalancer({ ports: PROXY_PORT_START + 4, domainTargets: { 'test1.example.com': 'localhost', 'test2.example.com': '127.0.0.1' }, portMapper: (context) => { // Use different backend ports based on domain if (context.domain === 'test1.example.com') { return TEST_PORT_START; } else { return TEST_PORT_START + 1; } }, defaultTarget: 'localhost', name: 'Smart Domain Load Balancer' }) ] }); // Start the SmartProxy await smartProxy.start(); }); // Test 1: Simple identity port mapping (5000 -> 4000) tap.test('should map port using identity function', async () => { const response = await createTestClient(PROXY_PORT_START, TEST_DATA); expect(response).toEqual(`Server ${TEST_PORT_START} says: ${TEST_DATA}`); }); // Test 2: Offset port mapping (5001 -> 4001) tap.test('should map port using offset function', async () => { const response = await createTestClient(PROXY_PORT_START + 1, TEST_DATA); expect(response).toEqual(`Server ${TEST_PORT_START + 1} says: ${TEST_DATA}`); }); // Test 3: Dynamic port and host mapping (conditional logic) tap.test('should map port using dynamic logic', async () => { const response = await createTestClient(PROXY_PORT_START + 2, TEST_DATA); expect(response).toEqual(`Server ${TEST_PORT_START} says: ${TEST_DATA}`); }); // Test 4: Test reuse of createPortOffset helper tap.test('should use createPortOffset helper for port mapping', async () => { // Test the createPortOffset helper const offsetFn = createPortOffset(-1000); const context = { port: PROXY_PORT_START + 1, clientIp: '127.0.0.1', serverIp: '127.0.0.1', isTls: false, timestamp: Date.now(), connectionId: 'test-connection' } as IRouteContext; const mappedPort = offsetFn(context); expect(mappedPort).toEqual(TEST_PORT_START + 1); }); // Test 5: Test error handling for invalid port mapping functions tap.test('should handle errors in port mapping functions', async () => { // Create a route with a function that throws an error const errorRoute: IRouteConfig = { match: { ports: PROXY_PORT_START + 5 }, action: { type: 'forward', target: { host: 'localhost', port: () => { throw new Error('Test error in port mapping function'); } } }, name: 'Error Route' }; // Add the route to SmartProxy await smartProxy.updateRoutes([...smartProxy.settings.routes, errorRoute]); // The connection should fail or timeout try { await createTestClient(PROXY_PORT_START + 5, TEST_DATA); // Connection should not succeed expect(false).toBeTrue(); } catch (error) { // Connection failed as expected expect(true).toBeTrue(); } }); // Cleanup tap.test('cleanup port mapping test environment', async () => { await cleanup(); }); export default tap.start();