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 { CertificateData } from '../../certificate/models/certificate-types.js'; import type { ConnectionRecord, SmartProxyOptions, DomainConfig } from './models/interfaces.js'; /** * Manages NetworkProxy integration for TLS termination */ export class NetworkProxyBridge { private networkProxy: NetworkProxy | null = null; private port80Handler: Port80Handler | null = null; constructor(private settings: SmartProxyOptions) {} /** * 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); } // Convert and apply domain configurations to NetworkProxy await this.syncDomainConfigsToNetworkProxy(); } } /** * Handle certificate issuance or renewal events */ private handleCertificateEvent(data: CertificateData): 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: CertificateData): 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: ConnectionRecord, 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 domain configurations to NetworkProxy */ public async syncDomainConfigsToNetworkProxy(): 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 domain configs to NetworkProxy configs const proxyConfigs = this.networkProxy.convertSmartProxyConfigs( this.settings.domainConfigs, certPair ); // Log ACME-eligible domains const acmeEnabled = !!this.settings.acme?.enabled; if (acmeEnabled) { const acmeEligibleDomains = proxyConfigs .filter((config) => !config.hostName.includes('*')) // Exclude wildcards .map((config) => config.hostName); if (acmeEligibleDomains.length > 0) { console.log(`Domains eligible for ACME certificates: ${acmeEligibleDomains.join(', ')}`); // Register these domains with Port80Handler if available if (this.port80Handler) { this.registerDomainsWithPort80Handler(acmeEligibleDomains); } } else { console.log('No domains eligible for ACME certificates found in configuration'); } } // Update NetworkProxy with the converted configs await this.networkProxy.updateProxyConfigs(proxyConfigs); console.log(`Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`); } catch (err) { console.log(`Failed to sync configurations: ${err}`); } } /** * 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; } } }