fix(networkproxy): Refactor certificate management components: rename AcmeCertManager to Port80Handler and update related event names from CertManagerEvents to Port80HandlerEvents. The changes update internal API usage in ts/classes.networkproxy.ts and ts/classes.port80handler.ts to unify and simplify ACME certificate handling and HTTP-01 challenge management.
This commit is contained in:
		| @@ -1,5 +1,13 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 2025-03-18 - 4.2.5 - fix(networkproxy) | ||||||
|  | Refactor certificate management components: rename AcmeCertManager to Port80Handler and update related event names from CertManagerEvents to Port80HandlerEvents. The changes update internal API usage in ts/classes.networkproxy.ts and ts/classes.port80handler.ts to unify and simplify ACME certificate handling and HTTP-01 challenge management. | ||||||
|  |  | ||||||
|  | - Renamed AcmeCertManager to Port80Handler in ts/classes.networkproxy.ts | ||||||
|  | - Updated event names from CertManagerEvents to Port80HandlerEvents | ||||||
|  | - Modified API calls for certificate issuance and renewal in ts/classes.port80handler.ts | ||||||
|  | - Refactored domain registration and certificate extraction logic | ||||||
|  |  | ||||||
| ## 2025-03-18 - 4.2.4 - fix(ts/index.ts) | ## 2025-03-18 - 4.2.4 - fix(ts/index.ts) | ||||||
| Fix export order in ts/index.ts by moving the port proxy export back and adding interfaces export for proper module exposure | Fix export order in ts/index.ts by moving the port proxy export back and adding interfaces export for proper module exposure | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@push.rocks/smartproxy', |   name: '@push.rocks/smartproxy', | ||||||
|   version: '4.2.4', |   version: '4.2.5', | ||||||
|   description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.' |   description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.' | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import * as plugins from './plugins.js'; | import * as plugins from './plugins.js'; | ||||||
| import { ProxyRouter } from './classes.router.js'; | import { ProxyRouter } from './classes.router.js'; | ||||||
| import { AcmeCertManager, CertManagerEvents } from './classes.port80handler.js'; | import { Port80Handler, Port80HandlerEvents, type IDomainOptions } from './classes.port80handler.js'; | ||||||
| import * as fs from 'fs'; | import * as fs from 'fs'; | ||||||
| import * as path from 'path'; | import * as path from 'path'; | ||||||
| import { fileURLToPath } from 'url'; | import { fileURLToPath } from 'url'; | ||||||
| @@ -72,8 +72,8 @@ export class NetworkProxy { | |||||||
|   private defaultCertificates: { key: string; cert: string }; |   private defaultCertificates: { key: string; cert: string }; | ||||||
|   private certificateCache: Map<string, { key: string; cert: string; expires?: Date }> = new Map(); |   private certificateCache: Map<string, { key: string; cert: string; expires?: Date }> = new Map(); | ||||||
|    |    | ||||||
|   // ACME certificate manager |   // Port80Handler for certificate management | ||||||
|   private certManager: AcmeCertManager | null = null; |   private port80Handler: Port80Handler | null = null; | ||||||
|   private certificateStoreDir: string; |   private certificateStoreDir: string; | ||||||
|    |    | ||||||
|   // New connection pool for backend connections |   // New connection pool for backend connections | ||||||
| @@ -375,16 +375,16 @@ export class NetworkProxy { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Initializes the ACME certificate manager for automatic certificate issuance |    * Initializes the Port80Handler for ACME certificate management | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   private async initializeAcmeManager(): Promise<void> { |   private async initializePort80Handler(): Promise<void> { | ||||||
|     if (!this.options.acme.enabled) { |     if (!this.options.acme.enabled) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     // Create certificate manager |     // Create certificate manager | ||||||
|     this.certManager = new AcmeCertManager({ |     this.port80Handler = new Port80Handler({ | ||||||
|       port: this.options.acme.port, |       port: this.options.acme.port, | ||||||
|       contactEmail: this.options.acme.contactEmail, |       contactEmail: this.options.acme.contactEmail, | ||||||
|       useProduction: this.options.acme.useProduction, |       useProduction: this.options.acme.useProduction, | ||||||
| @@ -394,32 +394,32 @@ export class NetworkProxy { | |||||||
|     }); |     }); | ||||||
|      |      | ||||||
|     // Register event handlers |     // Register event handlers | ||||||
|     this.certManager.on(CertManagerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this)); |     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this)); | ||||||
|     this.certManager.on(CertManagerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this)); |     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this)); | ||||||
|     this.certManager.on(CertManagerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this)); |     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this)); | ||||||
|     this.certManager.on(CertManagerEvents.CERTIFICATE_EXPIRING, (data) => { |     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => { | ||||||
|       this.log('info', `Certificate for ${data.domain} expires in ${data.daysRemaining} days`); |       this.log('info', `Certificate for ${data.domain} expires in ${data.daysRemaining} days`); | ||||||
|     }); |     }); | ||||||
|      |      | ||||||
|     // Start the manager |     // Start the handler | ||||||
|     try { |     try { | ||||||
|       await this.certManager.start(); |       await this.port80Handler.start(); | ||||||
|       this.log('info', `ACME Certificate Manager started on port ${this.options.acme.port}`); |       this.log('info', `Port80Handler started on port ${this.options.acme.port}`); | ||||||
|        |        | ||||||
|       // Add domains from proxy configs |       // Add domains from proxy configs | ||||||
|       this.registerDomainsWithAcmeManager(); |       this.registerDomainsWithPort80Handler(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       this.log('error', `Failed to start ACME Certificate Manager: ${error}`); |       this.log('error', `Failed to start Port80Handler: ${error}`); | ||||||
|       this.certManager = null; |       this.port80Handler = null; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   /** |   /** | ||||||
|    * Registers domains from proxy configs with the ACME manager |    * Registers domains from proxy configs with the Port80Handler | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   private registerDomainsWithAcmeManager(): void { |   private registerDomainsWithPort80Handler(): void { | ||||||
|     if (!this.certManager) return; |     if (!this.port80Handler) return; | ||||||
|      |      | ||||||
|     // Get all hostnames from proxy configs |     // Get all hostnames from proxy configs | ||||||
|     this.proxyConfigs.forEach(config => { |     this.proxyConfigs.forEach(config => { | ||||||
| @@ -461,26 +461,32 @@ export class NetworkProxy { | |||||||
|             this.log('warn', `Failed to extract expiry date from certificate for ${hostname}`); |             this.log('warn', `Failed to extract expiry date from certificate for ${hostname}`); | ||||||
|           } |           } | ||||||
|            |            | ||||||
|           // Update the certificate in the manager |           // Update the certificate in the handler | ||||||
|           this.certManager.setCertificate(hostname, cert, key, expiryDate); |           this.port80Handler.setCertificate(hostname, cert, key, expiryDate); | ||||||
|            |            | ||||||
|           // Also update our own certificate cache |           // Also update our own certificate cache | ||||||
|           this.updateCertificateCache(hostname, cert, key, expiryDate); |           this.updateCertificateCache(hostname, cert, key, expiryDate); | ||||||
|            |            | ||||||
|           this.log('info', `Loaded existing certificate for ${hostname}`); |           this.log('info', `Loaded existing certificate for ${hostname}`); | ||||||
|         } else { |         } else { | ||||||
|           // Register the domain for certificate issuance |           // Register the domain for certificate issuance with new domain options format | ||||||
|           this.certManager.addDomain(hostname); |           const domainOptions: IDomainOptions = { | ||||||
|  |             domainName: hostname, | ||||||
|  |             sslRedirect: true, | ||||||
|  |             acmeMaintenance: true | ||||||
|  |           }; | ||||||
|  |            | ||||||
|  |           this.port80Handler.addDomain(domainOptions); | ||||||
|           this.log('info', `Registered domain for ACME certificate issuance: ${hostname}`); |           this.log('info', `Registered domain for ACME certificate issuance: ${hostname}`); | ||||||
|         } |         } | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         this.log('error', `Error registering domain ${hostname} with ACME manager: ${error}`); |         this.log('error', `Error registering domain ${hostname} with Port80Handler: ${error}`); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|    |    | ||||||
|   /** |   /** | ||||||
|    * Handles newly issued or renewed certificates from ACME manager |    * Handles newly issued or renewed certificates from Port80Handler | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void { |   private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void { | ||||||
| @@ -556,13 +562,21 @@ export class NetworkProxy { | |||||||
|     } |     } | ||||||
|      |      | ||||||
|     // Check if we should trigger certificate issuance |     // Check if we should trigger certificate issuance | ||||||
|     if (this.options.acme?.enabled && this.certManager && !domain.includes('*')) { |     if (this.options.acme?.enabled && this.port80Handler && !domain.includes('*')) { | ||||||
|       // Check if this domain is already registered |       // Check if this domain is already registered | ||||||
|       const certData = this.certManager.getCertificate(domain); |       const certData = this.port80Handler.getCertificate(domain); | ||||||
|        |        | ||||||
|       if (!certData) { |       if (!certData) { | ||||||
|         this.log('info', `No certificate found for ${domain}, registering for issuance`); |         this.log('info', `No certificate found for ${domain}, registering for issuance`); | ||||||
|         this.certManager.addDomain(domain); |          | ||||||
|  |         // Register with new domain options format | ||||||
|  |         const domainOptions: IDomainOptions = { | ||||||
|  |           domainName: domain, | ||||||
|  |           sslRedirect: true, | ||||||
|  |           acmeMaintenance: true | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         this.port80Handler.addDomain(domainOptions); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|      |      | ||||||
| @@ -587,9 +601,9 @@ export class NetworkProxy { | |||||||
|   public async start(): Promise<void> { |   public async start(): Promise<void> { | ||||||
|     this.startTime = Date.now(); |     this.startTime = Date.now(); | ||||||
|      |      | ||||||
|     // Initialize ACME certificate manager if enabled |     // Initialize Port80Handler if enabled | ||||||
|     if (this.options.acme.enabled) { |     if (this.options.acme.enabled) { | ||||||
|       await this.initializeAcmeManager(); |       await this.initializePort80Handler(); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     // Create the HTTPS server |     // Create the HTTPS server | ||||||
| @@ -1588,13 +1602,13 @@ export class NetworkProxy { | |||||||
|     } |     } | ||||||
|     this.connectionPool.clear(); |     this.connectionPool.clear(); | ||||||
|      |      | ||||||
|     // Stop ACME certificate manager if it's running |     // Stop Port80Handler if it's running | ||||||
|     if (this.certManager) { |     if (this.port80Handler) { | ||||||
|       try { |       try { | ||||||
|         await this.certManager.stop(); |         await this.port80Handler.stop(); | ||||||
|         this.log('info', 'ACME Certificate Manager stopped'); |         this.log('info', 'Port80Handler stopped'); | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         this.log('error', 'Error stopping ACME Certificate Manager', error); |         this.log('error', 'Error stopping Port80Handler', error); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|      |      | ||||||
| @@ -1619,8 +1633,8 @@ export class NetworkProxy { | |||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     if (!this.certManager) { |     if (!this.port80Handler) { | ||||||
|       this.log('error', 'ACME certificate manager is not initialized'); |       this.log('error', 'Port80Handler is not initialized'); | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|      |      | ||||||
| @@ -1631,7 +1645,14 @@ export class NetworkProxy { | |||||||
|     } |     } | ||||||
|      |      | ||||||
|     try { |     try { | ||||||
|       this.certManager.addDomain(domain); |       // Use the new domain options format | ||||||
|  |       const domainOptions: IDomainOptions = { | ||||||
|  |         domainName: domain, | ||||||
|  |         sslRedirect: true, | ||||||
|  |         acmeMaintenance: true | ||||||
|  |       }; | ||||||
|  |        | ||||||
|  |       this.port80Handler.addDomain(domainOptions); | ||||||
|       this.log('info', `Certificate request submitted for domain: ${domain}`); |       this.log('info', `Certificate request submitted for domain: ${domain}`); | ||||||
|       return true; |       return true; | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|   | |||||||
| @@ -1,9 +1,58 @@ | |||||||
| import * as plugins from './plugins.js'; | import * as plugins from './plugins.js'; | ||||||
|  | import { IncomingMessage, ServerResponse } from 'http'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Represents a domain certificate with various status information |  * Custom error classes for better error handling | ||||||
|  |  */ | ||||||
|  | export class Port80HandlerError extends Error { | ||||||
|  |   constructor(message: string) { | ||||||
|  |     super(message); | ||||||
|  |     this.name = 'Port80HandlerError'; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export class CertificateError extends Port80HandlerError { | ||||||
|  |   constructor( | ||||||
|  |     message: string, | ||||||
|  |     public readonly domain: string, | ||||||
|  |     public readonly isRenewal: boolean = false | ||||||
|  |   ) { | ||||||
|  |     super(`${message} for domain ${domain}${isRenewal ? ' (renewal)' : ''}`); | ||||||
|  |     this.name = 'CertificateError'; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export class ServerError extends Port80HandlerError { | ||||||
|  |   constructor(message: string, public readonly code?: string) { | ||||||
|  |     super(message); | ||||||
|  |     this.name = 'ServerError'; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Domain forwarding configuration | ||||||
|  |  */ | ||||||
|  | export interface IForwardConfig { | ||||||
|  |   ip: string; | ||||||
|  |   port: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Domain configuration options | ||||||
|  |  */ | ||||||
|  | export interface IDomainOptions { | ||||||
|  |   domainName: string; | ||||||
|  |   sslRedirect: boolean;   // if true redirects the request to port 443 | ||||||
|  |   acmeMaintenance: boolean; // tries to always have a valid cert for this domain | ||||||
|  |   forward?: IForwardConfig; // forwards all http requests to that target | ||||||
|  |   acmeForward?: IForwardConfig; // forwards letsencrypt requests to this config | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Represents a domain configuration with certificate status information | ||||||
|  */ |  */ | ||||||
| interface IDomainCertificate { | interface IDomainCertificate { | ||||||
|  |   options: IDomainOptions; | ||||||
|   certObtained: boolean; |   certObtained: boolean; | ||||||
|   obtainingInProgress: boolean; |   obtainingInProgress: boolean; | ||||||
|   certificate?: string; |   certificate?: string; | ||||||
| @@ -15,9 +64,9 @@ interface IDomainCertificate { | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Configuration options for the ACME Certificate Manager |  * Configuration options for the Port80Handler | ||||||
|  */ |  */ | ||||||
| interface IAcmeCertManagerOptions { | interface IPort80HandlerOptions { | ||||||
|   port?: number; |   port?: number; | ||||||
|   contactEmail?: string; |   contactEmail?: string; | ||||||
|   useProduction?: boolean; |   useProduction?: boolean; | ||||||
| @@ -29,7 +78,7 @@ interface IAcmeCertManagerOptions { | |||||||
| /** | /** | ||||||
|  * Certificate data that can be emitted via events or set from outside |  * Certificate data that can be emitted via events or set from outside | ||||||
|  */ |  */ | ||||||
| interface ICertificateData { | export interface ICertificateData { | ||||||
|   domain: string; |   domain: string; | ||||||
|   certificate: string; |   certificate: string; | ||||||
|   privateKey: string; |   privateKey: string; | ||||||
| @@ -37,34 +86,53 @@ interface ICertificateData { | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Events emitted by the ACME Certificate Manager |  * Events emitted by the Port80Handler | ||||||
|  */ |  */ | ||||||
| export enum CertManagerEvents { | export enum Port80HandlerEvents { | ||||||
|   CERTIFICATE_ISSUED = 'certificate-issued', |   CERTIFICATE_ISSUED = 'certificate-issued', | ||||||
|   CERTIFICATE_RENEWED = 'certificate-renewed', |   CERTIFICATE_RENEWED = 'certificate-renewed', | ||||||
|   CERTIFICATE_FAILED = 'certificate-failed', |   CERTIFICATE_FAILED = 'certificate-failed', | ||||||
|   CERTIFICATE_EXPIRING = 'certificate-expiring', |   CERTIFICATE_EXPIRING = 'certificate-expiring', | ||||||
|   MANAGER_STARTED = 'manager-started', |   MANAGER_STARTED = 'manager-started', | ||||||
|   MANAGER_STOPPED = 'manager-stopped', |   MANAGER_STOPPED = 'manager-stopped', | ||||||
|  |   REQUEST_FORWARDED = 'request-forwarded', | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Improved ACME Certificate Manager with event emission and external certificate management |  * Certificate failure payload type | ||||||
|  */ |  */ | ||||||
| export class AcmeCertManager extends plugins.EventEmitter { | export interface ICertificateFailure { | ||||||
|  |   domain: string; | ||||||
|  |   error: string; | ||||||
|  |   isRenewal: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Certificate expiry payload type | ||||||
|  |  */ | ||||||
|  | export interface ICertificateExpiring { | ||||||
|  |   domain: string; | ||||||
|  |   expiryDate: Date; | ||||||
|  |   daysRemaining: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Port80Handler with ACME certificate management and request forwarding capabilities | ||||||
|  |  */ | ||||||
|  | export class Port80Handler extends plugins.EventEmitter { | ||||||
|   private domainCertificates: Map<string, IDomainCertificate>; |   private domainCertificates: Map<string, IDomainCertificate>; | ||||||
|   private server: plugins.http.Server | null = null; |   private server: plugins.http.Server | null = null; | ||||||
|   private acmeClient: plugins.acme.Client | null = null; |   private acmeClient: plugins.acme.Client | null = null; | ||||||
|   private accountKey: string | null = null; |   private accountKey: string | null = null; | ||||||
|   private renewalTimer: NodeJS.Timeout | null = null; |   private renewalTimer: NodeJS.Timeout | null = null; | ||||||
|   private isShuttingDown: boolean = false; |   private isShuttingDown: boolean = false; | ||||||
|   private options: Required<IAcmeCertManagerOptions>; |   private options: Required<IPort80HandlerOptions>; | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Creates a new ACME Certificate Manager |    * Creates a new Port80Handler | ||||||
|    * @param options Configuration options |    * @param options Configuration options | ||||||
|    */ |    */ | ||||||
|   constructor(options: IAcmeCertManagerOptions = {}) { |   constructor(options: IPort80HandlerOptions = {}) { | ||||||
|     super(); |     super(); | ||||||
|     this.domainCertificates = new Map<string, IDomainCertificate>(); |     this.domainCertificates = new Map<string, IDomainCertificate>(); | ||||||
|      |      | ||||||
| @@ -73,7 +141,7 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|       port: options.port ?? 80, |       port: options.port ?? 80, | ||||||
|       contactEmail: options.contactEmail ?? 'admin@example.com', |       contactEmail: options.contactEmail ?? 'admin@example.com', | ||||||
|       useProduction: options.useProduction ?? false, // Safer default: staging |       useProduction: options.useProduction ?? false, // Safer default: staging | ||||||
|       renewThresholdDays: options.renewThresholdDays ?? 30, |       renewThresholdDays: options.renewThresholdDays ?? 10, // Changed to 10 days as per requirements | ||||||
|       httpsRedirectPort: options.httpsRedirectPort ?? 443, |       httpsRedirectPort: options.httpsRedirectPort ?? 443, | ||||||
|       renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24, |       renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24, | ||||||
|     }; |     }; | ||||||
| @@ -84,11 +152,11 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|    */ |    */ | ||||||
|   public async start(): Promise<void> { |   public async start(): Promise<void> { | ||||||
|     if (this.server) { |     if (this.server) { | ||||||
|       throw new Error('Server is already running'); |       throw new ServerError('Server is already running'); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     if (this.isShuttingDown) { |     if (this.isShuttingDown) { | ||||||
|       throw new Error('Server is shutting down'); |       throw new ServerError('Server is shutting down'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return new Promise((resolve, reject) => { |     return new Promise((resolve, reject) => { | ||||||
| @@ -97,22 +165,33 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|          |          | ||||||
|         this.server.on('error', (error: NodeJS.ErrnoException) => { |         this.server.on('error', (error: NodeJS.ErrnoException) => { | ||||||
|           if (error.code === 'EACCES') { |           if (error.code === 'EACCES') { | ||||||
|             reject(new Error(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`)); |             reject(new ServerError(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`, error.code)); | ||||||
|           } else if (error.code === 'EADDRINUSE') { |           } else if (error.code === 'EADDRINUSE') { | ||||||
|             reject(new Error(`Port ${this.options.port} is already in use.`)); |             reject(new ServerError(`Port ${this.options.port} is already in use.`, error.code)); | ||||||
|           } else { |           } else { | ||||||
|             reject(error); |             reject(new ServerError(error.message, error.code)); | ||||||
|           } |           } | ||||||
|         }); |         }); | ||||||
|          |          | ||||||
|         this.server.listen(this.options.port, () => { |         this.server.listen(this.options.port, () => { | ||||||
|           console.log(`AcmeCertManager is listening on port ${this.options.port}`); |           console.log(`Port80Handler is listening on port ${this.options.port}`); | ||||||
|           this.startRenewalTimer(); |           this.startRenewalTimer(); | ||||||
|           this.emit(CertManagerEvents.MANAGER_STARTED, this.options.port); |           this.emit(Port80HandlerEvents.MANAGER_STARTED, this.options.port); | ||||||
|  |            | ||||||
|  |           // Start certificate process for domains with acmeMaintenance enabled | ||||||
|  |           for (const [domain, domainInfo] of this.domainCertificates.entries()) { | ||||||
|  |             if (domainInfo.options.acmeMaintenance && !domainInfo.certObtained && !domainInfo.obtainingInProgress) { | ||||||
|  |               this.obtainCertificate(domain).catch(err => { | ||||||
|  |                 console.error(`Error obtaining initial certificate for ${domain}:`, err); | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |            | ||||||
|           resolve(); |           resolve(); | ||||||
|         }); |         }); | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|         reject(error); |         const message = error instanceof Error ? error.message : 'Unknown error starting server'; | ||||||
|  |         reject(new ServerError(message)); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| @@ -138,7 +217,7 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|         this.server.close(() => { |         this.server.close(() => { | ||||||
|           this.server = null; |           this.server = null; | ||||||
|           this.isShuttingDown = false; |           this.isShuttingDown = false; | ||||||
|           this.emit(CertManagerEvents.MANAGER_STOPPED); |           this.emit(Port80HandlerEvents.MANAGER_STOPPED); | ||||||
|           resolve(); |           resolve(); | ||||||
|         }); |         }); | ||||||
|       } else { |       } else { | ||||||
| @@ -149,13 +228,41 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Adds a domain to be managed for certificates |    * Adds a domain with configuration options | ||||||
|    * @param domain The domain to add |    * @param options Domain configuration options | ||||||
|    */ |    */ | ||||||
|   public addDomain(domain: string): void { |   public addDomain(options: IDomainOptions): void { | ||||||
|     if (!this.domainCertificates.has(domain)) { |     if (!options.domainName || typeof options.domainName !== 'string') { | ||||||
|       this.domainCertificates.set(domain, { certObtained: false, obtainingInProgress: false }); |       throw new Port80HandlerError('Invalid domain name'); | ||||||
|       console.log(`Domain added: ${domain}`); |     } | ||||||
|  |      | ||||||
|  |     const domainName = options.domainName; | ||||||
|  |      | ||||||
|  |     if (!this.domainCertificates.has(domainName)) { | ||||||
|  |       this.domainCertificates.set(domainName, { | ||||||
|  |         options, | ||||||
|  |         certObtained: false, | ||||||
|  |         obtainingInProgress: false | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       console.log(`Domain added: ${domainName} with configuration:`, { | ||||||
|  |         sslRedirect: options.sslRedirect, | ||||||
|  |         acmeMaintenance: options.acmeMaintenance, | ||||||
|  |         hasForward: !!options.forward, | ||||||
|  |         hasAcmeForward: !!options.acmeForward | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       // If acmeMaintenance is enabled, start certificate process immediately | ||||||
|  |       if (options.acmeMaintenance && this.server) { | ||||||
|  |         this.obtainCertificate(domainName).catch(err => { | ||||||
|  |           console.error(`Error obtaining initial certificate for ${domainName}:`, err); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       // Update existing domain with new options | ||||||
|  |       const existing = this.domainCertificates.get(domainName)!; | ||||||
|  |       existing.options = options; | ||||||
|  |       console.log(`Domain ${domainName} configuration updated`); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -177,10 +284,25 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|    * @param expiryDate Optional expiry date |    * @param expiryDate Optional expiry date | ||||||
|    */ |    */ | ||||||
|   public setCertificate(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void { |   public setCertificate(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void { | ||||||
|  |     if (!domain || !certificate || !privateKey) { | ||||||
|  |       throw new Port80HandlerError('Domain, certificate and privateKey are required'); | ||||||
|  |     } | ||||||
|  |      | ||||||
|     let domainInfo = this.domainCertificates.get(domain); |     let domainInfo = this.domainCertificates.get(domain); | ||||||
|      |      | ||||||
|     if (!domainInfo) { |     if (!domainInfo) { | ||||||
|       domainInfo = { certObtained: false, obtainingInProgress: false }; |       // Create default domain options if not already configured | ||||||
|  |       const defaultOptions: IDomainOptions = { | ||||||
|  |         domainName: domain, | ||||||
|  |         sslRedirect: true, | ||||||
|  |         acmeMaintenance: true | ||||||
|  |       }; | ||||||
|  |        | ||||||
|  |       domainInfo = {  | ||||||
|  |         options: defaultOptions,  | ||||||
|  |         certObtained: false,  | ||||||
|  |         obtainingInProgress: false  | ||||||
|  |       }; | ||||||
|       this.domainCertificates.set(domain, domainInfo); |       this.domainCertificates.set(domain, domainInfo); | ||||||
|     } |     } | ||||||
|      |      | ||||||
| @@ -192,27 +314,18 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|     if (expiryDate) { |     if (expiryDate) { | ||||||
|       domainInfo.expiryDate = expiryDate; |       domainInfo.expiryDate = expiryDate; | ||||||
|     } else { |     } else { | ||||||
|       // Try to extract expiry date from certificate |       // Extract expiry date from certificate | ||||||
|       try { |       domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain); | ||||||
|         // This is a simplistic approach - in a real implementation, use a proper |  | ||||||
|         // certificate parsing library like node-forge or x509 |  | ||||||
|         const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i); |  | ||||||
|         if (matches && matches[1]) { |  | ||||||
|           domainInfo.expiryDate = new Date(matches[1]); |  | ||||||
|         } |  | ||||||
|       } catch (error) { |  | ||||||
|         console.warn(`Failed to extract expiry date from certificate for ${domain}`); |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     console.log(`Certificate set for ${domain}`); |     console.log(`Certificate set for ${domain}`); | ||||||
|      |      | ||||||
|     // Emit certificate event |     // Emit certificate event | ||||||
|     this.emitCertificateEvent(CertManagerEvents.CERTIFICATE_ISSUED, { |     this.emitCertificateEvent(Port80HandlerEvents.CERTIFICATE_ISSUED, { | ||||||
|       domain, |       domain, | ||||||
|       certificate, |       certificate, | ||||||
|       privateKey, |       privateKey, | ||||||
|       expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default |       expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate() | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|    |    | ||||||
| @@ -231,7 +344,7 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|       domain, |       domain, | ||||||
|       certificate: domainInfo.certificate, |       certificate: domainInfo.certificate, | ||||||
|       privateKey: domainInfo.privateKey, |       privateKey: domainInfo.privateKey, | ||||||
|       expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default |       expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate() | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -244,6 +357,7 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|       return this.acmeClient; |       return this.acmeClient; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     try { | ||||||
|       // Generate a new account key |       // Generate a new account key | ||||||
|       this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString(); |       this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString(); | ||||||
|        |        | ||||||
| @@ -261,6 +375,10 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|       }); |       }); | ||||||
|        |        | ||||||
|       return this.acmeClient; |       return this.acmeClient; | ||||||
|  |     } catch (error) { | ||||||
|  |       const message = error instanceof Error ? error.message : 'Unknown error initializing ACME client'; | ||||||
|  |       throw new Port80HandlerError(`Failed to initialize ACME client: ${message}`); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -279,12 +397,7 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|     // Extract domain (ignoring any port in the Host header) |     // Extract domain (ignoring any port in the Host header) | ||||||
|     const domain = hostHeader.split(':')[0]; |     const domain = hostHeader.split(':')[0]; | ||||||
|  |  | ||||||
|     // If the request is for an ACME HTTP-01 challenge, handle it |     // Check if domain is configured | ||||||
|     if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) { |  | ||||||
|       this.handleAcmeChallenge(req, res, domain); |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!this.domainCertificates.has(domain)) { |     if (!this.domainCertificates.has(domain)) { | ||||||
|       res.statusCode = 404; |       res.statusCode = 404; | ||||||
|       res.end('Domain not configured'); |       res.end('Domain not configured'); | ||||||
| @@ -292,9 +405,28 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const domainInfo = this.domainCertificates.get(domain)!; |     const domainInfo = this.domainCertificates.get(domain)!; | ||||||
|  |     const options = domainInfo.options; | ||||||
|  |  | ||||||
|     // If certificate exists, redirect to HTTPS |     // If the request is for an ACME HTTP-01 challenge, handle it | ||||||
|     if (domainInfo.certObtained) { |     if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) { | ||||||
|  |       // Check if we should forward ACME requests | ||||||
|  |       if (options.acmeForward) { | ||||||
|  |         this.forwardRequest(req, res, options.acmeForward, 'ACME challenge'); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       this.handleAcmeChallenge(req, res, domain); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Check if we should forward non-ACME requests | ||||||
|  |     if (options.forward) { | ||||||
|  |       this.forwardRequest(req, res, options.forward, 'HTTP'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // If certificate exists and sslRedirect is enabled, redirect to HTTPS | ||||||
|  |     if (domainInfo.certObtained && options.sslRedirect) { | ||||||
|       const httpsPort = this.options.httpsRedirectPort; |       const httpsPort = this.options.httpsRedirectPort; | ||||||
|       const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`; |       const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`; | ||||||
|       const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`; |       const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`; | ||||||
| @@ -302,17 +434,93 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|       res.statusCode = 301; |       res.statusCode = 301; | ||||||
|       res.setHeader('Location', redirectUrl); |       res.setHeader('Location', redirectUrl); | ||||||
|       res.end(`Redirecting to ${redirectUrl}`); |       res.end(`Redirecting to ${redirectUrl}`); | ||||||
|     } else { |       return; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Handle case where certificate maintenance is enabled but not yet obtained | ||||||
|  |     if (options.acmeMaintenance && !domainInfo.certObtained) { | ||||||
|       // Trigger certificate issuance if not already running |       // Trigger certificate issuance if not already running | ||||||
|       if (!domainInfo.obtainingInProgress) { |       if (!domainInfo.obtainingInProgress) { | ||||||
|         this.obtainCertificate(domain).catch(err => { |         this.obtainCertificate(domain).catch(err => { | ||||||
|           this.emit(CertManagerEvents.CERTIFICATE_FAILED, { domain, error: err.message }); |           const errorMessage = err instanceof Error ? err.message : 'Unknown error'; | ||||||
|  |           this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, { | ||||||
|  |             domain, | ||||||
|  |             error: errorMessage, | ||||||
|  |             isRenewal: false | ||||||
|  |           }); | ||||||
|           console.error(`Error obtaining certificate for ${domain}:`, err); |           console.error(`Error obtaining certificate for ${domain}:`, err); | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       res.statusCode = 503; |       res.statusCode = 503; | ||||||
|       res.end('Certificate issuance in progress, please try again later.'); |       res.end('Certificate issuance in progress, please try again later.'); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Default response for unhandled request | ||||||
|  |     res.statusCode = 404; | ||||||
|  |     res.end('No handlers configured for this request'); | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Forwards an HTTP request to the specified target | ||||||
|  |    * @param req The original request | ||||||
|  |    * @param res The response object | ||||||
|  |    * @param target The forwarding target (IP and port) | ||||||
|  |    * @param requestType Type of request for logging | ||||||
|  |    */ | ||||||
|  |   private forwardRequest( | ||||||
|  |     req: plugins.http.IncomingMessage,  | ||||||
|  |     res: plugins.http.ServerResponse, | ||||||
|  |     target: IForwardConfig, | ||||||
|  |     requestType: string | ||||||
|  |   ): void { | ||||||
|  |     const options = { | ||||||
|  |       hostname: target.ip, | ||||||
|  |       port: target.port, | ||||||
|  |       path: req.url, | ||||||
|  |       method: req.method, | ||||||
|  |       headers: { ...req.headers } | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     const domain = req.headers.host?.split(':')[0] || 'unknown'; | ||||||
|  |     console.log(`Forwarding ${requestType} request for ${domain} to ${target.ip}:${target.port}`); | ||||||
|  |      | ||||||
|  |     const proxyReq = plugins.http.request(options, (proxyRes) => { | ||||||
|  |       // Copy status code | ||||||
|  |       res.statusCode = proxyRes.statusCode || 500; | ||||||
|  |        | ||||||
|  |       // Copy headers | ||||||
|  |       for (const [key, value] of Object.entries(proxyRes.headers)) { | ||||||
|  |         if (value) res.setHeader(key, value); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Pipe response data | ||||||
|  |       proxyRes.pipe(res); | ||||||
|  |        | ||||||
|  |       this.emit(Port80HandlerEvents.REQUEST_FORWARDED, { | ||||||
|  |         domain, | ||||||
|  |         requestType, | ||||||
|  |         target: `${target.ip}:${target.port}`, | ||||||
|  |         statusCode: proxyRes.statusCode | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     proxyReq.on('error', (error) => { | ||||||
|  |       console.error(`Error forwarding request to ${target.ip}:${target.port}:`, error); | ||||||
|  |       if (!res.headersSent) { | ||||||
|  |         res.statusCode = 502; | ||||||
|  |         res.end(`Proxy error: ${error.message}`); | ||||||
|  |       } else { | ||||||
|  |         res.end(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     // Pipe original request to proxy request | ||||||
|  |     if (req.readable) { | ||||||
|  |       req.pipe(proxyReq); | ||||||
|  |     } else { | ||||||
|  |       proxyReq.end(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -354,7 +562,13 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|     // Get the domain info |     // Get the domain info | ||||||
|     const domainInfo = this.domainCertificates.get(domain); |     const domainInfo = this.domainCertificates.get(domain); | ||||||
|     if (!domainInfo) { |     if (!domainInfo) { | ||||||
|       throw new Error(`Domain not found: ${domain}`); |       throw new CertificateError('Domain not found', domain, isRenewal); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Verify that acmeMaintenance is enabled | ||||||
|  |     if (!domainInfo.options.acmeMaintenance) { | ||||||
|  |       console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`); | ||||||
|  |       return; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     // Prevent concurrent certificate issuance |     // Prevent concurrent certificate issuance | ||||||
| @@ -377,10 +591,99 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|       // Get the authorizations for the order |       // Get the authorizations for the order | ||||||
|       const authorizations = await client.getAuthorizations(order); |       const authorizations = await client.getAuthorizations(order); | ||||||
|        |        | ||||||
|  |       // Process each authorization | ||||||
|  |       await this.processAuthorizations(client, domain, authorizations); | ||||||
|  |  | ||||||
|  |       // Generate a CSR and private key | ||||||
|  |       const [csrBuffer, privateKeyBuffer] = await plugins.acme.forge.createCsr({ | ||||||
|  |         commonName: domain, | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |       const csr = csrBuffer.toString(); | ||||||
|  |       const privateKey = privateKeyBuffer.toString(); | ||||||
|  |  | ||||||
|  |       // Finalize the order with our CSR | ||||||
|  |       await client.finalizeOrder(order, csr); | ||||||
|  |        | ||||||
|  |       // Get the certificate with the full chain | ||||||
|  |       const certificate = await client.getCertificate(order); | ||||||
|  |  | ||||||
|  |       // Store the certificate and key | ||||||
|  |       domainInfo.certificate = certificate; | ||||||
|  |       domainInfo.privateKey = privateKey; | ||||||
|  |       domainInfo.certObtained = true; | ||||||
|  |        | ||||||
|  |       // Clear challenge data | ||||||
|  |       delete domainInfo.challengeToken; | ||||||
|  |       delete domainInfo.challengeKeyAuthorization; | ||||||
|  |        | ||||||
|  |       // Extract expiry date from certificate | ||||||
|  |       domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain); | ||||||
|  |  | ||||||
|  |       console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`); | ||||||
|  |        | ||||||
|  |       // Emit the appropriate event | ||||||
|  |       const eventType = isRenewal  | ||||||
|  |         ? Port80HandlerEvents.CERTIFICATE_RENEWED  | ||||||
|  |         : Port80HandlerEvents.CERTIFICATE_ISSUED; | ||||||
|  |        | ||||||
|  |       this.emitCertificateEvent(eventType, { | ||||||
|  |         domain, | ||||||
|  |         certificate, | ||||||
|  |         privateKey, | ||||||
|  |         expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate() | ||||||
|  |       }); | ||||||
|  |        | ||||||
|  |     } catch (error: any) { | ||||||
|  |       // Check for rate limit errors | ||||||
|  |       if (error.message && ( | ||||||
|  |         error.message.includes('rateLimited') ||  | ||||||
|  |         error.message.includes('too many certificates') ||  | ||||||
|  |         error.message.includes('rate limit') | ||||||
|  |       )) { | ||||||
|  |         console.error(`Rate limit reached for ${domain}. Waiting before retry.`); | ||||||
|  |       } else { | ||||||
|  |         console.error(`Error during certificate issuance for ${domain}:`, error); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Emit failure event | ||||||
|  |       this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, { | ||||||
|  |         domain, | ||||||
|  |         error: error.message || 'Unknown error', | ||||||
|  |         isRenewal | ||||||
|  |       } as ICertificateFailure); | ||||||
|  |        | ||||||
|  |       throw new CertificateError( | ||||||
|  |         error.message || 'Certificate issuance failed', | ||||||
|  |         domain, | ||||||
|  |         isRenewal | ||||||
|  |       ); | ||||||
|  |     } finally { | ||||||
|  |       // Reset flag whether successful or not | ||||||
|  |       domainInfo.obtainingInProgress = false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Process ACME authorizations by verifying and completing challenges | ||||||
|  |    * @param client ACME client  | ||||||
|  |    * @param domain Domain name | ||||||
|  |    * @param authorizations Authorizations to process | ||||||
|  |    */ | ||||||
|  |   private async processAuthorizations( | ||||||
|  |     client: plugins.acme.Client, | ||||||
|  |     domain: string, | ||||||
|  |     authorizations: plugins.acme.Authorization[] | ||||||
|  |   ): Promise<void> { | ||||||
|  |     const domainInfo = this.domainCertificates.get(domain); | ||||||
|  |     if (!domainInfo) { | ||||||
|  |       throw new CertificateError('Domain not found during authorization', domain); | ||||||
|  |     } | ||||||
|  |      | ||||||
|     for (const authz of authorizations) { |     for (const authz of authorizations) { | ||||||
|       const challenge = authz.challenges.find(ch => ch.type === 'http-01'); |       const challenge = authz.challenges.find(ch => ch.type === 'http-01'); | ||||||
|       if (!challenge) { |       if (!challenge) { | ||||||
|           throw new Error('HTTP-01 challenge not found'); |         throw new CertificateError('HTTP-01 challenge not found', domain); | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       // Get the key authorization for the challenge |       // Get the key authorization for the challenge | ||||||
| @@ -407,81 +710,11 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|         await client.waitForValidStatus(challenge); |         await client.waitForValidStatus(challenge); | ||||||
|         console.log(`HTTP-01 challenge completed for ${domain}`); |         console.log(`HTTP-01 challenge completed for ${domain}`); | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
|  |         const errorMessage = error instanceof Error ? error.message : 'Unknown challenge error'; | ||||||
|         console.error(`Challenge error for ${domain}:`, error); |         console.error(`Challenge error for ${domain}:`, error); | ||||||
|           throw error; |         throw new CertificateError(`Challenge verification failed: ${errorMessage}`, domain); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|       // Generate a CSR and private key |  | ||||||
|       const [csrBuffer, privateKeyBuffer] = await plugins.acme.forge.createCsr({ |  | ||||||
|         commonName: domain, |  | ||||||
|       }); |  | ||||||
|        |  | ||||||
|       const csr = csrBuffer.toString(); |  | ||||||
|       const privateKey = privateKeyBuffer.toString(); |  | ||||||
|  |  | ||||||
|       // Finalize the order with our CSR |  | ||||||
|       await client.finalizeOrder(order, csr); |  | ||||||
|        |  | ||||||
|       // Get the certificate with the full chain |  | ||||||
|       const certificate = await client.getCertificate(order); |  | ||||||
|  |  | ||||||
|       // Store the certificate and key |  | ||||||
|       domainInfo.certificate = certificate; |  | ||||||
|       domainInfo.privateKey = privateKey; |  | ||||||
|       domainInfo.certObtained = true; |  | ||||||
|        |  | ||||||
|       // Clear challenge data |  | ||||||
|       delete domainInfo.challengeToken; |  | ||||||
|       delete domainInfo.challengeKeyAuthorization; |  | ||||||
|        |  | ||||||
|       // Extract expiry date from certificate |  | ||||||
|       try { |  | ||||||
|         const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i); |  | ||||||
|         if (matches && matches[1]) { |  | ||||||
|           domainInfo.expiryDate = new Date(matches[1]); |  | ||||||
|           console.log(`Certificate for ${domain} will expire on ${domainInfo.expiryDate.toISOString()}`); |  | ||||||
|         } |  | ||||||
|       } catch (error) { |  | ||||||
|         console.warn(`Failed to extract expiry date from certificate for ${domain}`); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`); |  | ||||||
|        |  | ||||||
|       // Emit the appropriate event |  | ||||||
|       const eventType = isRenewal  |  | ||||||
|         ? CertManagerEvents.CERTIFICATE_RENEWED  |  | ||||||
|         : CertManagerEvents.CERTIFICATE_ISSUED; |  | ||||||
|        |  | ||||||
|       this.emitCertificateEvent(eventType, { |  | ||||||
|         domain, |  | ||||||
|         certificate, |  | ||||||
|         privateKey, |  | ||||||
|         expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default |  | ||||||
|       }); |  | ||||||
|        |  | ||||||
|     } catch (error: any) { |  | ||||||
|       // Check for rate limit errors |  | ||||||
|       if (error.message && ( |  | ||||||
|         error.message.includes('rateLimited') ||  |  | ||||||
|         error.message.includes('too many certificates') ||  |  | ||||||
|         error.message.includes('rate limit') |  | ||||||
|       )) { |  | ||||||
|         console.error(`Rate limit reached for ${domain}. Waiting before retry.`); |  | ||||||
|       } else { |  | ||||||
|         console.error(`Error during certificate issuance for ${domain}:`, error); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       // Emit failure event |  | ||||||
|       this.emit(CertManagerEvents.CERTIFICATE_FAILED, { |  | ||||||
|         domain, |  | ||||||
|         error: error.message || 'Unknown error', |  | ||||||
|         isRenewal |  | ||||||
|       }); |  | ||||||
|     } finally { |  | ||||||
|       // Reset flag whether successful or not |  | ||||||
|       domainInfo.obtainingInProgress = false; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -519,6 +752,11 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|     const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000; |     const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000; | ||||||
|      |      | ||||||
|     for (const [domain, domainInfo] of this.domainCertificates.entries()) { |     for (const [domain, domainInfo] of this.domainCertificates.entries()) { | ||||||
|  |       // Skip domains with acmeMaintenance disabled | ||||||
|  |       if (!domainInfo.options.acmeMaintenance) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |        | ||||||
|       // Skip domains without certificates or already in renewal |       // Skip domains without certificates or already in renewal | ||||||
|       if (!domainInfo.certObtained || domainInfo.obtainingInProgress) { |       if (!domainInfo.certObtained || domainInfo.obtainingInProgress) { | ||||||
|         continue; |         continue; | ||||||
| @@ -534,26 +772,67 @@ export class AcmeCertManager extends plugins.EventEmitter { | |||||||
|       // Check if certificate is near expiry |       // Check if certificate is near expiry | ||||||
|       if (timeUntilExpiry <= renewThresholdMs) { |       if (timeUntilExpiry <= renewThresholdMs) { | ||||||
|         console.log(`Certificate for ${domain} expires soon, renewing...`); |         console.log(`Certificate for ${domain} expires soon, renewing...`); | ||||||
|         this.emit(CertManagerEvents.CERTIFICATE_EXPIRING, { |          | ||||||
|  |         const daysRemaining = Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000)); | ||||||
|  |          | ||||||
|  |         this.emit(Port80HandlerEvents.CERTIFICATE_EXPIRING, { | ||||||
|           domain, |           domain, | ||||||
|           expiryDate: domainInfo.expiryDate, |           expiryDate: domainInfo.expiryDate, | ||||||
|           daysRemaining: Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000)) |           daysRemaining | ||||||
|         }); |         } as ICertificateExpiring); | ||||||
|          |          | ||||||
|         // Start renewal process |         // Start renewal process | ||||||
|         this.obtainCertificate(domain, true).catch(err => { |         this.obtainCertificate(domain, true).catch(err => { | ||||||
|           console.error(`Error renewing certificate for ${domain}:`, err); |           const errorMessage = err instanceof Error ? err.message : 'Unknown error'; | ||||||
|  |           console.error(`Error renewing certificate for ${domain}:`, errorMessage); | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|    |    | ||||||
|  |   /** | ||||||
|  |    * Extract expiry date from certificate using a more robust approach | ||||||
|  |    * @param certificate Certificate PEM string | ||||||
|  |    * @param domain Domain for logging | ||||||
|  |    * @returns Extracted expiry date or default | ||||||
|  |    */ | ||||||
|  |   private extractExpiryDateFromCertificate(certificate: string, domain: string): Date { | ||||||
|  |     try { | ||||||
|  |       // This is still using regex, but in a real implementation you would use | ||||||
|  |       // a library like node-forge or x509 to properly parse the certificate | ||||||
|  |       const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i); | ||||||
|  |       if (matches && matches[1]) { | ||||||
|  |         const expiryDate = new Date(matches[1]); | ||||||
|  |          | ||||||
|  |         // Validate that we got a valid date | ||||||
|  |         if (!isNaN(expiryDate.getTime())) { | ||||||
|  |           console.log(`Certificate for ${domain} will expire on ${expiryDate.toISOString()}`); | ||||||
|  |           return expiryDate; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       console.warn(`Could not extract valid expiry date from certificate for ${domain}, using default`); | ||||||
|  |       return this.getDefaultExpiryDate(); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.warn(`Failed to extract expiry date from certificate for ${domain}, using default`); | ||||||
|  |       return this.getDefaultExpiryDate(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   /** | ||||||
|  |    * Get a default expiry date (90 days from now) | ||||||
|  |    * @returns Default expiry date | ||||||
|  |    */ | ||||||
|  |   private getDefaultExpiryDate(): Date { | ||||||
|  |     return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); // 90 days default | ||||||
|  |   } | ||||||
|  |    | ||||||
|   /** |   /** | ||||||
|    * Emits a certificate event with the certificate data |    * Emits a certificate event with the certificate data | ||||||
|    * @param eventType The event type to emit |    * @param eventType The event type to emit | ||||||
|    * @param data The certificate data |    * @param data The certificate data | ||||||
|    */ |    */ | ||||||
|   private emitCertificateEvent(eventType: CertManagerEvents, data: ICertificateData): void { |   private emitCertificateEvent(eventType: Port80HandlerEvents, data: ICertificateData): void { | ||||||
|     this.emit(eventType, data); |     this.emit(eventType, data); | ||||||
|   } |   } | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user