dcrouter/ts/classes.dcrouter.ts

602 lines
20 KiB
TypeScript
Raw Normal View History

2025-05-08 01:13:54 +00:00
import * as plugins from './plugins.js';
import * as paths from './paths.js';
// Certificate types are available via plugins.tsclass
2025-05-07 14:33:20 +00:00
// Import the email server and its configuration
import { UnifiedEmailServer, type IUnifiedEmailServerOptions } from './mail/routing/classes.unified.email.server.js';
import type { IDomainRule, EmailProcessingMode } from './mail/routing/classes.email.config.js';
import type { IEmailRoute } from './mail/routing/interfaces.js';
2025-05-08 01:13:54 +00:00
import { logger } from './logger.js';
2025-05-21 00:12:49 +00:00
// Import the email configuration helpers directly from mail/delivery
import { configureEmailStorage, configureEmailServer } from './mail/delivery/index.js';
export interface IDcRouterOptions {
/**
* Direct SmartProxy configuration - gives full control over HTTP/HTTPS and TCP/SNI traffic
* This is the preferred way to configure HTTP/HTTPS and general TCP/SNI traffic
*/
smartProxyConfig?: plugins.smartproxy.ISmartProxyOptions;
/**
* Email server configuration
* This enables all email handling with pattern-based routing
*/
emailConfig?: IUnifiedEmailServerOptions;
2025-05-20 11:04:09 +00:00
/**
* Custom email port configuration
* Allows configuring specific ports for email handling
* This overrides the default port mapping in the emailConfig
*/
emailPortConfig?: {
/** External to internal port mapping */
portMapping?: Record<number, number>;
/** Custom port configuration for specific ports */
portSettings?: Record<number, any>;
/** Path to store received emails */
receivedEmailsPath?: string;
};
/** 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;
2025-05-21 00:12:49 +00:00
/** Path to CA certificate file (for custom CAs) */
caPath?: string;
};
2025-05-07 14:33:20 +00:00
/** DNS server configuration */
dnsServerConfig?: plugins.smartdns.IDnsServerOptions;
2025-05-19 17:34:48 +00:00
/** DNS challenge configuration for ACME (optional) */
dnsChallenge?: {
/** Cloudflare API key for DNS challenges */
cloudflareApiKey?: string;
/** Other DNS providers can be added here */
};
}
/**
* DcRouter can be run on ingress and egress to and from a datacenter site.
*/
2025-05-07 14:33:20 +00:00
/**
* Context passed to HTTP routing rules
*/
/**
* Context passed to port proxy (SmartProxy) routing rules
*/
export interface PortProxyRuleContext {
proxy: plugins.smartproxy.SmartProxy;
routes: plugins.smartproxy.IRouteConfig[];
2025-05-07 14:33:20 +00:00
}
export class DcRouter {
2025-05-07 14:33:20 +00:00
public options: IDcRouterOptions;
// Core services
2025-05-07 14:33:20 +00:00
public smartProxy?: plugins.smartproxy.SmartProxy;
public dnsServer?: plugins.smartdns.DnsServer;
2025-05-27 14:06:22 +00:00
public emailServer?: UnifiedEmailServer;
2025-05-20 11:04:09 +00:00
// Environment access
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
2025-05-21 02:17:18 +00:00
constructor(optionsArg: IDcRouterOptions) {
// Set defaults in options
this.options = {
...optionsArg
};
2025-05-20 11:04:09 +00:00
2025-05-07 14:33:20 +00:00
}
public async start() {
console.log('Starting DcRouter services...');
try {
// Set up SmartProxy for HTTP/HTTPS and all traffic including email routes
await this.setupSmartProxy();
// Set up unified email handling if configured
if (this.options.emailConfig) {
await this.setupUnifiedEmailHandling();
2025-05-20 11:04:09 +00:00
// Apply custom email storage configuration if available
2025-05-27 14:06:22 +00:00
if (this.emailServer && this.options.emailPortConfig?.receivedEmailsPath) {
2025-05-20 11:04:09 +00:00
logger.log('info', 'Applying custom email storage configuration');
2025-05-27 14:06:22 +00:00
configureEmailStorage(this.emailServer, this.options);
2025-05-20 11:04:09 +00:00
}
}
// 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 with direct configuration and automatic email routes
*/
private async setupSmartProxy(): Promise<void> {
2025-05-19 17:34:48 +00:00
console.log('[DcRouter] Setting up SmartProxy...');
let routes: plugins.smartproxy.IRouteConfig[] = [];
let acmeConfig: plugins.smartproxy.IAcmeOptions | undefined;
// If user provides full SmartProxy config, use it directly
if (this.options.smartProxyConfig) {
routes = this.options.smartProxyConfig.routes || [];
acmeConfig = this.options.smartProxyConfig.acme;
2025-05-19 17:34:48 +00:00
console.log(`[DcRouter] Found ${routes.length} routes in config`);
console.log(`[DcRouter] ACME config present: ${!!acmeConfig}`);
}
// If email config exists, automatically add email routes
if (this.options.emailConfig) {
const emailRoutes = this.generateEmailRoutes(this.options.emailConfig);
2025-05-20 19:46:59 +00:00
console.log(`Email Routes are:`)
console.log(emailRoutes)
2025-05-21 00:12:49 +00:00
routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy
}
// Merge TLS/ACME configuration if provided at root level
if (this.options.tls && !acmeConfig) {
acmeConfig = {
accountEmail: this.options.tls.contactEmail,
enabled: true,
useProduction: true,
autoRenew: true,
renewThresholdDays: 30
};
}
2025-05-19 17:34:48 +00:00
// Configure DNS challenge if available
let challengeHandlers: any[] = [];
if (this.options.dnsChallenge?.cloudflareApiKey) {
console.log('Configuring Cloudflare DNS challenge for ACME');
const cloudflareAccount = new plugins.cloudflare.CloudflareAccount(this.options.dnsChallenge.cloudflareApiKey);
const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(cloudflareAccount);
challengeHandlers.push(dns01Handler);
}
// If we have routes or need a basic SmartProxy instance, create it
if (routes.length > 0 || this.options.smartProxyConfig) {
console.log('Setting up SmartProxy with combined configuration');
// Create SmartProxy configuration
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
...this.options.smartProxyConfig,
routes,
acme: acmeConfig
};
2025-05-19 17:34:48 +00:00
// If we have DNS challenge handlers, enhance the config
if (challengeHandlers.length > 0) {
// We'll need to pass this to SmartProxy somehow
// For now, we'll set it as a property
(smartProxyConfig as any).acmeChallengeHandlers = challengeHandlers;
(smartProxyConfig as any).acmeChallengePriority = ['dns-01', 'http-01'];
}
// Create SmartProxy instance
2025-05-19 17:34:48 +00:00
console.log('[DcRouter] Creating SmartProxy instance with config:', JSON.stringify({
routeCount: smartProxyConfig.routes?.length,
acmeEnabled: smartProxyConfig.acme?.enabled,
acmeEmail: smartProxyConfig.acme?.email,
certProvisionFunction: !!smartProxyConfig.certProvisionFunction
}, null, 2));
this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
// Set up event listeners
this.smartProxy.on('error', (err) => {
2025-05-19 17:34:48 +00:00
console.error('[DcRouter] SmartProxy error:', err);
console.error('[DcRouter] Error stack:', err.stack);
});
if (acmeConfig) {
this.smartProxy.on('certificate-issued', (event) => {
2025-05-19 17:34:48 +00:00
console.log(`[DcRouter] Certificate issued for ${event.domain}, expires ${event.expiryDate}`);
});
this.smartProxy.on('certificate-renewed', (event) => {
2025-05-19 17:34:48 +00:00
console.log(`[DcRouter] Certificate renewed for ${event.domain}, expires ${event.expiryDate}`);
});
this.smartProxy.on('certificate-failed', (event) => {
console.error(`[DcRouter] Certificate failed for ${event.domain}:`, event.error);
});
}
// Start SmartProxy
2025-05-19 17:34:48 +00:00
console.log('[DcRouter] Starting SmartProxy...');
await this.smartProxy.start();
2025-05-19 17:34:48 +00:00
console.log('[DcRouter] SmartProxy started successfully');
console.log(`SmartProxy started with ${routes.length} routes`);
}
}
/**
* Generate SmartProxy routes for email configuration
*/
private generateEmailRoutes(emailConfig: IUnifiedEmailServerOptions): plugins.smartproxy.IRouteConfig[] {
const emailRoutes: plugins.smartproxy.IRouteConfig[] = [];
2025-05-20 11:04:09 +00:00
// Get the custom port mapping if available, otherwise use defaults
const defaultPortMapping = {
25: 10025, // SMTP
587: 10587, // Submission
465: 10465 // SMTPS
};
// Use custom port mapping if provided, otherwise fall back to defaults
const portMapping = this.options.emailPortConfig?.portMapping || defaultPortMapping;
// Create routes for each email port
for (const port of emailConfig.ports) {
2025-05-20 11:04:09 +00:00
// Calculate the internal port using the mapping
const internalPort = portMapping[port] || port + 10000;
// Create a descriptive name for the route based on the port
let routeName = 'email-route';
let tlsMode = 'passthrough';
// Handle different email ports differently
switch (port) {
case 25: // SMTP
2025-05-20 11:04:09 +00:00
routeName = 'smtp-route';
tlsMode = 'passthrough'; // STARTTLS handled by email server
break;
case 587: // Submission
2025-05-20 11:04:09 +00:00
routeName = 'submission-route';
tlsMode = 'passthrough'; // STARTTLS handled by email server
break;
case 465: // SMTPS
2025-05-20 11:04:09 +00:00
routeName = 'smtps-route';
tlsMode = 'terminate'; // Terminate TLS and re-encrypt to email server
break;
default:
routeName = `email-port-${port}-route`;
// For unknown ports, assume passthrough by default
tlsMode = 'passthrough';
// Check if we have specific settings for this port
if (this.options.emailPortConfig?.portSettings &&
this.options.emailPortConfig.portSettings[port]) {
const portSettings = this.options.emailPortConfig.portSettings[port];
// If this port requires TLS termination, set the mode accordingly
if (portSettings.terminateTls) {
tlsMode = 'terminate';
}
2025-05-20 11:04:09 +00:00
// Override the route name if specified
if (portSettings.routeName) {
routeName = portSettings.routeName;
}
}
break;
}
2025-05-20 11:04:09 +00:00
// Create the route configuration
const routeConfig: plugins.smartproxy.IRouteConfig = {
name: routeName,
match: {
ports: [port]
},
action: {
type: 'forward',
target: {
host: 'localhost', // Forward to internal email server
port: internalPort
},
tls: {
mode: tlsMode as any
}
}
};
// For TLS terminate mode, add certificate info
if (tlsMode === 'terminate') {
routeConfig.action.tls.certificate = 'auto';
}
// Add the route to our list
emailRoutes.push(routeConfig);
}
// Add email routes if configured
if (emailConfig.routes) {
for (const route of emailConfig.routes) {
emailRoutes.push({
name: route.name,
match: {
ports: emailConfig.ports,
domains: route.match.recipients ? [route.match.recipients.toString().split('@')[1]] : []
},
action: {
type: 'forward',
target: route.action.type === 'forward' && route.action.forward ? {
host: route.action.forward.host,
port: route.action.forward.port || 25
} : undefined,
tls: {
mode: 'passthrough'
}
}
});
}
}
return emailRoutes;
}
/**
* 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([
2025-05-27 14:06:22 +00:00
// Stop unified email server if running
this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', err)) : Promise.resolve(),
// Stop HTTP SmartProxy if running
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', 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 SmartProxy configuration
* @param config New SmartProxy configuration
*/
public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise<void> {
// Stop existing SmartProxy if running
if (this.smartProxy) {
await this.smartProxy.stop();
this.smartProxy = undefined;
}
// Update configuration
this.options.smartProxyConfig = config;
// Start new SmartProxy with updated configuration (will include email routes if configured)
await this.setupSmartProxy();
console.log('SmartProxy configuration updated');
}
/**
* Set up unified email handling with pattern-based routing
* This implements the consolidated emailConfig approach
*/
private async setupUnifiedEmailHandling(): Promise<void> {
if (!this.options.emailConfig) {
throw new Error('Email configuration is required for unified email handling');
}
// Apply port mapping if behind SmartProxy
const portMapping = this.options.emailPortConfig?.portMapping || {
25: 10025, // SMTP
587: 10587, // Submission
465: 10465 // SMTPS
};
// Create config with mapped ports
const emailConfig: IUnifiedEmailServerOptions = {
...this.options.emailConfig,
ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
hostname: 'localhost' // Listen on localhost for SmartProxy forwarding
};
// Create unified email server
this.emailServer = new UnifiedEmailServer(this, emailConfig);
// Set up error handling
this.emailServer.on('error', (err: Error) => {
logger.log('error', `UnifiedEmailServer error: ${err.message}`);
});
2025-05-20 11:04:09 +00:00
// Start the server
await this.emailServer.start();
2025-05-20 11:04:09 +00:00
logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`);
2025-05-07 14:33:20 +00:00
}
2025-05-07 14:33:20 +00:00
/**
* Update the unified email configuration
* @param config New email configuration
2025-05-07 14:33:20 +00:00
*/
public async updateEmailConfig(config: IUnifiedEmailServerOptions): Promise<void> {
// Stop existing email components
await this.stopUnifiedEmailComponents();
// Update configuration
this.options.emailConfig = config;
// Start email handling with new configuration
await this.setupUnifiedEmailHandling();
console.log('Unified email configuration updated');
}
/**
* Stop all unified email components
*/
private async stopUnifiedEmailComponents(): Promise<void> {
try {
2025-05-27 14:06:22 +00:00
// Stop the unified email server which contains all components
if (this.emailServer) {
await this.emailServer.stop();
logger.log('info', 'Unified email server stopped');
2025-05-27 14:06:22 +00:00
this.emailServer = undefined;
}
logger.log('info', 'All unified email components stopped');
} catch (error) {
logger.log('error', `Error stopping unified email components: ${error.message}`);
throw error;
}
}
/**
* Update domain rules for email routing
* @param rules New domain rules to apply
*/
public async updateEmailRoutes(routes: IEmailRoute[]): Promise<void> {
// Validate that email config exists
if (!this.options.emailConfig) {
throw new Error('Email configuration is required before updating routes');
}
// Update the configuration
this.options.emailConfig.routes = routes;
// Update the unified email server if it exists
2025-05-27 14:06:22 +00:00
if (this.emailServer) {
this.emailServer.updateRoutes(routes);
}
console.log(`Email routes updated with ${routes.length} routes`);
}
/**
* Get statistics from all components
*/
public getStats(): any {
const stats: any = {
2025-05-27 14:06:22 +00:00
emailServer: this.emailServer?.getStats()
};
return stats;
}
2025-05-20 19:46:59 +00:00
/**
* Configure MTA for email handling with custom port and storage settings
* @param config Configuration for the MTA service
*/
public async configureEmailMta(config: {
internalPort: number;
host?: string;
secure?: boolean;
storagePath?: string;
portMapping?: Record<number, number>;
}): Promise<boolean> {
logger.log('info', 'Configuring MTA service with custom settings');
// Update email port configuration
if (!this.options.emailPortConfig) {
this.options.emailPortConfig = {};
}
// Configure storage paths for received emails
if (config.storagePath) {
// Set the storage path for received emails
this.options.emailPortConfig.receivedEmailsPath = config.storagePath;
}
// Apply port mapping if provided
if (config.portMapping) {
this.options.emailPortConfig.portMapping = {
...this.options.emailPortConfig.portMapping,
...config.portMapping
};
logger.log('info', `Updated MTA port mappings: ${JSON.stringify(this.options.emailPortConfig.portMapping)}`);
}
2025-05-21 00:12:49 +00:00
// Use the dedicated helper to configure the email server
// Pass through the options specified by the implementation
2025-05-27 14:06:22 +00:00
if (this.emailServer) {
configureEmailServer(this.emailServer, {
2025-05-21 00:12:49 +00:00
ports: [config.internalPort], // Use whatever port the implementation specifies
hostname: config.host,
tls: config.secure ? {
// Basic TLS settings if secure mode is enabled
certPath: this.options.tls?.certPath,
keyPath: this.options.tls?.keyPath,
caPath: this.options.tls?.caPath
} : undefined,
storagePath: config.storagePath
});
}
2025-05-20 19:46:59 +00:00
// If email handling is already set up, restart it to apply changes
2025-05-27 14:06:22 +00:00
if (this.emailServer) {
2025-05-20 19:46:59 +00:00
logger.log('info', 'Restarting unified email handling to apply MTA configuration changes');
await this.stopUnifiedEmailComponents();
await this.setupUnifiedEmailHandling();
}
return true;
}
}
// Re-export email server types for convenience
export type { IUnifiedEmailServerOptions, IDomainRule, EmailProcessingMode };
2025-05-24 01:00:30 +00:00
export default DcRouter;