diff --git a/test/test.httpproxy.ts b/test/test.httpproxy.ts index e066a69..fc1fe4a 100644 --- a/test/test.httpproxy.ts +++ b/test/test.httpproxy.ts @@ -232,10 +232,10 @@ tap.test('should start the proxy server', async () => { }, action: { type: 'forward', - target: { + targets: [{ host: 'localhost', port: 3100 - }, + }], tls: { mode: 'terminate' }, diff --git a/test/test.metrics-collector.ts b/test/test.metrics-collector.ts index aaf92ae..7f4bc25 100644 --- a/test/test.metrics-collector.ts +++ b/test/test.metrics-collector.ts @@ -29,7 +29,7 @@ tap.test('MetricsCollector provides accurate metrics', async (tools) => { match: { ports: 8700 }, action: { type: 'forward', - target: { host: 'localhost', port: 9995 } + targets: [{ host: 'localhost', port: 9995 }] } }, { @@ -37,7 +37,7 @@ tap.test('MetricsCollector provides accurate metrics', async (tools) => { match: { ports: 8701 }, action: { type: 'forward', - target: { host: 'localhost', port: 9995 } + targets: [{ host: 'localhost', port: 9995 }] } } ], diff --git a/test/test.route-config.ts b/test/test.route-config.ts index f4125e4..63dd6f1 100644 --- a/test/test.route-config.ts +++ b/test/test.route-config.ts @@ -56,8 +56,8 @@ tap.test('Routes: Should create basic HTTP route', async () => { expect(httpRoute.match.ports).toEqual(80); expect(httpRoute.match.domains).toEqual('example.com'); expect(httpRoute.action.type).toEqual('forward'); - expect(httpRoute.action.target?.host).toEqual('localhost'); - expect(httpRoute.action.target?.port).toEqual(3000); + expect(httpRoute.action.targets?.[0]?.host).toEqual('localhost'); + expect(httpRoute.action.targets?.[0]?.port).toEqual(3000); expect(httpRoute.name).toEqual('Basic HTTP Route'); }); @@ -74,8 +74,8 @@ tap.test('Routes: Should create HTTPS route with TLS termination', async () => { expect(httpsRoute.action.type).toEqual('forward'); expect(httpsRoute.action.tls?.mode).toEqual('terminate'); expect(httpsRoute.action.tls?.certificate).toEqual('auto'); - expect(httpsRoute.action.target?.host).toEqual('localhost'); - expect(httpsRoute.action.target?.port).toEqual(8080); + expect(httpsRoute.action.targets?.[0]?.host).toEqual('localhost'); + expect(httpsRoute.action.targets?.[0]?.port).toEqual(8080); expect(httpsRoute.name).toEqual('HTTPS Route'); }); @@ -131,10 +131,10 @@ tap.test('Routes: Should create load balancer route', async () => { // Validate the route configuration expect(lbRoute.match.domains).toEqual('app.example.com'); expect(lbRoute.action.type).toEqual('forward'); - expect(Array.isArray(lbRoute.action.target?.host)).toBeTrue(); - expect((lbRoute.action.target?.host as string[]).length).toEqual(3); - expect((lbRoute.action.target?.host as string[])[0]).toEqual('10.0.0.1'); - expect(lbRoute.action.target?.port).toEqual(8080); + expect(Array.isArray(lbRoute.action.targets?.[0]?.host)).toBeTrue(); + expect((lbRoute.action.targets?.[0]?.host as string[]).length).toEqual(3); + expect((lbRoute.action.targets?.[0]?.host as string[])[0]).toEqual('10.0.0.1'); + expect(lbRoute.action.targets?.[0]?.port).toEqual(8080); expect(lbRoute.action.tls?.mode).toEqual('terminate'); }); @@ -152,8 +152,8 @@ tap.test('Routes: Should create API route with CORS', async () => { expect(apiRoute.match.path).toEqual('/v1/*'); expect(apiRoute.action.type).toEqual('forward'); expect(apiRoute.action.tls?.mode).toEqual('terminate'); - expect(apiRoute.action.target?.host).toEqual('localhost'); - expect(apiRoute.action.target?.port).toEqual(3000); + expect(apiRoute.action.targets?.[0]?.host).toEqual('localhost'); + expect(apiRoute.action.targets?.[0]?.port).toEqual(3000); // Check CORS headers expect(apiRoute.headers).toBeDefined(); @@ -177,8 +177,8 @@ tap.test('Routes: Should create WebSocket route', async () => { expect(wsRoute.match.path).toEqual('/socket'); expect(wsRoute.action.type).toEqual('forward'); expect(wsRoute.action.tls?.mode).toEqual('terminate'); - expect(wsRoute.action.target?.host).toEqual('localhost'); - expect(wsRoute.action.target?.port).toEqual(5000); + expect(wsRoute.action.targets?.[0]?.host).toEqual('localhost'); + expect(wsRoute.action.targets?.[0]?.port).toEqual(5000); // Check WebSocket configuration expect(wsRoute.action.websocket).toBeDefined(); @@ -294,13 +294,13 @@ tap.test('Edge Case - Wildcard Domains and Path Matching', async () => { const bestMatch = findBestMatchingRoute(routes, { domain: 'api.example.com', path: '/api/users', port: 443 }); expect(bestMatch).not.toBeUndefined(); if (bestMatch) { - expect(bestMatch.action.target.port).toEqual(3001); // Should match the exact domain route + expect(bestMatch.action.targets[0].port).toEqual(3001); // Should match the exact domain route } // Test with a different subdomain - should only match the wildcard route const otherMatches = findMatchingRoutes(routes, { domain: 'other.example.com', path: '/api/products', port: 443 }); expect(otherMatches.length).toEqual(1); - expect(otherMatches[0].action.target.port).toEqual(3000); // Should match the wildcard domain route + expect(otherMatches[0].action.targets[0].port).toEqual(3000); // Should match the wildcard domain route }); tap.test('Edge Case - Disabled Routes', async () => { @@ -316,7 +316,7 @@ tap.test('Edge Case - Disabled Routes', async () => { // Should only find the enabled route expect(matches.length).toEqual(1); - expect(matches[0].action.target.port).toEqual(3000); + expect(matches[0].action.targets[0].port).toEqual(3000); }); tap.test('Edge Case - Complex Path and Headers Matching', async () => { @@ -452,7 +452,7 @@ tap.test('Wildcard Domain Handling', async () => { expect(bestSpecificMatch).not.toBeUndefined(); if (bestSpecificMatch) { // Find which route was matched - const matchedPort = bestSpecificMatch.action.target.port; + const matchedPort = bestSpecificMatch.action.targets[0].port; console.log(`Matched route with port: ${matchedPort}`); // Verify it's the specific subdomain route (with highest priority) @@ -465,7 +465,7 @@ tap.test('Wildcard Domain Handling', async () => { expect(bestWildcardMatch).not.toBeUndefined(); if (bestWildcardMatch) { // Find which route was matched - const matchedPort = bestWildcardMatch.action.target.port; + const matchedPort = bestWildcardMatch.action.targets[0].port; console.log(`Matched route with port: ${matchedPort}`); // Verify it's the wildcard subdomain route (with medium priority) @@ -513,7 +513,7 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => { expect(webServerMatch).not.toBeUndefined(); if (webServerMatch) { expect(webServerMatch.action.type).toEqual('forward'); - expect(webServerMatch.action.target.host).toEqual('web-server'); + expect(webServerMatch.action.targets[0].host).toEqual('web-server'); } // Web server (HTTP redirect via socket handler) @@ -532,7 +532,7 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => { expect(apiMatch).not.toBeUndefined(); if (apiMatch) { expect(apiMatch.action.type).toEqual('forward'); - expect(apiMatch.action.target.host).toEqual('api-server'); + expect(apiMatch.action.targets[0].host).toEqual('api-server'); } // WebSocket server @@ -544,7 +544,7 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => { expect(wsMatch).not.toBeUndefined(); if (wsMatch) { expect(wsMatch.action.type).toEqual('forward'); - expect(wsMatch.action.target.host).toEqual('websocket-server'); + expect(wsMatch.action.targets[0].host).toEqual('websocket-server'); expect(wsMatch.action.websocket?.enabled).toBeTrue(); } diff --git a/test/test.route-utils.ts b/test/test.route-utils.ts index c06c982..5d38511 100644 --- a/test/test.route-utils.ts +++ b/test/test.route-utils.ts @@ -134,10 +134,10 @@ tap.test('Route Validation - validateRouteAction', async () => { // Valid forward action const validForwardAction: IRouteAction = { type: 'forward', - target: { + targets: [{ host: 'localhost', port: 3000 - } + }] }; const validForwardResult = validateRouteAction(validForwardAction); expect(validForwardResult.valid).toBeTrue(); @@ -154,14 +154,14 @@ tap.test('Route Validation - validateRouteAction', async () => { expect(validSocketResult.valid).toBeTrue(); expect(validSocketResult.errors.length).toEqual(0); - // Invalid action (missing target) + // Invalid action (missing targets) const invalidAction: IRouteAction = { type: 'forward' }; const invalidResult = validateRouteAction(invalidAction); expect(invalidResult.valid).toBeFalse(); expect(invalidResult.errors.length).toBeGreaterThan(0); - expect(invalidResult.errors[0]).toInclude('Target is required'); + expect(invalidResult.errors[0]).toInclude('Targets array is required'); // Invalid action (missing socket handler) const invalidSocketAction: IRouteAction = { @@ -180,7 +180,7 @@ tap.test('Route Validation - validateRouteConfig', async () => { expect(validResult.valid).toBeTrue(); expect(validResult.errors.length).toEqual(0); - // Invalid route config (missing target) + // Invalid route config (missing targets) const invalidRoute: IRouteConfig = { match: { domains: 'example.com', @@ -309,16 +309,16 @@ tap.test('Route Utilities - mergeRouteConfigs', async () => { const actionOverride: Partial = { action: { type: 'forward', - target: { + targets: [{ host: 'new-host.local', port: 5000 - } + }] } }; const actionMergedRoute = mergeRouteConfigs(baseRoute, actionOverride); - expect(actionMergedRoute.action.target.host).toEqual('new-host.local'); - expect(actionMergedRoute.action.target.port).toEqual(5000); + expect(actionMergedRoute.action.targets?.[0]?.host).toEqual('new-host.local'); + expect(actionMergedRoute.action.targets?.[0]?.port).toEqual(5000); // Test replacing action with socket handler const typeChangeOverride: Partial = { @@ -336,7 +336,7 @@ tap.test('Route Utilities - mergeRouteConfigs', async () => { const typeChangedRoute = mergeRouteConfigs(baseRoute, typeChangeOverride); expect(typeChangedRoute.action.type).toEqual('socket-handler'); expect(typeChangedRoute.action.socketHandler).toBeDefined(); - expect(typeChangedRoute.action.target).toBeUndefined(); + expect(typeChangedRoute.action.targets).toBeUndefined(); }); tap.test('Route Matching - routeMatchesDomain', async () => { @@ -379,10 +379,10 @@ tap.test('Route Matching - routeMatchesPort', async () => { }, action: { type: 'forward', - target: { + targets: [{ host: 'localhost', port: 3000 - } + }] } }; @@ -393,10 +393,10 @@ tap.test('Route Matching - routeMatchesPort', async () => { }, action: { type: 'forward', - target: { + targets: [{ host: 'localhost', port: 3000 - } + }] } }; @@ -427,10 +427,10 @@ tap.test('Route Matching - routeMatchesPath', async () => { }, action: { type: 'forward', - target: { + targets: [{ host: 'localhost', port: 3000 - } + }] } }; @@ -443,10 +443,10 @@ tap.test('Route Matching - routeMatchesPath', async () => { }, action: { type: 'forward', - target: { + targets: [{ host: 'localhost', port: 3000 - } + }] } }; @@ -458,10 +458,10 @@ tap.test('Route Matching - routeMatchesPath', async () => { }, action: { type: 'forward', - target: { + targets: [{ host: 'localhost', port: 3000 - } + }] } }; @@ -494,10 +494,10 @@ tap.test('Route Matching - routeMatchesHeaders', async () => { }, action: { type: 'forward', - target: { + targets: [{ host: 'localhost', port: 3000 - } + }] } }; @@ -641,7 +641,7 @@ tap.test('Route Utilities - cloneRoute', async () => { expect(clonedRoute.name).toEqual(originalRoute.name); expect(clonedRoute.match.domains).toEqual(originalRoute.match.domains); expect(clonedRoute.action.type).toEqual(originalRoute.action.type); - expect(clonedRoute.action.target.port).toEqual(originalRoute.action.target.port); + expect(clonedRoute.action.targets?.[0]?.port).toEqual(originalRoute.action.targets?.[0]?.port); // Modify the clone and check that the original is unchanged clonedRoute.name = 'Modified Clone'; @@ -656,8 +656,8 @@ tap.test('Route Helpers - createHttpRoute', async () => { expect(route.match.domains).toEqual('example.com'); expect(route.match.ports).toEqual(80); expect(route.action.type).toEqual('forward'); - expect(route.action.target.host).toEqual('localhost'); - expect(route.action.target.port).toEqual(3000); + expect(route.action.targets?.[0]?.host).toEqual('localhost'); + expect(route.action.targets?.[0]?.port).toEqual(3000); const validationResult = validateRouteConfig(route); expect(validationResult.valid).toBeTrue(); @@ -790,11 +790,11 @@ tap.test('Route Helpers - createLoadBalancerRoute', async () => { expect(route.match.domains).toEqual('loadbalancer.example.com'); expect(route.match.ports).toEqual(443); expect(route.action.type).toEqual('forward'); - expect(Array.isArray(route.action.target.host)).toBeTrue(); - if (Array.isArray(route.action.target.host)) { - expect(route.action.target.host.length).toEqual(3); + expect(route.action.targets).toBeDefined(); + if (route.action.targets && Array.isArray(route.action.targets[0]?.host)) { + expect((route.action.targets[0].host as string[]).length).toEqual(3); } - expect(route.action.target.port).toEqual(8080); + expect(route.action.targets?.[0]?.port).toEqual(8080); expect(route.action.tls.mode).toEqual('terminate'); const validationResult = validateRouteConfig(route); @@ -819,7 +819,7 @@ tap.test('Route Patterns - createApiGatewayRoute', async () => { expect(apiGatewayRoute.match.domains).toEqual('api.example.com'); expect(apiGatewayRoute.match.path).toInclude('/v1'); expect(apiGatewayRoute.action.type).toEqual('forward'); - expect(apiGatewayRoute.action.target.port).toEqual(3000); + expect(apiGatewayRoute.action.targets?.[0]?.port).toEqual(3000); // Check TLS configuration if (apiGatewayRoute.action.tls) { @@ -854,7 +854,7 @@ tap.test('Route Patterns - createWebSocketPattern', async () => { expect(wsRoute.match.domains).toEqual('ws.example.com'); expect(wsRoute.match.path).toEqual('/socket'); expect(wsRoute.action.type).toEqual('forward'); - expect(wsRoute.action.target.port).toEqual(3000); + expect(wsRoute.action.targets?.[0]?.port).toEqual(3000); // Check TLS configuration if (wsRoute.action.tls) { @@ -891,8 +891,8 @@ tap.test('Route Patterns - createLoadBalancerRoute pattern', async () => { expect(lbRoute.action.type).toEqual('forward'); // Check target hosts - if (Array.isArray(lbRoute.action.target.host)) { - expect(lbRoute.action.target.host.length).toEqual(3); + if (lbRoute.action.targets && Array.isArray(lbRoute.action.targets[0]?.host)) { + expect((lbRoute.action.targets[0].host as string[]).length).toEqual(3); } // Check TLS configuration diff --git a/test/test.shared-security-manager-limits.node.ts b/test/test.shared-security-manager-limits.node.ts index 85b36e6..f0494eb 100644 --- a/test/test.shared-security-manager-limits.node.ts +++ b/test/test.shared-security-manager-limits.node.ts @@ -38,15 +38,17 @@ tap.test('Per-IP connection limits validation', async () => { // Track connections up to limit for (let i = 1; i <= 5; i++) { - securityManager.trackConnectionByIP(testIP, `conn${i}`); + // Validate BEFORE tracking the connection (checking if we can add a new connection) const result = securityManager.validateIP(testIP); expect(result.allowed).toBeTrue(); + // Now track the connection + securityManager.trackConnectionByIP(testIP, `conn${i}`); } // Verify we're at the limit expect(securityManager.getConnectionCountByIP(testIP)).toEqual(5); - // Next connection should be rejected + // Next connection should be rejected (we're already at 5) const result = securityManager.validateIP(testIP); expect(result.allowed).toBeFalse(); expect(result.reason).toInclude('Maximum connections per IP'); @@ -61,21 +63,16 @@ tap.test('Connection rate limiting', async () => { const testIP = '192.168.1.102'; // Make connections at the rate limit + // Note: validateIP() already tracks timestamps internally for rate limiting for (let i = 0; i < 10; i++) { const result = securityManager.validateIP(testIP); expect(result.allowed).toBeTrue(); - securityManager.trackConnectionByIP(testIP, `conn${i}`); } // Next connection should exceed rate limit const result = securityManager.validateIP(testIP); expect(result.allowed).toBeFalse(); expect(result.reason).toInclude('Connection rate limit'); - - // Clean up connections - for (let i = 0; i < 10; i++) { - securityManager.removeConnectionByIP(testIP, `conn${i}`); - } }); tap.test('Route-level connection limits', async () => { @@ -93,7 +90,8 @@ tap.test('Route-level connection limits', async () => { clientIp: '192.168.1.103', serverIp: '0.0.0.0', timestamp: Date.now(), - connectionId: 'test-conn' + connectionId: 'test-conn', + isTls: true }; // Test with connection counts below limit diff --git a/test/test.smartproxy.ts b/test/test.smartproxy.ts index e8b3ecb..864b07d 100644 --- a/test/test.smartproxy.ts +++ b/test/test.smartproxy.ts @@ -73,10 +73,10 @@ tap.test('setup port proxy test environment', async () => { }, action: { type: 'forward', - target: { + targets: [{ host: 'localhost', port: TEST_SERVER_PORT - } + }] } } ], @@ -112,10 +112,10 @@ tap.test('should forward TCP connections to custom host', async () => { }, action: { type: 'forward', - target: { + targets: [{ host: '127.0.0.1', port: TEST_SERVER_PORT - } + }] } } ], @@ -157,10 +157,10 @@ tap.test('should forward connections to custom IP', async () => { }, action: { type: 'forward', - target: { + targets: [{ host: '127.0.0.1', port: targetServerPort - } + }] } } ], @@ -252,10 +252,10 @@ tap.test('should support optional source IP preservation in chained proxies', as }, action: { type: 'forward', - target: { + targets: [{ host: 'localhost', port: PROXY_PORT + 5 - } + }] } } ], @@ -273,10 +273,10 @@ tap.test('should support optional source IP preservation in chained proxies', as }, action: { type: 'forward', - target: { + targets: [{ host: 'localhost', port: TEST_SERVER_PORT - } + }] } } ], @@ -311,10 +311,10 @@ tap.test('should support optional source IP preservation in chained proxies', as }, action: { type: 'forward', - target: { + targets: [{ host: 'localhost', port: PROXY_PORT + 7 - } + }] } } ], @@ -334,10 +334,10 @@ tap.test('should support optional source IP preservation in chained proxies', as }, action: { type: 'forward', - target: { + targets: [{ host: 'localhost', port: TEST_SERVER_PORT - } + }] } } ], @@ -377,10 +377,10 @@ tap.test('should use round robin for multiple target hosts in domain config', as }, action: { type: 'forward' as const, - target: { + targets: [{ host: ['hostA', 'hostB'], // Array of hosts for round-robin port: 80 - } + }] } }; @@ -400,9 +400,9 @@ tap.test('should use round robin for multiple target hosts in domain config', as // For route-based approach, the actual round-robin logic happens in connection handling // Just make sure our config has the expected hosts - expect(Array.isArray(routeConfig.action.targets[0].host)).toBeTrue(); - expect(routeConfig.action.targets[0].host).toContain('hostA'); - expect(routeConfig.action.targets[0].host).toContain('hostB'); + expect(Array.isArray(routeConfig.action.targets![0].host)).toBeTrue(); + expect(routeConfig.action.targets![0].host).toContain('hostA'); + expect(routeConfig.action.targets![0].host).toContain('hostB'); }); // CLEANUP: Tear down all servers and proxies diff --git a/ts/core/utils/shared-security-manager.ts b/ts/core/utils/shared-security-manager.ts index a8f088a..5f7573c 100644 --- a/ts/core/utils/shared-security-manager.ts +++ b/ts/core/utils/shared-security-manager.ts @@ -13,7 +13,8 @@ import { trackConnection, removeConnection, cleanupExpiredRateLimits, - parseBasicAuthHeader + parseBasicAuthHeader, + normalizeIP } from './security-utils.js'; /** @@ -78,7 +79,15 @@ export class SharedSecurityManager { * @returns Number of connections from this IP */ public getConnectionCountByIP(ip: string): number { - return this.connectionsByIP.get(ip)?.connections.size || 0; + // Check all normalized variants of the IP + const variants = normalizeIP(ip); + for (const variant of variants) { + const info = this.connectionsByIP.get(variant); + if (info) { + return info.connections.size; + } + } + return 0; } /** @@ -88,7 +97,19 @@ export class SharedSecurityManager { * @param connectionId - The connection ID to associate */ public trackConnectionByIP(ip: string, connectionId: string): void { - trackConnection(ip, connectionId, this.connectionsByIP); + // Check if any variant already exists + const variants = normalizeIP(ip); + let existingKey: string | null = null; + + for (const variant of variants) { + if (this.connectionsByIP.has(variant)) { + existingKey = variant; + break; + } + } + + // Use existing key or the original IP + trackConnection(existingKey || ip, connectionId, this.connectionsByIP); } /** @@ -98,7 +119,15 @@ export class SharedSecurityManager { * @param connectionId - The connection ID to remove */ public removeConnectionByIP(ip: string, connectionId: string): void { - removeConnection(ip, connectionId, this.connectionsByIP); + // Check all variants to find where the connection is tracked + const variants = normalizeIP(ip); + + for (const variant of variants) { + if (this.connectionsByIP.has(variant)) { + removeConnection(variant, connectionId, this.connectionsByIP); + break; + } + } } /**