import * as plugins from '../../plugins.js'; import type { DomainConfig } from '../../forwarding/config/domain-config.js'; import type { CertificateData, DomainForwardConfig, DomainOptions } from '../models/certificate-types.js'; import { Port80HandlerEvents, CertProvisionerEvents } from '../events/certificate-events.js'; import { Port80Handler } from '../../port80handler/classes.port80handler.js'; // We need to define this interface until we migrate NetworkProxyBridge interface NetworkProxyBridge { applyExternalCertificate(certData: CertificateData): void; } // This will be imported after NetworkProxyBridge is migrated // import type { NetworkProxyBridge } from '../../proxies/smart-proxy/network-proxy-bridge.js'; // For backward compatibility export type ISmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01'; /** * Type for static certificate provisioning */ export type CertProvisionObject = plugins.tsclass.network.ICert | 'http01' | 'dns01'; /** * CertProvisioner manages certificate provisioning and renewal workflows, * unifying static certificates and HTTP-01 challenges via Port80Handler. */ export class CertProvisioner extends plugins.EventEmitter { private domainConfigs: DomainConfig[]; private port80Handler: Port80Handler; private networkProxyBridge: NetworkProxyBridge; private certProvisionFunction?: (domain: string) => Promise; private forwardConfigs: DomainForwardConfig[]; private renewThresholdDays: number; private renewCheckIntervalHours: number; private autoRenew: boolean; private renewManager?: plugins.taskbuffer.TaskManager; // Track provisioning type per domain private provisionMap: Map; /** * @param domainConfigs Array of domain configuration objects * @param port80Handler HTTP-01 challenge handler instance * @param networkProxyBridge Bridge for applying external certificates * @param certProvider Optional callback returning a static cert or 'http01' * @param renewThresholdDays Days before expiry to trigger renewals * @param renewCheckIntervalHours Interval in hours to check for renewals * @param autoRenew Whether to automatically schedule renewals * @param forwardConfigs Domain forwarding configurations for ACME challenges */ constructor( domainConfigs: DomainConfig[], port80Handler: Port80Handler, networkProxyBridge: NetworkProxyBridge, certProvider?: (domain: string) => Promise, renewThresholdDays: number = 30, renewCheckIntervalHours: number = 24, autoRenew: boolean = true, forwardConfigs: DomainForwardConfig[] = [] ) { super(); this.domainConfigs = domainConfigs; this.port80Handler = port80Handler; this.networkProxyBridge = networkProxyBridge; this.certProvisionFunction = certProvider; this.renewThresholdDays = renewThresholdDays; this.renewCheckIntervalHours = renewCheckIntervalHours; this.autoRenew = autoRenew; this.provisionMap = new Map(); this.forwardConfigs = forwardConfigs; } /** * Start initial provisioning and schedule renewals. */ public async start(): Promise { // Subscribe to Port80Handler certificate events this.setupEventSubscriptions(); // Apply external forwarding for ACME challenges this.setupForwardingConfigs(); // Initial provisioning for all domains await this.provisionAllDomains(); // Schedule renewals if enabled if (this.autoRenew) { this.scheduleRenewals(); } } /** * Set up event subscriptions for certificate events */ private setupEventSubscriptions(): void { // We need to reimplement subscribeToPort80Handler here this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, (data: CertificateData) => { this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, { ...data, source: 'http01', isRenewal: false }); }); this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, (data: CertificateData) => { this.emit(CertProvisionerEvents.CERTIFICATE_RENEWED, { ...data, source: 'http01', isRenewal: true }); }); this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, (error) => { this.emit(CertProvisionerEvents.CERTIFICATE_FAILED, error); }); } /** * Set up forwarding configurations for the Port80Handler */ private setupForwardingConfigs(): void { for (const config of this.forwardConfigs) { const domainOptions: DomainOptions = { domainName: config.domain, sslRedirect: config.sslRedirect || false, acmeMaintenance: false, forward: config.forwardConfig, acmeForward: config.acmeForwardConfig }; this.port80Handler.addDomain(domainOptions); } } /** * Provision certificates for all configured domains */ private async provisionAllDomains(): Promise { const domains = this.domainConfigs.flatMap(cfg => cfg.domains); for (const domain of domains) { await this.provisionDomain(domain); } } /** * Provision a certificate for a single domain * @param domain Domain to provision */ private async provisionDomain(domain: string): Promise { const isWildcard = domain.includes('*'); let provision: CertProvisionObject = 'http01'; // Try to get a certificate from the provision function if (this.certProvisionFunction) { try { provision = await this.certProvisionFunction(domain); } catch (err) { console.error(`certProvider error for ${domain}:`, err); } } else if (isWildcard) { // No certProvider: cannot handle wildcard without DNS-01 support console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`); return; } // Handle different provisioning methods if (provision === 'http01') { if (isWildcard) { console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`); return; } this.provisionMap.set(domain, 'http01'); this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true }); } else if (provision === 'dns01') { // DNS-01 challenges would be handled by the certProvisionFunction this.provisionMap.set(domain, 'dns01'); // DNS-01 handling would go here if implemented } else { // Static certificate (e.g., DNS-01 provisioned or user-provided) this.provisionMap.set(domain, 'static'); const certObj = provision as plugins.tsclass.network.ICert; const certData: CertificateData = { domain: certObj.domainName, certificate: certObj.publicKey, privateKey: certObj.privateKey, expiryDate: new Date(certObj.validUntil), source: 'static', isRenewal: false }; this.networkProxyBridge.applyExternalCertificate(certData); this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, certData); } } /** * Schedule certificate renewals using a task manager */ private scheduleRenewals(): void { this.renewManager = new plugins.taskbuffer.TaskManager(); const renewTask = new plugins.taskbuffer.Task({ name: 'CertificateRenewals', taskFunction: async () => await this.performRenewals() }); const hours = this.renewCheckIntervalHours; const cronExpr = `0 0 */${hours} * * *`; this.renewManager.addAndScheduleTask(renewTask, cronExpr); this.renewManager.start(); } /** * Perform renewals for all domains that need it */ private async performRenewals(): Promise { for (const [domain, type] of this.provisionMap.entries()) { // Skip wildcard domains for HTTP-01 challenges if (domain.includes('*') && type === 'http01') continue; try { await this.renewDomain(domain, type); } catch (err) { console.error(`Renewal error for ${domain}:`, err); } } } /** * Renew a certificate for a specific domain * @param domain Domain to renew * @param provisionType Type of provisioning for this domain */ private async renewDomain(domain: string, provisionType: 'http01' | 'dns01' | 'static'): Promise { if (provisionType === 'http01') { await this.port80Handler.renewCertificate(domain); } else if ((provisionType === 'static' || provisionType === 'dns01') && this.certProvisionFunction) { const provision = await this.certProvisionFunction(domain); if (provision !== 'http01' && provision !== 'dns01') { const certObj = provision as plugins.tsclass.network.ICert; const certData: CertificateData = { domain: certObj.domainName, certificate: certObj.publicKey, privateKey: certObj.privateKey, expiryDate: new Date(certObj.validUntil), source: 'static', isRenewal: true }; this.networkProxyBridge.applyExternalCertificate(certData); this.emit(CertProvisionerEvents.CERTIFICATE_RENEWED, certData); } } } /** * Stop all scheduled renewal tasks. */ public async stop(): Promise { if (this.renewManager) { this.renewManager.stop(); } } /** * Request a certificate on-demand for the given domain. * @param domain Domain name to provision */ public async requestCertificate(domain: string): Promise { const isWildcard = domain.includes('*'); // Determine provisioning method let provision: CertProvisionObject = 'http01'; if (this.certProvisionFunction) { provision = await this.certProvisionFunction(domain); } else if (isWildcard) { // Cannot perform HTTP-01 on wildcard without certProvider throw new Error(`Cannot request certificate for wildcard domain without certProvisionFunction: ${domain}`); } if (provision === 'http01') { if (isWildcard) { throw new Error(`Cannot request HTTP-01 certificate for wildcard domain: ${domain}`); } await this.port80Handler.renewCertificate(domain); } else if (provision === 'dns01') { // DNS-01 challenges would be handled by external mechanisms // This is a placeholder for future implementation console.log(`DNS-01 challenge requested for ${domain}`); } else { // Static certificate (e.g., DNS-01 provisioned) supports wildcards const certObj = provision as plugins.tsclass.network.ICert; const certData: CertificateData = { domain: certObj.domainName, certificate: certObj.publicKey, privateKey: certObj.privateKey, expiryDate: new Date(certObj.validUntil), source: 'static', isRenewal: false }; this.networkProxyBridge.applyExternalCertificate(certData); this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, certData); } } /** * Add a new domain for certificate provisioning * @param domain Domain to add * @param options Domain configuration options */ public async addDomain(domain: string, options?: { sslRedirect?: boolean; acmeMaintenance?: boolean; }): Promise { const domainOptions: DomainOptions = { domainName: domain, sslRedirect: options?.sslRedirect || true, acmeMaintenance: options?.acmeMaintenance || true }; this.port80Handler.addDomain(domainOptions); await this.provisionDomain(domain); } } // For backward compatibility export { CertProvisioner as CertificateProvisioner }