import * as plugins from './plugins.js'; import type { IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js'; import { ConnectionManager } from './classes.pp.connectionmanager.js'; import { SecurityManager } from './classes.pp.securitymanager.js'; import { DomainConfigManager } from './classes.pp.domainconfigmanager.js'; import { TlsManager } from './classes.pp.tlsmanager.js'; import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js'; import { TimeoutManager } from './classes.pp.timeoutmanager.js'; import { AcmeManager } from './classes.pp.acmemanager.js'; import { PortRangeManager } from './classes.pp.portrangemanager.js'; import { ConnectionHandler } from './classes.pp.connectionhandler.js'; /** * PortProxy - Main class that coordinates all components */ export class PortProxy { private netServers: plugins.net.Server[] = []; private connectionLogger: NodeJS.Timeout | null = null; private isShuttingDown: boolean = false; // Component managers private connectionManager: ConnectionManager; private securityManager: SecurityManager; public domainConfigManager: DomainConfigManager; private tlsManager: TlsManager; private networkProxyBridge: NetworkProxyBridge; private timeoutManager: TimeoutManager; private acmeManager: AcmeManager; private portRangeManager: PortRangeManager; private connectionHandler: ConnectionHandler; constructor(settingsArg: IPortProxySettings) { // Set reasonable defaults for all settings this.settings = { ...settingsArg, targetIP: settingsArg.targetIP || 'localhost', initialDataTimeout: settingsArg.initialDataTimeout || 120000, socketTimeout: settingsArg.socketTimeout || 3600000, inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, maxConnectionLifetime: settingsArg.maxConnectionLifetime || 86400000, inactivityTimeout: settingsArg.inactivityTimeout || 14400000, gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true, keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true, keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, disableInactivityCheck: settingsArg.disableInactivityCheck || false, enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, enableDetailedLogging: settingsArg.enableDetailedLogging || false, enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, allowSessionTicket: settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true, maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, networkProxyPort: settingsArg.networkProxyPort || 8443, acme: settingsArg.acme || { enabled: false, port: 80, contactEmail: 'admin@example.com', useProduction: false, renewThresholdDays: 30, autoRenew: true, certificateStore: './certs', skipConfiguredCerts: false, }, }; // Initialize component managers this.timeoutManager = new TimeoutManager(this.settings); this.securityManager = new SecurityManager(this.settings); this.connectionManager = new ConnectionManager( this.settings, this.securityManager, this.timeoutManager ); this.domainConfigManager = new DomainConfigManager(this.settings); this.tlsManager = new TlsManager(this.settings); this.networkProxyBridge = new NetworkProxyBridge(this.settings); this.portRangeManager = new PortRangeManager(this.settings); this.acmeManager = new AcmeManager(this.settings, this.networkProxyBridge); // Initialize connection handler this.connectionHandler = new ConnectionHandler( this.settings, this.connectionManager, this.securityManager, this.domainConfigManager, this.tlsManager, this.networkProxyBridge, this.timeoutManager, this.portRangeManager ); } /** * The settings for the port proxy */ public settings: IPortProxySettings; /** * Start the proxy server */ public async start() { // Don't start if already shutting down if (this.isShuttingDown) { console.log("Cannot start PortProxy while it's shutting down"); return; } // Initialize and start NetworkProxy if needed if ( this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0 ) { await this.networkProxyBridge.initialize(); await this.networkProxyBridge.start(); } // Validate port configuration const configWarnings = this.portRangeManager.validateConfiguration(); if (configWarnings.length > 0) { console.log("Port configuration warnings:"); for (const warning of configWarnings) { console.log(` - ${warning}`); } } // Get listening ports from PortRangeManager const listeningPorts = this.portRangeManager.getListeningPorts(); // Create servers for each port for (const port of listeningPorts) { const server = plugins.net.createServer((socket) => { // Check if shutting down if (this.isShuttingDown) { socket.end(); socket.destroy(); return; } // Delegate to connection handler this.connectionHandler.handleConnection(socket); }).on('error', (err: Error) => { console.log(`Server Error on port ${port}: ${err.message}`); }); server.listen(port, () => { const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port); console.log( `PortProxy -> OK: Now listening on port ${port}${ this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : '' }${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}` ); }); this.netServers.push(server); } // Set up periodic connection logging and inactivity checks this.connectionLogger = setInterval(() => { // Immediately return if shutting down if (this.isShuttingDown) return; // Perform inactivity check this.connectionManager.performInactivityCheck(); // Log connection statistics const now = Date.now(); let maxIncoming = 0; let maxOutgoing = 0; let tlsConnections = 0; let nonTlsConnections = 0; let completedTlsHandshakes = 0; let pendingTlsHandshakes = 0; let keepAliveConnections = 0; let networkProxyConnections = 0; // Get connection records for analysis const connectionRecords = this.connectionManager.getConnections(); // Analyze active connections for (const record of connectionRecords.values()) { // Track connection stats if (record.isTLS) { tlsConnections++; if (record.tlsHandshakeComplete) { completedTlsHandshakes++; } else { pendingTlsHandshakes++; } } else { nonTlsConnections++; } if (record.hasKeepAlive) { keepAliveConnections++; } if (record.usingNetworkProxy) { networkProxyConnections++; } maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime); if (record.outgoingStartTime) { maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime); } } // Get termination stats const terminationStats = this.connectionManager.getTerminationStats(); // Log detailed stats console.log( `Active connections: ${connectionRecords.size}. ` + `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` + `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` + `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` + `Termination stats: ${JSON.stringify({ IN: terminationStats.incoming, OUT: terminationStats.outgoing, })}` ); }, this.settings.inactivityCheckInterval || 60000); // Make sure the interval doesn't keep the process alive if (this.connectionLogger.unref) { this.connectionLogger.unref(); } } /** * Stop the proxy server */ public async stop() { console.log('PortProxy shutting down...'); this.isShuttingDown = true; // Stop accepting new connections const closeServerPromises: Promise[] = this.netServers.map( (server) => new Promise((resolve) => { if (!server.listening) { resolve(); return; } server.close((err) => { if (err) { console.log(`Error closing server: ${err.message}`); } resolve(); }); }) ); // Stop the connection logger if (this.connectionLogger) { clearInterval(this.connectionLogger); this.connectionLogger = null; } // Wait for servers to close await Promise.all(closeServerPromises); console.log('All servers closed. Cleaning up active connections...'); // Clean up all active connections this.connectionManager.clearConnections(); // Stop NetworkProxy await this.networkProxyBridge.stop(); // Clear all servers this.netServers = []; console.log('PortProxy shutdown complete.'); } /** * Updates the domain configurations for the proxy */ public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise { console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`); // Update domain configs in DomainConfigManager this.domainConfigManager.updateDomainConfigs(newDomainConfigs); // If NetworkProxy is initialized, resync the configurations if (this.networkProxyBridge.getNetworkProxy()) { await this.networkProxyBridge.syncDomainConfigsToNetworkProxy(); } } /** * Updates the ACME certificate settings */ public async updateAcmeSettings(acmeSettings: IPortProxySettings['acme']): Promise { console.log('Updating ACME certificate settings'); // Delegate to AcmeManager await this.acmeManager.updateAcmeSettings(acmeSettings); } /** * Requests a certificate for a specific domain */ public async requestCertificate(domain: string): Promise { // Delegate to AcmeManager return this.acmeManager.requestCertificate(domain); } /** * Get statistics about current connections */ public getStatistics(): any { const connectionRecords = this.connectionManager.getConnections(); const terminationStats = this.connectionManager.getTerminationStats(); let tlsConnections = 0; let nonTlsConnections = 0; let keepAliveConnections = 0; let networkProxyConnections = 0; // Analyze active connections for (const record of connectionRecords.values()) { if (record.isTLS) tlsConnections++; else nonTlsConnections++; if (record.hasKeepAlive) keepAliveConnections++; if (record.usingNetworkProxy) networkProxyConnections++; } return { activeConnections: connectionRecords.size, tlsConnections, nonTlsConnections, keepAliveConnections, networkProxyConnections, terminationStats }; } }