From b9be6533aed716ec1995f2f84fe2a8946b09de1e Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Fri, 6 Jun 2025 07:40:59 +0000 Subject: [PATCH] start fixing tests --- certs/static-route/meta.json | 6 +- readme.hints.md | 49 +++- readme.plan.md | 19 +- test/core/routing/test.path-matcher.ts | 7 +- test/core/utils/test.route-utils.ts | 110 -------- test/test.http-fix-verification.ts | 4 +- test/test.http-forwarding-fix.ts | 26 +- test/test.http-port8080-forwarding.ts | 45 ++- test/test.httpproxy.function-targets.ts | 32 +-- test/test.router.ts | 195 ++++++------- ts/core/models/index.ts | 1 + ts/core/models/socket-types.ts | 21 ++ ts/core/models/wrapped-socket.ts | 258 ++++-------------- ts/proxies/smart-proxy/http-proxy-bridge.ts | 8 +- .../smart-proxy/route-connection-handler.ts | 26 +- 15 files changed, 330 insertions(+), 477 deletions(-) delete mode 100644 test/core/utils/test.route-utils.ts create mode 100644 ts/core/models/socket-types.ts diff --git a/certs/static-route/meta.json b/certs/static-route/meta.json index 2670325..9335cea 100644 --- a/certs/static-route/meta.json +++ b/certs/static-route/meta.json @@ -1,5 +1,5 @@ { - "expiryDate": "2025-09-01T06:26:42.172Z", - "issueDate": "2025-06-03T06:26:42.172Z", - "savedAt": "2025-06-03T06:26:42.172Z" + "expiryDate": "2025-09-03T17:57:28.583Z", + "issueDate": "2025-06-05T17:57:28.583Z", + "savedAt": "2025-06-05T17:57:28.583Z" } \ No newline at end of file diff --git a/readme.hints.md b/readme.hints.md index 5603ed0..1f34552 100644 --- a/readme.hints.md +++ b/readme.hints.md @@ -802,4 +802,51 @@ class ProxyProtocolSocket { 📋 **Consider full WrappedSocket for future major version** - Cleaner architecture - Better maintainability -- But requires significant refactoring \ No newline at end of file +- But requires significant refactoring + +## WrappedSocket Implementation (PROXY Protocol Phase 1) - v19.5.19+ + +The WrappedSocket class has been implemented as the foundation for PROXY protocol support: + +### Implementation Details + +1. **Design Approach**: Uses JavaScript Proxy to delegate all Socket methods/properties to the underlying socket while allowing override of specific properties (remoteAddress, remotePort). + +2. **Key Design Decisions**: + - NOT a Duplex stream - Initially tried this approach but it created infinite loops + - Simple wrapper using Proxy pattern for transparent delegation + - All sockets are wrapped, not just those from trusted proxies + - Trusted proxy detection happens after wrapping + +3. **Usage Pattern**: + ```typescript + // In RouteConnectionHandler.handleConnection() + const wrappedSocket = new WrappedSocket(socket); + // Pass wrappedSocket throughout the flow + + // When calling socket-utils functions, extract underlying socket: + const underlyingSocket = getUnderlyingSocket(socket); + setupBidirectionalForwarding(underlyingSocket, targetSocket, {...}); + ``` + +4. **Important Implementation Notes**: + - Socket utility functions (setupBidirectionalForwarding, cleanupSocket) expect raw net.Socket + - Always extract underlying socket before passing to these utilities using `getUnderlyingSocket()` + - WrappedSocket preserves all Socket functionality through Proxy delegation + - TypeScript typing handled via index signature: `[key: string]: any` + +5. **Files Modified**: + - `ts/core/models/wrapped-socket.ts` - The WrappedSocket implementation + - `ts/core/models/socket-types.ts` - Helper functions and type guards + - `ts/proxies/smart-proxy/route-connection-handler.ts` - Updated to wrap all incoming sockets + - `ts/proxies/smart-proxy/connection-manager.ts` - Updated to accept WrappedSocket + - `ts/proxies/smart-proxy/http-proxy-bridge.ts` - Updated to handle WrappedSocket + +6. **Test Coverage**: + - `test/test.wrapped-socket-forwarding.ts` - Verifies data forwarding through wrapped sockets + +### Next Steps for PROXY Protocol +- Phase 2: Parse PROXY protocol header from trusted proxies +- Phase 3: Update real client IP/port after parsing +- Phase 4: Test with HAProxy and AWS ELB +- Phase 5: Documentation and configuration \ No newline at end of file diff --git a/readme.plan.md b/readme.plan.md index 869863e..cdb9852 100644 --- a/readme.plan.md +++ b/readme.plan.md @@ -70,14 +70,14 @@ interface IRouteAction { #### IMPORTANT: Phase 1 Must Be Completed First The `ProxyProtocolSocket` (WrappedSocket) is the foundation for all PROXY protocol functionality. This wrapper class must be implemented and integrated BEFORE any PROXY protocol parsing can begin. -#### Phase 1: ProxyProtocolSocket (WrappedSocket) Foundation - ✅ COMPLETED +#### Phase 1: ProxyProtocolSocket (WrappedSocket) Foundation - ✅ COMPLETED (v19.5.19) This phase creates the socket wrapper infrastructure that all subsequent phases depend on. 1. **Create WrappedSocket class** in `ts/core/models/wrapped-socket.ts` ✅ - - Basic socket wrapper that extends EventEmitter + - Used JavaScript Proxy pattern instead of EventEmitter (avoids infinite loops) - Properties for real client IP and port - Transparent getters that return real or socket IP/port - - Pass-through methods for all socket operations + - All socket methods/properties delegated via Proxy 2. **Implement core wrapper functionality** ✅ - Constructor accepts regular socket + optional metadata @@ -88,14 +88,13 @@ This phase creates the socket wrapper infrastructure that all subsequent phases 3. **Update ConnectionManager to handle wrapped sockets** ✅ - Accept either `net.Socket` or `WrappedSocket` - - Use duck typing or instanceof checks - - Ensure all IP lookups use the getter methods + - Created `getUnderlyingSocket()` helper for socket utilities + - All socket utility functions extract underlying socket -4. **Create comprehensive tests** ✅ - - Test wrapper with and without proxy info - - Verify getter fallback behavior - - Test event forwarding - - Test socket method pass-through +4. **Integration completed** ✅ + - All incoming sockets wrapped in RouteConnectionHandler + - Socket forwarding verified working with wrapped sockets + - Type safety maintained with index signature **Deliverables**: ✅ Working WrappedSocket that can wrap any socket and provide transparent access to client info. diff --git a/test/core/routing/test.path-matcher.ts b/test/core/routing/test.path-matcher.ts index 34db367..a1a9ad9 100644 --- a/test/core/routing/test.path-matcher.ts +++ b/test/core/routing/test.path-matcher.ts @@ -94,12 +94,13 @@ tap.test('PathMatcher - findAllMatches', async () => { const matches = PathMatcher.findAllMatches(patterns, '/api/users/123/profile'); - // All patterns should match (including /api/users as a prefix match) - expect(matches).toHaveLength(5); + // With the stricter path matching, /api/users won't match /api/users/123/profile + // Only patterns with wildcards, parameters, or exact matches will work + expect(matches).toHaveLength(4); // Verify all expected patterns are in the results const matchedPatterns = matches.map(m => m.pattern); - expect(matchedPatterns).toContain('/api/users'); + expect(matchedPatterns).not.toContain('/api/users'); // This won't match anymore (no prefix matching) expect(matchedPatterns).toContain('/api/users/:id'); expect(matchedPatterns).toContain('/api/users/:id/profile'); expect(matchedPatterns).toContain('/api/*'); diff --git a/test/core/utils/test.route-utils.ts b/test/core/utils/test.route-utils.ts deleted file mode 100644 index 03cbf94..0000000 --- a/test/core/utils/test.route-utils.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as routeUtils from '../../../ts/core/utils/route-utils.js'; - -// Test domain matching -tap.test('Route Utils - Domain Matching - exact domains', async () => { - expect(routeUtils.matchDomain('example.com', 'example.com')).toEqual(true); -}); - -tap.test('Route Utils - Domain Matching - wildcard domains', async () => { - expect(routeUtils.matchDomain('*.example.com', 'sub.example.com')).toEqual(true); - expect(routeUtils.matchDomain('*.example.com', 'another.sub.example.com')).toEqual(true); - expect(routeUtils.matchDomain('*.example.com', 'example.com')).toEqual(false); -}); - -tap.test('Route Utils - Domain Matching - case insensitivity', async () => { - expect(routeUtils.matchDomain('example.com', 'EXAMPLE.com')).toEqual(true); -}); - -tap.test('Route Utils - Domain Matching - multiple domain patterns', async () => { - expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'example.com')).toEqual(true); - expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'sub.test.com')).toEqual(true); - expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'something.else')).toEqual(false); -}); - -// Test path matching -tap.test('Route Utils - Path Matching - exact paths', async () => { - expect(routeUtils.matchPath('/api/users', '/api/users')).toEqual(true); -}); - -tap.test('Route Utils - Path Matching - wildcard paths', async () => { - expect(routeUtils.matchPath('/api/*', '/api/users')).toEqual(true); - expect(routeUtils.matchPath('/api/*', '/api/products')).toEqual(true); - expect(routeUtils.matchPath('/api/*', '/something/else')).toEqual(false); -}); - -tap.test('Route Utils - Path Matching - complex wildcard patterns', async () => { - expect(routeUtils.matchPath('/api/*/details', '/api/users/details')).toEqual(true); - expect(routeUtils.matchPath('/api/*/details', '/api/products/details')).toEqual(true); - expect(routeUtils.matchPath('/api/*/details', '/api/users/other')).toEqual(false); -}); - -// Test IP matching -tap.test('Route Utils - IP Matching - exact IPs', async () => { - expect(routeUtils.matchIpPattern('192.168.1.1', '192.168.1.1')).toEqual(true); -}); - -tap.test('Route Utils - IP Matching - wildcard IPs', async () => { - expect(routeUtils.matchIpPattern('192.168.1.*', '192.168.1.100')).toEqual(true); - expect(routeUtils.matchIpPattern('192.168.1.*', '192.168.2.1')).toEqual(false); -}); - -tap.test('Route Utils - IP Matching - CIDR notation', async () => { - expect(routeUtils.matchIpPattern('192.168.1.0/24', '192.168.1.100')).toEqual(true); - expect(routeUtils.matchIpPattern('192.168.1.0/24', '192.168.2.1')).toEqual(false); -}); - -tap.test('Route Utils - IP Matching - IPv6-mapped IPv4 addresses', async () => { - expect(routeUtils.matchIpPattern('192.168.1.1', '::ffff:192.168.1.1')).toEqual(true); -}); - -tap.test('Route Utils - IP Matching - IP authorization with allow/block lists', async () => { - // With allow and block lists - expect(routeUtils.isIpAuthorized('192.168.1.1', ['192.168.1.*'], ['192.168.1.5'])).toEqual(true); - expect(routeUtils.isIpAuthorized('192.168.1.5', ['192.168.1.*'], ['192.168.1.5'])).toEqual(false); - - // With only allow list - expect(routeUtils.isIpAuthorized('192.168.1.1', ['192.168.1.*'])).toEqual(true); - expect(routeUtils.isIpAuthorized('192.168.2.1', ['192.168.1.*'])).toEqual(false); - - // With only block list - expect(routeUtils.isIpAuthorized('192.168.1.5', undefined, ['192.168.1.5'])).toEqual(false); - expect(routeUtils.isIpAuthorized('192.168.1.1', undefined, ['192.168.1.5'])).toEqual(true); - - // With wildcard in allow list - expect(routeUtils.isIpAuthorized('192.168.1.1', ['*'], ['192.168.1.5'])).toEqual(true); -}); - -// Test route specificity calculation -tap.test('Route Utils - Route Specificity - calculating correctly', async () => { - const basicRoute = { domains: 'example.com' }; - const pathRoute = { domains: 'example.com', path: '/api' }; - const wildcardPathRoute = { domains: 'example.com', path: '/api/*' }; - const headerRoute = { domains: 'example.com', headers: { 'content-type': 'application/json' } }; - const complexRoute = { - domains: 'example.com', - path: '/api', - headers: { 'content-type': 'application/json' }, - clientIp: ['192.168.1.1'] - }; - - // Path routes should have higher specificity than domain-only routes - expect(routeUtils.calculateRouteSpecificity(pathRoute) > - routeUtils.calculateRouteSpecificity(basicRoute)).toEqual(true); - - // Exact path routes should have higher specificity than wildcard path routes - expect(routeUtils.calculateRouteSpecificity(pathRoute) > - routeUtils.calculateRouteSpecificity(wildcardPathRoute)).toEqual(true); - - // Routes with headers should have higher specificity than routes without - expect(routeUtils.calculateRouteSpecificity(headerRoute) > - routeUtils.calculateRouteSpecificity(basicRoute)).toEqual(true); - - // Complex routes should have the highest specificity - expect(routeUtils.calculateRouteSpecificity(complexRoute) > - routeUtils.calculateRouteSpecificity(pathRoute)).toEqual(true); - expect(routeUtils.calculateRouteSpecificity(complexRoute) > - routeUtils.calculateRouteSpecificity(headerRoute)).toEqual(true); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.http-fix-verification.ts b/test/test.http-fix-verification.ts index 65dd7ef..0440b10 100644 --- a/test/test.http-fix-verification.ts +++ b/test/test.http-fix-verification.ts @@ -54,7 +54,7 @@ tap.test('should detect and forward non-TLS connections on useHttpProxy ports', findMatchingRoute: (criteria: any) => ({ route: mockSettings.routes[0] }), - getAllRoutes: () => mockSettings.routes, + getRoutes: () => 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 => { @@ -182,7 +182,7 @@ tap.test('should handle TLS connections normally', async (tapTest) => { findMatchingRoute: (criteria: any) => ({ route: mockSettings.routes[0] }), - getAllRoutes: () => mockSettings.routes, + getRoutes: () => 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 => { diff --git a/test/test.http-forwarding-fix.ts b/test/test.http-forwarding-fix.ts index 1c024df..5da6b50 100644 --- a/test/test.http-forwarding-fix.ts +++ b/test/test.http-forwarding-fix.ts @@ -34,6 +34,7 @@ tap.test('should detect and forward non-TLS connections on HttpProxy ports', asy }; proxy['httpProxyBridge'].stop = async () => { console.log('Mock: HttpProxyBridge stopped'); + return Promise.resolve(); // Ensure it returns a resolved promise }; await proxy.start(); @@ -44,11 +45,14 @@ tap.test('should detect and forward non-TLS connections on HttpProxy ports', asy forwardedToHttpProxy = true; connectionPath = 'httpproxy'; console.log('Mock: Connection forwarded to HttpProxy with args:', args[0], 'on port:', args[2]?.localPort); - // Just close the connection for the test - args[1].end(); // socket.end() + // Properly close the connection for the test + const socket = args[1]; + socket.end(); + socket.destroy(); }; - // No need to mock getHttpProxy - the bridge already handles HttpProxy availability + // Mock getHttpProxy to indicate HttpProxy is available + (proxy as any).httpProxyBridge.getHttpProxy = () => ({ available: true }); // Make a connection to port 8080 const client = new net.Socket(); @@ -73,13 +77,16 @@ tap.test('should detect and forward non-TLS connections on HttpProxy ports', asy expect(connectionPath).toEqual('httpproxy'); client.destroy(); + + // Restore original method before stopping + (proxy as any).httpProxyBridge.forwardToHttpProxy = originalForward; + + console.log('About to stop proxy...'); await proxy.stop(); + console.log('Proxy stopped'); // Wait a bit to ensure port is released await new Promise(resolve => setTimeout(resolve, 100)); - - // Restore original method - (proxy as any).httpProxyBridge.forwardToHttpProxy = originalForward; }); // Test that verifies the fix detects non-TLS connections @@ -123,8 +130,10 @@ tap.test('should properly detect non-TLS connections on HttpProxy ports', async proxy['httpProxyBridge'].forwardToHttpProxy = async function(...args: any[]) { httpProxyForwardCalled = true; console.log('HttpProxy forward called with connectionId:', args[0]); - // Just end the connection - args[1].end(); + // Properly close the connection + const socket = args[1]; + socket.end(); + socket.destroy(); }; // Mock HttpProxyBridge methods @@ -136,6 +145,7 @@ tap.test('should properly detect non-TLS connections on HttpProxy ports', async }; proxy['httpProxyBridge'].stop = async () => { console.log('Mock: HttpProxyBridge stopped'); + return Promise.resolve(); // Ensure it returns a resolved promise }; // Mock getHttpProxy to return a truthy value diff --git a/test/test.http-port8080-forwarding.ts b/test/test.http-port8080-forwarding.ts index b0bd981..7e9da39 100644 --- a/test/test.http-port8080-forwarding.ts +++ b/test/test.http-port8080-forwarding.ts @@ -63,9 +63,21 @@ tap.test('should forward HTTP connections on port 8080', async (tapTest) => { } }; + console.log('Making HTTP request to proxy...'); const response = await new Promise((resolve, reject) => { - const req = http.request(options, (res) => resolve(res)); - req.on('error', reject); + const req = http.request(options, (res) => { + console.log('Got response from proxy:', res.statusCode); + resolve(res); + }); + req.on('error', (err) => { + console.error('Request error:', err); + reject(err); + }); + req.setTimeout(5000, () => { + console.error('Request timeout'); + req.destroy(); + reject(new Error('Request timeout')); + }); req.end(); }); @@ -85,6 +97,9 @@ tap.test('should forward HTTP connections on port 8080', async (tapTest) => { await new Promise((resolve) => { targetServer.close(() => resolve()); }); + + // Wait a bit to ensure port is fully released + await new Promise(resolve => setTimeout(resolve, 500)); }); tap.test('should handle basic HTTP request forwarding', async (tapTest) => { @@ -135,15 +150,30 @@ tap.test('should handle basic HTTP request forwarding', async (tapTest) => { } }; + console.log('Making HTTP request to proxy...'); const response = await new Promise((resolve, reject) => { - const req = http.request(options, (res) => resolve(res)); - req.on('error', reject); + const req = http.request(options, (res) => { + console.log('Got response from proxy:', res.statusCode); + resolve(res); + }); + req.on('error', (err) => { + console.error('Request error:', err); + reject(err); + }); + req.setTimeout(5000, () => { + console.error('Request timeout'); + req.destroy(); + reject(new Error('Request timeout')); + }); req.end(); }); let responseData = ''; response.setEncoding('utf8'); - response.on('data', chunk => responseData += chunk); + response.on('data', chunk => { + console.log('Received data chunk:', chunk); + responseData += chunk; + }); await new Promise(resolve => response.on('end', resolve)); expect(response.statusCode).toEqual(200); @@ -154,6 +184,9 @@ tap.test('should handle basic HTTP request forwarding', async (tapTest) => { await new Promise((resolve) => { targetServer.close(() => resolve()); }); + + // Wait a bit to ensure port is fully released + await new Promise(resolve => setTimeout(resolve, 500)); }); -tap.start(); \ No newline at end of file +export default tap.start(); \ No newline at end of file diff --git a/test/test.httpproxy.function-targets.ts b/test/test.httpproxy.function-targets.ts index cb18ae2..a5c1ec1 100644 --- a/test/test.httpproxy.function-targets.ts +++ b/test/test.httpproxy.function-targets.ts @@ -82,13 +82,16 @@ tap.test('setup HttpProxy function-based targets test environment', async (tools // Test static host/port routes tap.test('should support static host/port routes', async () => { + // Get proxy port first + const proxyPort = httpProxy.getListeningPort(); + const routes: IRouteConfig[] = [ { name: 'static-route', priority: 100, match: { domains: 'example.com', - ports: 0 + ports: proxyPort }, action: { type: 'forward', @@ -102,9 +105,6 @@ tap.test('should support static host/port routes', async () => { await httpProxy.updateRouteConfigs(routes); - // Get proxy port using the improved getListeningPort() method - const proxyPort = httpProxy.getListeningPort(); - // Make request to proxy const response = await makeRequest({ hostname: 'localhost', @@ -124,13 +124,14 @@ tap.test('should support static host/port routes', async () => { // Test function-based host tap.test('should support function-based host', async () => { + const proxyPort = httpProxy.getListeningPort(); const routes: IRouteConfig[] = [ { name: 'function-host-route', priority: 100, match: { domains: 'function.example.com', - ports: 0 + ports: proxyPort }, action: { type: 'forward', @@ -147,9 +148,6 @@ tap.test('should support function-based host', async () => { await httpProxy.updateRouteConfigs(routes); - // Get proxy port using the improved getListeningPort() method - const proxyPort = httpProxy.getListeningPort(); - // Make request to proxy const response = await makeRequest({ hostname: 'localhost', @@ -169,13 +167,14 @@ tap.test('should support function-based host', async () => { // Test function-based port tap.test('should support function-based port', async () => { + const proxyPort = httpProxy.getListeningPort(); const routes: IRouteConfig[] = [ { name: 'function-port-route', priority: 100, match: { domains: 'function-port.example.com', - ports: 0 + ports: proxyPort }, action: { type: 'forward', @@ -192,9 +191,6 @@ tap.test('should support function-based port', async () => { await httpProxy.updateRouteConfigs(routes); - // Get proxy port using the improved getListeningPort() method - const proxyPort = httpProxy.getListeningPort(); - // Make request to proxy const response = await makeRequest({ hostname: 'localhost', @@ -214,13 +210,14 @@ tap.test('should support function-based port', async () => { // Test function-based host AND port tap.test('should support function-based host AND port', async () => { + const proxyPort = httpProxy.getListeningPort(); const routes: IRouteConfig[] = [ { name: 'function-both-route', priority: 100, match: { domains: 'function-both.example.com', - ports: 0 + ports: proxyPort }, action: { type: 'forward', @@ -238,9 +235,6 @@ tap.test('should support function-based host AND port', async () => { await httpProxy.updateRouteConfigs(routes); - // Get proxy port using the improved getListeningPort() method - const proxyPort = httpProxy.getListeningPort(); - // Make request to proxy const response = await makeRequest({ hostname: 'localhost', @@ -260,13 +254,14 @@ tap.test('should support function-based host AND port', async () => { // Test context-based routing with path tap.test('should support context-based routing with path', async () => { + const proxyPort = httpProxy.getListeningPort(); const routes: IRouteConfig[] = [ { name: 'context-path-route', priority: 100, match: { domains: 'context.example.com', - ports: 0 + ports: proxyPort }, action: { type: 'forward', @@ -287,9 +282,6 @@ tap.test('should support context-based routing with path', async () => { await httpProxy.updateRouteConfigs(routes); - // Get proxy port using the improved getListeningPort() method - const proxyPort = httpProxy.getListeningPort(); - // Make request to proxy with /api path const apiResponse = await makeRequest({ hostname: 'localhost', diff --git a/test/test.router.ts b/test/test.router.ts index 18ae25a..7bc9786 100644 --- a/test/test.router.ts +++ b/test/test.router.ts @@ -1,10 +1,10 @@ import { expect, tap } from '@git.zone/tstest/tapbundle'; -import * as tsclass from '@tsclass/tsclass'; import * as http from 'http'; -import { ProxyRouter, type RouterResult } from '../ts/routing/router/proxy-router.js'; +import { HttpRouter, type RouterResult } from '../ts/routing/router/http-router.js'; +import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js'; // Test proxies and configurations -let router: ProxyRouter; +let router: HttpRouter; // Sample hostname for testing const TEST_DOMAIN = 'example.com'; @@ -23,33 +23,40 @@ function createMockRequest(host: string, url: string = '/'): http.IncomingMessag return req; } -// Helper: Creates a test proxy configuration -function createProxyConfig( +// Helper: Creates a test route configuration +function createRouteConfig( hostname: string, destinationIp: string = '10.0.0.1', destinationPort: number = 8080 -): tsclass.network.IReverseProxyConfig { +): IRouteConfig { return { - hostName: hostname, - publicKey: 'mock-cert', - privateKey: 'mock-key', - destinationIps: [destinationIp], - destinationPorts: [destinationPort], - } as tsclass.network.IReverseProxyConfig; + name: `route-${hostname}`, + match: { + domains: [hostname], + ports: 443 + }, + action: { + type: 'forward', + target: { + host: destinationIp, + port: destinationPort + } + } + }; } -// SETUP: Create a ProxyRouter instance -tap.test('setup proxy router test environment', async () => { - router = new ProxyRouter(); +// SETUP: Create an HttpRouter instance +tap.test('setup http router test environment', async () => { + router = new HttpRouter(); // Initialize with empty config - router.setNewProxyConfigs([]); + router.updateRoutes([]); }); // Test basic routing by hostname tap.test('should route requests by hostname', async () => { - const config = createProxyConfig(TEST_DOMAIN); - router.setNewProxyConfigs([config]); + const config = createRouteConfig(TEST_DOMAIN); + router.updateRoutes([config]); const req = createMockRequest(TEST_DOMAIN); const result = router.routeReq(req); @@ -60,8 +67,8 @@ tap.test('should route requests by hostname', async () => { // Test handling of hostname with port number tap.test('should handle hostname with port number', async () => { - const config = createProxyConfig(TEST_DOMAIN); - router.setNewProxyConfigs([config]); + const config = createRouteConfig(TEST_DOMAIN); + router.updateRoutes([config]); const req = createMockRequest(`${TEST_DOMAIN}:443`); const result = router.routeReq(req); @@ -72,8 +79,8 @@ tap.test('should handle hostname with port number', async () => { // Test case-insensitive hostname matching tap.test('should perform case-insensitive hostname matching', async () => { - const config = createProxyConfig(TEST_DOMAIN.toLowerCase()); - router.setNewProxyConfigs([config]); + const config = createRouteConfig(TEST_DOMAIN.toLowerCase()); + router.updateRoutes([config]); const req = createMockRequest(TEST_DOMAIN.toUpperCase()); const result = router.routeReq(req); @@ -84,8 +91,8 @@ tap.test('should perform case-insensitive hostname matching', async () => { // Test handling of unmatched hostnames tap.test('should return undefined for unmatched hostnames', async () => { - const config = createProxyConfig(TEST_DOMAIN); - router.setNewProxyConfigs([config]); + const config = createRouteConfig(TEST_DOMAIN); + router.updateRoutes([config]); const req = createMockRequest('unknown.domain.com'); const result = router.routeReq(req); @@ -95,18 +102,16 @@ tap.test('should return undefined for unmatched hostnames', async () => { // Test adding path patterns tap.test('should match requests using path patterns', async () => { - const config = createProxyConfig(TEST_DOMAIN); - router.setNewProxyConfigs([config]); - - // Add a path pattern to the config - router.setPathPattern(config, '/api/users'); + const config = createRouteConfig(TEST_DOMAIN); + config.match.path = '/api/users'; + router.updateRoutes([config]); // Test that path matches const req1 = createMockRequest(TEST_DOMAIN, '/api/users'); const result1 = router.routeReqWithDetails(req1); expect(result1).toBeTruthy(); - expect(result1.config).toEqual(config); + expect(result1.route).toEqual(config); expect(result1.pathMatch).toEqual('/api/users'); // Test that non-matching path doesn't match @@ -118,17 +123,16 @@ tap.test('should match requests using path patterns', async () => { // Test handling wildcard patterns tap.test('should support wildcard path patterns', async () => { - const config = createProxyConfig(TEST_DOMAIN); - router.setNewProxyConfigs([config]); - - router.setPathPattern(config, '/api/*'); + const config = createRouteConfig(TEST_DOMAIN); + config.match.path = '/api/*'; + router.updateRoutes([config]); // Test with path that matches the wildcard pattern const req = createMockRequest(TEST_DOMAIN, '/api/users/123'); const result = router.routeReqWithDetails(req); expect(result).toBeTruthy(); - expect(result.config).toEqual(config); + expect(result.route).toEqual(config); expect(result.pathMatch).toEqual('/api'); // Print the actual value to diagnose issues @@ -139,31 +143,31 @@ tap.test('should support wildcard path patterns', async () => { // Test extracting path parameters tap.test('should extract path parameters from URL', async () => { - const config = createProxyConfig(TEST_DOMAIN); - router.setNewProxyConfigs([config]); - - router.setPathPattern(config, '/users/:id/profile'); + const config = createRouteConfig(TEST_DOMAIN); + config.match.path = '/users/:id/profile'; + router.updateRoutes([config]); const req = createMockRequest(TEST_DOMAIN, '/users/123/profile'); const result = router.routeReqWithDetails(req); expect(result).toBeTruthy(); - expect(result.config).toEqual(config); + expect(result.route).toEqual(config); expect(result.pathParams).toBeTruthy(); expect(result.pathParams.id).toEqual('123'); }); // Test multiple configs for same hostname with different paths tap.test('should support multiple configs for same hostname with different paths', async () => { - const apiConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.1', 8001); - const webConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.2', 8002); + const apiConfig = createRouteConfig(TEST_DOMAIN, '10.0.0.1', 8001); + apiConfig.match.path = '/api'; + apiConfig.name = 'api-route'; + + const webConfig = createRouteConfig(TEST_DOMAIN, '10.0.0.2', 8002); + webConfig.match.path = '/web'; + webConfig.name = 'web-route'; // Add both configs - router.setNewProxyConfigs([apiConfig, webConfig]); - - // Set different path patterns - router.setPathPattern(apiConfig, '/api'); - router.setPathPattern(webConfig, '/web'); + router.updateRoutes([apiConfig, webConfig]); // Test API path routes to API config const apiReq = createMockRequest(TEST_DOMAIN, '/api/users'); @@ -186,8 +190,8 @@ tap.test('should support multiple configs for same hostname with different paths // Test wildcard subdomains tap.test('should match wildcard subdomains', async () => { - const wildcardConfig = createProxyConfig(TEST_WILDCARD); - router.setNewProxyConfigs([wildcardConfig]); + const wildcardConfig = createRouteConfig(TEST_WILDCARD); + router.updateRoutes([wildcardConfig]); // Test that subdomain.example.com matches *.example.com const req = createMockRequest('subdomain.example.com'); @@ -199,8 +203,8 @@ tap.test('should match wildcard subdomains', async () => { // Test TLD wildcards (example.*) tap.test('should match TLD wildcards', async () => { - const tldWildcardConfig = createProxyConfig('example.*'); - router.setNewProxyConfigs([tldWildcardConfig]); + const tldWildcardConfig = createRouteConfig('example.*'); + router.updateRoutes([tldWildcardConfig]); // Test that example.com matches example.* const req1 = createMockRequest('example.com'); @@ -222,8 +226,8 @@ tap.test('should match TLD wildcards', async () => { // Test complex pattern matching (*.lossless*) tap.test('should match complex wildcard patterns', async () => { - const complexWildcardConfig = createProxyConfig('*.lossless*'); - router.setNewProxyConfigs([complexWildcardConfig]); + const complexWildcardConfig = createRouteConfig('*.lossless*'); + router.updateRoutes([complexWildcardConfig]); // Test that sub.lossless.com matches *.lossless* const req1 = createMockRequest('sub.lossless.com'); @@ -245,10 +249,10 @@ tap.test('should match complex wildcard patterns', async () => { // Test default configuration fallback tap.test('should fall back to default configuration', async () => { - const defaultConfig = createProxyConfig('*'); - const specificConfig = createProxyConfig(TEST_DOMAIN); + const defaultConfig = createRouteConfig('*'); + const specificConfig = createRouteConfig(TEST_DOMAIN); - router.setNewProxyConfigs([defaultConfig, specificConfig]); + router.updateRoutes([defaultConfig, specificConfig]); // Test specific domain routes to specific config const specificReq = createMockRequest(TEST_DOMAIN); @@ -265,10 +269,10 @@ tap.test('should fall back to default configuration', async () => { // Test priority between exact and wildcard matches tap.test('should prioritize exact hostname over wildcard', async () => { - const wildcardConfig = createProxyConfig(TEST_WILDCARD); - const exactConfig = createProxyConfig(TEST_SUBDOMAIN); + const wildcardConfig = createRouteConfig(TEST_WILDCARD); + const exactConfig = createRouteConfig(TEST_SUBDOMAIN); - router.setNewProxyConfigs([wildcardConfig, exactConfig]); + router.updateRoutes([wildcardConfig, exactConfig]); // Test that exact match takes priority const req = createMockRequest(TEST_SUBDOMAIN); @@ -279,11 +283,11 @@ tap.test('should prioritize exact hostname over wildcard', async () => { // Test adding and removing configurations tap.test('should manage configurations correctly', async () => { - router.setNewProxyConfigs([]); + router.updateRoutes([]); // Add a config - const config = createProxyConfig(TEST_DOMAIN); - router.addProxyConfig(config); + const config = createRouteConfig(TEST_DOMAIN); + router.updateRoutes([config]); // Verify routing works const req = createMockRequest(TEST_DOMAIN); @@ -292,8 +296,7 @@ tap.test('should manage configurations correctly', async () => { expect(result).toEqual(config); // Remove the config and verify it no longer routes - const removed = router.removeProxyConfig(TEST_DOMAIN); - expect(removed).toBeTrue(); + router.updateRoutes([]); result = router.routeReq(req); expect(result).toBeUndefined(); @@ -301,13 +304,16 @@ tap.test('should manage configurations correctly', async () => { // Test path pattern specificity tap.test('should prioritize more specific path patterns', async () => { - const genericConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.1', 8001); - const specificConfig = createProxyConfig(TEST_DOMAIN, '10.0.0.2', 8002); + const genericConfig = createRouteConfig(TEST_DOMAIN, '10.0.0.1', 8001); + genericConfig.match.path = '/api/*'; + genericConfig.name = 'generic-api'; - router.setNewProxyConfigs([genericConfig, specificConfig]); + const specificConfig = createRouteConfig(TEST_DOMAIN, '10.0.0.2', 8002); + specificConfig.match.path = '/api/users'; + specificConfig.name = 'specific-api'; + specificConfig.priority = 10; // Higher priority - router.setPathPattern(genericConfig, '/api/*'); - router.setPathPattern(specificConfig, '/api/users'); + router.updateRoutes([genericConfig, specificConfig]); // The more specific '/api/users' should match before the '/api/*' wildcard const req = createMockRequest(TEST_DOMAIN, '/api/users'); @@ -316,24 +322,29 @@ tap.test('should prioritize more specific path patterns', async () => { expect(result).toEqual(specificConfig); }); -// Test getHostnames method -tap.test('should retrieve all configured hostnames', async () => { - router.setNewProxyConfigs([ - createProxyConfig(TEST_DOMAIN), - createProxyConfig(TEST_SUBDOMAIN) - ]); +// Test multiple hostnames +tap.test('should handle multiple configured hostnames', async () => { + const routes = [ + createRouteConfig(TEST_DOMAIN), + createRouteConfig(TEST_SUBDOMAIN) + ]; + router.updateRoutes(routes); - const hostnames = router.getHostnames(); + // Test first domain routes correctly + const req1 = createMockRequest(TEST_DOMAIN); + const result1 = router.routeReq(req1); + expect(result1).toEqual(routes[0]); - expect(hostnames.length).toEqual(2); - expect(hostnames).toContain(TEST_DOMAIN.toLowerCase()); - expect(hostnames).toContain(TEST_SUBDOMAIN.toLowerCase()); + // Test second domain routes correctly + const req2 = createMockRequest(TEST_SUBDOMAIN); + const result2 = router.routeReq(req2); + expect(result2).toEqual(routes[1]); }); // Test handling missing host header tap.test('should handle missing host header', async () => { - const defaultConfig = createProxyConfig('*'); - router.setNewProxyConfigs([defaultConfig]); + const defaultConfig = createRouteConfig('*'); + router.updateRoutes([defaultConfig]); const req = createMockRequest(''); req.headers.host = undefined; @@ -345,16 +356,15 @@ tap.test('should handle missing host header', async () => { // Test complex path parameters tap.test('should handle complex path parameters', async () => { - const config = createProxyConfig(TEST_DOMAIN); - router.setNewProxyConfigs([config]); - - router.setPathPattern(config, '/api/:version/users/:userId/posts/:postId'); + const config = createRouteConfig(TEST_DOMAIN); + config.match.path = '/api/:version/users/:userId/posts/:postId'; + router.updateRoutes([config]); const req = createMockRequest(TEST_DOMAIN, '/api/v1/users/123/posts/456'); const result = router.routeReqWithDetails(req); expect(result).toBeTruthy(); - expect(result.config).toEqual(config); + expect(result.route).toEqual(config); expect(result.pathParams).toBeTruthy(); expect(result.pathParams.version).toEqual('v1'); expect(result.pathParams.userId).toEqual('123'); @@ -367,10 +377,10 @@ tap.test('should handle many configurations efficiently', async () => { // Create many configs with different hostnames for (let i = 0; i < 100; i++) { - configs.push(createProxyConfig(`host-${i}.example.com`)); + configs.push(createRouteConfig(`host-${i}.example.com`)); } - router.setNewProxyConfigs(configs); + router.updateRoutes(configs); // Test middle of the list to avoid best/worst case const req = createMockRequest('host-50.example.com'); @@ -382,11 +392,12 @@ tap.test('should handle many configurations efficiently', async () => { // Test cleanup tap.test('cleanup proxy router test environment', async () => { // Clear all configurations - router.setNewProxyConfigs([]); + router.updateRoutes([]); - // Verify empty state - expect(router.getHostnames().length).toEqual(0); - expect(router.getProxyConfigs().length).toEqual(0); + // Verify empty state by testing that no routes match + const req = createMockRequest(TEST_DOMAIN); + const result = router.routeReq(req); + expect(result).toBeUndefined(); }); export default tap.start(); \ No newline at end of file diff --git a/ts/core/models/index.ts b/ts/core/models/index.ts index 562e5e7..e6d1d7b 100644 --- a/ts/core/models/index.ts +++ b/ts/core/models/index.ts @@ -6,3 +6,4 @@ export * from './common-types.js'; export * from './socket-augmentation.js'; export * from './route-context.js'; export * from './wrapped-socket.js'; +export * from './socket-types.js'; diff --git a/ts/core/models/socket-types.ts b/ts/core/models/socket-types.ts new file mode 100644 index 0000000..f408b8d --- /dev/null +++ b/ts/core/models/socket-types.ts @@ -0,0 +1,21 @@ +import * as net from 'net'; +import { WrappedSocket } from './wrapped-socket.js'; + +/** + * Type guard to check if a socket is a WrappedSocket + */ +export function isWrappedSocket(socket: net.Socket | WrappedSocket): socket is WrappedSocket { + return socket instanceof WrappedSocket || 'socket' in socket; +} + +/** + * Helper to get the underlying socket from either a Socket or WrappedSocket + */ +export function getUnderlyingSocket(socket: net.Socket | WrappedSocket): net.Socket { + return isWrappedSocket(socket) ? socket.socket : socket; +} + +/** + * Type that represents either a regular socket or a wrapped socket + */ +export type AnySocket = net.Socket | WrappedSocket; \ No newline at end of file diff --git a/ts/core/models/wrapped-socket.ts b/ts/core/models/wrapped-socket.ts index bc90767..2e4f7cd 100644 --- a/ts/core/models/wrapped-socket.ts +++ b/ts/core/models/wrapped-socket.ts @@ -1,4 +1,3 @@ -import { EventEmitter } from 'events'; import * as plugins from '../../plugins.js'; /** @@ -7,22 +6,66 @@ import * as plugins from '../../plugins.js'; * * This is the FOUNDATION for all PROXY protocol support and must be implemented * before any protocol parsing can occur. + * + * This implementation uses a Proxy to delegate all properties and methods + * to the underlying socket while allowing override of specific properties. */ -export class WrappedSocket extends EventEmitter { +export class WrappedSocket { + public readonly socket: plugins.net.Socket; private realClientIP?: string; private realClientPort?: number; + // Make TypeScript happy by declaring the Socket methods that will be proxied + [key: string]: any; + constructor( - public readonly socket: plugins.net.Socket, + socket: plugins.net.Socket, realClientIP?: string, realClientPort?: number ) { - super(); + this.socket = socket; this.realClientIP = realClientIP; this.realClientPort = realClientPort; - // Forward all socket events - this.forwardSocketEvents(); + // Create a proxy that delegates everything to the underlying socket + return new Proxy(this, { + get(target, prop, receiver) { + // Override specific properties + if (prop === 'remoteAddress') { + return target.remoteAddress; + } + if (prop === 'remotePort') { + return target.remotePort; + } + if (prop === 'socket') { + return target.socket; + } + if (prop === 'realClientIP') { + return target.realClientIP; + } + if (prop === 'realClientPort') { + return target.realClientPort; + } + if (prop === 'isFromTrustedProxy') { + return target.isFromTrustedProxy; + } + if (prop === 'setProxyInfo') { + return target.setProxyInfo.bind(target); + } + + // For all other properties/methods, delegate to the underlying socket + const value = target.socket[prop as keyof plugins.net.Socket]; + if (typeof value === 'function') { + return value.bind(target.socket); + } + return value; + }, + set(target, prop, value) { + // Set on the underlying socket + (target.socket as any)[prop] = value; + return true; + } + }) as any; } /** @@ -39,35 +82,6 @@ export class WrappedSocket extends EventEmitter { return this.realClientPort || this.socket.remotePort; } - /** - * Returns the remote family (IPv4 or IPv6) - */ - get remoteFamily(): string | undefined { - // If we have a real client IP, determine the family - if (this.realClientIP) { - if (this.realClientIP.includes(':')) { - return 'IPv6'; - } else { - return 'IPv4'; - } - } - return this.socket.remoteFamily; - } - - /** - * Returns the local address of the socket - */ - get localAddress(): string | undefined { - return this.socket.localAddress; - } - - /** - * Returns the local port of the socket - */ - get localPort(): number | undefined { - return this.socket.localPort; - } - /** * Indicates if this connection came through a trusted proxy */ @@ -82,178 +96,4 @@ export class WrappedSocket extends EventEmitter { this.realClientIP = ip; this.realClientPort = port; } - - // Pass-through all socket methods - write(data: any, encoding?: any, callback?: any): boolean { - return this.socket.write(data, encoding, callback); - } - - end(data?: any, encoding?: any, callback?: any): this { - this.socket.end(data, encoding, callback); - return this; - } - - destroy(error?: Error): this { - this.socket.destroy(error); - return this; - } - - pause(): this { - this.socket.pause(); - return this; - } - - resume(): this { - this.socket.resume(); - return this; - } - - setTimeout(timeout: number, callback?: () => void): this { - this.socket.setTimeout(timeout, callback); - return this; - } - - setNoDelay(noDelay?: boolean): this { - this.socket.setNoDelay(noDelay); - return this; - } - - setKeepAlive(enable?: boolean, initialDelay?: number): this { - this.socket.setKeepAlive(enable, initialDelay); - return this; - } - - ref(): this { - this.socket.ref(); - return this; - } - - unref(): this { - this.socket.unref(); - return this; - } - - /** - * Pipe to another stream - */ - pipe(destination: T, options?: { - end?: boolean; - }): T { - return this.socket.pipe(destination, options); - } - - /** - * Cork the stream - */ - cork(): void { - if ('cork' in this.socket && typeof this.socket.cork === 'function') { - this.socket.cork(); - } - } - - /** - * Uncork the stream - */ - uncork(): void { - if ('uncork' in this.socket && typeof this.socket.uncork === 'function') { - this.socket.uncork(); - } - } - - /** - * Get the number of bytes read - */ - get bytesRead(): number { - return this.socket.bytesRead; - } - - /** - * Get the number of bytes written - */ - get bytesWritten(): number { - return this.socket.bytesWritten; - } - - /** - * Check if the socket is connecting - */ - get connecting(): boolean { - return this.socket.connecting; - } - - /** - * Check if the socket is destroyed - */ - get destroyed(): boolean { - return this.socket.destroyed; - } - - /** - * Check if the socket is readable - */ - get readable(): boolean { - return this.socket.readable; - } - - /** - * Check if the socket is writable - */ - get writable(): boolean { - return this.socket.writable; - } - - /** - * Get pending status - */ - get pending(): boolean { - return this.socket.pending; - } - - /** - * Get ready state - */ - get readyState(): string { - return this.socket.readyState; - } - - /** - * Address info - */ - address(): plugins.net.AddressInfo | {} | null { - const addr = this.socket.address(); - if (addr === null) return null; - if (typeof addr === 'string') return addr as any; - return addr; - } - - /** - * Set socket encoding - */ - setEncoding(encoding?: BufferEncoding): this { - this.socket.setEncoding(encoding); - return this; - } - - /** - * Connect method (for client sockets) - */ - connect(options: plugins.net.SocketConnectOpts, connectionListener?: () => void): this; - connect(port: number, host?: string, connectionListener?: () => void): this; - connect(path: string, connectionListener?: () => void): this; - connect(...args: any[]): this { - (this.socket as any).connect(...args); - return this; - } - - /** - * Forward all events from the underlying socket - */ - private forwardSocketEvents(): void { - const events = ['data', 'end', 'close', 'error', 'drain', 'timeout', 'connect', 'ready', 'lookup']; - events.forEach(event => { - this.socket.on(event, (...args) => { - this.emit(event, ...args); - }); - }); - } } \ No newline at end of file diff --git a/ts/proxies/smart-proxy/http-proxy-bridge.ts b/ts/proxies/smart-proxy/http-proxy-bridge.ts index 2516b7d..dcb07ae 100644 --- a/ts/proxies/smart-proxy/http-proxy-bridge.ts +++ b/ts/proxies/smart-proxy/http-proxy-bridge.ts @@ -3,6 +3,7 @@ import { HttpProxy } from '../http-proxy/index.js'; import { setupBidirectionalForwarding } from '../../core/utils/socket-utils.js'; import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js'; import type { IRouteConfig } from './models/route-types.js'; +import { WrappedSocket } from '../../core/models/wrapped-socket.js'; export class HttpProxyBridge { private httpProxy: HttpProxy | null = null; @@ -98,7 +99,7 @@ export class HttpProxyBridge { */ public async forwardToHttpProxy( connectionId: string, - socket: plugins.net.Socket, + socket: plugins.net.Socket | WrappedSocket, record: IConnectionRecord, initialChunk: Buffer, httpProxyPort: number, @@ -125,7 +126,10 @@ export class HttpProxyBridge { } // Use centralized bidirectional forwarding - setupBidirectionalForwarding(socket, proxySocket, { + // Extract underlying socket if it's a WrappedSocket + const underlyingSocket = socket instanceof WrappedSocket ? socket.socket : socket; + + setupBidirectionalForwarding(underlyingSocket, proxySocket, { onClientData: (chunk) => { // Update stats if needed if (record) { diff --git a/ts/proxies/smart-proxy/route-connection-handler.ts b/ts/proxies/smart-proxy/route-connection-handler.ts index a8751e8..df16ef9 100644 --- a/ts/proxies/smart-proxy/route-connection-handler.ts +++ b/ts/proxies/smart-proxy/route-connection-handler.ts @@ -12,6 +12,7 @@ import { TimeoutManager } from './timeout-manager.js'; import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js'; import { cleanupSocket, createIndependentSocketHandlers, setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js'; import { WrappedSocket } from '../../core/models/wrapped-socket.js'; +import { getUnderlyingSocket } from '../../core/models/socket-types.js'; /** * Handles new connection processing and setup logic with support for route-based configuration @@ -192,7 +193,7 @@ export class RouteConnectionHandler { // If no routes require TLS handling and it's not port 443, route immediately if (!needsTlsHandling && localPort !== 443) { // Extract underlying socket for socket-utils functions - const underlyingSocket = socket instanceof WrappedSocket ? socket.socket : socket; + const underlyingSocket = getUnderlyingSocket(socket); // Set up proper socket handlers for immediate routing setupSocketHandlers( underlyingSocket, @@ -222,7 +223,7 @@ export class RouteConnectionHandler { ); // Route immediately for non-TLS connections - this.routeConnection(underlyingSocket, record, '', undefined); + this.routeConnection(socket, record, '', undefined); return; } @@ -379,8 +380,7 @@ export class RouteConnectionHandler { } // Find the appropriate route for this connection - const underlyingSocket = socket instanceof WrappedSocket ? socket.socket : socket; - this.routeConnection(underlyingSocket, record, serverName, chunk); + this.routeConnection(socket, record, serverName, chunk); }); } @@ -388,7 +388,7 @@ export class RouteConnectionHandler { * Route the connection based on match criteria */ private routeConnection( - socket: plugins.net.Socket, + socket: plugins.net.Socket | WrappedSocket, record: IConnectionRecord, serverName: string, initialChunk?: Buffer @@ -576,7 +576,7 @@ export class RouteConnectionHandler { * Handle a forward action for a route */ private handleForwardAction( - socket: plugins.net.Socket, + socket: plugins.net.Socket | WrappedSocket, record: IConnectionRecord, route: IRouteConfig, initialChunk?: Buffer @@ -893,7 +893,7 @@ export class RouteConnectionHandler { * Handle a socket-handler action for a route */ private async handleSocketHandlerAction( - socket: plugins.net.Socket, + socket: plugins.net.Socket | WrappedSocket, record: IConnectionRecord, route: IRouteConfig, initialChunk?: Buffer @@ -957,8 +957,9 @@ export class RouteConnectionHandler { }); try { - // Call the handler with socket AND context - const result = route.action.socketHandler(socket, routeContext); + // Call the handler with the appropriate socket (extract underlying if needed) + const handlerSocket = getUnderlyingSocket(socket); + const result = route.action.socketHandler(handlerSocket, routeContext); // Handle async handlers properly if (result instanceof Promise) { @@ -1012,7 +1013,7 @@ export class RouteConnectionHandler { * Sets up a direct connection to the target */ private setupDirectConnection( - socket: plugins.net.Socket, + socket: plugins.net.Socket | WrappedSocket, record: IConnectionRecord, serverName?: string, initialChunk?: Buffer, @@ -1162,7 +1163,10 @@ export class RouteConnectionHandler { } // Use centralized bidirectional forwarding setup - setupBidirectionalForwarding(socket, targetSocket, { + // Extract underlying sockets for socket-utils functions + const incomingSocket = getUnderlyingSocket(socket); + + setupBidirectionalForwarding(incomingSocket, targetSocket, { onClientData: (chunk) => { record.bytesReceived += chunk.length; this.timeoutManager.updateActivity(record);