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:
Philipp Kunz 2025-03-11 12:34:24 +00:00
parent 0674ca7163
commit 27a2bcb556
2 changed files with 189 additions and 5 deletions

View File

@ -65,6 +65,9 @@ export class NetworkProxy {
lastUsed: number;
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
*/

View File

@ -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();