diff --git a/changelog.md b/changelog.md index 0bf17cb..1f878b3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-05-02 - 9.0.0 - BREAKING CHANGE(acme) +Refactor ACME configuration and certificate provisioning by replacing legacy port80HandlerConfig with unified acme options and updating CertProvisioner event subscriptions + +- Remove deprecated port80HandlerConfig references and merge configuration into a single acme options schema +- Use buildPort80Handler factory for consistent Port80Handler instantiation +- Integrate subscribeToPort80Handler utility in CertProvisioner and NetworkProxyBridge for event management +- Update types in common modules and IPortProxySettings to reflect unified acme configurations +- Adjust documentation (readme.plan.md and code-level comments) to reflect the new refactored flow + ## 2025-05-02 - 8.0.0 - BREAKING CHANGE(certProvisioner) Refactor: Introduce unified CertProvisioner to centralize certificate provisioning and renewal; remove legacy ACME config from Port80Handler and update SmartProxy to delegate certificate lifecycle management. diff --git a/readme.plan.md b/readme.plan.md index 38ce935..77d1d55 100644 --- a/readme.plan.md +++ b/readme.plan.md @@ -1,47 +1,29 @@ -## Refactor: Introduce a Unified CertProvisioner for Certificate Lifecycle +# Project Simplification Plan -- [x] Ensure Port80Handler is challenge-only: - - Remove any internal scheduling and deprecated ACME flows (`getAcmeClient`, `processAuthorizations`, `handleAcmeChallenge`) from Port80Handler. - - Remove legacy ACME options (`renewThresholdDays`, `renewCheckIntervalHours`, `mongoDescriptor`, etc.) from `IPort80HandlerOptions`. - - Retain only methods for HTTP-01 challenge and direct renewals (`obtainCertificate`, `renewCertificate`, `getDomainCertificateStatus`). -- [x] Clean up deprecated `acme` configuration: - - Remove the `acme` property from `IPortProxySettings` and all legacy references in code. +This document outlines a roadmap to simplify and refactor the SmartProxy & NetworkProxy codebase for better maintainability, reduced duplication, and clearer configuration. -- [x] Implement `CertProvisioner` component: - - [x] Create class `ts/smartproxy/classes.pp.certprovisioner.ts`. - - [x] Constructor accepts: - * `domainConfigs: IDomainConfig[]` - * `port80Handler: Port80Handler` - * `networkProxyBridge: NetworkProxyBridge` - * optional `certProvider: (domain) => Promise` - * `renewThresholdDays`, `renewCheckIntervalHours`, `autoRenew` settings. - - Responsibilities: - * Initial provisioning: static vs HTTP-01. - * Subscribe to Port80Handler events (CERTIFICATE_ISSUED/RENEWED) and to static cert updates. - * Re-emit unified `'certificate'` events to SmartProxy. - * Central scheduling of renewals via `@push.rocks/taskbuffer`. +## Goals +- Eliminate duplicate code and shared types +- Unify certificate management flow across components +- Simplify configuration schemas and option handling +- Centralize plugin imports and module interfaces +- Strengthen type safety and linting +- Improve test coverage and CI integration -- [x] Refactor SmartProxy: - - [x] Remove existing scheduling / renewal logic. - - [x] Instantiate `CertProvisioner` in `start()`, delegate cert workflows entirely. - - [x] Forward CertProvisioner events to SmartProxy’s `'certificate'` listener. +## Plan +- [x] Extract all shared interfaces and types (e.g., certificate, proxy, domain configs) into a common `ts/common` module +- [x] Consolidate ACME/Port80Handler logic: + - [x] Merge standalone Port80Handler into a single certificate service + - [x] Remove duplicate ACME setup in SmartProxy and NetworkProxy +- [ ] Unify configuration options: + - [x] Merge `INetworkProxyOptions.acme`, `IPort80HandlerOptions`, and `port80HandlerConfig` into one schema + - [ ] Deprecate old option names and provide clear upgrade path +- [ ] Centralize plugin imports in `ts/plugins.ts` and update all modules to use it +- [ ] Remove legacy or unused code paths (e.g., old HTTP/2 fallback logic if obsolete) +- [ ] Enhance and expand test coverage: + - Add unit tests for certificate issuance, renewal, and error handling + - Add integration tests for HTTP challenge routing and request forwarding +- [ ] Update main README.md with architecture overview and configuration guide +- [ ] Review and prune external dependencies no longer needed -- [x] CertProvisioner lifecycle methods: - - [x] `start()`: provision all domains, start scheduler. - - [x] `stop()`: stop scheduler. - - [x] `requestCertificate(domain)`: on-demand provisioning. - -- [x] Handle static certificate auto-refresh: - - [x] In the renewal scheduler, for domains with static certs, re-call `certProvider(domain)` near expiry. - - [x] Apply returned cert via `networkProxyBridge.applyExternalCertificate()`. - -- [ ] Tests: - - Unit tests for `CertProvisioner`, mocking Port80Handler and `certProvider`: - * Validate initial provisioning and dynamic/static flows. - * Validate scheduling triggers correct renewals. - - Integration tests: - * Use actual in-memory Port80Handler with short intervals to verify renewals and event emission. - -- [ ] Documentation: - - Add code-level TS doc for `CertProvisioner` API (options, methods, events). - - Update root `README.md` and architecture diagrams to show `CertProvisioner` role. +Once these steps are complete, the project will be cleaner, easier to understand, and simpler to extend. \ No newline at end of file diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 4662021..79ba656 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -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.' } diff --git a/ts/common/acmeFactory.ts b/ts/common/acmeFactory.ts new file mode 100644 index 0000000..7b40147 --- /dev/null +++ b/ts/common/acmeFactory.ts @@ -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); +} \ No newline at end of file diff --git a/ts/common/eventUtils.ts b/ts/common/eventUtils.ts new file mode 100644 index 0000000..af86e3d --- /dev/null +++ b/ts/common/eventUtils.ts @@ -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); + } +} \ No newline at end of file diff --git a/ts/common/types.ts b/ts/common/types.ts new file mode 100644 index 0000000..4a3f70d --- /dev/null +++ b/ts/common/types.ts @@ -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 +} \ No newline at end of file diff --git a/ts/networkproxy/classes.np.certificatemanager.ts b/ts/networkproxy/classes.np.certificatemanager.ts index 100fe50..9181b45 100644 --- a/ts/networkproxy/classes.np.certificatemanager.ts +++ b/ts/networkproxy/classes.np.certificatemanager.ts @@ -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 diff --git a/ts/networkproxy/classes.np.types.ts b/ts/networkproxy/classes.np.types.ts index 9ffd56b..972e225 100644 --- a/ts/networkproxy/classes.np.types.ts +++ b/ts/networkproxy/classes.np.types.ts @@ -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; } /** diff --git a/ts/port80handler/classes.port80handler.ts b/ts/port80handler/classes.port80handler.ts index 21a600c..3f752ad 100644 --- a/ts/port80handler/classes.port80handler.ts +++ b/ts/port80handler/classes.port80handler.ts @@ -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; + private options: Required; /** * Creates a new Port80Handler * @param options Configuration options */ - constructor(options: IPort80HandlerOptions = {}) { + constructor(options: IAcmeOptions = {}) { super(); this.domainCertificates = new Map(); @@ -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 { + public getConfig(): Required { return { ...this.options }; } diff --git a/ts/smartproxy/classes.pp.certprovisioner.ts b/ts/smartproxy/classes.pp.certprovisioner.ts index 92b12d1..ba4f0fe 100644 --- a/ts/smartproxy/classes.pp.certprovisioner.ts +++ b/ts/smartproxy/classes.pp.certprovisioner.ts @@ -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 { // 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) diff --git a/ts/smartproxy/classes.pp.interfaces.ts b/ts/smartproxy/classes.pp.interfaces.ts index 176c483..3575f84 100644 --- a/ts/smartproxy/classes.pp.interfaces.ts +++ b/ts/smartproxy/classes.pp.interfaces.ts @@ -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, diff --git a/ts/smartproxy/classes.pp.networkproxybridge.ts b/ts/smartproxy/classes.pp.networkproxybridge.ts index 2c8b9d7..59c0514 100644 --- a/ts/smartproxy/classes.pp.networkproxybridge.ts +++ b/ts/smartproxy/classes.pp.networkproxybridge.ts @@ -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; } diff --git a/ts/smartproxy/classes.smartproxy.ts b/ts/smartproxy/classes.smartproxy.ts index 4bfb914..9e2964c 100644 --- a/ts/smartproxy/classes.smartproxy.ts +++ b/ts/smartproxy/classes.smartproxy.ts @@ -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 { - 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 { - 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 { 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 }; }