import * as plugins from '../../plugins.js'; import { NetworkProxy } from '../network-proxy/index.js'; import { Port80Handler } from '../../http/port80/port80-handler.js'; import { Port80HandlerEvents } from '../../core/models/common-types.js'; import { subscribeToPort80Handler } from '../../core/utils/event-utils.js'; import type { ICertificateData } from '../../certificate/models/certificate-types.js'; import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js'; import type { IRouteConfig } from './models/route-types.js'; /** * Manages NetworkProxy integration for TLS termination * * NetworkProxyBridge connects SmartProxy with NetworkProxy to handle TLS termination. * It converts route configurations to NetworkProxy configuration format and manages * certificate provisioning through Port80Handler when ACME is enabled. * * It is used by SmartProxy for routes that have: * - TLS mode of 'terminate' or 'terminate-and-reencrypt' * - Certificate set to 'auto' or custom certificate */ export class NetworkProxyBridge { private networkProxy: NetworkProxy | null = null; private port80Handler: Port80Handler | null = null; constructor(private settings: ISmartProxyOptions) {} /** * Set the Port80Handler to use for certificate management */ public setPort80Handler(handler: Port80Handler): void { this.port80Handler = handler; // Subscribe to certificate events subscribeToPort80Handler(handler, { onCertificateIssued: this.handleCertificateEvent.bind(this), onCertificateRenewed: this.handleCertificateEvent.bind(this) }); // If NetworkProxy is already initialized, connect it with Port80Handler if (this.networkProxy) { this.networkProxy.setExternalPort80Handler(handler); } console.log('Port80Handler connected to NetworkProxyBridge'); } /** * Initialize NetworkProxy instance */ public async initialize(): Promise { if (!this.networkProxy && this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) { // Configure NetworkProxy options based on PortProxy settings const networkProxyOptions: any = { port: this.settings.networkProxyPort!, portProxyIntegration: true, logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info', useExternalPort80Handler: !!this.port80Handler // Use Port80Handler if available }; this.networkProxy = new NetworkProxy(networkProxyOptions); console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`); // Connect Port80Handler if available if (this.port80Handler) { this.networkProxy.setExternalPort80Handler(this.port80Handler); } // Apply route configurations to NetworkProxy await this.syncRoutesToNetworkProxy(this.settings.routes || []); } } /** * Handle certificate issuance or renewal events */ private handleCertificateEvent(data: ICertificateData): void { if (!this.networkProxy) return; console.log(`Received certificate for ${data.domain} from Port80Handler, updating NetworkProxy`); try { // Find existing config for this domain const existingConfigs = this.networkProxy.getProxyConfigs() .filter(config => config.hostName === data.domain); if (existingConfigs.length > 0) { // Update existing configs with new certificate for (const config of existingConfigs) { config.privateKey = data.privateKey; config.publicKey = data.certificate; } // Apply updated configs this.networkProxy.updateProxyConfigs(existingConfigs) .then(() => console.log(`Updated certificate for ${data.domain} in NetworkProxy`)) .catch(err => console.log(`Error updating certificate in NetworkProxy: ${err}`)); } else { // Create a new config for this domain console.log(`No existing config found for ${data.domain}, creating new config in NetworkProxy`); } } catch (err) { console.log(`Error handling certificate event: ${err}`); } } /** * Apply an external (static) certificate into NetworkProxy */ public applyExternalCertificate(data: ICertificateData): void { if (!this.networkProxy) { console.log(`NetworkProxy not initialized: cannot apply external certificate for ${data.domain}`); return; } this.handleCertificateEvent(data); } /** * Get the NetworkProxy instance */ public getNetworkProxy(): NetworkProxy | null { return this.networkProxy; } /** * Get the NetworkProxy port */ public getNetworkProxyPort(): number { return this.networkProxy ? this.networkProxy.getListeningPort() : this.settings.networkProxyPort || 8443; } /** * Start NetworkProxy */ public async start(): Promise { if (this.networkProxy) { await this.networkProxy.start(); console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`); } } /** * Stop NetworkProxy */ public async stop(): Promise { if (this.networkProxy) { try { console.log('Stopping NetworkProxy...'); await this.networkProxy.stop(); console.log('NetworkProxy stopped successfully'); } catch (err) { console.log(`Error stopping NetworkProxy: ${err}`); } } } /** * Register domains with Port80Handler */ public registerDomainsWithPort80Handler(domains: string[]): void { if (!this.port80Handler) { console.log('Cannot register domains - Port80Handler not initialized'); return; } for (const domain of domains) { // Skip wildcards if (domain.includes('*')) { console.log(`Skipping wildcard domain for ACME: ${domain}`); continue; } // Register the domain try { this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true }); console.log(`Registered domain with Port80Handler: ${domain}`); } catch (err) { console.log(`Error registering domain ${domain} with Port80Handler: ${err}`); } } } /** * Forwards a TLS connection to a NetworkProxy for handling */ public forwardToNetworkProxy( connectionId: string, socket: plugins.net.Socket, record: IConnectionRecord, initialData: Buffer, customProxyPort?: number, onError?: (reason: string) => void ): void { // Ensure NetworkProxy is initialized if (!this.networkProxy) { console.log( `[${connectionId}] NetworkProxy not initialized. Cannot forward connection.` ); if (onError) { onError('network_proxy_not_initialized'); } return; } // Use the custom port if provided, otherwise use the default NetworkProxy port const proxyPort = customProxyPort || this.networkProxy.getListeningPort(); const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally if (this.settings.enableDetailedLogging) { console.log( `[${connectionId}] Forwarding TLS connection to NetworkProxy at ${proxyHost}:${proxyPort}` ); } // Create a connection to the NetworkProxy const proxySocket = plugins.net.connect({ host: proxyHost, port: proxyPort, }); // Store the outgoing socket in the record record.outgoing = proxySocket; record.outgoingStartTime = Date.now(); record.usingNetworkProxy = true; // Set up error handlers proxySocket.on('error', (err) => { console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`); if (onError) { onError('network_proxy_connect_error'); } }); // Handle connection to NetworkProxy proxySocket.on('connect', () => { if (this.settings.enableDetailedLogging) { console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`); } // First send the initial data that contains the TLS ClientHello proxySocket.write(initialData); // Now set up bidirectional piping between client and NetworkProxy socket.pipe(proxySocket); proxySocket.pipe(socket); // Update activity on data transfer (caller should handle this) if (this.settings.enableDetailedLogging) { console.log(`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`); } }); } /** * Synchronizes routes to NetworkProxy * * This method converts route configurations to NetworkProxy format and updates * the NetworkProxy with the converted configurations. It handles: * * - Extracting domain, target, and certificate information from routes * - Converting TLS mode settings to NetworkProxy configuration * - Applying security and advanced settings * - Registering domains for ACME certificate provisioning when needed * * @param routes The route configurations to sync to NetworkProxy */ public async syncRoutesToNetworkProxy(routes: IRouteConfig[]): Promise { if (!this.networkProxy) { console.log('Cannot sync configurations - NetworkProxy not initialized'); return; } try { // Get SSL certificates from assets // Import fs directly since it's not in plugins const fs = await import('fs'); let certPair; try { certPair = { key: fs.readFileSync('assets/certs/key.pem', 'utf8'), cert: fs.readFileSync('assets/certs/cert.pem', 'utf8'), }; } catch (certError) { console.log(`Warning: Could not read default certificates: ${certError}`); console.log( 'Using empty certificate placeholders - ACME will generate proper certificates if enabled' ); // Use empty placeholders - NetworkProxy will use its internal defaults // or ACME will generate proper ones if enabled certPair = { key: '', cert: '', }; } // Convert routes to NetworkProxy configs const proxyConfigs = this.convertRoutesToNetworkProxyConfigs(routes, certPair); // Update the proxy configs await this.networkProxy.updateProxyConfigs(proxyConfigs); console.log(`Synced ${proxyConfigs.length} configurations to NetworkProxy`); } catch (err) { console.log(`Error syncing routes to NetworkProxy: ${err}`); } } /** * Convert routes to NetworkProxy configuration format * * This method transforms route-based configuration to NetworkProxy's configuration format. * It processes each route and creates appropriate NetworkProxy configs for domains * that require TLS termination. * * @param routes Array of route configurations to convert * @param defaultCertPair Default certificate to use if no custom certificate is specified * @returns Array of NetworkProxy configurations */ public convertRoutesToNetworkProxyConfigs( routes: IRouteConfig[], defaultCertPair: { key: string; cert: string } ): plugins.tsclass.network.IReverseProxyConfig[] { const configs: plugins.tsclass.network.IReverseProxyConfig[] = []; for (const route of routes) { // Skip routes without domains if (!route.match.domains) continue; // Skip non-forward routes if (route.action.type !== 'forward') continue; // Skip routes without TLS configuration if (!route.action.tls || !route.action.target) continue; // Get domains from route const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; // Create a config for each domain for (const domain of domains) { // Determine if this route requires TLS termination const needsTermination = route.action.tls.mode === 'terminate' || route.action.tls.mode === 'terminate-and-reencrypt'; // Skip passthrough domains for NetworkProxy if (route.action.tls.mode === 'passthrough') continue; // Get certificate let certKey = defaultCertPair.key; let certCert = defaultCertPair.cert; // Use custom certificate if specified if (route.action.tls.certificate !== 'auto' && typeof route.action.tls.certificate === 'object') { certKey = route.action.tls.certificate.key; certCert = route.action.tls.certificate.cert; } // Determine target hosts and ports const targetHosts = Array.isArray(route.action.target.host) ? route.action.target.host : [route.action.target.host]; const targetPort = route.action.target.port; // Create NetworkProxy config const config: plugins.tsclass.network.IReverseProxyConfig = { hostName: domain, privateKey: certKey, publicKey: certCert, destinationIps: targetHosts, destinationPorts: [targetPort], // Use backendProtocol for TLS re-encryption: backendProtocol: route.action.tls.mode === 'terminate-and-reencrypt' ? 'http2' : 'http1', // Add rewriteHostHeader for host header handling: rewriteHostHeader: route.action.advanced?.headers ? true : false }; configs.push(config); } } return configs; } /** * @deprecated This method is deprecated and will be removed in a future version. * Use syncRoutesToNetworkProxy() instead. * * This legacy method exists only for backward compatibility and * simply forwards to syncRoutesToNetworkProxy(). */ public async syncDomainConfigsToNetworkProxy(): Promise { console.log('Method syncDomainConfigsToNetworkProxy is deprecated. Use syncRoutesToNetworkProxy instead.'); await this.syncRoutesToNetworkProxy(this.settings.routes || []); } /** * Request a certificate for a specific domain */ public async requestCertificate(domain: string): Promise { // Delegate to Port80Handler if available if (this.port80Handler) { try { // Check if the domain is already registered const cert = this.port80Handler.getCertificate(domain); if (cert) { console.log(`Certificate already exists for ${domain}`); return true; } // Register the domain for certificate issuance this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true }); console.log(`Domain ${domain} registered for certificate issuance`); return true; } catch (err) { console.log(`Error requesting certificate: ${err}`); return false; } } // Fall back to NetworkProxy if Port80Handler is not available if (!this.networkProxy) { console.log('Cannot request certificate - NetworkProxy not initialized'); return false; } if (!this.settings.acme?.enabled) { console.log('Cannot request certificate - ACME is not enabled'); return false; } try { const result = await this.networkProxy.requestCertificate(domain); if (result) { console.log(`Certificate request for ${domain} submitted successfully`); } else { console.log(`Certificate request for ${domain} failed`); } return result; } catch (err) { console.log(`Error requesting certificate: ${err}`); return false; } } }