import * as plugins from '../../plugins.js'; import { createLogger, RouteManager, convertLegacyConfigToRouteConfig } from './models/types.js'; import type { INetworkProxyOptions, ILogger, IReverseProxyConfig } from './models/types.js'; import type { IRouteConfig } from '../smart-proxy/models/route-types.js'; import type { IRouteContext, IHttpRouteContext } from '../../core/models/route-context.js'; import { createBaseRouteContext } from '../../core/models/route-context.js'; import { CertificateManager } from './certificate-manager.js'; import { ConnectionPool } from './connection-pool.js'; import { RequestHandler, type IMetricsTracker } from './request-handler.js'; import { WebSocketHandler } from './websocket-handler.js'; import { ProxyRouter } from '../../http/router/index.js'; import { RouteRouter } from '../../http/router/route-router.js'; import { Port80Handler } from '../../http/port80/port80-handler.js'; import { FunctionCache } from './function-cache.js'; /** * NetworkProxy provides a reverse proxy with TLS termination, WebSocket support, * automatic certificate management, and high-performance connection pooling. */ export class NetworkProxy implements IMetricsTracker { // Provide a minimal JSON representation to avoid circular references during deep equality checks public toJSON(): any { return {}; } // Configuration public options: INetworkProxyOptions; public routes: IRouteConfig[] = []; // Server instances (HTTP/2 with HTTP/1 fallback) public httpsServer: any; // Core components private certificateManager: CertificateManager; private connectionPool: ConnectionPool; private requestHandler: RequestHandler; private webSocketHandler: WebSocketHandler; private legacyRouter = new ProxyRouter(); // Legacy router for backward compatibility private router = new RouteRouter(); // New modern router private routeManager: RouteManager; private functionCache: FunctionCache; // 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 SmartProxy 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 SmartProxy integration connectionPoolSize: optionsArg.connectionPoolSize || 50, portProxyIntegration: optionsArg.portProxyIntegration || false, useExternalPort80Handler: optionsArg.useExternalPort80Handler || false, // Backend protocol (http1 or http2) backendProtocol: optionsArg.backendProtocol || 'http1', // Default ACME options acme: { enabled: optionsArg.acme?.enabled || false, port: optionsArg.acme?.port || 80, accountEmail: optionsArg.acme?.accountEmail || '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 route manager this.routeManager = new RouteManager(this.logger); // Initialize function cache this.functionCache = new FunctionCache(this.logger, { maxCacheSize: this.options.functionCacheSize || 1000, defaultTtl: this.options.functionCacheTtl || 5000 }); // Initialize other components this.certificateManager = new CertificateManager(this.options); this.connectionPool = new ConnectionPool(this.options); this.requestHandler = new RequestHandler( this.options, this.connectionPool, this.legacyRouter, // Still use legacy router for backward compatibility this.routeManager, this.functionCache, this.router // Pass the new modern router as well ); this.webSocketHandler = new WebSocketHandler( this.options, this.connectionPool, this.legacyRouter, this.routes // Pass current routes to WebSocketHandler ); // Connect request handler to this metrics tracker this.requestHandler.setMetricsTracker(this); // Initialize with any provided routes if (this.options.routes && this.options.routes.length > 0) { this.updateRouteConfigs(this.options.routes); } } /** * 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 SmartProxy to determine where to forward connections */ public getListeningPort(): number { // If the server is running, get the actual listening port if (this.httpsServer && this.httpsServer.address()) { const address = this.httpsServer.address(); if (address && typeof address === 'object' && 'port' in address) { return address.port; } } // Fallback to configured port 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 SmartProxy 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, functionCache: this.functionCache.getStats() }; } /** * 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 HTTP/2 server with HTTP/1 fallback this.httpsServer = plugins.http2.createSecureServer( { key: this.certificateManager.getDefaultCertificates().key, cert: this.certificateManager.getDefaultCertificates().cert, allowHTTP1: true, ALPNProtocols: ['h2', 'http/1.1'] } ); // Track raw TCP connections for metrics and limits this.setupConnectionTracking(); // Handle incoming HTTP/2 streams this.httpsServer.on('stream', (stream: any, headers: any) => { this.requestHandler.handleHttp2(stream, headers); }); // Handle HTTP/1.x fallback requests this.httpsServer.on('request', (req: any, res: any) => { this.requestHandler.handleRequest(req, res); }); // Share server with certificate manager for dynamic contexts this.certificateManager.setHttpsServer(this.httpsServer); // Setup WebSocket support on HTTP/1 fallback this.webSocketHandler.initialize(this.httpsServer); // Start metrics logging this.setupMetricsCollection(); // Start periodic connection pool cleanup 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 SmartProxy by inspecting the source port const localPort = connection.localPort || 0; const remotePort = connection.remotePort || 0; // If this connection is from a SmartProxy (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 SmartProxy (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 SmartProxy 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 the route configurations - this is the primary method for configuring NetworkProxy * @param routes The new route configurations to use */ public async updateRouteConfigs(routes: IRouteConfig[]): Promise { this.logger.info(`Updating route configurations (${routes.length} routes)`); // Update routes in RouteManager, modern router, WebSocketHandler, and SecurityManager this.routeManager.updateRoutes(routes); this.router.setRoutes(routes); this.webSocketHandler.setRoutes(routes); this.requestHandler.securityManager.setRoutes(routes); this.routes = routes; // Directly update the certificate manager with the new routes // This will extract domains and handle certificate provisioning this.certificateManager.updateRouteConfigs(routes); // Collect all domains and certificates for configuration const currentHostnames = new Set(); const certificateUpdates = new Map(); // Process each route to extract domain and certificate information for (const route of routes) { // Skip non-forward routes or routes without domains if (route.action.type !== 'forward' || !route.match.domains) { continue; } // Get domains from route const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; // Process each domain for (const domain of domains) { // Skip wildcard domains for direct host configuration if (domain.includes('*')) { continue; } currentHostnames.add(domain); // Check if we have a static certificate for this domain if (route.action.tls?.certificate && route.action.tls.certificate !== 'auto') { certificateUpdates.set(domain, { cert: route.action.tls.certificate.cert, key: route.action.tls.certificate.key }); } } } // Update certificate cache with any static certificates for (const [domain, certData] of certificateUpdates.entries()) { try { this.certificateManager.updateCertificateCache( domain, certData.cert, certData.key ); this.activeContexts.add(domain); } catch (error) { this.logger.error(`Failed to add SSL context for ${domain}`, 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); } } // Create legacy proxy configs for the router // This is only needed for backward compatibility with ProxyRouter // and will be removed in the future const legacyConfigs: IReverseProxyConfig[] = []; for (const domain of currentHostnames) { // Find route for this domain const route = routes.find(r => { const domains = Array.isArray(r.match.domains) ? r.match.domains : [r.match.domains]; return domains.includes(domain); }); if (!route || route.action.type !== 'forward' || !route.action.target) { continue; } // Skip routes with function-based targets - we'll handle them during request processing if (typeof route.action.target.host === 'function' || typeof route.action.target.port === 'function') { this.logger.info(`Domain ${domain} uses function-based targets - will be handled at request time`); continue; } // Extract static target information const targetHosts = Array.isArray(route.action.target.host) ? route.action.target.host : [route.action.target.host]; const targetPort = route.action.target.port; // Get certificate information const certData = certificateUpdates.get(domain); const defaultCerts = this.certificateManager.getDefaultCertificates(); legacyConfigs.push({ hostName: domain, destinationIps: targetHosts, destinationPorts: [targetPort], privateKey: certData?.key || defaultCerts.key, publicKey: certData?.cert || defaultCerts.cert }); } // Update the router with legacy configs // Handle both old and new router interfaces if (typeof this.router.setRoutes === 'function') { this.router.setRoutes(routes); } else if (typeof this.router.setNewProxyConfigs === 'function') { this.router.setNewProxyConfigs(legacyConfigs); } else { this.logger.warn('Router has no recognized configuration method'); } this.logger.info(`Route configuration updated with ${routes.length} routes and ${legacyConfigs.length} proxy configs`); } // Legacy methods have been removed. // Please use updateRouteConfigs() directly with modern route-based configuration. /** * 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); } /** * Update certificate for a domain * * This method allows direct updates of certificates from external sources * like Port80Handler or custom certificate providers. * * @param domain The domain to update certificate for * @param certificate The new certificate (public key) * @param privateKey The new private key * @param expiryDate Optional expiry date */ public updateCertificate( domain: string, certificate: string, privateKey: string, expiryDate?: Date ): void { this.logger.info(`Updating certificate for ${domain}`); this.certificateManager.updateCertificateCache(domain, certificate, privateKey, expiryDate); } /** * Gets all route configurations currently in use */ public getRouteConfigs(): IRouteConfig[] { return this.routeManager.getRoutes(); } }