update
This commit is contained in:
@ -12,7 +12,7 @@ export interface ICertStatus {
|
||||
status: 'valid' | 'pending' | 'expired' | 'error';
|
||||
expiryDate?: Date;
|
||||
issueDate?: Date;
|
||||
source: 'static' | 'acme';
|
||||
source: 'static' | 'acme' | 'custom';
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ export interface ICertificateData {
|
||||
ca?: string;
|
||||
expiryDate: Date;
|
||||
issueDate: Date;
|
||||
source?: 'static' | 'acme' | 'custom';
|
||||
}
|
||||
|
||||
export class SmartCertManager {
|
||||
@ -50,6 +51,12 @@ export class SmartCertManager {
|
||||
// ACME state manager reference
|
||||
private acmeStateManager: AcmeStateManager | null = null;
|
||||
|
||||
// Custom certificate provision function
|
||||
private certProvisionFunction?: (domain: string) => Promise<plugins.tsclass.network.ICert | 'http01'>;
|
||||
|
||||
// Whether to fallback to ACME if custom provision fails
|
||||
private certProvisionFallbackToAcme: boolean = true;
|
||||
|
||||
constructor(
|
||||
private routes: IRouteConfig[],
|
||||
private certDir: string = './certs',
|
||||
@ -89,6 +96,20 @@ export class SmartCertManager {
|
||||
this.globalAcmeDefaults = defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom certificate provision function
|
||||
*/
|
||||
public setCertProvisionFunction(fn: (domain: string) => Promise<plugins.tsclass.network.ICert | 'http01'>): void {
|
||||
this.certProvisionFunction = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to fallback to ACME if custom provision fails
|
||||
*/
|
||||
public setCertProvisionFallbackToAcme(fallback: boolean): void {
|
||||
this.certProvisionFallbackToAcme = fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set callback for updating routes (used for challenge routes)
|
||||
*/
|
||||
@ -212,15 +233,6 @@ export class SmartCertManager {
|
||||
route: IRouteConfig,
|
||||
domains: string[]
|
||||
): Promise<void> {
|
||||
if (!this.smartAcme) {
|
||||
throw new Error(
|
||||
'SmartAcme not initialized. This usually means no ACME email was provided. ' +
|
||||
'Please ensure you have configured ACME with an email address either:\n' +
|
||||
'1. In the top-level "acme" configuration\n' +
|
||||
'2. In the route\'s "tls.acme" configuration'
|
||||
);
|
||||
}
|
||||
|
||||
const primaryDomain = domains[0];
|
||||
const routeName = route.name || primaryDomain;
|
||||
|
||||
@ -229,10 +241,68 @@ export class SmartCertManager {
|
||||
if (existingCert && this.isCertificateValid(existingCert)) {
|
||||
logger.log('info', `Using existing valid certificate for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
|
||||
await this.applyCertificate(primaryDomain, existingCert);
|
||||
this.updateCertStatus(routeName, 'valid', 'acme', existingCert);
|
||||
this.updateCertStatus(routeName, 'valid', existingCert.source || 'acme', existingCert);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for custom provision function first
|
||||
if (this.certProvisionFunction) {
|
||||
try {
|
||||
logger.log('info', `Attempting custom certificate provision for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
|
||||
const result = await this.certProvisionFunction(primaryDomain);
|
||||
|
||||
if (result === 'http01') {
|
||||
logger.log('info', `Custom function returned 'http01', falling back to Let's Encrypt for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
|
||||
// Continue with existing ACME logic below
|
||||
} else {
|
||||
// Use custom certificate
|
||||
const customCert = result as plugins.tsclass.network.ICert;
|
||||
|
||||
// Convert to internal certificate format
|
||||
const certData: ICertificateData = {
|
||||
cert: customCert.cert,
|
||||
key: customCert.key,
|
||||
ca: customCert.ca || '',
|
||||
issueDate: new Date(),
|
||||
expiryDate: this.extractExpiryDate(customCert.cert),
|
||||
source: 'custom'
|
||||
};
|
||||
|
||||
// Store and apply certificate
|
||||
await this.certStore.saveCertificate(routeName, certData);
|
||||
await this.applyCertificate(primaryDomain, certData);
|
||||
this.updateCertStatus(routeName, 'valid', 'custom', certData);
|
||||
|
||||
logger.log('info', `Custom certificate applied for ${primaryDomain}`, {
|
||||
domain: primaryDomain,
|
||||
expiryDate: certData.expiryDate,
|
||||
component: 'certificate-manager'
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Custom cert provision failed for ${primaryDomain}: ${error.message}`, {
|
||||
domain: primaryDomain,
|
||||
error: error.message,
|
||||
component: 'certificate-manager'
|
||||
});
|
||||
// Check if we should fallback to ACME
|
||||
if (!this.certProvisionFallbackToAcme) {
|
||||
throw error;
|
||||
}
|
||||
logger.log('info', `Falling back to Let's Encrypt for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.smartAcme) {
|
||||
throw new Error(
|
||||
'SmartAcme not initialized. This usually means no ACME email was provided. ' +
|
||||
'Please ensure you have configured ACME with an email address either:\n' +
|
||||
'1. In the top-level "acme" configuration\n' +
|
||||
'2. In the route\'s "tls.acme" configuration'
|
||||
);
|
||||
}
|
||||
|
||||
// Apply renewal threshold from global defaults or route config
|
||||
const renewThreshold = route.action.tls?.acme?.renewBeforeDays ||
|
||||
this.globalAcmeDefaults?.renewThresholdDays ||
|
||||
@ -280,7 +350,8 @@ export class SmartCertManager {
|
||||
key: cert.privateKey,
|
||||
ca: cert.publicKey, // Use same as cert for now
|
||||
expiryDate: new Date(cert.validUntil),
|
||||
issueDate: new Date(cert.created)
|
||||
issueDate: new Date(cert.created),
|
||||
source: 'acme'
|
||||
};
|
||||
|
||||
await this.certStore.saveCertificate(routeName, certData);
|
||||
@ -328,7 +399,8 @@ export class SmartCertManager {
|
||||
cert,
|
||||
key,
|
||||
expiryDate: certInfo.validTo,
|
||||
issueDate: certInfo.validFrom
|
||||
issueDate: certInfo.validFrom,
|
||||
source: 'static'
|
||||
};
|
||||
|
||||
// Save to store for consistency
|
||||
@ -399,6 +471,19 @@ export class SmartCertManager {
|
||||
return cert.expiryDate > expiryThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract expiry date from a PEM certificate
|
||||
*/
|
||||
private extractExpiryDate(_certPem: string): Date {
|
||||
// For now, we'll default to 90 days for custom certificates
|
||||
// In production, you might want to use a proper X.509 parser
|
||||
// or require the custom cert provider to include expiry info
|
||||
logger.log('info', 'Using default 90-day expiry for custom certificate', {
|
||||
component: 'certificate-manager'
|
||||
});
|
||||
return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add challenge route to SmartProxy
|
||||
|
Reference in New Issue
Block a user