import * as plugins from '../../plugins.js'; // Importing from the new structure import { ConnectionManager } from './connection-manager.js'; import { SecurityManager } from './security-manager.js'; import { DomainConfigManager } from './domain-config-manager.js'; import { TlsManager } from './tls-manager.js'; import { NetworkProxyBridge } from './network-proxy-bridge.js'; import { TimeoutManager } from './timeout-manager.js'; import { PortRangeManager } from './port-range-manager.js'; import { ConnectionHandler } from './connection-handler.js'; // External dependencies from migrated modules import { Port80Handler } from '../../http/port80/port80-handler.js'; import { CertProvisioner } from '../../certificate/providers/cert-provisioner.js'; import type { CertificateData } from '../../certificate/models/certificate-types.js'; import { buildPort80Handler } from '../../certificate/acme/acme-factory.js'; import type { ForwardingType } from '../../forwarding/config/forwarding-types.js'; import { createPort80HandlerOptions } from '../../common/port80-adapter.js'; // Import types from models import type { SmartProxyOptions, DomainConfig } from './models/interfaces.js'; // Provide backward compatibility types export type { SmartProxyOptions as IPortProxySettings, DomainConfig as IDomainConfig }; /** * SmartProxy - Main class that coordinates all components */ export class SmartProxy extends plugins.EventEmitter { private netServers: plugins.net.Server[] = []; private connectionLogger: NodeJS.Timeout | null = null; private isShuttingDown: boolean = false; // Component managers private connectionManager: ConnectionManager; private securityManager: SecurityManager; public domainConfigManager: DomainConfigManager; private tlsManager: TlsManager; private networkProxyBridge: NetworkProxyBridge; private timeoutManager: TimeoutManager; private portRangeManager: PortRangeManager; private connectionHandler: ConnectionHandler; // Port80Handler for ACME certificate management private port80Handler: Port80Handler | null = null; // CertProvisioner for unified certificate workflows private certProvisioner?: CertProvisioner; constructor(settingsArg: SmartProxyOptions) { super(); // Set reasonable defaults for all settings this.settings = { ...settingsArg, targetIP: settingsArg.targetIP || 'localhost', initialDataTimeout: settingsArg.initialDataTimeout || 120000, socketTimeout: settingsArg.socketTimeout || 3600000, inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, maxConnectionLifetime: settingsArg.maxConnectionLifetime || 86400000, inactivityTimeout: settingsArg.inactivityTimeout || 14400000, gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true, keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true, keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, disableInactivityCheck: settingsArg.disableInactivityCheck || false, enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, enableDetailedLogging: settingsArg.enableDetailedLogging || false, enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, allowSessionTicket: settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true, maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, networkProxyPort: settingsArg.networkProxyPort || 8443, acme: settingsArg.acme || {}, globalPortRanges: settingsArg.globalPortRanges || [], }; // Set default ACME options if not provided if (!this.settings.acme || Object.keys(this.settings.acme).length === 0) { this.settings.acme = { enabled: false, port: 80, accountEmail: 'admin@example.com', useProduction: false, renewThresholdDays: 30, autoRenew: true, certificateStore: './certs', skipConfiguredCerts: false, httpsRedirectPort: this.settings.fromPort, renewCheckIntervalHours: 24, domainForwards: [] }; } // Initialize component managers this.timeoutManager = new TimeoutManager(this.settings); this.securityManager = new SecurityManager(this.settings); this.connectionManager = new ConnectionManager( this.settings, this.securityManager, this.timeoutManager ); this.domainConfigManager = new DomainConfigManager(this.settings); this.tlsManager = new TlsManager(this.settings); this.networkProxyBridge = new NetworkProxyBridge(this.settings); this.portRangeManager = new PortRangeManager(this.settings); // Initialize connection handler this.connectionHandler = new ConnectionHandler( this.settings, this.connectionManager, this.securityManager, this.domainConfigManager, this.tlsManager, this.networkProxyBridge, this.timeoutManager, this.portRangeManager ); } /** * The settings for the port proxy */ public settings: SmartProxyOptions; /** * Initialize the Port80Handler for ACME certificate management */ private async initializePort80Handler(): Promise { const config = this.settings.acme!; if (!config.enabled) { console.log('ACME is disabled in configuration'); return; } try { // Build and start the Port80Handler this.port80Handler = buildPort80Handler({ ...config, httpsRedirectPort: config.httpsRedirectPort || this.settings.fromPort }); // Share Port80Handler with NetworkProxyBridge before start this.networkProxyBridge.setPort80Handler(this.port80Handler); await this.port80Handler.start(); console.log(`Port80Handler started on port ${config.port}`); } catch (err) { console.log(`Error initializing Port80Handler: ${err}`); } } /** * Start the proxy server */ public async start() { // Don't start if already shutting down if (this.isShuttingDown) { console.log("Cannot start SmartProxy while it's shutting down"); return; } // Process domain configs // Note: ensureForwardingConfig is no longer needed since forwarding is now required // Initialize domain config manager with the processed configs this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs); // Initialize Port80Handler if enabled await this.initializePort80Handler(); // Initialize CertProvisioner for unified certificate workflows if (this.port80Handler) { const acme = this.settings.acme!; // Convert domain forwards to use the new forwarding system if possible const domainForwards = acme.domainForwards?.map(f => { // If the domain has a forwarding config in domainConfigs, use that const domainConfig = this.settings.domainConfigs.find( dc => dc.domains.some(d => d === f.domain) ); if (domainConfig?.forwarding) { return { domain: f.domain, forwardConfig: f.forwardConfig, acmeForwardConfig: f.acmeForwardConfig, sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false }; } // Otherwise use the existing configuration return { domain: f.domain, forwardConfig: f.forwardConfig, acmeForwardConfig: f.acmeForwardConfig, sslRedirect: f.sslRedirect || false }; }) || []; this.certProvisioner = new CertProvisioner( this.settings.domainConfigs, this.port80Handler, this.networkProxyBridge, this.settings.certProvisionFunction, acme.renewThresholdDays!, acme.renewCheckIntervalHours!, acme.autoRenew!, domainForwards ); this.certProvisioner.on('certificate', (certData) => { this.emit('certificate', { domain: certData.domain, publicKey: certData.certificate, privateKey: certData.privateKey, expiryDate: certData.expiryDate, source: certData.source, isRenewal: certData.isRenewal }); }); await this.certProvisioner.start(); console.log('CertProvisioner started'); } // Initialize and start NetworkProxy if needed if ( this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0 ) { await this.networkProxyBridge.initialize(); await this.networkProxyBridge.start(); } // Validate port configuration const configWarnings = this.portRangeManager.validateConfiguration(); if (configWarnings.length > 0) { console.log("Port configuration warnings:"); for (const warning of configWarnings) { console.log(` - ${warning}`); } } // Get listening ports from PortRangeManager const listeningPorts = this.portRangeManager.getListeningPorts(); // Create servers for each port for (const port of listeningPorts) { const server = plugins.net.createServer((socket) => { // Check if shutting down if (this.isShuttingDown) { socket.end(); socket.destroy(); return; } // Delegate to connection handler this.connectionHandler.handleConnection(socket); }).on('error', (err: Error) => { console.log(`Server Error on port ${port}: ${err.message}`); }); server.listen(port, () => { const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port); console.log( `SmartProxy -> OK: Now listening on port ${port}${ this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : '' }${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}` ); }); this.netServers.push(server); } // Set up periodic connection logging and inactivity checks this.connectionLogger = setInterval(() => { // Immediately return if shutting down if (this.isShuttingDown) return; // Perform inactivity check this.connectionManager.performInactivityCheck(); // Log connection statistics const now = Date.now(); let maxIncoming = 0; let maxOutgoing = 0; let tlsConnections = 0; let nonTlsConnections = 0; let completedTlsHandshakes = 0; let pendingTlsHandshakes = 0; let keepAliveConnections = 0; let networkProxyConnections = 0; // Get connection records for analysis const connectionRecords = this.connectionManager.getConnections(); // Analyze active connections for (const record of connectionRecords.values()) { // Track connection stats if (record.isTLS) { tlsConnections++; if (record.tlsHandshakeComplete) { completedTlsHandshakes++; } else { pendingTlsHandshakes++; } } else { nonTlsConnections++; } if (record.hasKeepAlive) { keepAliveConnections++; } if (record.usingNetworkProxy) { networkProxyConnections++; } maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime); if (record.outgoingStartTime) { maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime); } } // Get termination stats const terminationStats = this.connectionManager.getTerminationStats(); // Log detailed stats console.log( `Active connections: ${connectionRecords.size}. ` + `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` + `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` + `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` + `Termination stats: ${JSON.stringify({ IN: terminationStats.incoming, OUT: terminationStats.outgoing, })}` ); }, this.settings.inactivityCheckInterval || 60000); // Make sure the interval doesn't keep the process alive if (this.connectionLogger.unref) { this.connectionLogger.unref(); } } /** * Stop the proxy server */ public async stop() { console.log('SmartProxy shutting down...'); this.isShuttingDown = true; // Stop CertProvisioner if active if (this.certProvisioner) { await this.certProvisioner.stop(); console.log('CertProvisioner stopped'); } // Stop the Port80Handler if running if (this.port80Handler) { try { await this.port80Handler.stop(); console.log('Port80Handler stopped'); this.port80Handler = null; } catch (err) { console.log(`Error stopping Port80Handler: ${err}`); } } // Stop accepting new connections const closeServerPromises: Promise[] = this.netServers.map( (server) => new Promise((resolve) => { if (!server.listening) { resolve(); return; } server.close((err) => { if (err) { console.log(`Error closing server: ${err.message}`); } resolve(); }); }) ); // Stop the connection logger if (this.connectionLogger) { clearInterval(this.connectionLogger); this.connectionLogger = null; } // Wait for servers to close await Promise.all(closeServerPromises); console.log('All servers closed. Cleaning up active connections...'); // Clean up all active connections this.connectionManager.clearConnections(); // Stop NetworkProxy await this.networkProxyBridge.stop(); // Clear all servers this.netServers = []; console.log('SmartProxy shutdown complete.'); } /** * Updates the domain configurations for the proxy */ public async updateDomainConfigs(newDomainConfigs: DomainConfig[]): Promise { console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`); // Update domain configs in DomainConfigManager this.domainConfigManager.updateDomainConfigs(newDomainConfigs); // If NetworkProxy is initialized, resync the configurations if (this.networkProxyBridge.getNetworkProxy()) { await this.networkProxyBridge.syncDomainConfigsToNetworkProxy(); } // If Port80Handler is running, provision certificates based on forwarding type if (this.port80Handler && this.settings.acme?.enabled) { for (const domainConfig of newDomainConfigs) { // Skip certificate provisioning for http-only or passthrough configs that don't need certs const forwardingType = domainConfig.forwarding.type; const needsCertificate = forwardingType === 'https-terminate-to-http' || forwardingType === 'https-terminate-to-https'; // Skip certificate provisioning if ACME is explicitly disabled for this domain const acmeDisabled = domainConfig.forwarding.acme?.enabled === false; if (!needsCertificate || acmeDisabled) { if (this.settings.enableDetailedLogging) { console.log(`Skipping certificate provisioning for ${domainConfig.domains.join(', ')} (${forwardingType})`); } continue; } for (const domain of domainConfig.domains) { const isWildcard = domain.includes('*'); let provision: string | plugins.tsclass.network.ICert = 'http01'; // Check for ACME forwarding configuration in the domain const forwardAcmeChallenges = domainConfig.forwarding.acme?.forwardChallenges; if (this.settings.certProvisionFunction) { try { provision = await this.settings.certProvisionFunction(domain); } catch (err) { console.log(`certProvider error for ${domain}: ${err}`); } } else if (isWildcard) { console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`); continue; } if (provision === 'http01') { if (isWildcard) { console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`); continue; } // Create Port80Handler options from the forwarding configuration const port80Config = createPort80HandlerOptions(domain, domainConfig.forwarding); this.port80Handler.addDomain(port80Config); console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`); } else { // Static certificate (e.g., DNS-01 provisioned) supports wildcards const certObj = provision as plugins.tsclass.network.ICert; const certData: CertificateData = { domain: certObj.domainName, certificate: certObj.publicKey, privateKey: certObj.privateKey, expiryDate: new Date(certObj.validUntil) }; this.networkProxyBridge.applyExternalCertificate(certData); console.log(`Applied static certificate for ${domain} from certProvider`); } } } console.log('Provisioned certificates for new domains'); } } /** * Request a certificate for a specific domain */ public async requestCertificate(domain: string): Promise { // Validate domain format if (!this.isValidDomain(domain)) { console.log(`Invalid domain format: ${domain}`); return false; } // Use Port80Handler if available if (this.port80Handler) { try { // Check if we already have a certificate const cert = this.port80Handler.getCertificate(domain); if (cert) { console.log(`Certificate already exists for ${domain}, valid until ${cert.expiryDate.toISOString()}`); return true; } // Register 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 registering domain with Port80Handler: ${err}`); return false; } } // Fall back to NetworkProxyBridge return this.networkProxyBridge.requestCertificate(domain); } /** * Validates if a domain name is valid for certificate issuance */ private isValidDomain(domain: string): boolean { // Very basic domain validation if (!domain || domain.length === 0) { return false; } // Check for wildcard domains (they can't get ACME certs) if (domain.includes('*')) { console.log(`Wildcard domains like "${domain}" are not supported for ACME certificates`); return false; } // Check if domain has at least one dot and no invalid characters 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])?)*$/; if (!validDomainRegex.test(domain)) { console.log(`Domain "${domain}" has invalid format`); return false; } return true; } /** * Get statistics about current connections */ public getStatistics(): any { const connectionRecords = this.connectionManager.getConnections(); const terminationStats = this.connectionManager.getTerminationStats(); let tlsConnections = 0; let nonTlsConnections = 0; let keepAliveConnections = 0; let networkProxyConnections = 0; // Analyze active connections for (const record of connectionRecords.values()) { if (record.isTLS) tlsConnections++; else nonTlsConnections++; if (record.hasKeepAlive) keepAliveConnections++; if (record.usingNetworkProxy) networkProxyConnections++; } return { activeConnections: connectionRecords.size, tlsConnections, nonTlsConnections, keepAliveConnections, networkProxyConnections, terminationStats, acmeEnabled: !!this.port80Handler, port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null }; } /** * Get a list of eligible domains for ACME certificates */ public getEligibleDomainsForCertificates(): string[] { // Collect all non-wildcard domains from domain configs const domains: string[] = []; for (const config of this.settings.domainConfigs) { // Skip domains that can't be used with ACME const eligibleDomains = config.domains.filter(domain => !domain.includes('*') && this.isValidDomain(domain) ); domains.push(...eligibleDomains); } return domains; } /** * Get status of certificates managed by Port80Handler */ public getCertificateStatus(): any { if (!this.port80Handler) { return { enabled: false, message: 'Port80Handler is not enabled' }; } // Get eligible domains const eligibleDomains = this.getEligibleDomainsForCertificates(); const certificateStatus: Record = {}; // Check each domain for (const domain of eligibleDomains) { const cert = this.port80Handler.getCertificate(domain); if (cert) { const now = new Date(); const expiryDate = cert.expiryDate; const daysRemaining = Math.floor((expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)); certificateStatus[domain] = { status: 'valid', expiryDate: expiryDate.toISOString(), daysRemaining, renewalNeeded: daysRemaining <= (this.settings.acme?.renewThresholdDays ?? 0) }; } else { certificateStatus[domain] = { status: 'missing', message: 'No certificate found' }; } } const acme = this.settings.acme!; return { enabled: true, port: acme.port!, useProduction: acme.useProduction!, autoRenew: acme.autoRenew!, certificates: certificateStatus }; } }