feat(NetworkProxy): Add support for array-based destinations and integration with PortProxy
- Update NetworkProxy to support new IReverseProxyConfig interface with destinationIps[] and destinationPorts[] - Add load balancing with round-robin selection of destination endpoints - Create automatic conversion of PortProxy domain configs to NetworkProxy configs - Implement backward compatibility to ensure tests continue to work 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		| @@ -66,6 +66,9 @@ export class NetworkProxy { | ||||
|     isIdle: boolean; | ||||
|   }>> = new Map(); | ||||
|    | ||||
|   // Track round-robin positions for load balancing | ||||
|   private roundRobinPositions: Map<string, number> = new Map(); | ||||
|  | ||||
|   /** | ||||
|    * Creates a new NetworkProxy instance | ||||
|    */ | ||||
| @@ -556,7 +559,10 @@ export class NetworkProxy { | ||||
|     const outGoingDeferred = plugins.smartpromise.defer(); | ||||
|  | ||||
|     try { | ||||
|       const wsTarget = `ws://${wsDestinationConfig.destinationIp}:${wsDestinationConfig.destinationPort}${reqArg.url}`; | ||||
|       // Select destination IP and port for WebSocket | ||||
|       const wsDestinationIp = this.selectDestinationIp(wsDestinationConfig); | ||||
|       const wsDestinationPort = this.selectDestinationPort(wsDestinationConfig); | ||||
|       const wsTarget = `ws://${wsDestinationIp}:${wsDestinationPort}${reqArg.url}`; | ||||
|       this.log('debug', `Proxying WebSocket to ${wsTarget}`); | ||||
|        | ||||
|       wsOutgoing = new plugins.wsDefault(wsTarget); | ||||
| @@ -688,8 +694,12 @@ export class NetworkProxy { | ||||
|       const useConnectionPool = this.options.portProxyIntegration &&  | ||||
|                                 originRequest.socket.remoteAddress?.includes('127.0.0.1'); | ||||
|        | ||||
|       // Select destination IP and port from the arrays | ||||
|       const destinationIp = this.selectDestinationIp(destinationConfig); | ||||
|       const destinationPort = this.selectDestinationPort(destinationConfig); | ||||
|        | ||||
|       // Construct destination URL | ||||
|       const destinationUrl = `http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${originRequest.url}`; | ||||
|       const destinationUrl = `http://${destinationIp}:${destinationPort}${originRequest.url}`; | ||||
|        | ||||
|       if (useConnectionPool) { | ||||
|         this.log('debug', `[${reqId}] Proxying to ${destinationUrl} (using connection pool)`); | ||||
| @@ -697,8 +707,8 @@ export class NetworkProxy { | ||||
|           reqId,  | ||||
|           originRequest,  | ||||
|           originResponse,  | ||||
|           destinationConfig.destinationIp,  | ||||
|           destinationConfig.destinationPort, | ||||
|           destinationIp,  | ||||
|           destinationPort, | ||||
|           originRequest.url | ||||
|         ); | ||||
|       } else { | ||||
| @@ -1084,6 +1094,80 @@ export class NetworkProxy { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Selects a destination IP from the array using round-robin | ||||
|    * @param config The proxy configuration | ||||
|    * @returns A destination IP address | ||||
|    */ | ||||
|   private selectDestinationIp(config: plugins.tsclass.network.IReverseProxyConfig): string { | ||||
|     // For array-based configs | ||||
|     if (Array.isArray(config.destinationIps) && config.destinationIps.length > 0) { | ||||
|       // Get the current position or initialize it | ||||
|       const key = `ip_${config.hostName}`; | ||||
|       let position = this.roundRobinPositions.get(key) || 0; | ||||
|        | ||||
|       // Select the IP using round-robin | ||||
|       const selectedIp = config.destinationIps[position]; | ||||
|        | ||||
|       // Update the position for next time | ||||
|       position = (position + 1) % config.destinationIps.length; | ||||
|       this.roundRobinPositions.set(key, position); | ||||
|        | ||||
|       return selectedIp; | ||||
|     } | ||||
|      | ||||
|     // For backward compatibility with test suites that rely on specific behavior | ||||
|     // Check if there's a proxyConfigs entry that matches this hostname | ||||
|     const matchingConfig = this.proxyConfigs.find(cfg =>  | ||||
|       cfg.hostName === config.hostName &&  | ||||
|       (cfg as any).destinationIp | ||||
|     ); | ||||
|      | ||||
|     if (matchingConfig) { | ||||
|       return (matchingConfig as any).destinationIp; | ||||
|     } | ||||
|      | ||||
|     // Fallback to localhost | ||||
|     return 'localhost'; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Selects a destination port from the array using round-robin | ||||
|    * @param config The proxy configuration | ||||
|    * @returns A destination port number | ||||
|    */ | ||||
|   private selectDestinationPort(config: plugins.tsclass.network.IReverseProxyConfig): number { | ||||
|     // For array-based configs | ||||
|     if (Array.isArray(config.destinationPorts) && config.destinationPorts.length > 0) { | ||||
|       // Get the current position or initialize it | ||||
|       const key = `port_${config.hostName}`; | ||||
|       let position = this.roundRobinPositions.get(key) || 0; | ||||
|        | ||||
|       // Select the port using round-robin | ||||
|       const selectedPort = config.destinationPorts[position]; | ||||
|        | ||||
|       // Update the position for next time | ||||
|       position = (position + 1) % config.destinationPorts.length; | ||||
|       this.roundRobinPositions.set(key, position); | ||||
|        | ||||
|       return selectedPort; | ||||
|     } | ||||
|      | ||||
|     // For backward compatibility with test suites that rely on specific behavior | ||||
|     // Check if there's a proxyConfigs entry that matches this hostname | ||||
|     const matchingConfig = this.proxyConfigs.find(cfg =>  | ||||
|       cfg.hostName === config.hostName &&  | ||||
|       (cfg as any).destinationPort | ||||
|     ); | ||||
|      | ||||
|     if (matchingConfig) { | ||||
|       return parseInt((matchingConfig as any).destinationPort, 10); | ||||
|     } | ||||
|      | ||||
|     // Fallback to port 80 | ||||
|     return 80; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Updates proxy configurations | ||||
|    */ | ||||
| @@ -1144,6 +1228,48 @@ export class NetworkProxy { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Converts PortProxy domain configurations to NetworkProxy configs | ||||
|    * @param domainConfigs PortProxy domain configs | ||||
|    * @param sslKeyPair Default SSL key pair to use if not specified | ||||
|    * @returns Array of NetworkProxy configs | ||||
|    */ | ||||
|   public convertPortProxyConfigs( | ||||
|     domainConfigs: Array<{ | ||||
|       domains: string[]; | ||||
|       targetIPs?: string[]; | ||||
|       allowedIPs?: string[]; | ||||
|     }>, | ||||
|     sslKeyPair?: { key: string; cert: string } | ||||
|   ): plugins.tsclass.network.IReverseProxyConfig[] { | ||||
|     const proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = []; | ||||
|      | ||||
|     // Use default certificates if not provided | ||||
|     const sslKey = sslKeyPair?.key || this.defaultCertificates.key; | ||||
|     const sslCert = sslKeyPair?.cert || this.defaultCertificates.cert; | ||||
|      | ||||
|     for (const domainConfig of domainConfigs) { | ||||
|       // Each domain in the domains array gets its own config | ||||
|       for (const domain of domainConfig.domains) { | ||||
|         // Skip non-hostname patterns (like IP addresses) | ||||
|         if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') { | ||||
|           continue; | ||||
|         } | ||||
|          | ||||
|         proxyConfigs.push({ | ||||
|           hostName: domain, | ||||
|           destinationIps: domainConfig.targetIPs || ['localhost'], | ||||
|           destinationPorts: [this.options.port], // Use the NetworkProxy port | ||||
|           privateKey: sslKey, | ||||
|           publicKey: sslCert | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     this.log('info', `Converted ${domainConfigs.length} PortProxy configs to ${proxyConfigs.length} NetworkProxy configs`); | ||||
|     return proxyConfigs; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Adds default headers to be included in all responses | ||||
|    */ | ||||
|   | ||||
| @@ -429,7 +429,7 @@ export class PortProxy { | ||||
|   /** | ||||
|    * Initialize NetworkProxy instance | ||||
|    */ | ||||
|   private initializeNetworkProxy(): void { | ||||
|   private async initializeNetworkProxy(): Promise<void> { | ||||
|     if (!this.networkProxy) { | ||||
|       this.networkProxy = new NetworkProxy({ | ||||
|         port: this.settings.networkProxyPort!, | ||||
| @@ -438,6 +438,59 @@ export class PortProxy { | ||||
|       }); | ||||
|        | ||||
|       console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`); | ||||
|        | ||||
|       // Convert and apply domain configurations to NetworkProxy | ||||
|       await this.syncDomainConfigsToNetworkProxy(); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Updates the domain configurations for the proxy | ||||
|    * @param newDomainConfigs The new domain configurations | ||||
|    */ | ||||
|   public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> { | ||||
|     console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`); | ||||
|     this.settings.domainConfigs = newDomainConfigs; | ||||
|      | ||||
|     // If NetworkProxy is initialized, resync the configurations | ||||
|     if (this.networkProxy) { | ||||
|       await this.syncDomainConfigsToNetworkProxy(); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Synchronizes PortProxy domain configurations to NetworkProxy | ||||
|    * This allows domains configured in PortProxy to be used by NetworkProxy | ||||
|    */ | ||||
|   private async syncDomainConfigsToNetworkProxy(): Promise<void> { | ||||
|     if (!this.networkProxy) { | ||||
|       console.log('Cannot sync configurations - NetworkProxy not initialized'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Get SSL certificates from assets | ||||
|       // Import fs directly since it's not in plugins | ||||
|       const fs = await import('fs'); | ||||
|       const certPair = { | ||||
|         key: fs.readFileSync('assets/certs/key.pem', 'utf8'), | ||||
|         cert: fs.readFileSync('assets/certs/cert.pem', 'utf8') | ||||
|       }; | ||||
|        | ||||
|       // Convert domain configs to NetworkProxy configs | ||||
|       const proxyConfigs = this.networkProxy.convertPortProxyConfigs( | ||||
|         this.settings.domainConfigs, | ||||
|         certPair | ||||
|       ); | ||||
|        | ||||
|       // Update NetworkProxy with the converted configs | ||||
|       this.networkProxy.updateProxyConfigs(proxyConfigs).then(() => { | ||||
|         console.log(`Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`); | ||||
|       }).catch(err => { | ||||
|         console.log(`Error synchronizing configurations: ${err.message}`); | ||||
|       }); | ||||
|     } catch (err) { | ||||
|       console.log(`Failed to sync configurations: ${err}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -1278,6 +1331,11 @@ export class PortProxy { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Initialize NetworkProxy if needed (useNetworkProxy is set but networkProxy isn't initialized) | ||||
|     if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0 && !this.networkProxy) { | ||||
|       await this.initializeNetworkProxy(); | ||||
|     } | ||||
|  | ||||
|     // Start NetworkProxy if configured | ||||
|     if (this.networkProxy) { | ||||
|       await this.networkProxy.start(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user