BREAKING CHANGE(acme): Refactor ACME configuration and certificate provisioning by replacing legacy port80HandlerConfig with unified acme options and updating CertProvisioner event subscriptions
This commit is contained in:
		| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '8.0.0', | ||||
|   version: '9.0.0', | ||||
|   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.' | ||||
| } | ||||
|   | ||||
							
								
								
									
										23
									
								
								ts/common/acmeFactory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								ts/common/acmeFactory.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import * as fs from 'fs'; | ||||
| import * as path from 'path'; | ||||
| import type { IAcmeOptions } from './types.js'; | ||||
| import { Port80Handler } from '../port80handler/classes.port80handler.js'; | ||||
|  | ||||
| /** | ||||
|  * Factory to create a Port80Handler with common setup. | ||||
|  * Ensures the certificate store directory exists and instantiates the handler. | ||||
|  * @param options Port80Handler configuration options | ||||
|  * @returns A new Port80Handler instance | ||||
|  */ | ||||
| export function buildPort80Handler( | ||||
|   options: IAcmeOptions | ||||
| ): Port80Handler { | ||||
|   if (options.certificateStore) { | ||||
|     const certStorePath = path.resolve(options.certificateStore); | ||||
|     if (!fs.existsSync(certStorePath)) { | ||||
|       fs.mkdirSync(certStorePath, { recursive: true }); | ||||
|       console.log(`Created certificate store directory: ${certStorePath}`); | ||||
|     } | ||||
|   } | ||||
|   return new Port80Handler(options); | ||||
| } | ||||
							
								
								
									
										34
									
								
								ts/common/eventUtils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								ts/common/eventUtils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import type { Port80Handler } from '../port80handler/classes.port80handler.js'; | ||||
| import { Port80HandlerEvents } from './types.js'; | ||||
| import type { ICertificateData, ICertificateFailure, ICertificateExpiring } from './types.js'; | ||||
|  | ||||
| /** | ||||
|  * Subscribers callback definitions for Port80Handler events | ||||
|  */ | ||||
| export interface Port80HandlerSubscribers { | ||||
|   onCertificateIssued?: (data: ICertificateData) => void; | ||||
|   onCertificateRenewed?: (data: ICertificateData) => void; | ||||
|   onCertificateFailed?: (data: ICertificateFailure) => void; | ||||
|   onCertificateExpiring?: (data: ICertificateExpiring) => void; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Subscribes to Port80Handler events based on provided callbacks | ||||
|  */ | ||||
| export function subscribeToPort80Handler( | ||||
|   handler: Port80Handler, | ||||
|   subscribers: Port80HandlerSubscribers | ||||
| ): void { | ||||
|   if (subscribers.onCertificateIssued) { | ||||
|     handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, subscribers.onCertificateIssued); | ||||
|   } | ||||
|   if (subscribers.onCertificateRenewed) { | ||||
|     handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, subscribers.onCertificateRenewed); | ||||
|   } | ||||
|   if (subscribers.onCertificateFailed) { | ||||
|     handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, subscribers.onCertificateFailed); | ||||
|   } | ||||
|   if (subscribers.onCertificateExpiring) { | ||||
|     handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, subscribers.onCertificateExpiring); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										89
									
								
								ts/common/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								ts/common/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| /** | ||||
|  * Shared types for certificate management and domain options | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * 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 | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Certificate data that can be emitted via events or set from outside | ||||
|  */ | ||||
| export interface ICertificateData { | ||||
|   domain: string; | ||||
|   certificate: string; | ||||
|   privateKey: string; | ||||
|   expiryDate: Date; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Events emitted by the Port80Handler | ||||
|  */ | ||||
| export enum Port80HandlerEvents { | ||||
|   CERTIFICATE_ISSUED = 'certificate-issued', | ||||
|   CERTIFICATE_RENEWED = 'certificate-renewed', | ||||
|   CERTIFICATE_FAILED = 'certificate-failed', | ||||
|   CERTIFICATE_EXPIRING = 'certificate-expiring', | ||||
|   MANAGER_STARTED = 'manager-started', | ||||
|   MANAGER_STOPPED = 'manager-stopped', | ||||
|   REQUEST_FORWARDED = 'request-forwarded', | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Certificate failure payload type | ||||
|  */ | ||||
| export interface ICertificateFailure { | ||||
|   domain: string; | ||||
|   error: string; | ||||
|   isRenewal: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Certificate expiry payload type | ||||
|  */ | ||||
| export interface ICertificateExpiring { | ||||
|   domain: string; | ||||
|   expiryDate: Date; | ||||
|   daysRemaining: number; | ||||
| } | ||||
| /** | ||||
|  * Forwarding configuration for specific domains in ACME setup | ||||
|  */ | ||||
| export interface IDomainForwardConfig { | ||||
|   domain: string; | ||||
|   forwardConfig?: IForwardConfig; | ||||
|   acmeForwardConfig?: IForwardConfig; | ||||
|   sslRedirect?: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Unified ACME configuration options used across proxies and handlers | ||||
|  */ | ||||
| export interface IAcmeOptions { | ||||
|   enabled?: boolean;              // Whether ACME is enabled | ||||
|   port?: number;                  // Port to listen on for ACME challenges (default: 80) | ||||
|   contactEmail?: string;          // Email for Let's Encrypt account | ||||
|   useProduction?: boolean;        // Use production environment (default: staging) | ||||
|   httpsRedirectPort?: number;     // Port to redirect HTTP requests to HTTPS (default: 443) | ||||
|   renewThresholdDays?: number;    // Days before expiry to renew certificates | ||||
|   renewCheckIntervalHours?: number; // How often to check for renewals (in hours) | ||||
|   autoRenew?: boolean;            // Whether to automatically renew certificates | ||||
|   certificateStore?: string;      // Directory to store certificates | ||||
|   skipConfiguredCerts?: boolean;  // Skip domains with existing certificates | ||||
|   domainForwards?: IDomainForwardConfig[]; // Domain-specific forwarding configs | ||||
| } | ||||
| @@ -3,7 +3,11 @@ import * as fs from 'fs'; | ||||
| import * as path from 'path'; | ||||
| import { fileURLToPath } from 'url'; | ||||
| import { type INetworkProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './classes.np.types.js'; | ||||
| import { Port80Handler, Port80HandlerEvents, type IDomainOptions } from '../port80handler/classes.port80handler.js'; | ||||
| import { Port80Handler } from '../port80handler/classes.port80handler.js'; | ||||
| import { Port80HandlerEvents } from '../common/types.js'; | ||||
| import { buildPort80Handler } from '../common/acmeFactory.js'; | ||||
| import { subscribeToPort80Handler } from '../common/eventUtils.js'; | ||||
| import type { IDomainOptions } from '../common/types.js'; | ||||
|  | ||||
| /** | ||||
|  * Manages SSL certificates for NetworkProxy including ACME integration | ||||
| @@ -101,12 +105,14 @@ export class CertificateManager { | ||||
|     this.port80Handler = handler; | ||||
|     this.externalPort80Handler = true; | ||||
|      | ||||
|     // Register event handlers | ||||
|     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this)); | ||||
|     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this)); | ||||
|     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this)); | ||||
|     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => { | ||||
|       this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`); | ||||
|     // Subscribe to Port80Handler events | ||||
|     subscribeToPort80Handler(this.port80Handler, { | ||||
|       onCertificateIssued: this.handleCertificateIssued.bind(this), | ||||
|       onCertificateRenewed: this.handleCertificateIssued.bind(this), | ||||
|       onCertificateFailed: this.handleCertificateFailed.bind(this), | ||||
|       onCertificateExpiring: (data) => { | ||||
|         this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     this.logger.info('External Port80Handler connected to CertificateManager'); | ||||
| @@ -348,8 +354,8 @@ export class CertificateManager { | ||||
|       return null; | ||||
|     } | ||||
|      | ||||
|     // Create certificate manager | ||||
|     this.port80Handler = new Port80Handler({ | ||||
|     // Build and configure Port80Handler | ||||
|     this.port80Handler = buildPort80Handler({ | ||||
|       port: this.options.acme.port, | ||||
|       contactEmail: this.options.acme.contactEmail, | ||||
|       useProduction: this.options.acme.useProduction, | ||||
| @@ -358,13 +364,14 @@ export class CertificateManager { | ||||
|       certificateStore: this.options.acme.certificateStore, | ||||
|       skipConfiguredCerts: this.options.acme.skipConfiguredCerts | ||||
|     }); | ||||
|      | ||||
|     // Register event handlers | ||||
|     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this)); | ||||
|     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this)); | ||||
|     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this)); | ||||
|     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => { | ||||
|       this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`); | ||||
|     // Subscribe to Port80Handler events | ||||
|     subscribeToPort80Handler(this.port80Handler, { | ||||
|       onCertificateIssued: this.handleCertificateIssued.bind(this), | ||||
|       onCertificateRenewed: this.handleCertificateIssued.bind(this), | ||||
|       onCertificateFailed: this.handleCertificateFailed.bind(this), | ||||
|       onCertificateExpiring: (data) => { | ||||
|         this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`); | ||||
|       } | ||||
|     }); | ||||
|      | ||||
|     // Start the handler | ||||
|   | ||||
| @@ -1,5 +1,10 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * Configuration options for NetworkProxy | ||||
|  */ | ||||
| import type { IAcmeOptions } from '../common/types.js'; | ||||
|  | ||||
| /** | ||||
|  * Configuration options for NetworkProxy | ||||
|  */ | ||||
| @@ -24,16 +29,7 @@ export interface INetworkProxyOptions { | ||||
|   backendProtocol?: 'http1' | 'http2'; | ||||
|    | ||||
|   // ACME certificate management options | ||||
|   acme?: { | ||||
|     enabled?: boolean;              // Whether to enable automatic certificate management | ||||
|     port?: number;                  // Port to listen on for ACME challenges (default: 80) | ||||
|     contactEmail?: string;          // Email for Let's Encrypt account  | ||||
|     useProduction?: boolean;        // Whether to use Let's Encrypt production (default: false for staging) | ||||
|     renewThresholdDays?: number;    // Days before expiry to renew certificates (default: 30) | ||||
|     autoRenew?: boolean;            // Whether to automatically renew certificates (default: true) | ||||
|     certificateStore?: string;      // Directory to store certificates (default: ./certs) | ||||
|     skipConfiguredCerts?: boolean;  // Skip domains that already have certificates configured | ||||
|   }; | ||||
|   acme?: IAcmeOptions; | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -1,5 +1,14 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { IncomingMessage, ServerResponse } from 'http'; | ||||
| import { Port80HandlerEvents } from '../common/types.js'; | ||||
| import type { | ||||
|   IForwardConfig, | ||||
|   IDomainOptions, | ||||
|   ICertificateData, | ||||
|   ICertificateFailure, | ||||
|   ICertificateExpiring, | ||||
|   IAcmeOptions | ||||
| } from '../common/types.js'; | ||||
| // (fs and path I/O moved to CertProvisioner) | ||||
| // ACME HTTP-01 challenge handler storing tokens in memory (diskless) | ||||
| class DisklessHttp01Handler { | ||||
| @@ -45,24 +54,6 @@ export class ServerError extends Port80HandlerError { | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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 | ||||
| @@ -80,55 +71,8 @@ interface IDomainCertificate { | ||||
| /** | ||||
|  * Configuration options for the Port80Handler | ||||
|  */ | ||||
| interface IPort80HandlerOptions { | ||||
|   port?: number; | ||||
|   contactEmail?: string; | ||||
|   useProduction?: boolean; | ||||
|   httpsRedirectPort?: number; | ||||
|   enabled?: boolean; // Whether ACME is enabled at all | ||||
|   // (Persistence moved to CertProvisioner) | ||||
| } | ||||
| // Port80Handler options moved to common types | ||||
|  | ||||
| /** | ||||
|  * Certificate data that can be emitted via events or set from outside | ||||
|  */ | ||||
| export interface ICertificateData { | ||||
|   domain: string; | ||||
|   certificate: string; | ||||
|   privateKey: string; | ||||
|   expiryDate: Date; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Events emitted by the Port80Handler | ||||
|  */ | ||||
| export enum Port80HandlerEvents { | ||||
|   CERTIFICATE_ISSUED = 'certificate-issued', | ||||
|   CERTIFICATE_RENEWED = 'certificate-renewed', | ||||
|   CERTIFICATE_FAILED = 'certificate-failed', | ||||
|   CERTIFICATE_EXPIRING = 'certificate-expiring', | ||||
|   MANAGER_STARTED = 'manager-started', | ||||
|   MANAGER_STOPPED = 'manager-stopped', | ||||
|   REQUEST_FORWARDED = 'request-forwarded', | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Certificate failure payload type | ||||
|  */ | ||||
| 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 | ||||
| @@ -144,13 +88,13 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|   // Renewal scheduling is handled externally by SmartProxy | ||||
|   // (Removed internal renewal timer) | ||||
|   private isShuttingDown: boolean = false; | ||||
|   private options: Required<IPort80HandlerOptions>; | ||||
|   private options: Required<IAcmeOptions>; | ||||
|  | ||||
|   /** | ||||
|    * Creates a new Port80Handler | ||||
|    * @param options Configuration options | ||||
|    */ | ||||
|   constructor(options: IPort80HandlerOptions = {}) { | ||||
|   constructor(options: IAcmeOptions = {}) { | ||||
|     super(); | ||||
|     this.domainCertificates = new Map<string, IDomainCertificate>(); | ||||
|      | ||||
| @@ -160,7 +104,13 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|       contactEmail: options.contactEmail ?? 'admin@example.com', | ||||
|       useProduction: options.useProduction ?? false, // Safer default: staging | ||||
|       httpsRedirectPort: options.httpsRedirectPort ?? 443, | ||||
|       enabled: options.enabled ?? true // Enable by default | ||||
|       enabled: options.enabled ?? true, // Enable by default | ||||
|       certificateStore: options.certificateStore ?? './certs', | ||||
|       skipConfiguredCerts: options.skipConfiguredCerts ?? false, | ||||
|       renewThresholdDays: options.renewThresholdDays ?? 30, | ||||
|       renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24, | ||||
|       autoRenew: options.autoRenew ?? true, | ||||
|       domainForwards: options.domainForwards ?? [] | ||||
|     }; | ||||
|   } | ||||
|  | ||||
| @@ -810,7 +760,7 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|    * Gets configuration details | ||||
|    * @returns Current configuration | ||||
|    */ | ||||
|   public getConfig(): Required<IPort80HandlerOptions> { | ||||
|   public getConfig(): Required<IAcmeOptions> { | ||||
|     return { ...this.options }; | ||||
|   } | ||||
|    | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import type { IDomainConfig, ISmartProxyCertProvisionObject } from './classes.pp.interfaces.js'; | ||||
| import { Port80Handler, Port80HandlerEvents, type ICertificateData } from '../port80handler/classes.port80handler.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'; | ||||
|  | ||||
| /** | ||||
| @@ -56,11 +59,13 @@ export class CertProvisioner extends plugins.EventEmitter { | ||||
|    */ | ||||
|   public async start(): Promise<void> { | ||||
|     // Subscribe to Port80Handler certificate events | ||||
|     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, (data: ICertificateData) => { | ||||
|       this.emit('certificate', { ...data, source: 'http01', isRenewal: false }); | ||||
|     }); | ||||
|     this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, (data: ICertificateData) => { | ||||
|       this.emit('certificate', { ...data, source: 'http01', isRenewal: true }); | ||||
|     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 }); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // Apply external forwarding for ACME challenges (e.g. Synology) | ||||
|   | ||||
| @@ -21,6 +21,7 @@ export interface IDomainConfig { | ||||
| } | ||||
|  | ||||
| /** Port proxy settings including global allowed port ranges */ | ||||
| import type { IAcmeOptions } from '../common/types.js'; | ||||
| export interface IPortProxySettings { | ||||
|   fromPort: number; | ||||
|   toPort: number; | ||||
| @@ -83,31 +84,8 @@ export interface IPortProxySettings { | ||||
|   useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy | ||||
|   networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443) | ||||
|  | ||||
|   // Port80Handler configuration (replaces ACME configuration) | ||||
|   port80HandlerConfig?: { | ||||
|     enabled?: boolean; // Whether to enable automatic certificate management | ||||
|     port?: number; // Port to listen on for ACME challenges (default: 80)  | ||||
|     contactEmail?: string; // Email for Let's Encrypt account | ||||
|     useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging) | ||||
|     renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30) | ||||
|     autoRenew?: boolean; // Whether to automatically renew certificates (default: true) | ||||
|     certificateStore?: string; // Directory to store certificates (default: ./certs) | ||||
|     skipConfiguredCerts?: boolean; // Skip domains that already have certificates | ||||
|     httpsRedirectPort?: number; // Port to redirect HTTP requests to HTTPS (default: 443) | ||||
|     renewCheckIntervalHours?: number; // How often to check for renewals (default: 24) | ||||
|     // Domain-specific forwarding configurations | ||||
|     domainForwards?: Array<{ | ||||
|       domain: string; | ||||
|       forwardConfig?: { | ||||
|         ip: string; | ||||
|         port: number; | ||||
|       }; | ||||
|       acmeForwardConfig?: { | ||||
|         ip: string; | ||||
|         port: number; | ||||
|       }; | ||||
|     }>; | ||||
|   }; | ||||
|   // ACME configuration options for SmartProxy | ||||
|   acme?: IAcmeOptions; | ||||
|    | ||||
|   /** | ||||
|    * Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges, | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { NetworkProxy } from '../networkproxy/classes.np.networkproxy.js'; | ||||
| import { Port80Handler, Port80HandlerEvents, type ICertificateData } from '../port80handler/classes.port80handler.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 { IConnectionRecord, IPortProxySettings, IDomainConfig } from './classes.pp.interfaces.js'; | ||||
|  | ||||
| /** | ||||
| @@ -18,9 +21,11 @@ export class NetworkProxyBridge { | ||||
|   public setPort80Handler(handler: Port80Handler): void { | ||||
|     this.port80Handler = handler; | ||||
|      | ||||
|     // Register for certificate events | ||||
|     handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateEvent.bind(this)); | ||||
|     handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateEvent.bind(this)); | ||||
|     // Subscribe to certificate events | ||||
|     subscribeToPort80Handler(handler, { | ||||
|       onCertificateIssued: this.handleCertificateEvent.bind(this), | ||||
|       onCertificateRenewed: this.handleCertificateEvent.bind(this) | ||||
|     }); | ||||
|      | ||||
|     // If NetworkProxy is already initialized, connect it with Port80Handler | ||||
|     if (this.networkProxy) { | ||||
| @@ -284,7 +289,7 @@ export class NetworkProxyBridge { | ||||
|       ); | ||||
|  | ||||
|       // Log ACME-eligible domains | ||||
|       const acmeEnabled = !!this.settings.port80HandlerConfig?.enabled; | ||||
|       const acmeEnabled = !!this.settings.acme?.enabled; | ||||
|       if (acmeEnabled) { | ||||
|         const acmeEligibleDomains = proxyConfigs | ||||
|           .filter((config) => !config.hostName.includes('*')) // Exclude wildcards | ||||
| @@ -345,7 +350,7 @@ export class NetworkProxyBridge { | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     if (!this.settings.port80HandlerConfig?.enabled) { | ||||
|     if (!this.settings.acme?.enabled) { | ||||
|       console.log('Cannot request certificate - ACME is not enabled'); | ||||
|       return false; | ||||
|     } | ||||
|   | ||||
| @@ -10,9 +10,8 @@ import { PortRangeManager } from './classes.pp.portrangemanager.js'; | ||||
| import { ConnectionHandler } from './classes.pp.connectionhandler.js'; | ||||
| import { Port80Handler } from '../port80handler/classes.port80handler.js'; | ||||
| import { CertProvisioner } from './classes.pp.certprovisioner.js'; | ||||
| import type { ICertificateData } from '../port80handler/classes.port80handler.js'; | ||||
| import * as path from 'path'; | ||||
| import * as fs from 'fs'; | ||||
| import type { ICertificateData } from '../common/types.js'; | ||||
| import { buildPort80Handler } from '../common/acmeFactory.js'; | ||||
|  | ||||
| /** | ||||
|  * SmartProxy - Main class that coordinates all components | ||||
| @@ -67,13 +66,13 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, | ||||
|       extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, | ||||
|       networkProxyPort: settingsArg.networkProxyPort || 8443, | ||||
|       port80HandlerConfig: settingsArg.port80HandlerConfig || {}, | ||||
|       acme: settingsArg.acme || {}, | ||||
|       globalPortRanges: settingsArg.globalPortRanges || [], | ||||
|     }; | ||||
|      | ||||
|     // Set default port80HandlerConfig if not provided | ||||
|     if (!this.settings.port80HandlerConfig || Object.keys(this.settings.port80HandlerConfig).length === 0) { | ||||
|       this.settings.port80HandlerConfig = { | ||||
|     // Set default ACME options if not provided | ||||
|     if (!this.settings.acme || Object.keys(this.settings.acme).length === 0) { | ||||
|       this.settings.acme = { | ||||
|         enabled: false, | ||||
|         port: 80, | ||||
|         contactEmail: 'admin@example.com', | ||||
| @@ -83,7 +82,8 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|         certificateStore: './certs', | ||||
|         skipConfiguredCerts: false, | ||||
|         httpsRedirectPort: this.settings.fromPort, | ||||
|         renewCheckIntervalHours: 24 | ||||
|         renewCheckIntervalHours: 24, | ||||
|         domainForwards: [] | ||||
|       }; | ||||
|     } | ||||
|      | ||||
| @@ -122,40 +122,20 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|    * Initialize the Port80Handler for ACME certificate management | ||||
|    */ | ||||
|   private async initializePort80Handler(): Promise<void> { | ||||
|     const config = this.settings.port80HandlerConfig; | ||||
|      | ||||
|     if (!config || !config.enabled) { | ||||
|       console.log('Port80Handler is disabled in configuration'); | ||||
|     const config = this.settings.acme!; | ||||
|     if (!config.enabled) { | ||||
|       console.log('ACME is disabled in configuration'); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|       // Ensure the certificate store directory exists | ||||
|       if (config.certificateStore) { | ||||
|         const certStorePath = path.resolve(config.certificateStore); | ||||
|         if (!fs.existsSync(certStorePath)) { | ||||
|           fs.mkdirSync(certStorePath, { recursive: true }); | ||||
|           console.log(`Created certificate store directory: ${certStorePath}`); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Create Port80Handler with options from config | ||||
|       this.port80Handler = new Port80Handler({ | ||||
|         port: config.port, | ||||
|         contactEmail: config.contactEmail, | ||||
|         useProduction: config.useProduction, | ||||
|         httpsRedirectPort: config.httpsRedirectPort || this.settings.fromPort, | ||||
|         enabled: config.enabled, | ||||
|         certificateStore: config.certificateStore, | ||||
|         skipConfiguredCerts: config.skipConfiguredCerts | ||||
|       // Build and start the Port80Handler | ||||
|       this.port80Handler = buildPort80Handler({ | ||||
|         ...config, | ||||
|         httpsRedirectPort: config.httpsRedirectPort || this.settings.fromPort | ||||
|       }); | ||||
|        | ||||
|        | ||||
|        | ||||
|       // Share Port80Handler with NetworkProxyBridge | ||||
|       // Share Port80Handler with NetworkProxyBridge before start | ||||
|       this.networkProxyBridge.setPort80Handler(this.port80Handler); | ||||
|        | ||||
|       // Start Port80Handler | ||||
|       await this.port80Handler.start(); | ||||
|       console.log(`Port80Handler started on port ${config.port}`); | ||||
|     } catch (err) { | ||||
| @@ -177,20 +157,20 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     await this.initializePort80Handler(); | ||||
|     // Initialize CertProvisioner for unified certificate workflows | ||||
|     if (this.port80Handler) { | ||||
|       const acme = this.settings.acme!; | ||||
|       this.certProvisioner = new CertProvisioner( | ||||
|         this.settings.domainConfigs, | ||||
|         this.port80Handler, | ||||
|         this.networkProxyBridge, | ||||
|         this.settings.certProvider, | ||||
|         this.settings.port80HandlerConfig?.renewThresholdDays || 30, | ||||
|         this.settings.port80HandlerConfig?.renewCheckIntervalHours || 24, | ||||
|         this.settings.port80HandlerConfig?.autoRenew !== false, | ||||
|         // External ACME forwarding for specific domains | ||||
|         this.settings.port80HandlerConfig?.domainForwards?.map(f => ({ | ||||
|         acme.renewThresholdDays!, | ||||
|         acme.renewCheckIntervalHours!, | ||||
|         acme.autoRenew!, | ||||
|         acme.domainForwards?.map(f => ({ | ||||
|           domain: f.domain, | ||||
|           forwardConfig: f.forwardConfig, | ||||
|           acmeForwardConfig: f.acmeForwardConfig, | ||||
|           sslRedirect: false | ||||
|           sslRedirect: f.sslRedirect || false | ||||
|         })) || [] | ||||
|       ); | ||||
|       this.certProvisioner.on('certificate', (certData) => { | ||||
| @@ -405,7 +385,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     } | ||||
|      | ||||
|     // If Port80Handler is running, provision certificates per new domain | ||||
|     if (this.port80Handler && this.settings.port80HandlerConfig?.enabled) { | ||||
|     if (this.port80Handler && this.settings.acme?.enabled) { | ||||
|       for (const domainConfig of newDomainConfigs) { | ||||
|         for (const domain of domainConfig.domains) { | ||||
|           if (domain.includes('*')) continue; | ||||
| @@ -441,72 +421,6 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Updates the Port80Handler configuration | ||||
|    */ | ||||
|   public async updatePort80HandlerConfig(config: IPortProxySettings['port80HandlerConfig']): Promise<void> { | ||||
|     if (!config) return; | ||||
|      | ||||
|     console.log('Updating Port80Handler configuration'); | ||||
|      | ||||
|     // Update the settings | ||||
|     this.settings.port80HandlerConfig = { | ||||
|       ...this.settings.port80HandlerConfig, | ||||
|       ...config | ||||
|     }; | ||||
|      | ||||
|     // Check if we need to restart Port80Handler | ||||
|     let needsRestart = false; | ||||
|      | ||||
|     // Restart if enabled state changed | ||||
|     if (this.port80Handler && config.enabled === false) { | ||||
|       needsRestart = true; | ||||
|     } else if (!this.port80Handler && config.enabled === true) { | ||||
|       needsRestart = true; | ||||
|     } else if (this.port80Handler && ( | ||||
|       config.port !== undefined ||  | ||||
|       config.contactEmail !== undefined || | ||||
|       config.useProduction !== undefined || | ||||
|       config.renewThresholdDays !== undefined || | ||||
|       config.renewCheckIntervalHours !== undefined | ||||
|     )) { | ||||
|       // Restart if critical settings changed | ||||
|       needsRestart = true; | ||||
|     } | ||||
|      | ||||
|     if (needsRestart) { | ||||
|       // Stop if running | ||||
|       if (this.port80Handler) { | ||||
|         try { | ||||
|           await this.port80Handler.stop(); | ||||
|           this.port80Handler = null; | ||||
|           console.log('Stopped Port80Handler for configuration update'); | ||||
|         } catch (err) { | ||||
|           console.log(`Error stopping Port80Handler: ${err}`); | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       // Start with new config if enabled | ||||
|       if (this.settings.port80HandlerConfig.enabled) { | ||||
|         await this.initializePort80Handler(); | ||||
|         console.log('Restarted Port80Handler with new configuration'); | ||||
|       } | ||||
|     } else if (this.port80Handler) { | ||||
|       // Just update domain forwards if they changed | ||||
|       if (config.domainForwards) { | ||||
|         for (const forward of config.domainForwards) { | ||||
|           this.port80Handler.addDomain({ | ||||
|             domainName: forward.domain, | ||||
|             sslRedirect: true, | ||||
|             acmeMaintenance: true, | ||||
|             forward: forward.forwardConfig, | ||||
|             acmeForward: forward.acmeForwardConfig | ||||
|           }); | ||||
|         } | ||||
|         console.log('Updated domain forwards in Port80Handler'); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Perform scheduled renewals for managed domains | ||||
| @@ -514,7 +428,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|   private async performRenewals(): Promise<void> { | ||||
|     if (!this.port80Handler) return; | ||||
|     const statuses = this.port80Handler.getDomainCertificateStatus(); | ||||
|     const threshold = this.settings.port80HandlerConfig.renewThresholdDays ?? 30; | ||||
|     const threshold = this.settings.acme?.renewThresholdDays ?? 30; | ||||
|     const now = new Date(); | ||||
|     for (const [domain, status] of statuses.entries()) { | ||||
|       if (!status.certObtained || status.obtainingInProgress || !status.expiryDate) continue; | ||||
| @@ -621,7 +535,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       networkProxyConnections, | ||||
|       terminationStats, | ||||
|       acmeEnabled: !!this.port80Handler, | ||||
|       port80HandlerPort: this.port80Handler ? this.settings.port80HandlerConfig?.port : null | ||||
|       port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null | ||||
|     }; | ||||
|   } | ||||
|    | ||||
| @@ -672,7 +586,7 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|           status: 'valid', | ||||
|           expiryDate: expiryDate.toISOString(), | ||||
|           daysRemaining, | ||||
|           renewalNeeded: daysRemaining <= this.settings.port80HandlerConfig.renewThresholdDays | ||||
|           renewalNeeded: daysRemaining <= (this.settings.acme?.renewThresholdDays ?? 0) | ||||
|         }; | ||||
|       } else { | ||||
|         certificateStatus[domain] = { | ||||
| @@ -682,11 +596,12 @@ export class SmartProxy extends plugins.EventEmitter { | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     const acme = this.settings.acme!; | ||||
|     return { | ||||
|       enabled: true, | ||||
|       port: this.settings.port80HandlerConfig.port, | ||||
|       useProduction: this.settings.port80HandlerConfig.useProduction, | ||||
|       autoRenew: this.settings.port80HandlerConfig.autoRenew, | ||||
|       port: acme.port!, | ||||
|       useProduction: acme.useProduction!, | ||||
|       autoRenew: acme.autoRenew!, | ||||
|       certificates: certificateStatus | ||||
|     }; | ||||
|   } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user