326 lines
11 KiB
TypeScript
326 lines
11 KiB
TypeScript
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<CertProvisionObject>;
|
|
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<string, 'http01' | 'dns01' | 'static'>;
|
|
|
|
/**
|
|
* @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<CertProvisionObject>,
|
|
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<void> {
|
|
// 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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 } |