200 lines
8.4 KiB
TypeScript
200 lines
8.4 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import type { IDomainConfig, ISmartProxyCertProvisionObject } from './classes.pp.interfaces.js';
|
|
import { Port80Handler } from '../port80handler/classes.port80handler.js';
|
|
import { Port80HandlerEvents } from '../common/types.js';
|
|
import { subscribeToPort80Handler } from '../common/eventUtils.js';
|
|
import type { ICertificateData } from '../common/types.js';
|
|
import type { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
|
|
|
|
/**
|
|
* CertProvisioner manages certificate provisioning and renewal workflows,
|
|
* unifying static certificates and HTTP-01 challenges via Port80Handler.
|
|
*/
|
|
export class CertProvisioner extends plugins.EventEmitter {
|
|
private domainConfigs: IDomainConfig[];
|
|
private port80Handler: Port80Handler;
|
|
private networkProxyBridge: NetworkProxyBridge;
|
|
private certProvisionFunction?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
|
|
private forwardConfigs: Array<{ domain: string; forwardConfig?: { ip: string; port: number }; acmeForwardConfig?: { ip: string; port: number }; sslRedirect: boolean }>;
|
|
private renewThresholdDays: number;
|
|
private renewCheckIntervalHours: number;
|
|
private autoRenew: boolean;
|
|
private renewManager?: plugins.taskbuffer.TaskManager;
|
|
// Track provisioning type per domain: 'http01' or 'static'
|
|
private provisionMap: Map<string, 'http01' | '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
|
|
*/
|
|
constructor(
|
|
domainConfigs: IDomainConfig[],
|
|
port80Handler: Port80Handler,
|
|
networkProxyBridge: NetworkProxyBridge,
|
|
certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>,
|
|
renewThresholdDays: number = 30,
|
|
renewCheckIntervalHours: number = 24,
|
|
autoRenew: boolean = true,
|
|
forwardConfigs: Array<{ domain: string; forwardConfig?: { ip: string; port: number }; acmeForwardConfig?: { ip: string; port: number }; sslRedirect: boolean }> = []
|
|
) {
|
|
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
|
|
subscribeToPort80Handler(this.port80Handler, {
|
|
onCertificateIssued: (data: ICertificateData) => {
|
|
this.emit('certificate', { ...data, source: 'http01', isRenewal: false });
|
|
},
|
|
onCertificateRenewed: (data: ICertificateData) => {
|
|
this.emit('certificate', { ...data, source: 'http01', isRenewal: true });
|
|
}
|
|
});
|
|
|
|
// Apply external forwarding for ACME challenges (e.g. Synology)
|
|
for (const f of this.forwardConfigs) {
|
|
this.port80Handler.addDomain({
|
|
domainName: f.domain,
|
|
sslRedirect: f.sslRedirect,
|
|
acmeMaintenance: false,
|
|
forward: f.forwardConfig,
|
|
acmeForward: f.acmeForwardConfig
|
|
});
|
|
}
|
|
// Initial provisioning for all domains
|
|
const domains = this.domainConfigs.flatMap(cfg => cfg.domains);
|
|
for (const domain of domains) {
|
|
const isWildcard = domain.includes('*');
|
|
let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01';
|
|
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}`);
|
|
continue;
|
|
}
|
|
if (provision === 'http01') {
|
|
if (isWildcard) {
|
|
console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`);
|
|
continue;
|
|
}
|
|
this.provisionMap.set(domain, 'http01');
|
|
this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true });
|
|
} else {
|
|
// Static certificate (e.g., DNS-01 provisioned or user-provided) supports wildcard domains
|
|
this.provisionMap.set(domain, 'static');
|
|
const certObj = provision as plugins.tsclass.network.ICert;
|
|
const certData: ICertificateData = {
|
|
domain: certObj.domainName,
|
|
certificate: certObj.publicKey,
|
|
privateKey: certObj.privateKey,
|
|
expiryDate: new Date(certObj.validUntil)
|
|
};
|
|
this.networkProxyBridge.applyExternalCertificate(certData);
|
|
this.emit('certificate', { ...certData, source: 'static', isRenewal: false });
|
|
}
|
|
}
|
|
|
|
// Schedule renewals if enabled
|
|
if (this.autoRenew) {
|
|
this.renewManager = new plugins.taskbuffer.TaskManager();
|
|
const renewTask = new plugins.taskbuffer.Task({
|
|
name: 'CertificateRenewals',
|
|
taskFunction: async () => {
|
|
for (const [domain, type] of this.provisionMap.entries()) {
|
|
// Skip wildcard domains
|
|
if (domain.includes('*')) continue;
|
|
try {
|
|
if (type === 'http01') {
|
|
await this.port80Handler.renewCertificate(domain);
|
|
} else if (type === 'static' && this.certProvisionFunction) {
|
|
const provision2 = await this.certProvisionFunction(domain);
|
|
if (provision2 !== 'http01') {
|
|
const certObj = provision2 as plugins.tsclass.network.ICert;
|
|
const certData: ICertificateData = {
|
|
domain: certObj.domainName,
|
|
certificate: certObj.publicKey,
|
|
privateKey: certObj.privateKey,
|
|
expiryDate: new Date(certObj.validUntil)
|
|
};
|
|
this.networkProxyBridge.applyExternalCertificate(certData);
|
|
this.emit('certificate', { ...certData, source: 'static', isRenewal: true });
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(`Renewal error for ${domain}:`, err);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
const hours = this.renewCheckIntervalHours;
|
|
const cronExpr = `0 0 */${hours} * * *`;
|
|
this.renewManager.addAndScheduleTask(renewTask, cronExpr);
|
|
this.renewManager.start();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop all scheduled renewal tasks.
|
|
*/
|
|
public async stop(): Promise<void> {
|
|
// Stop scheduled renewals
|
|
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: ISmartProxyCertProvisionObject | 'http01' = '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 {
|
|
// Static certificate (e.g., DNS-01 provisioned) supports wildcards
|
|
const certObj = provision as plugins.tsclass.network.ICert;
|
|
const certData: ICertificateData = {
|
|
domain: certObj.domainName,
|
|
certificate: certObj.publicKey,
|
|
privateKey: certObj.privateKey,
|
|
expiryDate: new Date(certObj.validUntil)
|
|
};
|
|
this.networkProxyBridge.applyExternalCertificate(certData);
|
|
this.emit('certificate', { ...certData, source: 'static', isRenewal: false });
|
|
}
|
|
}
|
|
} |