update structure
This commit is contained in:
@ -1,27 +1,40 @@
|
||||
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';
|
||||
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: IDomainConfig[];
|
||||
private domainConfigs: DomainConfig[];
|
||||
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 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: 'http01' or 'static'
|
||||
private provisionMap: Map<string, 'http01' | 'static'>;
|
||||
// Track provisioning type per domain
|
||||
private provisionMap: Map<string, 'http01' | 'dns01' | 'static'>;
|
||||
|
||||
/**
|
||||
* @param domainConfigs Array of domain configuration objects
|
||||
@ -31,16 +44,17 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
* @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: IDomainConfig[],
|
||||
domainConfigs: DomainConfig[],
|
||||
port80Handler: Port80Handler,
|
||||
networkProxyBridge: NetworkProxyBridge,
|
||||
certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>,
|
||||
certProvider?: (domain: string) => Promise<CertProvisionObject>,
|
||||
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 }> = []
|
||||
forwardConfigs: DomainForwardConfig[] = []
|
||||
) {
|
||||
super();
|
||||
this.domainConfigs = domainConfigs;
|
||||
@ -59,99 +73,180 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
*/
|
||||
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 });
|
||||
}
|
||||
});
|
||||
this.setupEventSubscriptions();
|
||||
|
||||
// Apply external forwarding for ACME challenges
|
||||
this.setupForwardingConfigs();
|
||||
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
await this.provisionAllDomains();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
});
|
||||
const hours = this.renewCheckIntervalHours;
|
||||
const cronExpr = `0 0 */${hours} * * *`;
|
||||
this.renewManager.addAndScheduleTask(renewTask, cronExpr);
|
||||
this.renewManager.start();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,7 +254,6 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
* Stop all scheduled renewal tasks.
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
// Stop scheduled renewals
|
||||
if (this.renewManager) {
|
||||
this.renewManager.stop();
|
||||
}
|
||||
@ -171,30 +265,62 @@ export class CertProvisioner extends plugins.EventEmitter {
|
||||
*/
|
||||
public async requestCertificate(domain: string): Promise<void> {
|
||||
const isWildcard = domain.includes('*');
|
||||
|
||||
// Determine provisioning method
|
||||
let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01';
|
||||
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: ICertificateData = {
|
||||
const certData: CertificateData = {
|
||||
domain: certObj.domainName,
|
||||
certificate: certObj.publicKey,
|
||||
privateKey: certObj.privateKey,
|
||||
expiryDate: new Date(certObj.validUntil)
|
||||
expiryDate: new Date(certObj.validUntil),
|
||||
source: 'static',
|
||||
isRenewal: false
|
||||
};
|
||||
|
||||
this.networkProxyBridge.applyExternalCertificate(certData);
|
||||
this.emit('certificate', { ...certData, source: 'static', isRenewal: false });
|
||||
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 }
|
Reference in New Issue
Block a user