565 lines
18 KiB
TypeScript
565 lines
18 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
import * as paths from './paths.js';
|
|
|
|
// Certificate types are available via plugins.tsclass
|
|
|
|
// Import the consolidated email config
|
|
import type { IEmailConfig, IDomainRule } from './mail/routing/classes.email.config.js';
|
|
import { DomainRouter } from './mail/routing/classes.domain.router.js';
|
|
import { UnifiedEmailServer } from './mail/routing/classes.unified.email.server.js';
|
|
import { UnifiedDeliveryQueue, type IQueueOptions } from './mail/delivery/classes.delivery.queue.js';
|
|
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from './mail/delivery/classes.delivery.system.js';
|
|
import { UnifiedRateLimiter, type IHierarchicalRateLimits } from './mail/delivery/classes.unified.rate.limiter.js';
|
|
import { logger } from './logger.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;
|
|
|
|
/**
|
|
* Consolidated email configuration
|
|
* This enables all email handling with pattern-based routing
|
|
*/
|
|
emailConfig?: IEmailConfig;
|
|
|
|
/** 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;
|
|
routes: plugins.smartproxy.IRouteConfig[];
|
|
}
|
|
|
|
export class DcRouter {
|
|
public options: IDcRouterOptions;
|
|
|
|
// Core services
|
|
public smartProxy?: plugins.smartproxy.SmartProxy;
|
|
public dnsServer?: plugins.smartdns.DnsServer;
|
|
|
|
// Unified email components
|
|
public domainRouter?: DomainRouter;
|
|
public unifiedEmailServer?: UnifiedEmailServer;
|
|
public deliveryQueue?: UnifiedDeliveryQueue;
|
|
public deliverySystem?: MultiModeDeliverySystem;
|
|
public rateLimiter?: UnifiedRateLimiter;
|
|
|
|
// 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 {
|
|
// 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();
|
|
}
|
|
|
|
// 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> {
|
|
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;
|
|
}
|
|
|
|
// If email config exists, automatically add email routes
|
|
if (this.options.emailConfig) {
|
|
const emailRoutes = this.generateEmailRoutes(this.options.emailConfig);
|
|
routes = [...routes, ...emailRoutes];
|
|
}
|
|
|
|
// 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
|
|
};
|
|
}
|
|
|
|
// 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
|
|
};
|
|
|
|
// Create SmartProxy instance
|
|
this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
|
|
|
|
// Set up event listeners
|
|
this.smartProxy.on('error', (err) => {
|
|
console.error('SmartProxy error:', err);
|
|
});
|
|
|
|
if (acmeConfig) {
|
|
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}`);
|
|
});
|
|
}
|
|
|
|
// Start SmartProxy
|
|
await this.smartProxy.start();
|
|
|
|
console.log(`SmartProxy started with ${routes.length} routes`);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Generate SmartProxy routes for email configuration
|
|
*/
|
|
private generateEmailRoutes(emailConfig: IEmailConfig): plugins.smartproxy.IRouteConfig[] {
|
|
const emailRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
|
|
// Create routes for each email port
|
|
for (const port of emailConfig.ports) {
|
|
// Handle different email ports differently
|
|
switch (port) {
|
|
case 25: // SMTP
|
|
emailRoutes.push({
|
|
name: 'smtp-route',
|
|
match: {
|
|
ports: [25]
|
|
},
|
|
action: {
|
|
type: 'forward',
|
|
target: {
|
|
host: 'localhost', // Forward to internal email server
|
|
port: 10025 // Internal email server port
|
|
},
|
|
// No TLS termination for port 25 (STARTTLS handled by email server)
|
|
tls: {
|
|
mode: 'passthrough'
|
|
}
|
|
}
|
|
});
|
|
break;
|
|
|
|
case 587: // Submission
|
|
emailRoutes.push({
|
|
name: 'submission-route',
|
|
match: {
|
|
ports: [587]
|
|
},
|
|
action: {
|
|
type: 'forward',
|
|
target: {
|
|
host: 'localhost',
|
|
port: 10587
|
|
},
|
|
tls: {
|
|
mode: 'passthrough' // STARTTLS handled by email server
|
|
}
|
|
}
|
|
});
|
|
break;
|
|
|
|
case 465: // SMTPS
|
|
emailRoutes.push({
|
|
name: 'smtps-route',
|
|
match: {
|
|
ports: [465]
|
|
},
|
|
action: {
|
|
type: 'forward',
|
|
target: {
|
|
host: 'localhost',
|
|
port: 10465
|
|
},
|
|
tls: {
|
|
mode: 'terminate', // Terminate TLS and re-encrypt to email server
|
|
certificate: 'auto'
|
|
}
|
|
}
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Add domain-specific email routes if configured
|
|
if (emailConfig.domainRules) {
|
|
for (const rule of emailConfig.domainRules) {
|
|
// Extract domain from pattern (e.g., "*@example.com" -> "example.com")
|
|
const domain = rule.pattern.split('@')[1];
|
|
|
|
if (domain && rule.mode === 'forward' && rule.target) {
|
|
emailRoutes.push({
|
|
name: `email-forward-${domain}`,
|
|
match: {
|
|
ports: emailConfig.ports,
|
|
domains: [domain]
|
|
},
|
|
action: {
|
|
type: 'forward',
|
|
target: {
|
|
host: rule.target.server,
|
|
port: rule.target.port || 25
|
|
},
|
|
tls: {
|
|
mode: rule.target.useTls ? 'terminate-and-reencrypt' : '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([
|
|
// Stop unified email components if running
|
|
this.domainRouter ? this.stopUnifiedEmailComponents().catch(err => console.error('Error stopping unified email components:', 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> {
|
|
logger.log('info', 'Setting up unified email handling with pattern-based routing');
|
|
|
|
if (!this.options.emailConfig) {
|
|
throw new Error('Email configuration is required for unified email handling');
|
|
}
|
|
|
|
const emailConfig = this.options.emailConfig;
|
|
|
|
// Map external ports to internal ports
|
|
const portMapping = {
|
|
25: 10025, // SMTP
|
|
587: 10587, // Submission
|
|
465: 10465 // SMTPS
|
|
};
|
|
|
|
// Create internal email server configuration
|
|
const internalEmailConfig: IEmailConfig = {
|
|
...emailConfig,
|
|
ports: emailConfig.ports.map(port => portMapping[port] || port + 10000),
|
|
hostname: 'localhost' // Listen on localhost for SmartProxy forwarding
|
|
};
|
|
|
|
try {
|
|
// Create domain router for pattern matching
|
|
this.domainRouter = new DomainRouter({
|
|
domainRules: emailConfig.domainRules,
|
|
defaultMode: emailConfig.defaultMode,
|
|
defaultServer: emailConfig.defaultServer,
|
|
defaultPort: emailConfig.defaultPort,
|
|
defaultTls: emailConfig.defaultTls
|
|
});
|
|
|
|
// Initialize the rate limiter
|
|
this.rateLimiter = new UnifiedRateLimiter({
|
|
global: {
|
|
maxMessagesPerMinute: 100,
|
|
maxRecipientsPerMessage: 100,
|
|
maxConnectionsPerIP: 20,
|
|
maxErrorsPerIP: 10,
|
|
maxAuthFailuresPerIP: 5
|
|
}
|
|
});
|
|
|
|
// Initialize the unified delivery queue
|
|
const queueOptions: IQueueOptions = {
|
|
storageType: emailConfig.queue?.storageType || 'memory',
|
|
persistentPath: emailConfig.queue?.persistentPath,
|
|
maxRetries: emailConfig.queue?.maxRetries,
|
|
baseRetryDelay: emailConfig.queue?.baseRetryDelay,
|
|
maxRetryDelay: emailConfig.queue?.maxRetryDelay
|
|
};
|
|
|
|
this.deliveryQueue = new UnifiedDeliveryQueue(queueOptions);
|
|
await this.deliveryQueue.initialize();
|
|
|
|
// Initialize the delivery system
|
|
const deliveryOptions: IMultiModeDeliveryOptions = {
|
|
globalRateLimit: 100, // Default to 100 emails per minute
|
|
concurrentDeliveries: 10
|
|
};
|
|
|
|
this.deliverySystem = new MultiModeDeliverySystem(this.deliveryQueue, deliveryOptions);
|
|
await this.deliverySystem.start();
|
|
|
|
// Initialize the unified email server with internal configuration
|
|
this.unifiedEmailServer = new UnifiedEmailServer({
|
|
ports: internalEmailConfig.ports,
|
|
hostname: internalEmailConfig.hostname,
|
|
maxMessageSize: emailConfig.maxMessageSize,
|
|
auth: emailConfig.auth,
|
|
tls: emailConfig.tls,
|
|
domainRules: emailConfig.domainRules,
|
|
defaultMode: emailConfig.defaultMode,
|
|
defaultServer: emailConfig.defaultServer,
|
|
defaultPort: emailConfig.defaultPort,
|
|
defaultTls: emailConfig.defaultTls
|
|
});
|
|
|
|
// Set up event listeners
|
|
this.unifiedEmailServer.on('error', (err) => {
|
|
logger.log('error', `UnifiedEmailServer error: ${err.message}`);
|
|
});
|
|
|
|
// Connect the unified email server with the delivery queue
|
|
this.unifiedEmailServer.on('emailProcessed', (email, mode, rule) => {
|
|
this.deliveryQueue!.enqueue(email, mode, rule).catch(err => {
|
|
logger.log('error', `Failed to enqueue email: ${err.message}`);
|
|
});
|
|
});
|
|
|
|
// Start the unified email server
|
|
await this.unifiedEmailServer.start();
|
|
|
|
logger.log('info', `Unified email handling configured with ${emailConfig.domainRules.length} domain rules on internal ports`);
|
|
logger.log('info', `Email server listening on ports: ${internalEmailConfig.ports.join(', ')}`);
|
|
} catch (error) {
|
|
logger.log('error', `Error setting up unified email handling: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the unified email configuration
|
|
* @param config New email configuration
|
|
*/
|
|
public async updateEmailConfig(config: IEmailConfig): 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 {
|
|
// Stop all components in the correct order
|
|
|
|
// 1. Stop the unified email server first
|
|
if (this.unifiedEmailServer) {
|
|
await this.unifiedEmailServer.stop();
|
|
logger.log('info', 'Unified email server stopped');
|
|
this.unifiedEmailServer = undefined;
|
|
}
|
|
|
|
// 2. Stop the delivery system
|
|
if (this.deliverySystem) {
|
|
await this.deliverySystem.stop();
|
|
logger.log('info', 'Delivery system stopped');
|
|
this.deliverySystem = undefined;
|
|
}
|
|
|
|
// 3. Stop the delivery queue
|
|
if (this.deliveryQueue) {
|
|
await this.deliveryQueue.shutdown();
|
|
logger.log('info', 'Delivery queue shut down');
|
|
this.deliveryQueue = undefined;
|
|
}
|
|
|
|
// 4. Stop the rate limiter
|
|
if (this.rateLimiter) {
|
|
this.rateLimiter.stop();
|
|
logger.log('info', 'Rate limiter stopped');
|
|
this.rateLimiter = undefined;
|
|
}
|
|
|
|
// 5. Clear the domain router
|
|
this.domainRouter = 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 updateDomainRules(rules: IDomainRule[]): Promise<void> {
|
|
// Validate that email config exists
|
|
if (!this.options.emailConfig) {
|
|
throw new Error('Email configuration is required before updating domain rules');
|
|
}
|
|
|
|
// Update the configuration
|
|
this.options.emailConfig.domainRules = rules;
|
|
|
|
// Update the domain router if it exists
|
|
if (this.domainRouter) {
|
|
this.domainRouter.updateRules(rules);
|
|
}
|
|
|
|
// Update the unified email server if it exists
|
|
if (this.unifiedEmailServer) {
|
|
this.unifiedEmailServer.updateDomainRules(rules);
|
|
}
|
|
|
|
console.log(`Domain rules updated with ${rules.length} rules`);
|
|
}
|
|
|
|
/**
|
|
* Get statistics from all components
|
|
*/
|
|
public getStats(): any {
|
|
const stats: any = {
|
|
unifiedEmailServer: this.unifiedEmailServer?.getStats(),
|
|
deliveryQueue: this.deliveryQueue?.getStats(),
|
|
deliverySystem: this.deliverySystem?.getStats(),
|
|
rateLimiter: this.rateLimiter?.getStats()
|
|
};
|
|
|
|
return stats;
|
|
}
|
|
}
|
|
|
|
export default DcRouter;
|