import * as plugins from '../plugins.js'; import { type INetworkProxyOptions, type ILogger, createLogger, type IReverseProxyConfig } from './classes.np.types.js'; import { CertificateManager } from './classes.np.certificatemanager.js'; import { ConnectionPool } from './classes.np.connectionpool.js'; import { RequestHandler, type IMetricsTracker } from './classes.np.requesthandler.js'; import { WebSocketHandler } from './classes.np.websockethandler.js'; import { ProxyRouter } from '../classes.router.js'; import { Port80Handler } from '../port80handler/classes.port80handler.js'; /** * NetworkProxy provides a reverse proxy with TLS termination, WebSocket support, * automatic certificate management, and high-performance connection pooling. */ export class NetworkProxy implements IMetricsTracker { // Configuration public options: INetworkProxyOptions; public proxyConfigs: IReverseProxyConfig[] = []; // Server instances public httpsServer: plugins.https.Server; // Core components private certificateManager: CertificateManager; private connectionPool: ConnectionPool; private requestHandler: RequestHandler; private webSocketHandler: WebSocketHandler; private router = new ProxyRouter(); // State tracking public socketMap = new plugins.lik.ObjectMap(); public activeContexts: Set = new Set(); public connectedClients: number = 0; public startTime: number = 0; public requestsServed: number = 0; public failedRequests: number = 0; // Tracking for PortProxy integration private portProxyConnections: number = 0; private tlsTerminatedConnections: number = 0; // Timers private metricsInterval: NodeJS.Timeout; private connectionPoolCleanupInterval: NodeJS.Timeout; // Logger private logger: ILogger; /** * Creates a new NetworkProxy instance */ constructor(optionsArg: INetworkProxyOptions) { // Set default options this.options = { port: optionsArg.port, maxConnections: optionsArg.maxConnections || 10000, keepAliveTimeout: optionsArg.keepAliveTimeout || 120000, // 2 minutes headersTimeout: optionsArg.headersTimeout || 60000, // 1 minute logLevel: optionsArg.logLevel || 'info', cors: optionsArg.cors || { allowOrigin: '*', allowMethods: 'GET, POST, PUT, DELETE, OPTIONS', allowHeaders: 'Content-Type, Authorization', maxAge: 86400 }, // Defaults for PortProxy integration connectionPoolSize: optionsArg.connectionPoolSize || 50, portProxyIntegration: optionsArg.portProxyIntegration || false, useExternalPort80Handler: optionsArg.useExternalPort80Handler || false, // Default ACME options acme: { enabled: optionsArg.acme?.enabled || false, port: optionsArg.acme?.port || 80, contactEmail: optionsArg.acme?.contactEmail || 'admin@example.com', useProduction: optionsArg.acme?.useProduction || false, // Default to staging for safety renewThresholdDays: optionsArg.acme?.renewThresholdDays || 30, autoRenew: optionsArg.acme?.autoRenew !== false, // Default to true certificateStore: optionsArg.acme?.certificateStore || './certs', skipConfiguredCerts: optionsArg.acme?.skipConfiguredCerts || false } }; // Initialize logger this.logger = createLogger(this.options.logLevel); // Initialize components this.certificateManager = new CertificateManager(this.options); this.connectionPool = new ConnectionPool(this.options); this.requestHandler = new RequestHandler(this.options, this.connectionPool, this.router); this.webSocketHandler = new WebSocketHandler(this.options, this.connectionPool, this.router); // Connect request handler to this metrics tracker this.requestHandler.setMetricsTracker(this); } /** * Implements IMetricsTracker interface to increment request counters */ public incrementRequestsServed(): void { this.requestsServed++; } /** * Implements IMetricsTracker interface to increment failed request counters */ public incrementFailedRequests(): void { this.failedRequests++; } /** * Returns the port number this NetworkProxy is listening on * Useful for PortProxy to determine where to forward connections */ public getListeningPort(): number { return this.options.port; } /** * Updates the server capacity settings * @param maxConnections Maximum number of simultaneous connections * @param keepAliveTimeout Keep-alive timeout in milliseconds * @param connectionPoolSize Size of the connection pool per backend */ public updateCapacity(maxConnections?: number, keepAliveTimeout?: number, connectionPoolSize?: number): void { if (maxConnections !== undefined) { this.options.maxConnections = maxConnections; this.logger.info(`Updated max connections to ${maxConnections}`); } if (keepAliveTimeout !== undefined) { this.options.keepAliveTimeout = keepAliveTimeout; if (this.httpsServer) { this.httpsServer.keepAliveTimeout = keepAliveTimeout; this.logger.info(`Updated keep-alive timeout to ${keepAliveTimeout}ms`); } } if (connectionPoolSize !== undefined) { this.options.connectionPoolSize = connectionPoolSize; this.logger.info(`Updated connection pool size to ${connectionPoolSize}`); // Clean up excess connections in the pool this.connectionPool.cleanupConnectionPool(); } } /** * Returns current server metrics * Useful for PortProxy to determine which NetworkProxy to use for load balancing */ public getMetrics(): any { return { activeConnections: this.connectedClients, totalRequests: this.requestsServed, failedRequests: this.failedRequests, portProxyConnections: this.portProxyConnections, tlsTerminatedConnections: this.tlsTerminatedConnections, connectionPoolSize: this.connectionPool.getPoolStatus(), uptime: Math.floor((Date.now() - this.startTime) / 1000), memoryUsage: process.memoryUsage(), activeWebSockets: this.webSocketHandler.getConnectionInfo().activeConnections }; } /** * Sets an external Port80Handler for certificate management * This allows the NetworkProxy to use a centrally managed Port80Handler * instead of creating its own * * @param handler The Port80Handler instance to use */ public setExternalPort80Handler(handler: Port80Handler): void { // Connect it to the certificate manager this.certificateManager.setExternalPort80Handler(handler); } /** * Starts the proxy server */ public async start(): Promise { this.startTime = Date.now(); // Initialize Port80Handler if enabled and not using external handler if (this.options.acme?.enabled && !this.options.useExternalPort80Handler) { await this.certificateManager.initializePort80Handler(); } // Create the HTTPS server this.httpsServer = plugins.https.createServer( { key: this.certificateManager.getDefaultCertificates().key, cert: this.certificateManager.getDefaultCertificates().cert, SNICallback: (domain, cb) => this.certificateManager.handleSNI(domain, cb) }, (req, res) => this.requestHandler.handleRequest(req, res) ); // Configure server timeouts this.httpsServer.keepAliveTimeout = this.options.keepAliveTimeout; this.httpsServer.headersTimeout = this.options.headersTimeout; // Setup connection tracking this.setupConnectionTracking(); // Share HTTPS server with certificate manager this.certificateManager.setHttpsServer(this.httpsServer); // Setup WebSocket support this.webSocketHandler.initialize(this.httpsServer); // Start metrics collection this.setupMetricsCollection(); // Setup connection pool cleanup interval this.connectionPoolCleanupInterval = this.connectionPool.setupPeriodicCleanup(); // Start the server return new Promise((resolve) => { this.httpsServer.listen(this.options.port, () => { this.logger.info(`NetworkProxy started on port ${this.options.port}`); resolve(); }); }); } /** * Sets up tracking of TCP connections */ private setupConnectionTracking(): void { this.httpsServer.on('connection', (connection: plugins.net.Socket) => { // Check if max connections reached if (this.socketMap.getArray().length >= this.options.maxConnections) { this.logger.warn(`Max connections (${this.options.maxConnections}) reached, rejecting new connection`); connection.destroy(); return; } // Add connection to tracking this.socketMap.add(connection); this.connectedClients = this.socketMap.getArray().length; // Check for connection from PortProxy by inspecting the source port const localPort = connection.localPort || 0; const remotePort = connection.remotePort || 0; // If this connection is from a PortProxy (usually indicated by it coming from localhost) if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) { this.portProxyConnections++; this.logger.debug(`New connection from PortProxy (local: ${localPort}, remote: ${remotePort})`); } else { this.logger.debug(`New direct connection (local: ${localPort}, remote: ${remotePort})`); } // Setup connection cleanup handlers const cleanupConnection = () => { if (this.socketMap.checkForObject(connection)) { this.socketMap.remove(connection); this.connectedClients = this.socketMap.getArray().length; // If this was a PortProxy connection, decrement the counter if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) { this.portProxyConnections--; } this.logger.debug(`Connection closed. ${this.connectedClients} connections remaining`); } }; connection.on('close', cleanupConnection); connection.on('error', (err) => { this.logger.debug('Connection error', err); cleanupConnection(); }); connection.on('end', cleanupConnection); }); // Track TLS handshake completions this.httpsServer.on('secureConnection', (tlsSocket) => { this.tlsTerminatedConnections++; this.logger.debug('TLS handshake completed, connection secured'); }); } /** * Sets up metrics collection */ private setupMetricsCollection(): void { this.metricsInterval = setInterval(() => { const uptime = Math.floor((Date.now() - this.startTime) / 1000); const metrics = { uptime, activeConnections: this.connectedClients, totalRequests: this.requestsServed, failedRequests: this.failedRequests, portProxyConnections: this.portProxyConnections, tlsTerminatedConnections: this.tlsTerminatedConnections, activeWebSockets: this.webSocketHandler.getConnectionInfo().activeConnections, memoryUsage: process.memoryUsage(), activeContexts: Array.from(this.activeContexts), connectionPool: this.connectionPool.getPoolStatus() }; this.logger.debug('Proxy metrics', metrics); }, 60000); // Log metrics every minute // Don't keep process alive just for metrics if (this.metricsInterval.unref) { this.metricsInterval.unref(); } } /** * Updates proxy configurations */ public async updateProxyConfigs( proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[] ): Promise { this.logger.info(`Updating proxy configurations (${proxyConfigsArg.length} configs)`); // Update internal configs this.proxyConfigs = proxyConfigsArg; this.router.setNewProxyConfigs(proxyConfigsArg); // Collect all hostnames for cleanup later const currentHostNames = new Set(); // Add/update SSL contexts for each host for (const config of proxyConfigsArg) { currentHostNames.add(config.hostName); try { // Update certificate in cache this.certificateManager.updateCertificateCache( config.hostName, config.publicKey, config.privateKey ); this.activeContexts.add(config.hostName); } catch (error) { this.logger.error(`Failed to add SSL context for ${config.hostName}`, error); } } // Clean up removed contexts for (const hostname of this.activeContexts) { if (!currentHostNames.has(hostname)) { this.logger.info(`Hostname ${hostname} removed from configuration`); this.activeContexts.delete(hostname); } } // Register domains with Port80Handler if available const domainsForACME = Array.from(currentHostNames) .filter(domain => !domain.includes('*')); // Skip wildcard domains this.certificateManager.registerDomainsWithPort80Handler(domainsForACME); } /** * 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 defaultCerts = this.certificateManager.getDefaultCertificates(); const sslKey = sslKeyPair?.key || defaultCerts.key; const sslCert = sslKeyPair?.cert || defaultCerts.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.logger.info(`Converted ${domainConfigs.length} PortProxy configs to ${proxyConfigs.length} NetworkProxy configs`); return proxyConfigs; } /** * Adds default headers to be included in all responses */ public async addDefaultHeaders(headersArg: { [key: string]: string }): Promise { this.logger.info('Adding default headers', headersArg); this.requestHandler.setDefaultHeaders(headersArg); } /** * Stops the proxy server */ public async stop(): Promise { this.logger.info('Stopping NetworkProxy server'); // Clear intervals if (this.metricsInterval) { clearInterval(this.metricsInterval); } if (this.connectionPoolCleanupInterval) { clearInterval(this.connectionPoolCleanupInterval); } // Stop WebSocket handler this.webSocketHandler.shutdown(); // Close all tracked sockets for (const socket of this.socketMap.getArray()) { try { socket.destroy(); } catch (error) { this.logger.error('Error destroying socket', error); } } // Close all connection pool connections this.connectionPool.closeAllConnections(); // Stop Port80Handler if internally managed await this.certificateManager.stopPort80Handler(); // Close the HTTPS server return new Promise((resolve) => { this.httpsServer.close(() => { this.logger.info('NetworkProxy server stopped successfully'); resolve(); }); }); } /** * Requests a new certificate for a domain * This can be used to manually trigger certificate issuance * @param domain The domain to request a certificate for * @returns A promise that resolves when the request is submitted (not when the certificate is issued) */ public async requestCertificate(domain: string): Promise { return this.certificateManager.requestCertificate(domain); } /** * Gets all proxy configurations currently in use */ public getProxyConfigs(): plugins.tsclass.network.IReverseProxyConfig[] { return [...this.proxyConfigs]; } }