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:
parent
0674ca7163
commit
27a2bcb556
@ -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
|
||||
*/
|
||||
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user