feat(acme): Improve certificate management by adding global ACME configuration support and allowing route-level overrides. Enhanced error messages help identify missing ACME email and misconfigurations (e.g. wildcard domains). Documentation has been updated and new tests added to verify SmartCertManager behavior, ensuring a clearer migration path from legacy implementations.

This commit is contained in:
2025-05-18 18:29:59 +00:00
parent ac4645dff7
commit 68738137a0
14 changed files with 706 additions and 1472 deletions

View File

@ -1,6 +1,7 @@
import * as plugins from '../../plugins.js';
import { NetworkProxy } from '../network-proxy/index.js';
import type { IRouteConfig, IRouteTls } from './models/route-types.js';
import type { IAcmeOptions } from './models/interfaces.js';
import { CertStore } from './cert-store.js';
export interface ICertStatus {
@ -31,6 +32,9 @@ export class SmartCertManager {
// Track certificate status by route name
private certStatus: Map<string, ICertStatus> = new Map();
// Global ACME defaults from top-level configuration
private globalAcmeDefaults: IAcmeOptions | null = null;
// Callback to update SmartProxy routes for challenges
private updateRoutesCallback?: (routes: IRouteConfig[]) => Promise<void>;
@ -50,6 +54,13 @@ export class SmartCertManager {
this.networkProxy = networkProxy;
}
/**
* Set global ACME defaults from top-level configuration
*/
public setGlobalAcmeDefaults(defaults: IAcmeOptions): void {
this.globalAcmeDefaults = defaults;
}
/**
* Set callback for updating routes (used for challenge routes)
*/
@ -146,7 +157,12 @@ export class SmartCertManager {
domains: string[]
): Promise<void> {
if (!this.smartAcme) {
throw new Error('SmartAcme not initialized');
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];
@ -161,7 +177,12 @@ export class SmartCertManager {
return;
}
console.log(`Requesting ACME certificate for ${domains.join(', ')}`);
// Apply renewal threshold from global defaults or route config
const renewThreshold = route.action.tls?.acme?.renewBeforeDays ||
this.globalAcmeDefaults?.renewThresholdDays ||
30;
console.log(`Requesting ACME certificate for ${domains.join(', ')} (renew ${renewThreshold} days before expiry)`);
this.updateCertStatus(routeName, 'pending', 'acme');
try {
@ -303,7 +324,10 @@ export class SmartCertManager {
*/
private isCertificateValid(cert: ICertificateData): boolean {
const now = new Date();
const expiryThreshold = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); // 30 days
// Use renewal threshold from global defaults or fallback to 30 days
const renewThresholdDays = this.globalAcmeDefaults?.renewThresholdDays || 30;
const expiryThreshold = new Date(now.getTime() + renewThresholdDays * 24 * 60 * 60 * 1000);
return cert.expiryDate > expiryThreshold;
}
@ -417,12 +441,15 @@ export class SmartCertManager {
* Setup challenge handler integration with SmartProxy routing
*/
private setupChallengeHandler(http01Handler: plugins.smartacme.handlers.Http01MemoryHandler): void {
// Use challenge port from global config or default to 80
const challengePort = this.globalAcmeDefaults?.port || 80;
// Create a challenge route that delegates to SmartAcme's HTTP-01 handler
const challengeRoute: IRouteConfig = {
name: 'acme-challenge',
priority: 1000, // High priority
match: {
ports: 80,
ports: challengePort,
path: '/.well-known/acme-challenge/*'
},
action: {