245 lines
8.9 KiB
TypeScript
245 lines
8.9 KiB
TypeScript
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<any>;
|
|
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<any>,
|
|
action: (email: any) => Promise<any>
|
|
): void {
|
|
this.smtpRuleEngine?.createRule(priority, check, action);
|
|
}
|
|
}
|
|
|
|
export default DcRouter;
|