UNPKG

@push.rocks/smartproxy

Version:

A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.

679 lines (591 loc) 23.9 kB
import * as plugins from '../plugins.js'; import type { IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js'; import { ConnectionManager } from './classes.pp.connectionmanager.js'; import { SecurityManager } from './classes.pp.securitymanager.js'; import { DomainConfigManager } from './classes.pp.domainconfigmanager.js'; import { TlsManager } from './classes.pp.tlsmanager.js'; import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js'; import { TimeoutManager } from './classes.pp.timeoutmanager.js'; import { PortRangeManager } from './classes.pp.portrangemanager.js'; import { ConnectionHandler } from './classes.pp.connectionhandler.js'; import { Port80Handler, Port80HandlerEvents } from '../port80handler/classes.port80handler.js'; import * as path from 'path'; import * as fs from 'fs'; /** * SmartProxy - Main class that coordinates all components */ export class SmartProxy { 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; constructor(settingsArg: IPortProxySettings) { // 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, port80HandlerConfig: settingsArg.port80HandlerConfig || {}, globalPortRanges: settingsArg.globalPortRanges || [], }; // Set port80HandlerConfig defaults, using legacy acme config if available if (!this.settings.port80HandlerConfig || Object.keys(this.settings.port80HandlerConfig).length === 0) { if (this.settings.acme) { // Migrate from legacy acme config this.settings.port80HandlerConfig = { enabled: this.settings.acme.enabled, port: this.settings.acme.port || 80, contactEmail: this.settings.acme.contactEmail || 'admin@example.com', useProduction: this.settings.acme.useProduction || false, renewThresholdDays: this.settings.acme.renewThresholdDays || 30, autoRenew: this.settings.acme.autoRenew !== false, // Default to true certificateStore: this.settings.acme.certificateStore || './certs', skipConfiguredCerts: this.settings.acme.skipConfiguredCerts || false, httpsRedirectPort: this.settings.fromPort, renewCheckIntervalHours: 24 }; } else { // Set defaults if no config provided this.settings.port80HandlerConfig = { enabled: false, port: 80, contactEmail: 'admin@example.com', useProduction: false, renewThresholdDays: 30, autoRenew: true, certificateStore: './certs', skipConfiguredCerts: false, httpsRedirectPort: this.settings.fromPort, renewCheckIntervalHours: 24 }; } } // 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: IPortProxySettings; /** * Initialize the Port80Handler for ACME certificate management */ private async initializePort80Handler(): Promise<void> { const config = this.settings.port80HandlerConfig; if (!config || !config.enabled) { console.log('Port80Handler is disabled in configuration'); return; } try { // Ensure the certificate store directory exists if (config.certificateStore) { const certStorePath = path.resolve(config.certificateStore); if (!fs.existsSync(certStorePath)) { fs.mkdirSync(certStorePath, { recursive: true }); console.log(`Created certificate store directory: ${certStorePath}`); } } // Create Port80Handler with options from config this.port80Handler = new Port80Handler({ port: config.port, contactEmail: config.contactEmail, useProduction: config.useProduction, renewThresholdDays: config.renewThresholdDays, httpsRedirectPort: config.httpsRedirectPort || this.settings.fromPort, renewCheckIntervalHours: config.renewCheckIntervalHours, enabled: config.enabled, autoRenew: config.autoRenew, certificateStore: config.certificateStore, skipConfiguredCerts: config.skipConfiguredCerts }); // Register domain forwarding configurations if (config.domainForwards) { for (const forward of config.domainForwards) { this.port80Handler.addDomain({ domainName: forward.domain, sslRedirect: true, acmeMaintenance: true, forward: forward.forwardConfig, acmeForward: forward.acmeForwardConfig }); console.log(`Registered domain forwarding for ${forward.domain}`); } } // Register all non-wildcard domains from domain configs for (const domainConfig of this.settings.domainConfigs) { for (const domain of domainConfig.domains) { // Skip wildcards if (domain.includes('*')) continue; this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true }); console.log(`Registered domain ${domain} with Port80Handler`); } } // Set up event listeners this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, (certData) => { console.log(`Certificate issued for ${certData.domain}, valid until ${certData.expiryDate.toISOString()}`); }); this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, (certData) => { console.log(`Certificate renewed for ${certData.domain}, valid until ${certData.expiryDate.toISOString()}`); }); this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, (failureData) => { console.log(`Certificate ${failureData.isRenewal ? 'renewal' : 'issuance'} failed for ${failureData.domain}: ${failureData.error}`); }); this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (expiryData) => { console.log(`Certificate for ${expiryData.domain} is expiring in ${expiryData.daysRemaining} days`); }); // Share Port80Handler with NetworkProxyBridge this.networkProxyBridge.setPort80Handler(this.port80Handler); // Start 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 PortProxy while it's shutting down"); return; } // Initialize Port80Handler if enabled await this.initializePort80Handler(); // 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( `PortProxy -> 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('PortProxy shutting down...'); this.isShuttingDown = true; // 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<void>[] = this.netServers.map( (server) => new Promise<void>((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('PortProxy shutdown complete.'); } /** * Updates the domain configurations for the proxy */ public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> { 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, register non-wildcard domains if (this.port80Handler && this.settings.port80HandlerConfig?.enabled) { for (const domainConfig of newDomainConfigs) { for (const domain of domainConfig.domains) { // Skip wildcards if (domain.includes('*')) continue; this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true }); } } console.log('Registered non-wildcard domains with Port80Handler'); } } /** * Updates the Port80Handler configuration */ public async updatePort80HandlerConfig(config: IPortProxySettings['port80HandlerConfig']): Promise<void> { if (!config) return; console.log('Updating Port80Handler configuration'); // Update the settings this.settings.port80HandlerConfig = { ...this.settings.port80HandlerConfig, ...config }; // Check if we need to restart Port80Handler let needsRestart = false; // Restart if enabled state changed if (this.port80Handler && config.enabled === false) { needsRestart = true; } else if (!this.port80Handler && config.enabled === true) { needsRestart = true; } else if (this.port80Handler && ( config.port !== undefined || config.contactEmail !== undefined || config.useProduction !== undefined || config.renewThresholdDays !== undefined || config.renewCheckIntervalHours !== undefined )) { // Restart if critical settings changed needsRestart = true; } if (needsRestart) { // Stop if running if (this.port80Handler) { try { await this.port80Handler.stop(); this.port80Handler = null; console.log('Stopped Port80Handler for configuration update'); } catch (err) { console.log(`Error stopping Port80Handler: ${err}`); } } // Start with new config if enabled if (this.settings.port80HandlerConfig.enabled) { await this.initializePort80Handler(); console.log('Restarted Port80Handler with new configuration'); } } else if (this.port80Handler) { // Just update domain forwards if they changed if (config.domainForwards) { for (const forward of config.domainForwards) { this.port80Handler.addDomain({ domainName: forward.domain, sslRedirect: true, acmeMaintenance: true, forward: forward.forwardConfig, acmeForward: forward.acmeForwardConfig }); } console.log('Updated domain forwards in Port80Handler'); } } } /** * Request a certificate for a specific domain */ public async requestCertificate(domain: string): Promise<boolean> { // 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.port80HandlerConfig?.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<string, any> = {}; // 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.port80HandlerConfig.renewThresholdDays }; } else { certificateStatus[domain] = { status: 'missing', message: 'No certificate found' }; } } return { enabled: true, port: this.settings.port80HandlerConfig.port, useProduction: this.settings.port80HandlerConfig.useProduction, autoRenew: this.settings.port80HandlerConfig.autoRenew, certificates: certificateStatus }; } }