import { tap, expect } from '@git.zone/tstest/tapbundle'; import { RouteConnectionHandler } from '../ts/proxies/smart-proxy/route-connection-handler.js'; import type { ISmartProxyOptions } from '../ts/proxies/smart-proxy/models/interfaces.js'; import * as net from 'net'; // Direct test of the fix in RouteConnectionHandler tap.test('should detect and forward non-TLS connections on useHttpProxy ports', async (tapTest) => { // Create mock objects const mockSettings: ISmartProxyOptions = { useHttpProxy: [8080], httpProxyPort: 8844, routes: [{ name: 'test-route', match: { ports: 8080 }, action: { type: 'forward', target: { host: 'localhost', port: 8181 } } }] }; let httpProxyForwardCalled = false; let directConnectionCalled = false; // Create mocks for dependencies const mockHttpProxyBridge = { getHttpProxy: () => ({ available: true }), forwardToHttpProxy: async (...args: any[]) => { console.log('Mock: forwardToHttpProxy called'); httpProxyForwardCalled = true; } }; // Mock connection manager const mockConnectionManager = { createConnection: (socket: any) => ({ id: 'test-connection', localPort: 8080, remoteIP: '127.0.0.1', isTLS: false }), initiateCleanupOnce: () => {}, cleanupConnection: () => {}, getConnectionCount: () => 1, handleError: (type: string, record: any) => { return (error: Error) => { console.log(`Mock: Error handled for ${type}: ${error.message}`); }; } }; // Mock route manager that returns a matching route const mockRouteManager = { findMatchingRoute: (criteria: any) => ({ route: mockSettings.routes[0] }), getAllRoutes: () => mockSettings.routes, getRoutesForPort: (port: number) => mockSettings.routes.filter(r => { const ports = Array.isArray(r.match.ports) ? r.match.ports : [r.match.ports]; return ports.some(p => { if (typeof p === 'number') { return p === port; } else if (p && typeof p === 'object' && 'from' in p && 'to' in p) { return port >= p.from && port <= p.to; } return false; }); }) }; // Mock security manager const mockSecurityManager = { validateIP: () => ({ allowed: true }) }; // Create route connection handler instance const handler = new RouteConnectionHandler( mockSettings, mockConnectionManager as any, mockSecurityManager as any, // security manager {} as any, // tls manager mockHttpProxyBridge as any, {} as any, // timeout manager mockRouteManager as any ); // Override setupDirectConnection to track if it's called handler['setupDirectConnection'] = (...args: any[]) => { console.log('Mock: setupDirectConnection called'); directConnectionCalled = true; }; // Test: Create a mock socket representing non-TLS connection on port 8080 const mockSocket = { localPort: 8080, remoteAddress: '127.0.0.1', on: function(event: string, handler: Function) { return this; }, once: function(event: string, handler: Function) { // Capture the data handler if (event === 'data') { this._dataHandler = handler; } return this; }, end: () => {}, destroy: () => {}, pause: () => {}, resume: () => {}, removeListener: function() { return this; }, emit: () => {}, setNoDelay: () => {}, setKeepAlive: () => {}, _dataHandler: null as any } as any; // Simulate the handler processing the connection handler.handleConnection(mockSocket); // Simulate receiving non-TLS data if (mockSocket._dataHandler) { mockSocket._dataHandler(Buffer.from('GET / HTTP/1.1\r\nHost: test.local\r\n\r\n')); } // Give it a moment to process await new Promise(resolve => setTimeout(resolve, 100)); // Verify that the connection was forwarded to HttpProxy, not direct connection expect(httpProxyForwardCalled).toEqual(true); expect(directConnectionCalled).toEqual(false); }); // Test that verifies TLS connections still work normally tap.test('should handle TLS connections normally', async (tapTest) => { const mockSettings: ISmartProxyOptions = { useHttpProxy: [443], httpProxyPort: 8844, routes: [{ name: 'tls-route', match: { ports: 443 }, action: { type: 'forward', target: { host: 'localhost', port: 8443 }, tls: { mode: 'terminate' } } }] }; let httpProxyForwardCalled = false; const mockHttpProxyBridge = { getHttpProxy: () => ({ available: true }), forwardToHttpProxy: async (...args: any[]) => { httpProxyForwardCalled = true; } }; const mockConnectionManager = { createConnection: (socket: any) => ({ id: 'test-tls-connection', localPort: 443, remoteIP: '127.0.0.1', isTLS: true, tlsHandshakeComplete: false }), initiateCleanupOnce: () => {}, cleanupConnection: () => {}, getConnectionCount: () => 1, handleError: (type: string, record: any) => { return (error: Error) => { console.log(`Mock: Error handled for ${type}: ${error.message}`); }; } }; const mockTlsManager = { isTlsHandshake: (chunk: Buffer) => true, isClientHello: (chunk: Buffer) => true, extractSNI: (chunk: Buffer) => 'test.local' }; const mockRouteManager = { findMatchingRoute: (criteria: any) => ({ route: mockSettings.routes[0] }), getAllRoutes: () => mockSettings.routes, getRoutesForPort: (port: number) => mockSettings.routes.filter(r => { const ports = Array.isArray(r.match.ports) ? r.match.ports : [r.match.ports]; return ports.some(p => { if (typeof p === 'number') { return p === port; } else if (p && typeof p === 'object' && 'from' in p && 'to' in p) { return port >= p.from && port <= p.to; } return false; }); }) }; const mockSecurityManager = { validateIP: () => ({ allowed: true }) }; const handler = new RouteConnectionHandler( mockSettings, mockConnectionManager as any, mockSecurityManager as any, mockTlsManager as any, mockHttpProxyBridge as any, {} as any, mockRouteManager as any ); const mockSocket = { localPort: 443, remoteAddress: '127.0.0.1', on: function(event: string, handler: Function) { return this; }, once: function(event: string, handler: Function) { // Capture the data handler if (event === 'data') { this._dataHandler = handler; } return this; }, end: () => {}, destroy: () => {}, pause: () => {}, resume: () => {}, removeListener: function() { return this; }, emit: () => {}, setNoDelay: () => {}, setKeepAlive: () => {}, _dataHandler: null as any } as any; handler.handleConnection(mockSocket); // Simulate TLS handshake if (mockSocket._dataHandler) { const tlsHandshake = Buffer.from([0x16, 0x03, 0x01, 0x00, 0x05]); mockSocket._dataHandler(tlsHandshake); } await new Promise(resolve => setTimeout(resolve, 100)); // TLS connections with 'terminate' mode should go to HttpProxy expect(httpProxyForwardCalled).toEqual(true); }); export default tap.start();