update
This commit is contained in:
350
test/test.certificate-provision.ts
Normal file
350
test/test.certificate-provision.ts
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
import { SmartProxy } from '../ts/index.js';
|
||||||
|
import type { TSmartProxyCertProvisionObject } from '../ts/index.js';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
let testProxy: SmartProxy;
|
||||||
|
|
||||||
|
// Load test certificates from helpers
|
||||||
|
const testCert = fs.readFileSync(path.join(__dirname, 'helpers/test-cert.pem'), 'utf8');
|
||||||
|
const testKey = fs.readFileSync(path.join(__dirname, 'helpers/test-key.pem'), 'utf8');
|
||||||
|
|
||||||
|
tap.test('SmartProxy should support custom certificate provision function', async () => {
|
||||||
|
// Create test certificate object - the type is from plugins.tsclass.network.ICert
|
||||||
|
// but we return a simple object with the required properties
|
||||||
|
const testCertObject = {
|
||||||
|
cert: testCert,
|
||||||
|
key: testKey,
|
||||||
|
ca: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom certificate store for testing
|
||||||
|
const customCerts = new Map<string, typeof testCertObject>();
|
||||||
|
customCerts.set('test.example.com', testCertObject);
|
||||||
|
|
||||||
|
// Create proxy with custom certificate provision
|
||||||
|
testProxy = new SmartProxy({
|
||||||
|
certProvisionFunction: async (domain: string): Promise<TSmartProxyCertProvisionObject> => {
|
||||||
|
console.log(`Custom cert provision called for domain: ${domain}`);
|
||||||
|
|
||||||
|
// Return custom cert for known domains
|
||||||
|
if (customCerts.has(domain)) {
|
||||||
|
console.log(`Returning custom certificate for ${domain}`);
|
||||||
|
return customCerts.get(domain)! as unknown as TSmartProxyCertProvisionObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to Let's Encrypt for other domains
|
||||||
|
console.log(`Falling back to Let's Encrypt for ${domain}`);
|
||||||
|
return 'http01';
|
||||||
|
},
|
||||||
|
certProvisionFallbackToAcme: true,
|
||||||
|
acme: {
|
||||||
|
email: 'test@example.com',
|
||||||
|
useProduction: false
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'test-route',
|
||||||
|
match: {
|
||||||
|
ports: [443],
|
||||||
|
domains: ['test.example.com']
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 8080
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
mode: 'terminate',
|
||||||
|
certificate: 'auto'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(testProxy).toBeInstanceOf(SmartProxy);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Custom certificate provision function should be called', async () => {
|
||||||
|
let provisionCalled = false;
|
||||||
|
const provisionedDomains: string[] = [];
|
||||||
|
|
||||||
|
const testProxy2 = new SmartProxy({
|
||||||
|
certProvisionFunction: async (domain: string): Promise<TSmartProxyCertProvisionObject> => {
|
||||||
|
provisionCalled = true;
|
||||||
|
provisionedDomains.push(domain);
|
||||||
|
|
||||||
|
// Return a test certificate using the loaded files
|
||||||
|
// We need to return a proper cert object that satisfies the type
|
||||||
|
return {
|
||||||
|
cert: testCert,
|
||||||
|
key: testKey,
|
||||||
|
ca: ''
|
||||||
|
} as unknown as TSmartProxyCertProvisionObject;
|
||||||
|
},
|
||||||
|
acme: {
|
||||||
|
email: 'test@example.com',
|
||||||
|
useProduction: false,
|
||||||
|
port: 9080
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'custom-cert-route',
|
||||||
|
match: {
|
||||||
|
ports: [9443],
|
||||||
|
domains: ['custom.example.com']
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 8080
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
mode: 'terminate',
|
||||||
|
certificate: 'auto'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock the certificate manager to test our custom provision function
|
||||||
|
let certManagerCalled = false;
|
||||||
|
const origCreateCertManager = (testProxy2 as any).createCertificateManager;
|
||||||
|
(testProxy2 as any).createCertificateManager = async function(...args: any[]) {
|
||||||
|
const certManager = await origCreateCertManager.apply(testProxy2, args);
|
||||||
|
|
||||||
|
// Override provisionAllCertificates to track calls
|
||||||
|
const origProvisionAll = certManager.provisionAllCertificates;
|
||||||
|
certManager.provisionAllCertificates = async function() {
|
||||||
|
certManagerCalled = true;
|
||||||
|
await origProvisionAll.call(certManager);
|
||||||
|
};
|
||||||
|
|
||||||
|
return certManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the proxy (this will trigger certificate provisioning)
|
||||||
|
await testProxy2.start();
|
||||||
|
|
||||||
|
expect(certManagerCalled).toBeTrue();
|
||||||
|
expect(provisionCalled).toBeTrue();
|
||||||
|
expect(provisionedDomains).toContain('custom.example.com');
|
||||||
|
|
||||||
|
await testProxy2.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Should fallback to ACME when custom provision fails', async () => {
|
||||||
|
const failedDomains: string[] = [];
|
||||||
|
let acmeAttempted = false;
|
||||||
|
|
||||||
|
const testProxy3 = new SmartProxy({
|
||||||
|
certProvisionFunction: async (domain: string): Promise<TSmartProxyCertProvisionObject> => {
|
||||||
|
failedDomains.push(domain);
|
||||||
|
throw new Error('Custom provision failed for testing');
|
||||||
|
},
|
||||||
|
certProvisionFallbackToAcme: true,
|
||||||
|
acme: {
|
||||||
|
email: 'test@example.com',
|
||||||
|
useProduction: false,
|
||||||
|
port: 9080
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'fallback-route',
|
||||||
|
match: {
|
||||||
|
ports: [9444],
|
||||||
|
domains: ['fallback.example.com']
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 8080
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
mode: 'terminate',
|
||||||
|
certificate: 'auto'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock to track ACME attempts
|
||||||
|
const origCreateCertManager = (testProxy3 as any).createCertificateManager;
|
||||||
|
(testProxy3 as any).createCertificateManager = async function(...args: any[]) {
|
||||||
|
const certManager = await origCreateCertManager.apply(testProxy3, args);
|
||||||
|
|
||||||
|
// Mock SmartAcme to avoid real ACME calls
|
||||||
|
(certManager as any).smartAcme = {
|
||||||
|
getCertificateForDomain: async () => {
|
||||||
|
acmeAttempted = true;
|
||||||
|
throw new Error('Mocked ACME failure');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return certManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the proxy
|
||||||
|
await testProxy3.start();
|
||||||
|
|
||||||
|
// Custom provision should have failed
|
||||||
|
expect(failedDomains).toContain('fallback.example.com');
|
||||||
|
|
||||||
|
// ACME should have been attempted as fallback
|
||||||
|
expect(acmeAttempted).toBeTrue();
|
||||||
|
|
||||||
|
await testProxy3.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Should not fallback when certProvisionFallbackToAcme is false', async () => {
|
||||||
|
let errorThrown = false;
|
||||||
|
let errorMessage = '';
|
||||||
|
|
||||||
|
const testProxy4 = new SmartProxy({
|
||||||
|
certProvisionFunction: async (_domain: string): Promise<TSmartProxyCertProvisionObject> => {
|
||||||
|
throw new Error('Custom provision failed for testing');
|
||||||
|
},
|
||||||
|
certProvisionFallbackToAcme: false,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'no-fallback-route',
|
||||||
|
match: {
|
||||||
|
ports: [9445],
|
||||||
|
domains: ['no-fallback.example.com']
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 8080
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
mode: 'terminate',
|
||||||
|
certificate: 'auto'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock certificate manager to capture errors
|
||||||
|
const origCreateCertManager = (testProxy4 as any).createCertificateManager;
|
||||||
|
(testProxy4 as any).createCertificateManager = async function(...args: any[]) {
|
||||||
|
const certManager = await origCreateCertManager.apply(testProxy4, args);
|
||||||
|
|
||||||
|
// Override provisionAllCertificates to capture errors
|
||||||
|
const origProvisionAll = certManager.provisionAllCertificates;
|
||||||
|
certManager.provisionAllCertificates = async function() {
|
||||||
|
try {
|
||||||
|
await origProvisionAll.call(certManager);
|
||||||
|
} catch (e) {
|
||||||
|
errorThrown = true;
|
||||||
|
errorMessage = e.message;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return certManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await testProxy4.start();
|
||||||
|
} catch (e) {
|
||||||
|
// Expected to fail
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(errorThrown).toBeTrue();
|
||||||
|
expect(errorMessage).toInclude('Custom provision failed for testing');
|
||||||
|
|
||||||
|
await testProxy4.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('Should return http01 for unknown domains', async () => {
|
||||||
|
let returnedHttp01 = false;
|
||||||
|
let acmeAttempted = false;
|
||||||
|
|
||||||
|
const testProxy5 = new SmartProxy({
|
||||||
|
certProvisionFunction: async (domain: string): Promise<TSmartProxyCertProvisionObject> => {
|
||||||
|
if (domain === 'known.example.com') {
|
||||||
|
return {
|
||||||
|
cert: testCert,
|
||||||
|
key: testKey,
|
||||||
|
ca: ''
|
||||||
|
} as unknown as TSmartProxyCertProvisionObject;
|
||||||
|
}
|
||||||
|
returnedHttp01 = true;
|
||||||
|
return 'http01';
|
||||||
|
},
|
||||||
|
acme: {
|
||||||
|
email: 'test@example.com',
|
||||||
|
useProduction: false,
|
||||||
|
port: 9081
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'unknown-domain-route',
|
||||||
|
match: {
|
||||||
|
ports: [9446],
|
||||||
|
domains: ['unknown.example.com']
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 8080
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
mode: 'terminate',
|
||||||
|
certificate: 'auto'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock to track ACME attempts
|
||||||
|
const origCreateCertManager = (testProxy5 as any).createCertificateManager;
|
||||||
|
(testProxy5 as any).createCertificateManager = async function(...args: any[]) {
|
||||||
|
const certManager = await origCreateCertManager.apply(testProxy5, args);
|
||||||
|
|
||||||
|
// Mock SmartAcme to track attempts
|
||||||
|
(certManager as any).smartAcme = {
|
||||||
|
getCertificateForDomain: async () => {
|
||||||
|
acmeAttempted = true;
|
||||||
|
throw new Error('Mocked ACME failure');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return certManager;
|
||||||
|
};
|
||||||
|
|
||||||
|
await testProxy5.start();
|
||||||
|
|
||||||
|
// Should have returned http01 for unknown domain
|
||||||
|
expect(returnedHttp01).toBeTrue();
|
||||||
|
|
||||||
|
// ACME should have been attempted
|
||||||
|
expect(acmeAttempted).toBeTrue();
|
||||||
|
|
||||||
|
await testProxy5.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('cleanup', async () => {
|
||||||
|
// Clean up any test proxies
|
||||||
|
if (testProxy) {
|
||||||
|
await testProxy.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
@ -12,7 +12,7 @@ export interface ICertStatus {
|
|||||||
status: 'valid' | 'pending' | 'expired' | 'error';
|
status: 'valid' | 'pending' | 'expired' | 'error';
|
||||||
expiryDate?: Date;
|
expiryDate?: Date;
|
||||||
issueDate?: Date;
|
issueDate?: Date;
|
||||||
source: 'static' | 'acme';
|
source: 'static' | 'acme' | 'custom';
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,6 +22,7 @@ export interface ICertificateData {
|
|||||||
ca?: string;
|
ca?: string;
|
||||||
expiryDate: Date;
|
expiryDate: Date;
|
||||||
issueDate: Date;
|
issueDate: Date;
|
||||||
|
source?: 'static' | 'acme' | 'custom';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SmartCertManager {
|
export class SmartCertManager {
|
||||||
@ -50,6 +51,12 @@ export class SmartCertManager {
|
|||||||
// ACME state manager reference
|
// ACME state manager reference
|
||||||
private acmeStateManager: AcmeStateManager | null = null;
|
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(
|
constructor(
|
||||||
private routes: IRouteConfig[],
|
private routes: IRouteConfig[],
|
||||||
private certDir: string = './certs',
|
private certDir: string = './certs',
|
||||||
@ -89,6 +96,20 @@ export class SmartCertManager {
|
|||||||
this.globalAcmeDefaults = defaults;
|
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)
|
* Set callback for updating routes (used for challenge routes)
|
||||||
*/
|
*/
|
||||||
@ -212,15 +233,6 @@ export class SmartCertManager {
|
|||||||
route: IRouteConfig,
|
route: IRouteConfig,
|
||||||
domains: string[]
|
domains: string[]
|
||||||
): Promise<void> {
|
): 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 primaryDomain = domains[0];
|
||||||
const routeName = route.name || primaryDomain;
|
const routeName = route.name || primaryDomain;
|
||||||
|
|
||||||
@ -229,10 +241,68 @@ export class SmartCertManager {
|
|||||||
if (existingCert && this.isCertificateValid(existingCert)) {
|
if (existingCert && this.isCertificateValid(existingCert)) {
|
||||||
logger.log('info', `Using existing valid certificate for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
|
logger.log('info', `Using existing valid certificate for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
|
||||||
await this.applyCertificate(primaryDomain, existingCert);
|
await this.applyCertificate(primaryDomain, existingCert);
|
||||||
this.updateCertStatus(routeName, 'valid', 'acme', existingCert);
|
this.updateCertStatus(routeName, 'valid', existingCert.source || 'acme', existingCert);
|
||||||
return;
|
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
|
// Apply renewal threshold from global defaults or route config
|
||||||
const renewThreshold = route.action.tls?.acme?.renewBeforeDays ||
|
const renewThreshold = route.action.tls?.acme?.renewBeforeDays ||
|
||||||
this.globalAcmeDefaults?.renewThresholdDays ||
|
this.globalAcmeDefaults?.renewThresholdDays ||
|
||||||
@ -280,7 +350,8 @@ export class SmartCertManager {
|
|||||||
key: cert.privateKey,
|
key: cert.privateKey,
|
||||||
ca: cert.publicKey, // Use same as cert for now
|
ca: cert.publicKey, // Use same as cert for now
|
||||||
expiryDate: new Date(cert.validUntil),
|
expiryDate: new Date(cert.validUntil),
|
||||||
issueDate: new Date(cert.created)
|
issueDate: new Date(cert.created),
|
||||||
|
source: 'acme'
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.certStore.saveCertificate(routeName, certData);
|
await this.certStore.saveCertificate(routeName, certData);
|
||||||
@ -328,7 +399,8 @@ export class SmartCertManager {
|
|||||||
cert,
|
cert,
|
||||||
key,
|
key,
|
||||||
expiryDate: certInfo.validTo,
|
expiryDate: certInfo.validTo,
|
||||||
issueDate: certInfo.validFrom
|
issueDate: certInfo.validFrom,
|
||||||
|
source: 'static'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save to store for consistency
|
// Save to store for consistency
|
||||||
@ -399,6 +471,19 @@ export class SmartCertManager {
|
|||||||
return cert.expiryDate > expiryThreshold;
|
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
|
* Add challenge route to SmartProxy
|
||||||
|
@ -135,6 +135,12 @@ export interface ISmartProxyOptions {
|
|||||||
* or a static certificate object for immediate provisioning.
|
* or a static certificate object for immediate provisioning.
|
||||||
*/
|
*/
|
||||||
certProvisionFunction?: (domain: string) => Promise<TSmartProxyCertProvisionObject>;
|
certProvisionFunction?: (domain: string) => Promise<TSmartProxyCertProvisionObject>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to fallback to ACME if custom certificate provision fails.
|
||||||
|
* Default: true
|
||||||
|
*/
|
||||||
|
certProvisionFallbackToAcme?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -243,6 +243,16 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
certManager.setGlobalAcmeDefaults(this.settings.acme);
|
certManager.setGlobalAcmeDefaults(this.settings.acme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass down the custom certificate provision function if available
|
||||||
|
if (this.settings.certProvisionFunction) {
|
||||||
|
certManager.setCertProvisionFunction(this.settings.certProvisionFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass down the fallback to ACME setting
|
||||||
|
if (this.settings.certProvisionFallbackToAcme !== undefined) {
|
||||||
|
certManager.setCertProvisionFallbackToAcme(this.settings.certProvisionFallbackToAcme);
|
||||||
|
}
|
||||||
|
|
||||||
await certManager.initialize();
|
await certManager.initialize();
|
||||||
return certManager;
|
return certManager;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user