import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; import { SzDcRouterConnector } from './classes.dcr.sz.connector.js'; import type { SzPlatformService } from '../platformservice.js'; import { type IMtaConfig, MtaService } from '../mta/classes.mta.js'; // Certificate types are available via plugins.tsclass export interface IDcRouterOptions { platformServiceInstance?: SzPlatformService; /** SmartProxy (TCP/SNI) configuration */ smartProxyOptions?: plugins.smartproxy.ISmartProxyOptions; /** Reverse proxy host configurations for HTTP(S) layer */ reverseProxyConfigs?: plugins.smartproxy.IReverseProxyConfig[]; /** MTA (SMTP) service configuration */ mtaConfig?: IMtaConfig; /** Existing MTA service instance to use instead of creating a new one */ mtaServiceInstance?: MtaService; /** 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 szDcRouterConnector = new SzDcRouterConnector(this); public options: IDcRouterOptions; public smartProxy?: plugins.smartproxy.SmartProxy; public mta?: MtaService; public dnsServer?: plugins.smartdns.DnsServer; /** SMTP rule engine */ public smtpRuleEngine?: plugins.smartrule.SmartRule; constructor(optionsArg: IDcRouterOptions) { // Set defaults in options this.options = { ...optionsArg }; } public async start() { // Set up MTA service - use existing instance if provided if (this.options.mtaServiceInstance) { // Use provided MTA service instance this.mta = this.options.mtaServiceInstance; console.log('Using provided MTA service instance'); // Get the SMTP rule engine from the provided MTA this.smtpRuleEngine = this.mta.smtpRuleEngine; } 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'); // Initialize SMTP rule engine this.smtpRuleEngine = this.mta.smtpRuleEngine; } // TCP/SNI proxy (SmartProxy) if (this.options.smartProxyOptions) { // Lets setup smartacme let certProvisionFunction: plugins.smartproxy.ISmartProxyOptions['certProvisionFunction']; // Check if we can share certificate from MTA service if (this.options.mtaServiceInstance && this.mta) { // Share TLS certificate with MTA service (if available) console.log('Using MTA service certificate for SmartProxy'); // Create proxy function to get cert from MTA service certProvisionFunction = async (domainArg) => { // Get cert from provided MTA service if available if (this.mta && this.mta.certificate) { console.log(`Using MTA certificate for domain ${domainArg}`); // Return in the format expected by SmartProxy const certExpiry = this.mta.certificate.expiresAt; const certObj: plugins.tsclass.network.ICert = { id: `cert-${domainArg}`, domainName: domainArg, privateKey: this.mta.certificate.privateKey, publicKey: this.mta.certificate.publicKey, created: Date.now(), validUntil: certExpiry instanceof Date ? certExpiry.getTime() : Date.now() + 90 * 24 * 60 * 60 * 1000, csr: '' }; return certObj; } else { console.log(`No MTA certificate available for domain ${domainArg}, falling back to ACME`); // Return string literal instead of 'http01' enum value return null; // Let SmartProxy fall back to its default mechanism } }; } else if (true) { // Set up ACME for certificate provisioning const smartAcmeInstance = new plugins.smartacme.SmartAcme({ accountEmail: this.options.smartProxyOptions.acme.accountEmail, certManager: new plugins.smartacme.certmanagers.MongoCertManager({ mongoDbUrl: await this.szDcRouterConnector.getEnvVarOnDemand('MONGO_DB_URL'), mongoDbUser: await this.szDcRouterConnector.getEnvVarOnDemand('MONGO_DB_USER'), mongoDbPass: await this.szDcRouterConnector.getEnvVarOnDemand('MONGO_DB_PASS'), mongoDbName: await this.szDcRouterConnector.getEnvVarOnDemand('MONGO_DB_NAME'), }), environment: 'production', accountPrivateKey: await this.szDcRouterConnector.getEnvVarOnDemand('ACME_ACCOUNT_PRIVATE_KEY'), challengeHandlers: [ new plugins.smartacme.handlers.Dns01Handler(new plugins.cloudflare.CloudflareAccount('')) // TODO ], }); certProvisionFunction = async (domainArg) => { try { const domainSupported = await smartAcmeInstance.challengeHandlers[0].checkWetherDomainIsSupported(domainArg); if (!domainSupported) { return null; // Let SmartProxy handle with default mechanism } // Get the certificate and convert to ICert const cert = await smartAcmeInstance.getCertificateForDomain(domainArg); if (typeof cert === 'string') { return null; // String result indicates fallback } // Return in the format expected by SmartProxy const result: plugins.tsclass.network.ICert = { id: `cert-${domainArg}`, domainName: domainArg, privateKey: cert.privateKey, publicKey: cert.publicKey, created: Date.now(), validUntil: Date.now() + 90 * 24 * 60 * 60 * 1000, // 90 days csr: '' }; return result; } catch (err) { console.error(`Certificate error for ${domainArg}:`, err); return null; // Let SmartProxy handle with default mechanism } }; } // Create the SmartProxy instance with the appropriate cert provisioning function const smartProxyOptions = { ...this.options.smartProxyOptions, certProvisionFunction }; this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyOptions); // Configure SmartProxy for SMTP if we have an MTA service if (this.mta) { this.configureSmtpProxy(); } } // DNS server if (this.options.dnsServerConfig) { this.dnsServer = new plugins.smartdns.DnsServer(this.options.dnsServerConfig); } // Start SmartProxy if configured if (this.smartProxy) { await this.smartProxy.start(); } // Start MTA service if configured and it's our own service (not an external instance) if (this.mta && !this.options.mtaServiceInstance) { await this.mta.start(); } // Start DNS server if configured if (this.dnsServer) { await this.dnsServer.start(); } } /** * Configure SmartProxy for SMTP ports */ public configureSmtpProxy(): void { if (!this.smartProxy || !this.mta) return; const mtaPort = this.mta.config.smtp?.port || 25; try { // Configure SmartProxy to forward SMTP ports to the MTA service const settings = this.smartProxy.settings; // Ensure localhost target for MTA settings.targetIP = settings.targetIP || 'localhost'; // Forward all SMTP ports to the MTA port settings.toPort = mtaPort; // Initialize globalPortRanges if needed if (!settings.globalPortRanges) { settings.globalPortRanges = []; } // Add SMTP ports 25, 587, 465 if not already present for (const port of [25, 587, 465]) { if (!settings.globalPortRanges.some((r) => r.from <= port && port <= r.to)) { settings.globalPortRanges.push({ from: port, to: port }); } } console.log(`Configured SmartProxy for SMTP ports: 25, 587, 465 → localhost:${mtaPort}`); } catch (error) { console.error('Failed to configure SmartProxy for SMTP:', error); } } public async stop() { // Stop SmartProxy if (this.smartProxy) { await this.smartProxy.stop(); } // Stop MTA service if it's our own (not an external instance) if (this.mta && !this.options.mtaServiceInstance) { await this.mta.stop(); } // Stop DNS server if (this.dnsServer) { await this.dnsServer.stop(); } } /** * Register an SMTP routing rule */ public addSmtpRule( priority: number, check: (email: any) => Promise, action: (email: any) => Promise ): void { this.smtpRuleEngine?.createRule(priority, check, action); } } export default DcRouter;