update
This commit is contained in:
		| @@ -232,10 +232,10 @@ tap.test('should start the proxy server', async () => { | ||||
|       }, | ||||
|       action: { | ||||
|         type: 'forward', | ||||
|         target: { | ||||
|         targets: [{ | ||||
|           host: 'localhost', | ||||
|           port: 3100 | ||||
|         }, | ||||
|         }], | ||||
|         tls: { | ||||
|           mode: 'terminate' | ||||
|         }, | ||||
|   | ||||
| @@ -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 }] | ||||
|         } | ||||
|       } | ||||
|     ], | ||||
|   | ||||
| @@ -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(); | ||||
|   } | ||||
|    | ||||
|   | ||||
| @@ -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<IRouteConfig> = { | ||||
|     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<IRouteConfig> = { | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|   | ||||
		Reference in New Issue
	
	Block a user