import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; import { SmtpPortConfig, type ISmtpPortSettings } from './classes.smtp.portconfig.js'; import { EmailDomainRouter, type IEmailDomainRoutingConfig } from './classes.email.domainrouter.js'; import { type IMtaConfig, MtaService } from '../mta/classes.mta.js'; // Certificate types are available via plugins.tsclass /** * Configuration for SMTP forwarding functionality */ export interface ISmtpForwardingConfig { /** Whether SMTP forwarding is enabled */ enabled?: boolean; /** SMTP ports to listen on */ ports?: number[]; /** Default SMTP server hostname */ defaultServer: string; /** Default SMTP server port */ defaultPort?: number; /** Whether to use TLS when connecting to the default server */ useTls?: boolean; /** Preserve source IP address when forwarding */ preserveSourceIp?: boolean; /** Domain-specific routing rules */ domainRoutes?: Array<{ domain: string; server: string; port?: number; }>; } /** * Simple domain-based routing configuration */ export interface IDomainRoutingConfig { /** The domain name or pattern (e.g., example.com or *.example.com) */ domain: string; /** Target server hostname or IP */ targetServer: string; /** Target port */ targetPort: number; /** Enable HTTPS/TLS for this route */ useTls?: boolean; /** Allow incoming connections from these IP ranges (default: all) */ allowedIps?: string[]; } export interface IDcRouterOptions { /** HTTP/HTTPS domain-based routing */ httpDomainRoutes?: IDomainRoutingConfig[]; /** SMTP forwarding configuration */ smtpForwarding?: ISmtpForwardingConfig; /** MTA service configuration (if not using SMTP forwarding) */ mtaConfig?: IMtaConfig; /** Existing MTA service instance to use (if not using SMTP forwarding) */ mtaServiceInstance?: MtaService; /** TLS/certificate configuration */ tls?: { /** Contact email for ACME certificates */ contactEmail: string; /** Domain for main certificate */ domain?: string; /** Path to certificate file (if not using auto-provisioning) */ certPath?: string; /** Path to key file (if not using auto-provisioning) */ keyPath?: string; }; /** DNS server configuration */ dnsServerConfig?: plugins.smartdns.IDnsServerOptions; } /** * DcRouter can be run on ingress and egress to and from a datacenter site. */ /** * Context passed to HTTP routing rules */ /** * Context passed to port proxy (SmartProxy) routing rules */ export interface PortProxyRuleContext { proxy: plugins.smartproxy.SmartProxy; configs: plugins.smartproxy.IPortProxySettings['domainConfigs']; } export class DcRouter { public options: IDcRouterOptions; // Core services public smartProxy?: plugins.smartproxy.SmartProxy; public smtpProxy?: plugins.smartproxy.SmartProxy; public mta?: MtaService; public dnsServer?: plugins.smartdns.DnsServer; // Environment access private qenv = new plugins.qenv.Qenv('./', '.nogit/'); constructor(optionsArg: IDcRouterOptions) { // Set defaults in options this.options = { ...optionsArg }; } public async start() { console.log('Starting DcRouter services...'); try { // 1. Set up HTTP/HTTPS traffic handling with SmartProxy await this.setupHttpProxy(); // 2. Set up MTA or SMTP forwarding if (this.options.smtpForwarding?.enabled) { await this.setupSmtpForwarding(); } else { await this.setupMtaService(); } // 3. Set up DNS server if configured if (this.options.dnsServerConfig) { this.dnsServer = new plugins.smartdns.DnsServer(this.options.dnsServerConfig); await this.dnsServer.start(); console.log('DNS server started'); } console.log('DcRouter started successfully'); } catch (error) { console.error('Error starting DcRouter:', error); // Try to clean up any services that may have started await this.stop(); throw error; } } /** * Set up SmartProxy for HTTP/HTTPS traffic */ private async setupHttpProxy() { if (!this.options.httpDomainRoutes || this.options.httpDomainRoutes.length === 0) { console.log('No HTTP domain routes configured, skipping HTTP proxy setup'); return; } console.log('Setting up SmartProxy for HTTP/HTTPS traffic'); // Prepare SmartProxy configuration const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = { fromPort: 443, toPort: this.options.httpDomainRoutes[0].targetPort, targetIP: this.options.httpDomainRoutes[0].targetServer, sniEnabled: true, acme: { port: 80, enabled: true, autoRenew: true, useProduction: true, renewThresholdDays: 30, accountEmail: this.options.tls?.contactEmail || 'admin@example.com' // ACME requires an email }, globalPortRanges: [{ from: 443, to: 443 }], domainConfigs: [] }; // Create domain configs from the HTTP routes smartProxyConfig.domainConfigs = this.options.httpDomainRoutes.map(route => ({ domains: [route.domain], targetIPs: [route.targetServer], allowedIPs: route.allowedIps || ['0.0.0.0/0'], // Skip certificate management for wildcard domains as it's not supported by HTTP-01 challenges certificateManagement: !route.domain.includes('*') })); // Create and start the SmartProxy instance this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig); // Listen for certificate events this.smartProxy.on('certificate-issued', event => { console.log(`Certificate issued for ${event.domain}, expires ${event.expiryDate}`); }); this.smartProxy.on('certificate-renewed', event => { console.log(`Certificate renewed for ${event.domain}, expires ${event.expiryDate}`); }); await this.smartProxy.start(); console.log(`HTTP/HTTPS proxy configured with ${smartProxyConfig.domainConfigs.length} domain routes`); } /** * Set up the MTA service */ private async setupMtaService() { // Use existing MTA service if provided if (this.options.mtaServiceInstance) { this.mta = this.options.mtaServiceInstance; console.log('Using provided MTA service instance'); } else if (this.options.mtaConfig) { // Create new MTA service with the provided configuration this.mta = new MtaService(undefined, this.options.mtaConfig); console.log('Created new MTA service instance'); // Start the MTA service await this.mta.start(); console.log('MTA service started'); } } /** * Set up SMTP forwarding with SmartProxy */ private async setupSmtpForwarding() { if (!this.options.smtpForwarding) { return; } const forwarding = this.options.smtpForwarding; console.log('Setting up SMTP forwarding'); // Determine which ports to listen on const smtpPorts = forwarding.ports || [25, 587, 465]; // Create SmartProxy instance for SMTP forwarding this.smtpProxy = new plugins.smartproxy.SmartProxy({ // Listen on the first SMTP port fromPort: smtpPorts[0], // Forward to the default server toPort: forwarding.defaultPort || 25, targetIP: forwarding.defaultServer, // Enable SNI if port 465 is included (implicit TLS) sniEnabled: smtpPorts.includes(465), // Preserve source IP if requested preserveSourceIP: forwarding.preserveSourceIp || false, // Create domain configs for SMTP routing domainConfigs: forwarding.domainRoutes?.map(route => ({ domains: [route.domain], allowedIPs: ['0.0.0.0/0'], // Allow from anywhere by default targetIPs: [route.server] })) || [], // Include all SMTP ports in the global port ranges globalPortRanges: smtpPorts.map(port => ({ from: port, to: port })) }); // Start the SMTP proxy await this.smtpProxy.start(); console.log(`SMTP forwarding configured on ports ${smtpPorts.join(', ')}`); } /** * Check if a domain matches a pattern (including wildcard support) * @param domain The domain to check * @param pattern The pattern to match against (e.g., "*.example.com") * @returns Whether the domain matches the pattern */ private isDomainMatch(domain: string, pattern: string): boolean { // Normalize inputs domain = domain.toLowerCase(); pattern = pattern.toLowerCase(); // Check for exact match if (domain === pattern) { return true; } // Check for wildcard match (*.example.com) if (pattern.startsWith('*.')) { const patternSuffix = pattern.slice(2); // Remove the "*." prefix // Check if domain ends with the pattern suffix and has at least one character before it return domain.endsWith(patternSuffix) && domain.length > patternSuffix.length; } // No match return false; } public async stop() { console.log('Stopping DcRouter services...'); try { // Stop all services in parallel for faster shutdown await Promise.all([ // Stop HTTP SmartProxy if running this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping HTTP SmartProxy:', err)) : Promise.resolve(), // Stop SMTP SmartProxy if running this.smtpProxy ? this.smtpProxy.stop().catch(err => console.error('Error stopping SMTP SmartProxy:', err)) : Promise.resolve(), // Stop MTA service if it's our own (not an external instance) (this.mta && !this.options.mtaServiceInstance) ? this.mta.stop().catch(err => console.error('Error stopping MTA service:', err)) : Promise.resolve(), // Stop DNS server if running this.dnsServer ? this.dnsServer.stop().catch(err => console.error('Error stopping DNS server:', err)) : Promise.resolve() ]); console.log('All DcRouter services stopped'); } catch (error) { console.error('Error during DcRouter shutdown:', error); throw error; } } /** * Update HTTP domain routes * @param routes New HTTP domain routes */ public async updateHttpRoutes(routes: IDomainRoutingConfig[]): Promise { this.options.httpDomainRoutes = routes; // If SmartProxy is already running, we need to restart it with the new configuration if (this.smartProxy) { // Stop the existing SmartProxy await this.smartProxy.stop(); this.smartProxy = undefined; // Start a new SmartProxy with the updated configuration await this.setupHttpProxy(); } console.log(`Updated HTTP routes with ${routes.length} domains`); } /** * Update SMTP forwarding configuration * @param config New SMTP forwarding configuration */ public async updateSmtpForwarding(config: ISmtpForwardingConfig): Promise { // Stop existing SMTP proxy if running if (this.smtpProxy) { await this.smtpProxy.stop(); this.smtpProxy = undefined; } // Update configuration this.options.smtpForwarding = config; // Restart SMTP forwarding if enabled if (config.enabled) { await this.setupSmtpForwarding(); } console.log('SMTP forwarding configuration updated'); } } export default DcRouter;