/** * Reverse Proxy for Onebox * * Delegates to SmartProxy (running as Docker service) for production-grade reverse proxy * with TLS termination, WebSocket proxying, and zero-downtime configuration updates. * * Routes use Docker service names (e.g., onebox-hello-world:80) for container-to-container * communication within the Docker overlay network. */ import { logger } from '../logging.ts'; import { getErrorMessage } from '../utils/error.ts'; import { OneboxDatabase } from './database.ts'; import { SmartProxyManager } from './smartproxy.ts'; interface IProxyRoute { domain: string; targetHost: string; targetPort: number; serviceId: number; serviceName?: string; } export class OneboxReverseProxy { private oneboxRef: any; private database: OneboxDatabase; private smartProxy: SmartProxyManager; private routes: Map = new Map(); private httpPort = 8080; // Default to dev ports (will be overridden if production) private httpsPort = 8443; constructor(oneboxRef: any) { this.oneboxRef = oneboxRef; this.database = oneboxRef.database; this.smartProxy = new SmartProxyManager({ httpPort: this.httpPort, httpsPort: this.httpsPort, }); } /** * Initialize reverse proxy - SmartProxy runs as Docker service, no setup needed */ async init(): Promise { logger.info('Reverse proxy initialized (SmartProxy Docker service)'); } /** * Start the HTTP/HTTPS reverse proxy server * SmartProxy handles both HTTP and HTTPS on the configured ports */ async startHttp(port?: number): Promise { if (port) { this.httpPort = port; this.smartProxy.setPorts(this.httpPort, this.httpsPort); } try { await this.smartProxy.start(); logger.success(`Reverse proxy started on port ${this.httpPort} (SmartProxy Docker service)`); } catch (error) { logger.error(`Failed to start reverse proxy: ${getErrorMessage(error)}`); throw error; } } /** * Start HTTPS - SmartProxy already handles HTTPS when started * This method exists for interface compatibility */ async startHttps(port?: number): Promise { if (port) { this.httpsPort = port; this.smartProxy.setPorts(this.httpPort, this.httpsPort); } const status = this.smartProxy.getStatus(); if (status.running) { logger.info(`HTTPS already running on port ${this.httpsPort} via SmartProxy`); } else { await this.smartProxy.start(); } } /** * Stop the reverse proxy */ async stop(): Promise { await this.smartProxy.stop(); logger.info('Reverse proxy stopped'); } /** * Add a route for a service * Uses Docker service name for upstream (SmartProxy runs in same Docker network) */ async addRoute(serviceId: number, domain: string, targetPort: number): Promise { try { // Get service info from database const service = this.database.getServiceByID(serviceId); if (!service) { throw new Error(`Service not found: ${serviceId}`); } // Use Docker service name as upstream target // SmartProxy runs on the same Docker network, so it can resolve service names directly const serviceName = `onebox-${service.name}`; const targetHost = serviceName; const route: IProxyRoute = { domain, targetHost, targetPort, serviceId, serviceName, }; this.routes.set(domain, route); // Add route to SmartProxy using Docker service name const upstream = `${targetHost}:${targetPort}`; await this.smartProxy.addRoute(domain, upstream); logger.success(`Added proxy route: ${domain} -> ${upstream}`); } catch (error) { logger.error(`Failed to add route for ${domain}: ${getErrorMessage(error)}`); throw error; } } /** * Remove a route */ removeRoute(domain: string): void { if (this.routes.delete(domain)) { // Remove from SmartProxy (async but we don't wait) this.smartProxy.removeRoute(domain).catch((error) => { logger.error(`Failed to remove SmartProxy route for ${domain}: ${getErrorMessage(error)}`); }); logger.success(`Removed proxy route: ${domain}`); } else { logger.warn(`Route not found: ${domain}`); } } /** * Get all routes */ getRoutes(): IProxyRoute[] { return Array.from(this.routes.values()); } /** * Reload routes from database */ async reloadRoutes(): Promise { try { logger.info('Reloading proxy routes...'); // Clear local and SmartProxy routes this.routes.clear(); this.smartProxy.clear(); const services = this.database.getAllServices(); for (const service of services) { // Route by domain if running (containerID is the service ID for Swarm services) if (service.domain && service.status === 'running' && service.containerID) { await this.addRoute(service.id!, service.domain, service.port); } } logger.success(`Loaded ${this.routes.size} proxy routes`); } catch (error) { logger.error(`Failed to reload routes: ${getErrorMessage(error)}`); throw error; } } /** * Add TLS certificate for a domain * Sends PEM content to SmartProxy via Admin API */ async addCertificate(domain: string, certPem: string, keyPem: string): Promise { if (!certPem || !keyPem) { logger.warn(`Cannot add certificate for ${domain}: missing PEM content`); return; } await this.smartProxy.addCertificate(domain, certPem, keyPem); } /** * Remove TLS certificate for a domain */ removeCertificate(domain: string): void { this.smartProxy.removeCertificate(domain).catch((error) => { logger.error(`Failed to remove certificate for ${domain}: ${getErrorMessage(error)}`); }); } /** * Reload TLS certificates from database */ async reloadCertificates(): Promise { try { logger.info('Reloading TLS certificates from database...'); const certificates = this.database.getAllSSLCertificates(); for (const cert of certificates) { // Use fullchainPem for the cert (includes intermediates) and keyPem for the key if (cert.domain && cert.fullchainPem && cert.keyPem) { await this.smartProxy.addCertificate(cert.domain, cert.fullchainPem, cert.keyPem); } else { logger.warn(`Skipping certificate for ${cert.domain}: missing PEM content`); } } logger.success(`Loaded ${this.smartProxy.getCertificates().length} TLS certificates`); } catch (error) { logger.error(`Failed to reload certificates: ${getErrorMessage(error)}`); throw error; } } /** * Get status of reverse proxy */ getStatus() { const smartProxyStatus = this.smartProxy.getStatus(); return { http: { running: smartProxyStatus.running, port: smartProxyStatus.httpPort, }, https: { running: smartProxyStatus.running, port: smartProxyStatus.httpsPort, certificates: smartProxyStatus.certificates, }, routes: smartProxyStatus.routes, backend: 'smartproxy-docker', }; } }