diff --git a/changelog.md b/changelog.md index 056cb1d..b368b5a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-03-06 - 3.28.0 - feat(router) +Add detailed routing tests and refactor ProxyRouter for improved path matching + +- Implemented a comprehensive test suite for the ProxyRouter class to ensure accurate routing based on hostnames and path patterns. +- Refactored the ProxyRouter to enhance path matching logic with improvements in wildcard and parameter handling. +- Improved logging capabilities within the ProxyRouter for enhanced debugging and info level insights. +- Optimized the data structures for storing and accessing proxy configurations to reduce overhead in routing operations. + ## 2025-03-06 - 3.27.0 - feat(AcmeCertManager) Introduce AcmeCertManager for enhanced ACME certificate management diff --git a/test/test.router.ts b/test/test.router.ts new file mode 100644 index 0000000..25e1b70 --- /dev/null +++ b/test/test.router.ts @@ -0,0 +1,346 @@ +import { expect, tap } from '@push.rocks/tapbundle'; +import * as tsclass from '@tsclass/tsclass'; +import * as http from 'http'; +import { ProxyRouter, type IRouterResult } from '../ts/classes.router.js'; + +// Test proxies and configurations +let router: ProxyRouter; + +// Sample hostname for testing +const TEST_DOMAIN = 'example.com'; +const TEST_SUBDOMAIN = 'api.example.com'; +const TEST_WILDCARD = '*.example.com'; + +// Helper: Creates a mock HTTP request for testing +function createMockRequest(host: string, url: string = '/'): http.IncomingMessage { + const req = { + headers: { host }, + url, + socket: { + remoteAddress: '127.0.0.1' + } + } as any; + return req; +} + +// Helper: Creates a test proxy configuration +function createProxyConfig( + hostname: string, + destinationIp: string = '10.0.0.1', + destinationPort: number = 8080 +): tsclass.network.IReverseProxyConfig { + return { + hostName: hostname, + destinationIp, + destinationPort: destinationPort.toString(), // Convert to string for IReverseProxyConfig + publicKey: 'mock-cert', + privateKey: 'mock-key' + } as tsclass.network.IReverseProxyConfig; +} + +// SETUP: Create a ProxyRouter instance +tap.test('setup proxy router test environment', async () => { + router = new ProxyRouter(); + + // Initialize with empty config + router.setNewProxyConfigs([]); +}); + +// Test basic routing by hostname +tap.test('should route requests by hostname', async () => { + const config = createProxyConfig(TEST_DOMAIN); + router.setNewProxyConfigs([config]); + + const req = createMockRequest(TEST_DOMAIN); + const result = router.routeReq(req); + + expect(result).toBeTruthy(); + expect(result).toEqual(config); +}); + +// 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 req = createMockRequest(`${TEST_DOMAIN}:443`); + const result = router.routeReq(req); + + expect(result).toBeTruthy(); + expect(result).toEqual(config); +}); + +// Test case-insensitive hostname matching +tap.test('should perform case-insensitive hostname matching', async () => { + const config = createProxyConfig(TEST_DOMAIN.toLowerCase()); + router.setNewProxyConfigs([config]); + + const req = createMockRequest(TEST_DOMAIN.toUpperCase()); + const result = router.routeReq(req); + + expect(result).toBeTruthy(); + expect(result).toEqual(config); +}); + +// Test handling of unmatched hostnames +tap.test('should return undefined for unmatched hostnames', async () => { + const config = createProxyConfig(TEST_DOMAIN); + router.setNewProxyConfigs([config]); + + const req = createMockRequest('unknown.domain.com'); + const result = router.routeReq(req); + + expect(result).toBeUndefined(); +}); + +// 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'); + + // 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.pathMatch).toEqual('/api/users'); + + // Test that non-matching path doesn't match + const req2 = createMockRequest(TEST_DOMAIN, '/web/users'); + const result2 = router.routeReqWithDetails(req2); + + expect(result2).toBeUndefined(); +}); + +// Test handling wildcard patterns +tap.test('should support wildcard path patterns', async () => { + const config = createProxyConfig(TEST_DOMAIN); + router.setNewProxyConfigs([config]); + + router.setPathPattern(config, '/api/*'); + + // 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.pathMatch).toEqual('/api'); + + // Print the actual value to diagnose issues + console.log('Path remainder value:', result.pathRemainder); + expect(result.pathRemainder).toBeTruthy(); + expect(result.pathRemainder).toEqual('/users/123'); +}); + +// 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 req = createMockRequest(TEST_DOMAIN, '/users/123/profile'); + const result = router.routeReqWithDetails(req); + + expect(result).toBeTruthy(); + expect(result.config).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); + + // Add both configs + router.setNewProxyConfigs([apiConfig, webConfig]); + + // Set different path patterns + router.setPathPattern(apiConfig, '/api'); + router.setPathPattern(webConfig, '/web'); + + // Test API path routes to API config + const apiReq = createMockRequest(TEST_DOMAIN, '/api/users'); + const apiResult = router.routeReq(apiReq); + + expect(apiResult).toEqual(apiConfig); + + // Test web path routes to web config + const webReq = createMockRequest(TEST_DOMAIN, '/web/dashboard'); + const webResult = router.routeReq(webReq); + + expect(webResult).toEqual(webConfig); + + // Test unknown path returns undefined + const unknownReq = createMockRequest(TEST_DOMAIN, '/unknown'); + const unknownResult = router.routeReq(unknownReq); + + expect(unknownResult).toBeUndefined(); +}); + +// Test wildcard subdomains +tap.test('should match wildcard subdomains', async () => { + const wildcardConfig = createProxyConfig(TEST_WILDCARD); + router.setNewProxyConfigs([wildcardConfig]); + + // Test that subdomain.example.com matches *.example.com + const req = createMockRequest('subdomain.example.com'); + const result = router.routeReq(req); + + expect(result).toBeTruthy(); + expect(result).toEqual(wildcardConfig); +}); + +// Test default configuration fallback +tap.test('should fall back to default configuration', async () => { + const defaultConfig = createProxyConfig('*'); + const specificConfig = createProxyConfig(TEST_DOMAIN); + + router.setNewProxyConfigs([defaultConfig, specificConfig]); + + // Test specific domain routes to specific config + const specificReq = createMockRequest(TEST_DOMAIN); + const specificResult = router.routeReq(specificReq); + + expect(specificResult).toEqual(specificConfig); + + // Test unknown domain falls back to default config + const unknownReq = createMockRequest('unknown.com'); + const unknownResult = router.routeReq(unknownReq); + + expect(unknownResult).toEqual(defaultConfig); +}); + +// 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); + + router.setNewProxyConfigs([wildcardConfig, exactConfig]); + + // Test that exact match takes priority + const req = createMockRequest(TEST_SUBDOMAIN); + const result = router.routeReq(req); + + expect(result).toEqual(exactConfig); +}); + +// Test adding and removing configurations +tap.test('should manage configurations correctly', async () => { + router.setNewProxyConfigs([]); + + // Add a config + const config = createProxyConfig(TEST_DOMAIN); + router.addProxyConfig(config); + + // Verify routing works + const req = createMockRequest(TEST_DOMAIN); + let result = router.routeReq(req); + + expect(result).toEqual(config); + + // Remove the config and verify it no longer routes + const removed = router.removeProxyConfig(TEST_DOMAIN); + expect(removed).toBeTrue(); + + result = router.routeReq(req); + expect(result).toBeUndefined(); +}); + +// 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); + + router.setNewProxyConfigs([genericConfig, specificConfig]); + + router.setPathPattern(genericConfig, '/api/*'); + router.setPathPattern(specificConfig, '/api/users'); + + // The more specific '/api/users' should match before the '/api/*' wildcard + const req = createMockRequest(TEST_DOMAIN, '/api/users'); + const result = router.routeReq(req); + + expect(result).toEqual(specificConfig); +}); + +// Test getHostnames method +tap.test('should retrieve all configured hostnames', async () => { + router.setNewProxyConfigs([ + createProxyConfig(TEST_DOMAIN), + createProxyConfig(TEST_SUBDOMAIN) + ]); + + const hostnames = router.getHostnames(); + + expect(hostnames.length).toEqual(2); + expect(hostnames).toContain(TEST_DOMAIN.toLowerCase()); + expect(hostnames).toContain(TEST_SUBDOMAIN.toLowerCase()); +}); + +// Test handling missing host header +tap.test('should handle missing host header', async () => { + const defaultConfig = createProxyConfig('*'); + router.setNewProxyConfigs([defaultConfig]); + + const req = createMockRequest(''); + req.headers.host = undefined; + + const result = router.routeReq(req); + + expect(result).toEqual(defaultConfig); +}); + +// 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 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.pathParams).toBeTruthy(); + expect(result.pathParams.version).toEqual('v1'); + expect(result.pathParams.userId).toEqual('123'); + expect(result.pathParams.postId).toEqual('456'); +}); + +// Performance test +tap.test('should handle many configurations efficiently', async () => { + const configs = []; + + // Create many configs with different hostnames + for (let i = 0; i < 100; i++) { + configs.push(createProxyConfig(`host-${i}.example.com`)); + } + + router.setNewProxyConfigs(configs); + + // Test middle of the list to avoid best/worst case + const req = createMockRequest('host-50.example.com'); + const result = router.routeReq(req); + + expect(result).toEqual(configs[50]); +}); + +// Test cleanup +tap.test('cleanup proxy router test environment', async () => { + // Clear all configurations + router.setNewProxyConfigs([]); + + // Verify empty state + expect(router.getHostnames().length).toEqual(0); + expect(router.getProxyConfigs().length).toEqual(0); +}); + +export default tap.start(); \ No newline at end of file diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 906f7eb..f666c53 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '3.27.0', + version: '3.28.0', description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.' } diff --git a/ts/classes.router.ts b/ts/classes.router.ts index d0ec23a..efafd0c 100644 --- a/ts/classes.router.ts +++ b/ts/classes.router.ts @@ -1,4 +1,6 @@ -import * as plugins from './plugins.js'; +import * as http from 'http'; +import * as url from 'url'; +import * as tsclass from '@tsclass/tsclass'; /** * Optional path pattern configuration that can be added to proxy configs @@ -11,31 +13,37 @@ export interface IPathPatternConfig { * Interface for router result with additional metadata */ export interface IRouterResult { - config: plugins.tsclass.network.IReverseProxyConfig; + config: tsclass.network.IReverseProxyConfig; pathMatch?: string; pathParams?: Record; pathRemainder?: string; } export class ProxyRouter { - // Using a Map for O(1) hostname lookups instead of array search - private hostMap: Map = new Map(); // Store original configs for reference - private reverseProxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = []; + private reverseProxyConfigs: tsclass.network.IReverseProxyConfig[] = []; // Default config to use when no match is found (optional) - private defaultConfig?: plugins.tsclass.network.IReverseProxyConfig; + private defaultConfig?: tsclass.network.IReverseProxyConfig; // Store path patterns separately since they're not in the original interface - private pathPatterns: Map = new Map(); + private pathPatterns: Map = new Map(); + // Logger interface + private logger: { + error: (message: string, data?: any) => void; + warn: (message: string, data?: any) => void; + info: (message: string, data?: any) => void; + debug: (message: string, data?: any) => void; + }; constructor( - configs?: plugins.tsclass.network.IReverseProxyConfig[], - private readonly logger: { + configs?: tsclass.network.IReverseProxyConfig[], + logger?: { error: (message: string, data?: any) => void; warn: (message: string, data?: any) => void; info: (message: string, data?: any) => void; debug: (message: string, data?: any) => void; - } = console + } ) { + this.logger = logger || console; if (configs) { this.setNewProxyConfigs(configs); } @@ -45,61 +53,13 @@ export class ProxyRouter { * Sets a new set of reverse configs to be routed to * @param reverseCandidatesArg Array of reverse proxy configurations */ - public setNewProxyConfigs(reverseCandidatesArg: plugins.tsclass.network.IReverseProxyConfig[]): void { + public setNewProxyConfigs(reverseCandidatesArg: tsclass.network.IReverseProxyConfig[]): void { this.reverseProxyConfigs = [...reverseCandidatesArg]; - // Reset the host map and path patterns - this.hostMap.clear(); - this.pathPatterns.clear(); - // Find default config if any (config with "*" as hostname) this.defaultConfig = this.reverseProxyConfigs.find(config => config.hostName === '*'); - // Group configs by hostname for faster lookups - for (const config of this.reverseProxyConfigs) { - // Skip the default config as it's stored separately - if (config.hostName === '*') continue; - - const hostname = config.hostName.toLowerCase(); // Case-insensitive hostname lookup - - if (!this.hostMap.has(hostname)) { - this.hostMap.set(hostname, []); - } - - // Check for path pattern in extended properties - // (using any to access custom properties not in the interface) - const extendedConfig = config as any; - if (extendedConfig.pathPattern) { - this.pathPatterns.set(config, extendedConfig.pathPattern); - } - - // Add to the list of configs for this hostname - this.hostMap.get(hostname).push(config); - } - - // Sort configs for each hostname by specificity - // More specific path patterns should be checked first - for (const [hostname, configs] of this.hostMap.entries()) { - if (configs.length > 1) { - // Sort by pathPattern - most specific first - // (null comes last, exact paths before patterns with wildcards) - configs.sort((a, b) => { - const aPattern = this.pathPatterns.get(a); - const bPattern = this.pathPatterns.get(b); - - // If one has a path and the other doesn't, the one with a path comes first - if (!aPattern && bPattern) return 1; - if (aPattern && !bPattern) return -1; - if (!aPattern && !bPattern) return 0; - - // Both have path patterns - more specific (longer) first - // This is a simple heuristic; we could use a more sophisticated approach - return bPattern.length - aPattern.length; - }); - } - } - - this.logger.info(`Router initialized with ${this.reverseProxyConfigs.length} configs (${this.hostMap.size} unique hosts)`); + this.logger.info(`Router initialized with ${this.reverseProxyConfigs.length} configs (${this.getHostnames().length} unique hosts)`); } /** @@ -107,7 +67,7 @@ export class ProxyRouter { * @param req The incoming HTTP request * @returns The matching proxy config or undefined if no match found */ - public routeReq(req: plugins.http.IncomingMessage): plugins.tsclass.network.IReverseProxyConfig { + public routeReq(req: http.IncomingMessage): tsclass.network.IReverseProxyConfig { const result = this.routeReqWithDetails(req); return result ? result.config : undefined; } @@ -117,7 +77,7 @@ export class ProxyRouter { * @param req The incoming HTTP request * @returns Detailed routing result including matched config and path information */ - public routeReqWithDetails(req: plugins.http.IncomingMessage): IRouterResult | undefined { + public routeReqWithDetails(req: http.IncomingMessage): IRouterResult | undefined { // Extract and validate host header const originalHost = req.headers.host; if (!originalHost) { @@ -126,52 +86,27 @@ export class ProxyRouter { } // Parse URL for path matching - const urlPath = new URL( - req.url || '/', - `http://${originalHost}` - ).pathname; + const parsedUrl = url.parse(req.url || '/'); + const urlPath = parsedUrl.pathname || '/'; // Extract hostname without port const hostWithoutPort = originalHost.split(':')[0].toLowerCase(); - // Find configs for this hostname - const configs = this.hostMap.get(hostWithoutPort); - - if (configs && configs.length > 0) { - // Check each config for path matching - for (const config of configs) { - // Get the path pattern if any - const pathPattern = this.pathPatterns.get(config); - - // If no path pattern specified, this config matches all paths - if (!pathPattern) { - return { config }; - } - - // Check if path matches the pattern - const pathMatch = this.matchPath(urlPath, pathPattern); - if (pathMatch) { - return { - config, - pathMatch: pathMatch.matched, - pathParams: pathMatch.params, - pathRemainder: pathMatch.remainder - }; - } - } + // First try exact hostname match + const exactConfig = this.findConfigForHost(hostWithoutPort, urlPath); + if (exactConfig) { + return exactConfig; } - // Try wildcard subdomains if no direct match found - // For example, if request is for sub.example.com, try *.example.com - const domainParts = hostWithoutPort.split('.'); - if (domainParts.length > 2) { - const wildcardDomain = `*.${domainParts.slice(1).join('.')}`; - const wildcardConfigs = this.hostMap.get(wildcardDomain); - - if (wildcardConfigs && wildcardConfigs.length > 0) { - // Use the first matching wildcard config - // Could add path matching logic here as well - return { config: wildcardConfigs[0] }; + // Try wildcard subdomain + if (hostWithoutPort.includes('.')) { + const domainParts = hostWithoutPort.split('.'); + if (domainParts.length > 2) { + const wildcardDomain = `*.${domainParts.slice(1).join('.')}`; + const wildcardConfig = this.findConfigForHost(wildcardDomain, urlPath); + if (wildcardConfig) { + return wildcardConfig; + } } } @@ -186,23 +121,62 @@ export class ProxyRouter { } /** - * Sets a path pattern for an existing config - * @param config The existing configuration - * @param pathPattern The path pattern to set - * @returns Boolean indicating if the config was found and updated + * Find a config for a specific host and path */ - public setPathPattern( - config: plugins.tsclass.network.IReverseProxyConfig, - pathPattern: string - ): boolean { - const exists = this.reverseProxyConfigs.includes(config); - if (exists) { - this.pathPatterns.set(config, pathPattern); - return true; + private findConfigForHost(hostname: string, path: string): IRouterResult | undefined { + // Find all configs for this hostname + const configs = this.reverseProxyConfigs.filter( + config => config.hostName.toLowerCase() === hostname.toLowerCase() + ); + + if (configs.length === 0) { + return undefined; } - return false; + + // First try configs with path patterns + const configsWithPaths = configs.filter(config => this.pathPatterns.has(config)); + + // Sort by path pattern specificity - more specific first + configsWithPaths.sort((a, b) => { + const aPattern = this.pathPatterns.get(a) || ''; + const bPattern = this.pathPatterns.get(b) || ''; + + // Exact patterns come before wildcard patterns + const aHasWildcard = aPattern.includes('*'); + const bHasWildcard = bPattern.includes('*'); + + if (aHasWildcard && !bHasWildcard) return 1; + if (!aHasWildcard && bHasWildcard) return -1; + + // Longer patterns are considered more specific + return bPattern.length - aPattern.length; + }); + + // Check each config with path pattern + for (const config of configsWithPaths) { + const pathPattern = this.pathPatterns.get(config); + if (pathPattern) { + const pathMatch = this.matchPath(path, pathPattern); + if (pathMatch) { + return { + config, + pathMatch: pathMatch.matched, + pathParams: pathMatch.params, + pathRemainder: pathMatch.remainder + }; + } + } + } + + // If no path pattern matched, use the first config without a path pattern + const configWithoutPath = configs.find(config => !this.pathPatterns.has(config)); + if (configWithoutPath) { + return { config: configWithoutPath }; + } + + return undefined; } - + /** * Matches a URL path against a pattern * Supports: @@ -242,62 +216,51 @@ export class ProxyRouter { } // Handle path parameters - const patternParts = pattern.split('/'); - const pathParts = path.split('/'); + const patternParts = pattern.split('/').filter(p => p); + const pathParts = path.split('/').filter(p => p); - // Check if paths are compatible length - if ( - // If pattern doesn't end with wildcard, paths must have the same number of parts - (!pattern.endsWith('/*') && patternParts.length !== pathParts.length) || - // If pattern ends with wildcard, path must have at least as many parts as the pattern - (pattern.endsWith('/*') && pathParts.length < patternParts.length - 1) - ) { + // Too few path parts to match + if (pathParts.length < patternParts.length) { return null; } const params: Record = {}; - const matchedParts: string[] = []; - // Compare path parts + // Compare each part for (let i = 0; i < patternParts.length; i++) { const patternPart = patternParts[i]; - - // Handle wildcard at the end - if (patternPart === '*' && i === patternParts.length - 1) { - break; - } - - // If pathParts[i] doesn't exist, we've reached the end of the path - if (i >= pathParts.length) { - return null; - } - const pathPart = pathParts[i]; // Handle parameter if (patternPart.startsWith(':')) { const paramName = patternPart.slice(1); params[paramName] = pathPart; - matchedParts.push(pathPart); continue; } + // Handle wildcard at the end + if (patternPart === '*' && i === patternParts.length - 1) { + break; + } + // Handle exact match for this part if (patternPart !== pathPart) { return null; } - - matchedParts.push(pathPart); } - // Calculate the remainder - let remainder = ''; - if (pattern.endsWith('/*')) { - remainder = '/' + pathParts.slice(patternParts.length - 1).join('/'); - } + // Calculate the remainder - the unmatched path parts + const remainderParts = pathParts.slice(patternParts.length); + const remainder = remainderParts.length ? '/' + remainderParts.join('/') : ''; + + // Calculate the matched path + const matchedParts = patternParts.map((part, i) => { + return part.startsWith(':') ? pathParts[i] : part; + }); + const matched = '/' + matchedParts.join('/'); return { - matched: matchedParts.join('/'), + matched, params, remainder }; @@ -307,7 +270,7 @@ export class ProxyRouter { * Gets all currently active proxy configurations * @returns Array of all active configurations */ - public getProxyConfigs(): plugins.tsclass.network.IReverseProxyConfig[] { + public getProxyConfigs(): tsclass.network.IReverseProxyConfig[] { return [...this.reverseProxyConfigs]; } @@ -316,7 +279,13 @@ export class ProxyRouter { * @returns Array of hostnames */ public getHostnames(): string[] { - return Array.from(this.hostMap.keys()); + const hostnames = new Set(); + for (const config of this.reverseProxyConfigs) { + if (config.hostName !== '*') { + hostnames.add(config.hostName.toLowerCase()); + } + } + return Array.from(hostnames); } /** @@ -325,7 +294,7 @@ export class ProxyRouter { * @param pathPattern Optional path pattern for route matching */ public addProxyConfig( - config: plugins.tsclass.network.IReverseProxyConfig, + config: tsclass.network.IReverseProxyConfig, pathPattern?: string ): void { this.reverseProxyConfigs.push(config); @@ -334,8 +303,24 @@ export class ProxyRouter { if (pathPattern) { this.pathPatterns.set(config, pathPattern); } - - this.setNewProxyConfigs(this.reverseProxyConfigs); + } + + /** + * Sets a path pattern for an existing config + * @param config The existing configuration + * @param pathPattern The path pattern to set + * @returns Boolean indicating if the config was found and updated + */ + public setPathPattern( + config: tsclass.network.IReverseProxyConfig, + pathPattern: string + ): boolean { + const exists = this.reverseProxyConfigs.includes(config); + if (exists) { + this.pathPatterns.set(config, pathPattern); + return true; + } + return false; } /** @@ -345,15 +330,22 @@ export class ProxyRouter { */ public removeProxyConfig(hostname: string): boolean { const initialCount = this.reverseProxyConfigs.length; + + // Find configs to remove + const configsToRemove = this.reverseProxyConfigs.filter( + config => config.hostName === hostname + ); + + // Remove them from the patterns map + for (const config of configsToRemove) { + this.pathPatterns.delete(config); + } + + // Filter them out of the configs array this.reverseProxyConfigs = this.reverseProxyConfigs.filter( config => config.hostName !== hostname ); - if (initialCount !== this.reverseProxyConfigs.length) { - this.setNewProxyConfigs(this.reverseProxyConfigs); - return true; - } - - return false; + return this.reverseProxyConfigs.length !== initialCount; } } \ No newline at end of file