import * as plugins from '../../plugins.js'; import { logger } from '../../core/utils/logger.js'; // Rust bridge and helpers import { RustProxyBridge } from './rust-proxy-bridge.js'; import { RoutePreprocessor } from './route-preprocessor.js'; import { SocketHandlerServer } from './socket-handler-server.js'; import { RustMetricsAdapter } from './rust-metrics-adapter.js'; // Route management import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js'; import { RouteValidator } from './utils/route-validator.js'; import { Mutex } from './utils/mutex.js'; // Types import type { ISmartProxyOptions, TSmartProxyCertProvisionObject } from './models/interfaces.js'; import type { IRouteConfig } from './models/route-types.js'; import type { IMetrics } from './models/metrics-types.js'; /** * SmartProxy - Rust-backed proxy engine with TypeScript configuration API. * * All networking (TCP, TLS, HTTP reverse proxy, connection management, security, * NFTables) is handled by the Rust binary. TypeScript is only: * - The npm module interface (types, route helpers) * - The thin IPC wrapper (this class) * - Socket-handler callback relay (for JS-defined handlers) * - Certificate provisioning callbacks (certProvisionFunction) */ export class SmartProxy extends plugins.EventEmitter { public settings: ISmartProxyOptions; public routeManager: RouteManager; private bridge: RustProxyBridge; private preprocessor: RoutePreprocessor; private socketHandlerServer: SocketHandlerServer | null = null; private metricsAdapter: RustMetricsAdapter; private routeUpdateLock: Mutex; private stopping = false; constructor(settingsArg: ISmartProxyOptions) { super(); // Apply defaults this.settings = { ...settingsArg, initialDataTimeout: settingsArg.initialDataTimeout || 120000, socketTimeout: settingsArg.socketTimeout || 3600000, maxConnectionLifetime: settingsArg.maxConnectionLifetime || 86400000, inactivityTimeout: settingsArg.inactivityTimeout || 14400000, gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, }; // Normalize ACME options if (this.settings.acme) { if (this.settings.acme.accountEmail && !this.settings.acme.email) { this.settings.acme.email = this.settings.acme.accountEmail; } this.settings.acme = { enabled: this.settings.acme.enabled !== false, port: this.settings.acme.port || 80, email: this.settings.acme.email, useProduction: this.settings.acme.useProduction || false, renewThresholdDays: this.settings.acme.renewThresholdDays || 30, autoRenew: this.settings.acme.autoRenew !== false, certificateStore: this.settings.acme.certificateStore || './certs', skipConfiguredCerts: this.settings.acme.skipConfiguredCerts || false, renewCheckIntervalHours: this.settings.acme.renewCheckIntervalHours || 24, routeForwards: this.settings.acme.routeForwards || [], ...this.settings.acme, }; } // Validate routes if (this.settings.routes?.length) { const validation = RouteValidator.validateRoutes(this.settings.routes); if (!validation.valid) { RouteValidator.logValidationErrors(validation.errors); throw new Error(`Initial route validation failed: ${validation.errors.size} route(s) have errors`); } } // Create logger adapter const loggerAdapter = { debug: (message: string, data?: any) => logger.log('debug', message, data), info: (message: string, data?: any) => logger.log('info', message, data), warn: (message: string, data?: any) => logger.log('warn', message, data), error: (message: string, data?: any) => logger.log('error', message, data), }; // Initialize components this.routeManager = new RouteManager({ logger: loggerAdapter, enableDetailedLogging: this.settings.enableDetailedLogging, routes: this.settings.routes, }); this.bridge = new RustProxyBridge(); this.preprocessor = new RoutePreprocessor(); this.metricsAdapter = new RustMetricsAdapter( this.bridge, this.settings.metrics?.sampleIntervalMs ?? 1000 ); this.routeUpdateLock = new Mutex(); } /** * Start the proxy. * Spawns the Rust binary, configures socket relay if needed, sends routes, handles cert provisioning. */ public async start(): Promise { // Spawn Rust binary const spawned = await this.bridge.spawn(); if (!spawned) { throw new Error( 'RustProxy binary not found. Set SMARTPROXY_RUST_BINARY env var, install the platform package, ' + 'or build locally with: pnpm build' ); } // Handle unexpected exit (only emits error if not intentionally stopping) this.bridge.on('exit', (code: number | null, signal: string | null) => { if (this.stopping) return; logger.log('error', `RustProxy exited unexpectedly (code=${code}, signal=${signal})`, { component: 'smart-proxy' }); this.emit('error', new Error(`RustProxy exited (code=${code}, signal=${signal})`)); }); // Check if any routes need TS-side handling (socket handlers, dynamic functions) const hasHandlerRoutes = this.settings.routes.some( (r) => (r.action.type === 'socket-handler' && r.action.socketHandler) || r.action.targets?.some((t) => typeof t.host === 'function' || typeof t.port === 'function') ); // Start socket handler relay server (but don't tell Rust yet - proxy not started) if (hasHandlerRoutes) { this.socketHandlerServer = new SocketHandlerServer(this.preprocessor); await this.socketHandlerServer.start(); } // Preprocess routes (strip JS functions, convert socket-handler routes) const rustRoutes = this.preprocessor.preprocessForRust(this.settings.routes); // Build Rust config const config = this.buildRustConfig(rustRoutes); // Start the Rust proxy await this.bridge.startProxy(config); // Now that Rust proxy is running, configure socket handler relay if (this.socketHandlerServer) { await this.bridge.setSocketHandlerRelay(this.socketHandlerServer.getSocketPath()); } // Handle certProvisionFunction await this.provisionCertificatesViaCallback(); // Start metrics polling this.metricsAdapter.startPolling(); logger.log('info', 'SmartProxy started (Rust engine)', { component: 'smart-proxy' }); } /** * Stop the proxy. */ public async stop(): Promise { logger.log('info', 'SmartProxy shutting down...', { component: 'smart-proxy' }); this.stopping = true; // Stop metrics polling this.metricsAdapter.stopPolling(); // Remove exit listener before killing to avoid spurious error events this.bridge.removeAllListeners('exit'); // Stop Rust proxy try { await this.bridge.stopProxy(); } catch { // Ignore if already stopped } this.bridge.kill(); // Stop socket handler relay if (this.socketHandlerServer) { await this.socketHandlerServer.stop(); this.socketHandlerServer = null; } logger.log('info', 'SmartProxy shutdown complete.', { component: 'smart-proxy' }); } /** * Update routes atomically. */ public async updateRoutes(newRoutes: IRouteConfig[]): Promise { return this.routeUpdateLock.runExclusive(async () => { // Validate const validation = RouteValidator.validateRoutes(newRoutes); if (!validation.valid) { RouteValidator.logValidationErrors(validation.errors); throw new Error(`Route validation failed: ${validation.errors.size} route(s) have errors`); } // Preprocess for Rust const rustRoutes = this.preprocessor.preprocessForRust(newRoutes); // Send to Rust await this.bridge.updateRoutes(rustRoutes); // Update local route manager this.routeManager.updateRoutes(newRoutes); // Update socket handler relay if handler routes changed const hasHandlerRoutes = newRoutes.some( (r) => (r.action.type === 'socket-handler' && r.action.socketHandler) || r.action.targets?.some((t) => typeof t.host === 'function' || typeof t.port === 'function') ); if (hasHandlerRoutes && !this.socketHandlerServer) { this.socketHandlerServer = new SocketHandlerServer(this.preprocessor); await this.socketHandlerServer.start(); await this.bridge.setSocketHandlerRelay(this.socketHandlerServer.getSocketPath()); } else if (!hasHandlerRoutes && this.socketHandlerServer) { await this.socketHandlerServer.stop(); this.socketHandlerServer = null; } // Update stored routes this.settings.routes = newRoutes; // Handle cert provisioning for new routes await this.provisionCertificatesViaCallback(); logger.log('info', `Routes updated (${newRoutes.length} routes)`, { component: 'smart-proxy' }); }); } /** * Provision a certificate for a named route. */ public async provisionCertificate(routeName: string): Promise { await this.bridge.provisionCertificate(routeName); } /** * Force renewal of a certificate. */ public async renewCertificate(routeName: string): Promise { await this.bridge.renewCertificate(routeName); } /** * Get certificate status for a route (async - calls Rust). */ public async getCertificateStatus(routeName: string): Promise { return this.bridge.getCertificateStatus(routeName); } /** * Get the metrics interface. */ public getMetrics(): IMetrics { return this.metricsAdapter; } /** * Get statistics (async - calls Rust). */ public async getStatistics(): Promise { return this.bridge.getStatistics(); } /** * Add a listening port at runtime. */ public async addListeningPort(port: number): Promise { await this.bridge.addListeningPort(port); } /** * Remove a listening port at runtime. */ public async removeListeningPort(port: number): Promise { await this.bridge.removeListeningPort(port); } /** * Get all currently listening ports (async - calls Rust). */ public async getListeningPorts(): Promise { if (!this.bridge.running) return []; return this.bridge.getListeningPorts(); } /** * Get eligible domains for ACME certificates (sync - reads local routes). */ public getEligibleDomainsForCertificates(): string[] { const domains: string[] = []; for (const route of this.settings.routes || []) { if (!route.match.domains) continue; if ( route.action.type !== 'forward' || !route.action.tls || route.action.tls.mode === 'passthrough' || route.action.tls.certificate !== 'auto' ) continue; const routeDomains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; const eligible = routeDomains.filter((d) => !d.includes('*') && this.isValidDomain(d)); domains.push(...eligible); } return domains; } /** * Get NFTables status (async - calls Rust). */ public async getNfTablesStatus(): Promise> { return this.bridge.getNftablesStatus(); } // --- Private helpers --- /** * Build the Rust configuration object from TS settings. */ private buildRustConfig(routes: IRouteConfig[]): any { return { routes, defaults: this.settings.defaults, acme: this.settings.acme ? { enabled: this.settings.acme.enabled, email: this.settings.acme.email, useProduction: this.settings.acme.useProduction, port: this.settings.acme.port, renewThresholdDays: this.settings.acme.renewThresholdDays, autoRenew: this.settings.acme.autoRenew, certificateStore: this.settings.acme.certificateStore, renewCheckIntervalHours: this.settings.acme.renewCheckIntervalHours, } : undefined, connectionTimeout: this.settings.connectionTimeout, initialDataTimeout: this.settings.initialDataTimeout, socketTimeout: this.settings.socketTimeout, maxConnectionLifetime: this.settings.maxConnectionLifetime, gracefulShutdownTimeout: this.settings.gracefulShutdownTimeout, maxConnectionsPerIp: this.settings.maxConnectionsPerIP, connectionRateLimitPerMinute: this.settings.connectionRateLimitPerMinute, keepAliveTreatment: this.settings.keepAliveTreatment, keepAliveInactivityMultiplier: this.settings.keepAliveInactivityMultiplier, extendedKeepAliveLifetime: this.settings.extendedKeepAliveLifetime, acceptProxyProtocol: this.settings.acceptProxyProtocol, sendProxyProtocol: this.settings.sendProxyProtocol, }; } /** * For routes with certificate: 'auto', call certProvisionFunction if set. * If the callback returns a cert object, load it into Rust. * If it returns 'http01', let Rust handle ACME. */ private async provisionCertificatesViaCallback(): Promise { const provisionFn = this.settings.certProvisionFunction; if (!provisionFn) return; for (const route of this.settings.routes) { if (route.action.tls?.certificate !== 'auto') continue; if (!route.match.domains) continue; const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; for (const domain of domains) { if (domain.includes('*')) continue; try { const result: TSmartProxyCertProvisionObject = await provisionFn(domain); if (result === 'http01') { // Rust handles ACME for this domain continue; } // Got a static cert object - load it into Rust if (result && typeof result === 'object') { const certObj = result as plugins.tsclass.network.ICert; await this.bridge.loadCertificate( domain, certObj.publicKey, certObj.privateKey, ); logger.log('info', `Certificate loaded via provision function for ${domain}`, { component: 'smart-proxy' }); } } catch (err: any) { logger.log('warn', `certProvisionFunction failed for ${domain}: ${err.message}`, { component: 'smart-proxy' }); // Fallback to ACME if enabled if (this.settings.certProvisionFallbackToAcme !== false) { logger.log('info', `Falling back to ACME for ${domain}`, { component: 'smart-proxy' }); } } } } } private isValidDomain(domain: string): boolean { if (!domain || domain.length === 0) return false; if (domain.includes('*')) return false; const validDomainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; return validDomainRegex.test(domain); } }