From 455b08b36ca496a182ce4f5e8899834a564a1b44 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Sun, 18 May 2025 16:30:23 +0000 Subject: [PATCH] BREAKING CHANGE(certificates): Remove legacy certificate modules and Port80Handler; update documentation and route configurations to use SmartCertManager for certificate management. --- changelog.md | 8 + docs/certificate-management.md | 217 ++++++ readme.md | 18 +- readme.plan.md | 57 +- ts/00_commitinfo_data.ts | 2 +- ts/certificate/acme/acme-factory.ts | 48 -- ts/certificate/acme/challenge-handler.ts | 110 --- ts/certificate/acme/index.ts | 3 - ts/certificate/events/certificate-events.ts | 36 - ts/certificate/index.ts | 75 -- ts/certificate/models/certificate-types.ts | 109 --- ts/certificate/providers/cert-provisioner.ts | 519 ------------- ts/certificate/providers/index.ts | 3 - ts/certificate/storage/file-storage.ts | 234 ------ ts/certificate/storage/index.ts | 3 - ts/certificate/utils/certificate-helpers.ts | 50 -- ts/common/eventUtils.ts | 4 +- ts/core/models/common-types.ts | 2 +- ts/core/utils/event-utils.ts | 33 +- ts/http/models/http-types.ts | 12 +- ts/http/port80/acme-interfaces.ts | 169 ---- ts/http/port80/challenge-responder.ts | 246 ------ ts/http/port80/index.ts | 13 - ts/http/port80/port80-handler.ts | 728 ------------------ ts/index.ts | 25 +- ts/proxies/index.ts | 11 +- .../network-proxy/certificate-manager.ts | 595 ++++---------- ts/proxies/network-proxy/models/types.ts | 16 +- ts/proxies/network-proxy/network-proxy.ts | 29 +- ts/proxies/smart-proxy/models/index.ts | 3 +- ts/proxies/smart-proxy/models/interfaces.ts | 15 +- ts/proxies/smart-proxy/models/route-types.ts | 2 +- ts/proxies/smart-proxy/smart-proxy.ts | 3 +- 33 files changed, 494 insertions(+), 2904 deletions(-) create mode 100644 docs/certificate-management.md delete mode 100644 ts/certificate/acme/acme-factory.ts delete mode 100644 ts/certificate/acme/challenge-handler.ts delete mode 100644 ts/certificate/acme/index.ts delete mode 100644 ts/certificate/events/certificate-events.ts delete mode 100644 ts/certificate/index.ts delete mode 100644 ts/certificate/models/certificate-types.ts delete mode 100644 ts/certificate/providers/cert-provisioner.ts delete mode 100644 ts/certificate/providers/index.ts delete mode 100644 ts/certificate/storage/file-storage.ts delete mode 100644 ts/certificate/storage/index.ts delete mode 100644 ts/certificate/utils/certificate-helpers.ts delete mode 100644 ts/http/port80/acme-interfaces.ts delete mode 100644 ts/http/port80/challenge-responder.ts delete mode 100644 ts/http/port80/index.ts delete mode 100644 ts/http/port80/port80-handler.ts diff --git a/changelog.md b/changelog.md index 7559be1..bbf0c0a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-05-18 - 19.0.0 - BREAKING CHANGE(certificates) +Remove legacy certificate modules and Port80Handler; update documentation and route configurations to use SmartCertManager for certificate management. + +- Removed deprecated files under ts/certificate (acme, events, storage, providers) and ts/http/port80. +- Updated readme.md and docs/certificate-management.md to reflect new SmartCertManager integration and removal of Port80Handler. +- Updated route types and models to remove legacy certificate types and references to Port80Handler. +- Bumped major version to reflect breaking changes in certificate management. + ## 2025-05-18 - 18.2.0 - feat(smartproxy/certificate) Integrate HTTP-01 challenge handler into ACME certificate provisioning workflow diff --git a/docs/certificate-management.md b/docs/certificate-management.md new file mode 100644 index 0000000..dc7dd3b --- /dev/null +++ b/docs/certificate-management.md @@ -0,0 +1,217 @@ +# Certificate Management in SmartProxy v18+ + +## Overview + +SmartProxy v18+ introduces a simplified certificate management system using the new `SmartCertManager` class. This replaces the previous `Port80Handler` and multiple certificate-related modules with a unified, route-based approach. + +## Key Changes from Previous Versions + +- **No backward compatibility**: This is a clean break from the legacy certificate system +- **No separate Port80Handler**: ACME challenges are now handled as regular SmartProxy routes +- **Unified route-based configuration**: Certificates are configured directly in route definitions +- **Direct integration with @push.rocks/smartacme**: Leverages SmartAcme's built-in capabilities + +## Configuration + +### Route-Level Certificate Configuration + +Certificates are now configured at the route level using the `tls` property: + +```typescript +const route: IRouteConfig = { + name: 'secure-website', + match: { + ports: 443, + domains: ['example.com', 'www.example.com'] + }, + action: { + type: 'forward', + target: { host: 'localhost', port: 8080 }, + tls: { + mode: 'terminate', + certificate: 'auto', // Use ACME (Let's Encrypt) + acme: { + email: 'admin@example.com', + useProduction: true, + renewBeforeDays: 30 + } + } + } +}; +``` + +### Static Certificate Configuration + +For manually managed certificates: + +```typescript +const route: IRouteConfig = { + name: 'api-endpoint', + match: { + ports: 443, + domains: 'api.example.com' + }, + action: { + type: 'forward', + target: { host: 'localhost', port: 9000 }, + tls: { + mode: 'terminate', + certificate: { + certFile: './certs/api.crt', + keyFile: './certs/api.key', + ca: '...' // Optional CA chain + } + } + } +}; +``` + +## TLS Modes + +SmartProxy supports three TLS modes: + +1. **terminate**: Decrypt TLS at the proxy and forward plain HTTP +2. **passthrough**: Pass encrypted TLS traffic directly to the backend +3. **terminate-and-reencrypt**: Decrypt at proxy, then re-encrypt to backend + +## Certificate Storage + +Certificates are stored in the `./certs` directory by default: + +``` +./certs/ +├── route-name/ +│ ├── cert.pem +│ ├── key.pem +│ ├── ca.pem (if available) +│ └── meta.json +``` + +## ACME Integration + +### How It Works + +1. SmartProxy creates a high-priority route for ACME challenges +2. When ACME server makes requests to `/.well-known/acme-challenge/*`, SmartProxy handles them automatically +3. Certificates are obtained and stored locally +4. Automatic renewal checks every 12 hours + +### Configuration Options + +```typescript +export interface IRouteAcme { + email: string; // Contact email for ACME account + useProduction?: boolean; // Use production servers (default: false) + challengePort?: number; // Port for HTTP-01 challenges (default: 80) + renewBeforeDays?: number; // Days before expiry to renew (default: 30) +} +``` + +## Advanced Usage + +### Manual Certificate Operations + +```typescript +// Get certificate status +const status = proxy.getCertificateStatus('route-name'); +console.log(status); +// { +// domain: 'example.com', +// status: 'valid', +// source: 'acme', +// expiryDate: Date, +// issueDate: Date +// } + +// Force certificate renewal +await proxy.renewCertificate('route-name'); + +// Manually provision a certificate +await proxy.provisionCertificate('route-name'); +``` + +### Events + +SmartProxy emits certificate-related events: + +```typescript +proxy.on('certificate:issued', (event) => { + console.log(`New certificate for ${event.domain}`); +}); + +proxy.on('certificate:renewed', (event) => { + console.log(`Certificate renewed for ${event.domain}`); +}); + +proxy.on('certificate:expiring', (event) => { + console.log(`Certificate expiring soon for ${event.domain}`); +}); +``` + +## Migration from Previous Versions + +### Before (v17 and earlier) + +```typescript +// Old approach with Port80Handler +const smartproxy = new SmartProxy({ + port: 443, + acme: { + enabled: true, + accountEmail: 'admin@example.com', + // ... other ACME options + } +}); + +// Certificate provisioning was automatic or via certProvisionFunction +``` + +### After (v18+) + +```typescript +// New approach with route-based configuration +const smartproxy = new SmartProxy({ + routes: [{ + match: { ports: 443, domains: 'example.com' }, + action: { + type: 'forward', + target: { host: 'localhost', port: 8080 }, + tls: { + mode: 'terminate', + certificate: 'auto', + acme: { + email: 'admin@example.com', + useProduction: true + } + } + } + }] +}); +``` + +## Troubleshooting + +### Common Issues + +1. **Certificate not provisioning**: Ensure port 80 is accessible for ACME challenges +2. **ACME rate limits**: Use staging environment for testing +3. **Permission errors**: Ensure the certificate directory is writable + +### Debug Mode + +Enable detailed logging to troubleshoot certificate issues: + +```typescript +const proxy = new SmartProxy({ + enableDetailedLogging: true, + // ... other options +}); +``` + +## Best Practices + +1. **Always test with staging ACME servers first** +2. **Set up monitoring for certificate expiration** +3. **Use meaningful route names for easier certificate management** +4. **Store static certificates securely with appropriate permissions** +5. **Implement certificate status monitoring in production** \ No newline at end of file diff --git a/readme.md b/readme.md index 89f42d5..7d2479d 100644 --- a/readme.md +++ b/readme.md @@ -21,10 +21,10 @@ SmartProxy has been restructured using a modern, modular architecture with a uni │ ├── /models # Data models and interfaces │ ├── /utils # Shared utilities (IP validation, logging, etc.) │ └── /events # Common event definitions -├── /certificate # Certificate management -│ ├── /acme # ACME-specific functionality -│ ├── /providers # Certificate providers (static, ACME) -│ └── /storage # Certificate storage mechanisms +├── /certificate # Certificate management (deprecated in v18+) +│ ├── /acme # Moved to SmartCertManager +│ ├── /providers # Now integrated in route configuration +│ └── /storage # Now uses CertStore ├── /forwarding # Forwarding system │ ├── /handlers # Various forwarding handlers │ │ ├── base-handler.ts # Abstract base handler @@ -37,6 +37,8 @@ SmartProxy has been restructured using a modern, modular architecture with a uni │ │ ├── /models # SmartProxy-specific interfaces │ │ │ ├── route-types.ts # Route-based configuration types │ │ │ └── interfaces.ts # SmartProxy interfaces +│ │ ├── certificate-manager.ts # SmartCertManager (new in v18+) +│ │ ├── cert-store.ts # Certificate file storage │ │ ├── route-helpers.ts # Helper functions for creating routes │ │ ├── route-manager.ts # Route management system │ │ ├── smart-proxy.ts # Main SmartProxy class @@ -47,7 +49,7 @@ SmartProxy has been restructured using a modern, modular architecture with a uni │ ├── /sni # SNI handling components │ └── /alerts # TLS alerts system └── /http # HTTP-specific functionality - ├── /port80 # Port80Handler components + ├── /port80 # Port80Handler (removed in v18+) ├── /router # HTTP routing system └── /redirects # Redirect handlers ``` @@ -1411,6 +1413,12 @@ NetworkProxy now supports full route-based configuration including: - `useIPSets` (boolean, default true) - `qos`, `netProxyIntegration` (objects) +## Documentation + +- [Certificate Management](docs/certificate-management.md) - Detailed guide on certificate provisioning and ACME integration +- [Port Handling](docs/porthandling.md) - Dynamic port management and runtime configuration +- [NFTables Integration](docs/nftables-integration.md) - High-performance kernel-level forwarding + ## Troubleshooting ### SmartProxy diff --git a/readme.plan.md b/readme.plan.md index 8158964..193ad3f 100644 --- a/readme.plan.md +++ b/readme.plan.md @@ -1,5 +1,22 @@ # ACME/Certificate Simplification Plan for SmartProxy +## Current Status: Implementation in Progress + +### Completed Tasks: +- ✅ SmartCertManager class created +- ✅ CertStore class for file-based certificate storage +- ✅ Route types updated with new TLS/ACME interfaces +- ✅ Static route handler added to route-connection-handler.ts +- ✅ SmartProxy class updated to use SmartCertManager +- ✅ NetworkProxyBridge simplified by removing certificate logic +- ✅ HTTP index.ts updated to remove port80 exports +- ✅ Basic tests created for new certificate functionality +- ✅ SmartAcme integration completed using built-in MemoryCertManager + +### Remaining Tasks: +- ❌ Remove old certificate module and port80 directory +- ❌ Update documentation with new configuration format + ## Command to reread CLAUDE.md `reread /home/philkunz/.claude/CLAUDE.md` @@ -71,14 +88,13 @@ ts/proxies/smart-proxy/ ### Phase 1: Create SmartCertManager -#### 1.1 Create certificate-manager.ts +#### 1.1 Create certificate-manager.ts ✅ COMPLETED ```typescript // ts/proxies/smart-proxy/certificate-manager.ts import * as plugins from '../../plugins.js'; import { NetworkProxy } from '../network-proxy/index.js'; import type { IRouteConfig, IRouteTls } from './models/route-types.js'; import { CertStore } from './cert-store.js'; -import { AcmeClient } from './acme-client.js'; export interface ICertStatus { domain: string; @@ -578,7 +594,7 @@ class InMemoryCertManager implements plugins.smartacme.CertManager { } ``` -#### 1.2 Create cert-store.ts +#### 1.2 Create cert-store.ts ✅ COMPLETED ```typescript // ts/proxies/smart-proxy/cert-store.ts import * as plugins from '../../plugins.js'; @@ -675,7 +691,7 @@ export class CertStore { ### Phase 2: Update Route Types and Handler -#### 2.1 Update route-types.ts +#### 2.1 Update route-types.ts ✅ COMPLETED ```typescript // Add to ts/proxies/smart-proxy/models/route-types.ts @@ -742,7 +758,7 @@ export interface IRouteTls { } ``` -#### 2.2 Add Static Route Handler +#### 2.2 Add Static Route Handler ✅ COMPLETED ```typescript // Add to ts/proxies/smart-proxy/route-connection-handler.ts @@ -839,7 +855,7 @@ function getStatusText(status: number): string { ### Phase 3: SmartProxy Integration -#### 3.1 Update SmartProxy class +#### 3.1 Update SmartProxy class ✅ COMPLETED ```typescript // Changes to ts/proxies/smart-proxy/smart-proxy.ts @@ -1017,7 +1033,7 @@ export class SmartProxy extends plugins.EventEmitter { } ``` -#### 3.2 Simplify NetworkProxyBridge +#### 3.2 Simplify NetworkProxyBridge ✅ COMPLETED ```typescript // Simplified ts/proxies/smart-proxy/network-proxy-bridge.ts @@ -1323,7 +1339,7 @@ Certificates are stored in the `./certs` directory by default: ### Phase 5: Update HTTP Module -#### 5.1 Update http/index.ts +#### 5.1 Update http/index.ts ✅ COMPLETED ```typescript // ts/http/index.ts /** @@ -1388,25 +1404,26 @@ The simplification leverages SmartProxy's existing capabilities rather than rein ## Implementation Sequence -1. **Day 1: Core Implementation** +1. **Day 1: Core Implementation** ✅ COMPLETED - Create SmartCertManager class - - Create CertStore and AcmeClient + - Create CertStore - Update route types + - Integrated with SmartAcme's built-in handlers -2. **Day 2: Integration** +2. **Day 2: Integration** ✅ COMPLETED - Update SmartProxy to use SmartCertManager - Simplify NetworkProxyBridge - - Remove old certificate system + - Update HTTP index.ts -3. **Day 3: Testing** - - Create new tests using new format only - - No migration testing needed - - Test all new functionality +3. **Day 3: Testing** ✅ COMPLETED + - Created test.smartacme-integration.ts + - Verified SmartAcme handler access + - Verified certificate manager creation -4. **Day 4: Documentation & Cleanup** - - Update all documentation - - Clean up old files - - Final testing and validation +4. **Day 4: Documentation & Cleanup** 🔄 IN PROGRESS + - ❌ Update all documentation + - ❌ Clean up old files (certificate/ and port80/) + - ❌ Final testing and validation ## Risk Mitigation diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 0f292b3..dbfd6f1 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: '18.2.0', + version: '19.0.0', description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.' } diff --git a/ts/certificate/acme/acme-factory.ts b/ts/certificate/acme/acme-factory.ts deleted file mode 100644 index 3ebb58e..0000000 --- a/ts/certificate/acme/acme-factory.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import type { IAcmeOptions } from '../models/certificate-types.js'; -import { ensureCertificateDirectory } from '../utils/certificate-helpers.js'; -// We'll need to update this import when we move the Port80Handler -import { Port80Handler } from '../../http/port80/port80-handler.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) { - ensureCertificateDirectory(options.certificateStore); - console.log(`Ensured certificate store directory: ${options.certificateStore}`); - } - return new Port80Handler(options); -} - -/** - * Creates default ACME options with sensible defaults - * @param email Account email for ACME provider - * @param certificateStore Path to store certificates - * @param useProduction Whether to use production ACME servers - * @returns Configured ACME options - */ -export function createDefaultAcmeOptions( - email: string, - certificateStore: string, - useProduction: boolean = false -): IAcmeOptions { - return { - accountEmail: email, - enabled: true, - port: 80, - useProduction, - httpsRedirectPort: 443, - renewThresholdDays: 30, - renewCheckIntervalHours: 24, - autoRenew: true, - certificateStore, - skipConfiguredCerts: false - }; -} \ No newline at end of file diff --git a/ts/certificate/acme/challenge-handler.ts b/ts/certificate/acme/challenge-handler.ts deleted file mode 100644 index 751d710..0000000 --- a/ts/certificate/acme/challenge-handler.ts +++ /dev/null @@ -1,110 +0,0 @@ -import * as plugins from '../../plugins.js'; -import type { IAcmeOptions, ICertificateData } from '../models/certificate-types.js'; -import { CertificateEvents } from '../events/certificate-events.js'; - -/** - * Manages ACME challenges and certificate validation - */ -export class AcmeChallengeHandler extends plugins.EventEmitter { - private options: IAcmeOptions; - private client: any; // ACME client from plugins - private pendingChallenges: Map; - - /** - * Creates a new ACME challenge handler - * @param options ACME configuration options - */ - constructor(options: IAcmeOptions) { - super(); - this.options = options; - this.pendingChallenges = new Map(); - - // Initialize ACME client if needed - // This is just a placeholder implementation since we don't use the actual - // client directly in this implementation - it's handled by Port80Handler - this.client = null; - console.log('Created challenge handler with options:', - options.accountEmail, - options.useProduction ? 'production' : 'staging' - ); - } - - /** - * Gets or creates the ACME account key - */ - private getAccountKey(): Buffer { - // Implementation details would depend on plugin requirements - // This is a simplified version - if (!this.options.certificateStore) { - throw new Error('Certificate store is required for ACME challenges'); - } - - // This is just a placeholder - actual implementation would check for - // existing account key and create one if needed - return Buffer.from('account-key-placeholder'); - } - - /** - * Validates a domain using HTTP-01 challenge - * @param domain Domain to validate - * @param challengeToken ACME challenge token - * @param keyAuthorization Key authorization for the challenge - */ - public async handleHttpChallenge( - domain: string, - challengeToken: string, - keyAuthorization: string - ): Promise { - // Store challenge for response - this.pendingChallenges.set(challengeToken, keyAuthorization); - - try { - // Wait for challenge validation - this would normally be handled by the ACME client - await new Promise(resolve => setTimeout(resolve, 1000)); - this.emit(CertificateEvents.CERTIFICATE_ISSUED, { - domain, - success: true - }); - } catch (error) { - this.emit(CertificateEvents.CERTIFICATE_FAILED, { - domain, - error: error instanceof Error ? error.message : String(error), - isRenewal: false - }); - throw error; - } finally { - // Clean up the challenge - this.pendingChallenges.delete(challengeToken); - } - } - - /** - * Responds to an HTTP-01 challenge request - * @param token Challenge token from the request path - * @returns The key authorization if found - */ - public getChallengeResponse(token: string): string | null { - return this.pendingChallenges.get(token) || null; - } - - /** - * Checks if a request path is an ACME challenge - * @param path Request path - * @returns True if this is an ACME challenge request - */ - public isAcmeChallenge(path: string): boolean { - return path.startsWith('/.well-known/acme-challenge/'); - } - - /** - * Extracts the challenge token from an ACME challenge path - * @param path Request path - * @returns The challenge token if valid - */ - public extractChallengeToken(path: string): string | null { - if (!this.isAcmeChallenge(path)) return null; - - const parts = path.split('/'); - return parts[parts.length - 1] || null; - } -} \ No newline at end of file diff --git a/ts/certificate/acme/index.ts b/ts/certificate/acme/index.ts deleted file mode 100644 index 78448d5..0000000 --- a/ts/certificate/acme/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** - * ACME certificate provisioning - */ diff --git a/ts/certificate/events/certificate-events.ts b/ts/certificate/events/certificate-events.ts deleted file mode 100644 index 8269ac2..0000000 --- a/ts/certificate/events/certificate-events.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Certificate-related events emitted by certificate management components - */ -export enum CertificateEvents { - CERTIFICATE_ISSUED = 'certificate-issued', - CERTIFICATE_RENEWED = 'certificate-renewed', - CERTIFICATE_FAILED = 'certificate-failed', - CERTIFICATE_EXPIRING = 'certificate-expiring', - CERTIFICATE_APPLIED = 'certificate-applied', - // Events moved from Port80Handler for compatibility - MANAGER_STARTED = 'manager-started', - MANAGER_STOPPED = 'manager-stopped', -} - -/** - * Port80Handler-specific events including certificate-related ones - * @deprecated Use CertificateEvents and HttpEvents instead - */ -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 provider events - */ -export enum CertProvisionerEvents { - CERTIFICATE_ISSUED = 'certificate', - CERTIFICATE_RENEWED = 'certificate', - CERTIFICATE_FAILED = 'certificate-failed' -} \ No newline at end of file diff --git a/ts/certificate/index.ts b/ts/certificate/index.ts deleted file mode 100644 index 26f6289..0000000 --- a/ts/certificate/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Certificate management module for SmartProxy - * Provides certificate provisioning, storage, and management capabilities - */ - -// Certificate types and models -export * from './models/certificate-types.js'; - -// Certificate events -export * from './events/certificate-events.js'; - -// Certificate providers -export * from './providers/cert-provisioner.js'; - -// ACME related exports -export * from './acme/acme-factory.js'; -export * from './acme/challenge-handler.js'; - -// Certificate utilities -export * from './utils/certificate-helpers.js'; - -// Certificate storage -export * from './storage/file-storage.js'; - -// Convenience function to create a certificate provisioner with common settings -import { CertProvisioner } from './providers/cert-provisioner.js'; -import type { TCertProvisionObject } from './providers/cert-provisioner.js'; -import { buildPort80Handler } from './acme/acme-factory.js'; -import type { IAcmeOptions, IRouteForwardConfig } from './models/certificate-types.js'; -import type { IRouteConfig } from '../proxies/smart-proxy/models/route-types.js'; - -/** - * Interface for NetworkProxyBridge used by CertProvisioner - */ -interface ICertNetworkProxyBridge { - applyExternalCertificate(certData: any): void; -} - -/** - * Creates a complete certificate provisioning system with default settings - * @param routeConfigs Route configurations that may need certificates - * @param acmeOptions ACME options for certificate provisioning - * @param networkProxyBridge Bridge to apply certificates to network proxy - * @param certProvider Optional custom certificate provider - * @returns Configured CertProvisioner - */ -export function createCertificateProvisioner( - routeConfigs: IRouteConfig[], - acmeOptions: IAcmeOptions, - networkProxyBridge: ICertNetworkProxyBridge, - certProvider?: (domain: string) => Promise -): CertProvisioner { - // Build the Port80Handler for ACME challenges - const port80Handler = buildPort80Handler(acmeOptions); - - // Extract ACME-specific configuration - const { - renewThresholdDays = 30, - renewCheckIntervalHours = 24, - autoRenew = true, - routeForwards = [] - } = acmeOptions; - - // Create and return the certificate provisioner - return new CertProvisioner( - routeConfigs, - port80Handler, - networkProxyBridge, - certProvider, - renewThresholdDays, - renewCheckIntervalHours, - autoRenew, - routeForwards - ); -} diff --git a/ts/certificate/models/certificate-types.ts b/ts/certificate/models/certificate-types.ts deleted file mode 100644 index a3d9f11..0000000 --- a/ts/certificate/models/certificate-types.ts +++ /dev/null @@ -1,109 +0,0 @@ -import * as plugins from '../../plugins.js'; -import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js'; - -/** - * Certificate data structure containing all necessary information - * about a certificate - */ -export interface ICertificateData { - domain: string; - certificate: string; - privateKey: string; - expiryDate: Date; - // Optional source and renewal information for event emissions - source?: 'static' | 'http01' | 'dns01'; - isRenewal?: boolean; - // Reference to the route that requested this certificate (if available) - routeReference?: { - routeId?: string; - routeName?: string; - }; -} - -/** - * Certificates pair (private and public keys) - */ -export interface ICertificates { - privateKey: string; - publicKey: string; -} - -/** - * Certificate failure payload type - */ -export interface ICertificateFailure { - domain: string; - error: string; - isRenewal: boolean; - routeReference?: { - routeId?: string; - routeName?: string; - }; -} - -/** - * Certificate expiry payload type - */ -export interface ICertificateExpiring { - domain: string; - expiryDate: Date; - daysRemaining: number; - routeReference?: { - routeId?: string; - routeName?: string; - }; -} - -/** - * Route-specific forwarding configuration for ACME challenges - */ -export interface IRouteForwardConfig { - domain: string; - target: { - host: string; - port: number; - }; - sslRedirect?: boolean; -} - -/** - * Domain configuration options for Port80Handler - * - * This is used internally by the Port80Handler to manage domains - * but will eventually be replaced with route-based 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?: { - ip: string; - port: number; - }; // forwards all http requests to that target - acmeForward?: { - ip: string; - port: number; - }; // forwards letsencrypt requests to this config - routeReference?: { - routeId?: string; - routeName?: string; - }; -} - -/** - * Unified ACME configuration options used across proxies and handlers - */ -export interface IAcmeOptions { - accountEmail?: string; // Email for Let's Encrypt account - enabled?: boolean; // Whether ACME is enabled - port?: number; // Port to listen on for ACME challenges (default: 80) - 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 - routeForwards?: IRouteForwardConfig[]; // Route-specific forwarding configs -} - diff --git a/ts/certificate/providers/cert-provisioner.ts b/ts/certificate/providers/cert-provisioner.ts deleted file mode 100644 index 26f1683..0000000 --- a/ts/certificate/providers/cert-provisioner.ts +++ /dev/null @@ -1,519 +0,0 @@ -import * as plugins from '../../plugins.js'; -import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js'; -import type { ICertificateData, IRouteForwardConfig, IDomainOptions } from '../models/certificate-types.js'; -import { Port80HandlerEvents, CertProvisionerEvents } from '../events/certificate-events.js'; -import { Port80Handler } from '../../http/port80/port80-handler.js'; - -// Interface for NetworkProxyBridge -interface INetworkProxyBridge { - applyExternalCertificate(certData: ICertificateData): void; -} - -/** - * Type for static certificate provisioning - */ -export type TCertProvisionObject = plugins.tsclass.network.ICert | 'http01' | 'dns01'; - -/** - * Interface for routes that need certificates - */ -interface ICertRoute { - domain: string; - route: IRouteConfig; - tlsMode: 'terminate' | 'terminate-and-reencrypt'; -} - -/** - * CertProvisioner manages certificate provisioning and renewal workflows, - * unifying static certificates and HTTP-01 challenges via Port80Handler. - * - * This class directly works with route configurations instead of converting to domain configs. - */ -export class CertProvisioner extends plugins.EventEmitter { - private routeConfigs: IRouteConfig[]; - private certRoutes: ICertRoute[] = []; - private port80Handler: Port80Handler; - private networkProxyBridge: INetworkProxyBridge; - private certProvisionFunction?: (domain: string) => Promise; - private routeForwards: IRouteForwardConfig[]; - private renewThresholdDays: number; - private renewCheckIntervalHours: number; - private autoRenew: boolean; - private renewManager?: plugins.taskbuffer.TaskManager; - // Track provisioning type per domain - private provisionMap: Map; - - /** - * Extract routes that need certificates - * @param routes Route configurations - */ - private extractCertificateRoutesFromRoutes(routes: IRouteConfig[]): ICertRoute[] { - const certRoutes: ICertRoute[] = []; - - // Process all HTTPS routes that need certificates - for (const route of routes) { - // Only process routes with TLS termination that need certificates - if (route.action.type === 'forward' && - route.action.tls && - (route.action.tls.mode === 'terminate' || route.action.tls.mode === 'terminate-and-reencrypt') && - route.match.domains) { - - // Extract domains from the route - const domains = Array.isArray(route.match.domains) - ? route.match.domains - : [route.match.domains]; - - // For each domain in the route, create a certRoute entry - for (const domain of domains) { - // Skip wildcard domains that can't use ACME unless we have a certProvider - if (domain.includes('*') && (!this.certProvisionFunction || this.certProvisionFunction.length === 0)) { - console.warn(`Skipping wildcard domain that requires a certProvisionFunction: ${domain}`); - continue; - } - - certRoutes.push({ - domain, - route, - tlsMode: route.action.tls.mode - }); - } - } - } - - return certRoutes; - } - - /** - * Constructor for CertProvisioner - * - * @param routeConfigs Array of route configurations - * @param port80Handler HTTP-01 challenge handler instance - * @param networkProxyBridge Bridge for applying external certificates - * @param certProvider Optional callback returning a static cert or 'http01' - * @param renewThresholdDays Days before expiry to trigger renewals - * @param renewCheckIntervalHours Interval in hours to check for renewals - * @param autoRenew Whether to automatically schedule renewals - * @param routeForwards Route-specific forwarding configs for ACME challenges - */ - constructor( - routeConfigs: IRouteConfig[], - port80Handler: Port80Handler, - networkProxyBridge: INetworkProxyBridge, - certProvider?: (domain: string) => Promise, - renewThresholdDays: number = 30, - renewCheckIntervalHours: number = 24, - autoRenew: boolean = true, - routeForwards: IRouteForwardConfig[] = [] - ) { - super(); - this.routeConfigs = routeConfigs; - this.port80Handler = port80Handler; - this.networkProxyBridge = networkProxyBridge; - this.certProvisionFunction = certProvider; - this.renewThresholdDays = renewThresholdDays; - this.renewCheckIntervalHours = renewCheckIntervalHours; - this.autoRenew = autoRenew; - this.provisionMap = new Map(); - this.routeForwards = routeForwards; - - // Extract certificate routes during instantiation - this.certRoutes = this.extractCertificateRoutesFromRoutes(routeConfigs); - } - - /** - * Start initial provisioning and schedule renewals. - */ - public async start(): Promise { - // Subscribe to Port80Handler certificate events - this.setupEventSubscriptions(); - - // Apply route forwarding for ACME challenges - this.setupForwardingConfigs(); - - // Initial provisioning for all domains in routes - await this.provisionAllCertificates(); - - // Schedule renewals if enabled - if (this.autoRenew) { - this.scheduleRenewals(); - } - } - - /** - * Set up event subscriptions for certificate events - */ - private setupEventSubscriptions(): void { - this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, (data: ICertificateData) => { - // Add route reference if we have it - const routeRef = this.findRouteForDomain(data.domain); - const enhancedData: ICertificateData = { - ...data, - source: 'http01', - isRenewal: false, - routeReference: routeRef ? { - routeId: routeRef.route.name, - routeName: routeRef.route.name - } : undefined - }; - - this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, enhancedData); - }); - - this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, (data: ICertificateData) => { - // Add route reference if we have it - const routeRef = this.findRouteForDomain(data.domain); - const enhancedData: ICertificateData = { - ...data, - source: 'http01', - isRenewal: true, - routeReference: routeRef ? { - routeId: routeRef.route.name, - routeName: routeRef.route.name - } : undefined - }; - - this.emit(CertProvisionerEvents.CERTIFICATE_RENEWED, enhancedData); - }); - - this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, (error) => { - this.emit(CertProvisionerEvents.CERTIFICATE_FAILED, error); - }); - } - - /** - * Find a route for a given domain - */ - private findRouteForDomain(domain: string): ICertRoute | undefined { - return this.certRoutes.find(certRoute => certRoute.domain === domain); - } - - /** - * Set up forwarding configurations for the Port80Handler - */ - private setupForwardingConfigs(): void { - for (const config of this.routeForwards) { - const domainOptions: IDomainOptions = { - domainName: config.domain, - sslRedirect: config.sslRedirect || false, - acmeMaintenance: false, - forward: config.target ? { - ip: config.target.host, - port: config.target.port - } : undefined - }; - this.port80Handler.addDomain(domainOptions); - } - } - - /** - * Provision certificates for all routes that need them - */ - private async provisionAllCertificates(): Promise { - for (const certRoute of this.certRoutes) { - await this.provisionCertificateForRoute(certRoute); - } - } - - /** - * Provision a certificate for a route - */ - private async provisionCertificateForRoute(certRoute: ICertRoute): Promise { - const { domain, route } = certRoute; - const isWildcard = domain.includes('*'); - let provision: TCertProvisionObject = 'http01'; - - // Try to get a certificate from the provision function - if (this.certProvisionFunction) { - try { - provision = await this.certProvisionFunction(domain); - } catch (err) { - console.error(`certProvider error for ${domain} on route ${route.name || 'unnamed'}:`, err); - } - } else if (isWildcard) { - // No certProvider: cannot handle wildcard without DNS-01 support - console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`); - return; - } - - // Store the route reference with the provision type - this.provisionMap.set(domain, { - type: provision === 'http01' || provision === 'dns01' ? provision : 'static', - routeRef: certRoute - }); - - // Handle different provisioning methods - if (provision === 'http01') { - if (isWildcard) { - console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`); - return; - } - - this.port80Handler.addDomain({ - domainName: domain, - sslRedirect: true, - acmeMaintenance: true, - routeReference: { - routeId: route.name || domain, - routeName: route.name - } - }); - } else if (provision === 'dns01') { - // DNS-01 challenges would be handled by the certProvisionFunction - // DNS-01 handling would go here if implemented - console.log(`DNS-01 challenge type set for ${domain}`); - } else { - // Static certificate (e.g., DNS-01 provisioned or user-provided) - const certObj = provision as plugins.tsclass.network.ICert; - const certData: ICertificateData = { - domain: certObj.domainName, - certificate: certObj.publicKey, - privateKey: certObj.privateKey, - expiryDate: new Date(certObj.validUntil), - source: 'static', - isRenewal: false, - routeReference: { - routeId: route.name || domain, - routeName: route.name - } - }; - - this.networkProxyBridge.applyExternalCertificate(certData); - this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, certData); - } - } - - /** - * Schedule certificate renewals using a task manager - */ - private scheduleRenewals(): void { - this.renewManager = new plugins.taskbuffer.TaskManager(); - - const renewTask = new plugins.taskbuffer.Task({ - name: 'CertificateRenewals', - taskFunction: async () => await this.performRenewals() - }); - - const hours = this.renewCheckIntervalHours; - const cronExpr = `0 0 */${hours} * * *`; - - this.renewManager.addAndScheduleTask(renewTask, cronExpr); - this.renewManager.start(); - } - - /** - * Perform renewals for all domains that need it - */ - private async performRenewals(): Promise { - for (const [domain, info] of this.provisionMap.entries()) { - // Skip wildcard domains for HTTP-01 challenges - if (domain.includes('*') && info.type === 'http01') continue; - - try { - await this.renewCertificateForDomain(domain, info.type, info.routeRef); - } catch (err) { - console.error(`Renewal error for ${domain}:`, err); - } - } - } - - /** - * Renew a certificate for a specific domain - * @param domain Domain to renew - * @param provisionType Type of provisioning for this domain - * @param certRoute The route reference for this domain - */ - private async renewCertificateForDomain( - domain: string, - provisionType: 'http01' | 'dns01' | 'static', - certRoute?: ICertRoute - ): Promise { - if (provisionType === 'http01') { - await this.port80Handler.renewCertificate(domain); - } else if ((provisionType === 'static' || provisionType === 'dns01') && this.certProvisionFunction) { - const provision = await this.certProvisionFunction(domain); - - if (provision !== 'http01' && provision !== 'dns01') { - const certObj = provision as plugins.tsclass.network.ICert; - const routeRef = certRoute?.route; - - const certData: ICertificateData = { - domain: certObj.domainName, - certificate: certObj.publicKey, - privateKey: certObj.privateKey, - expiryDate: new Date(certObj.validUntil), - source: 'static', - isRenewal: true, - routeReference: routeRef ? { - routeId: routeRef.name || domain, - routeName: routeRef.name - } : undefined - }; - - this.networkProxyBridge.applyExternalCertificate(certData); - this.emit(CertProvisionerEvents.CERTIFICATE_RENEWED, certData); - } - } - } - - /** - * Stop all scheduled renewal tasks. - */ - public async stop(): Promise { - if (this.renewManager) { - this.renewManager.stop(); - } - } - - /** - * Request a certificate on-demand for the given domain. - * This will look for a matching route configuration and provision accordingly. - * - * @param domain Domain name to provision - */ - public async requestCertificate(domain: string): Promise { - const isWildcard = domain.includes('*'); - // Find matching route - const certRoute = this.findRouteForDomain(domain); - - // Determine provisioning method - let provision: TCertProvisionObject = 'http01'; - - if (this.certProvisionFunction) { - provision = await this.certProvisionFunction(domain); - } else if (isWildcard) { - // Cannot perform HTTP-01 on wildcard without certProvider - throw new Error(`Cannot request certificate for wildcard domain without certProvisionFunction: ${domain}`); - } - - if (provision === 'http01') { - if (isWildcard) { - throw new Error(`Cannot request HTTP-01 certificate for wildcard domain: ${domain}`); - } - await this.port80Handler.renewCertificate(domain); - } else if (provision === 'dns01') { - // DNS-01 challenges would be handled by external mechanisms - console.log(`DNS-01 challenge requested for ${domain}`); - } else { - // Static certificate (e.g., DNS-01 provisioned) supports wildcards - const certObj = provision as plugins.tsclass.network.ICert; - const certData: ICertificateData = { - domain: certObj.domainName, - certificate: certObj.publicKey, - privateKey: certObj.privateKey, - expiryDate: new Date(certObj.validUntil), - source: 'static', - isRenewal: false, - routeReference: certRoute ? { - routeId: certRoute.route.name || domain, - routeName: certRoute.route.name - } : undefined - }; - - this.networkProxyBridge.applyExternalCertificate(certData); - this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, certData); - } - } - - /** - * Add a new domain for certificate provisioning - * - * @param domain Domain to add - * @param options Domain configuration options - */ - public async addDomain(domain: string, options?: { - sslRedirect?: boolean; - acmeMaintenance?: boolean; - routeId?: string; - routeName?: string; - }): Promise { - const domainOptions: IDomainOptions = { - domainName: domain, - sslRedirect: options?.sslRedirect ?? true, - acmeMaintenance: options?.acmeMaintenance ?? true, - routeReference: { - routeId: options?.routeId, - routeName: options?.routeName - } - }; - - this.port80Handler.addDomain(domainOptions); - - // Find matching route or create a generic one - const existingRoute = this.findRouteForDomain(domain); - if (existingRoute) { - await this.provisionCertificateForRoute(existingRoute); - } else { - // We don't have a route, just provision the domain - const isWildcard = domain.includes('*'); - let provision: TCertProvisionObject = 'http01'; - - if (this.certProvisionFunction) { - provision = await this.certProvisionFunction(domain); - } else if (isWildcard) { - throw new Error(`Cannot request certificate for wildcard domain without certProvisionFunction: ${domain}`); - } - - this.provisionMap.set(domain, { - type: provision === 'http01' || provision === 'dns01' ? provision : 'static' - }); - - if (provision !== 'http01' && provision !== 'dns01') { - const certObj = provision as plugins.tsclass.network.ICert; - const certData: ICertificateData = { - domain: certObj.domainName, - certificate: certObj.publicKey, - privateKey: certObj.privateKey, - expiryDate: new Date(certObj.validUntil), - source: 'static', - isRenewal: false, - routeReference: { - routeId: options?.routeId, - routeName: options?.routeName - } - }; - - this.networkProxyBridge.applyExternalCertificate(certData); - this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, certData); - } - } - } - - /** - * Update routes with new configurations - * This replaces all existing routes with new ones and re-provisions certificates as needed - * - * @param newRoutes New route configurations to use - */ - public async updateRoutes(newRoutes: IRouteConfig[]): Promise { - // Store the new route configs - this.routeConfigs = newRoutes; - - // Extract new certificate routes - const newCertRoutes = this.extractCertificateRoutesFromRoutes(newRoutes); - - // Find domains that no longer need certificates - const oldDomains = new Set(this.certRoutes.map(r => r.domain)); - const newDomains = new Set(newCertRoutes.map(r => r.domain)); - - // Domains to remove - const domainsToRemove = [...oldDomains].filter(d => !newDomains.has(d)); - - // Remove obsolete domains from provision map - for (const domain of domainsToRemove) { - this.provisionMap.delete(domain); - } - - // Update the cert routes - this.certRoutes = newCertRoutes; - - // Provision certificates for new routes - for (const certRoute of newCertRoutes) { - if (!oldDomains.has(certRoute.domain)) { - await this.provisionCertificateForRoute(certRoute); - } - } - } -} - -// Type alias for backward compatibility -export type TSmartProxyCertProvisionObject = TCertProvisionObject; \ No newline at end of file diff --git a/ts/certificate/providers/index.ts b/ts/certificate/providers/index.ts deleted file mode 100644 index 92e723c..0000000 --- a/ts/certificate/providers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** - * Certificate providers - */ diff --git a/ts/certificate/storage/file-storage.ts b/ts/certificate/storage/file-storage.ts deleted file mode 100644 index 65ece3e..0000000 --- a/ts/certificate/storage/file-storage.ts +++ /dev/null @@ -1,234 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as plugins from '../../plugins.js'; -import type { ICertificateData, ICertificates } from '../models/certificate-types.js'; -import { ensureCertificateDirectory } from '../utils/certificate-helpers.js'; - -/** - * FileStorage provides file system storage for certificates - */ -export class FileStorage { - private storageDir: string; - - /** - * Creates a new file storage provider - * @param storageDir Directory to store certificates - */ - constructor(storageDir: string) { - this.storageDir = path.resolve(storageDir); - ensureCertificateDirectory(this.storageDir); - } - - /** - * Save a certificate to the file system - * @param domain Domain name - * @param certData Certificate data to save - */ - public async saveCertificate(domain: string, certData: ICertificateData): Promise { - const sanitizedDomain = this.sanitizeDomain(domain); - const certDir = path.join(this.storageDir, sanitizedDomain); - ensureCertificateDirectory(certDir); - - const certPath = path.join(certDir, 'fullchain.pem'); - const keyPath = path.join(certDir, 'privkey.pem'); - const metaPath = path.join(certDir, 'metadata.json'); - - // Write certificate and private key - await fs.promises.writeFile(certPath, certData.certificate, 'utf8'); - await fs.promises.writeFile(keyPath, certData.privateKey, 'utf8'); - - // Write metadata - const metadata = { - domain: certData.domain, - expiryDate: certData.expiryDate.toISOString(), - source: certData.source || 'unknown', - issuedAt: new Date().toISOString() - }; - - await fs.promises.writeFile( - metaPath, - JSON.stringify(metadata, null, 2), - 'utf8' - ); - } - - /** - * Load a certificate from the file system - * @param domain Domain name - * @returns Certificate data if found, null otherwise - */ - public async loadCertificate(domain: string): Promise { - const sanitizedDomain = this.sanitizeDomain(domain); - const certDir = path.join(this.storageDir, sanitizedDomain); - - if (!fs.existsSync(certDir)) { - return null; - } - - const certPath = path.join(certDir, 'fullchain.pem'); - const keyPath = path.join(certDir, 'privkey.pem'); - const metaPath = path.join(certDir, 'metadata.json'); - - try { - // Check if all required files exist - if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) { - return null; - } - - // Read certificate and private key - const certificate = await fs.promises.readFile(certPath, 'utf8'); - const privateKey = await fs.promises.readFile(keyPath, 'utf8'); - - // Try to read metadata if available - let expiryDate = new Date(); - let source: 'static' | 'http01' | 'dns01' | undefined; - - if (fs.existsSync(metaPath)) { - const metaContent = await fs.promises.readFile(metaPath, 'utf8'); - const metadata = JSON.parse(metaContent); - - if (metadata.expiryDate) { - expiryDate = new Date(metadata.expiryDate); - } - - if (metadata.source) { - source = metadata.source as 'static' | 'http01' | 'dns01'; - } - } - - return { - domain, - certificate, - privateKey, - expiryDate, - source - }; - } catch (error) { - console.error(`Error loading certificate for ${domain}:`, error); - return null; - } - } - - /** - * Delete a certificate from the file system - * @param domain Domain name - */ - public async deleteCertificate(domain: string): Promise { - const sanitizedDomain = this.sanitizeDomain(domain); - const certDir = path.join(this.storageDir, sanitizedDomain); - - if (!fs.existsSync(certDir)) { - return false; - } - - try { - // Recursively delete the certificate directory - await this.deleteDirectory(certDir); - return true; - } catch (error) { - console.error(`Error deleting certificate for ${domain}:`, error); - return false; - } - } - - /** - * List all domains with stored certificates - * @returns Array of domain names - */ - public async listCertificates(): Promise { - try { - const entries = await fs.promises.readdir(this.storageDir, { withFileTypes: true }); - return entries - .filter(entry => entry.isDirectory()) - .map(entry => entry.name); - } catch (error) { - console.error('Error listing certificates:', error); - return []; - } - } - - /** - * Check if a certificate is expiring soon - * @param domain Domain name - * @param thresholdDays Days threshold to consider expiring - * @returns Information about expiring certificate or null - */ - public async isExpiringSoon( - domain: string, - thresholdDays: number = 30 - ): Promise<{ domain: string; expiryDate: Date; daysRemaining: number } | null> { - const certData = await this.loadCertificate(domain); - - if (!certData) { - return null; - } - - const now = new Date(); - const expiryDate = certData.expiryDate; - const timeRemaining = expiryDate.getTime() - now.getTime(); - const daysRemaining = Math.floor(timeRemaining / (1000 * 60 * 60 * 24)); - - if (daysRemaining <= thresholdDays) { - return { - domain, - expiryDate, - daysRemaining - }; - } - - return null; - } - - /** - * Check all certificates for expiration - * @param thresholdDays Days threshold to consider expiring - * @returns List of expiring certificates - */ - public async getExpiringCertificates( - thresholdDays: number = 30 - ): Promise> { - const domains = await this.listCertificates(); - const expiringCerts = []; - - for (const domain of domains) { - const expiring = await this.isExpiringSoon(domain, thresholdDays); - if (expiring) { - expiringCerts.push(expiring); - } - } - - return expiringCerts; - } - - /** - * Delete a directory recursively - * @param directoryPath Directory to delete - */ - private async deleteDirectory(directoryPath: string): Promise { - if (fs.existsSync(directoryPath)) { - const entries = await fs.promises.readdir(directoryPath, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(directoryPath, entry.name); - - if (entry.isDirectory()) { - await this.deleteDirectory(fullPath); - } else { - await fs.promises.unlink(fullPath); - } - } - - await fs.promises.rmdir(directoryPath); - } - } - - /** - * Sanitize a domain name for use as a directory name - * @param domain Domain name - * @returns Sanitized domain name - */ - private sanitizeDomain(domain: string): string { - // Replace wildcard and any invalid filesystem characters - return domain.replace(/\*/g, '_wildcard_').replace(/[/\\:*?"<>|]/g, '_'); - } -} \ No newline at end of file diff --git a/ts/certificate/storage/index.ts b/ts/certificate/storage/index.ts deleted file mode 100644 index 72a6d68..0000000 --- a/ts/certificate/storage/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** - * Certificate storage mechanisms - */ diff --git a/ts/certificate/utils/certificate-helpers.ts b/ts/certificate/utils/certificate-helpers.ts deleted file mode 100644 index 21d9a60..0000000 --- a/ts/certificate/utils/certificate-helpers.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { fileURLToPath } from 'url'; -import type { ICertificates } from '../models/certificate-types.js'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -/** - * Loads the default SSL certificates from the assets directory - * @returns The certificate key pair - */ -export function loadDefaultCertificates(): ICertificates { - try { - // Need to adjust path from /ts/certificate/utils to /assets/certs - const certPath = path.join(__dirname, '..', '..', '..', 'assets', 'certs'); - const privateKey = fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'); - const publicKey = fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8'); - - if (!privateKey || !publicKey) { - throw new Error('Failed to load default certificates'); - } - - return { - privateKey, - publicKey - }; - } catch (error) { - console.error('Error loading default certificates:', error); - throw error; - } -} - -/** - * Checks if a certificate file exists at the specified path - * @param certPath Path to check for certificate - * @returns True if the certificate exists, false otherwise - */ -export function certificateExists(certPath: string): boolean { - return fs.existsSync(certPath); -} - -/** - * Ensures the certificate directory exists - * @param dirPath Path to the certificate directory - */ -export function ensureCertificateDirectory(dirPath: string): void { - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - } -} diff --git a/ts/common/eventUtils.ts b/ts/common/eventUtils.ts index ab1ba63..5415e07 100644 --- a/ts/common/eventUtils.ts +++ b/ts/common/eventUtils.ts @@ -1,4 +1,4 @@ -import type { Port80Handler } from '../http/port80/port80-handler.js'; +// Port80Handler removed - use SmartCertManager instead import { Port80HandlerEvents } from './types.js'; import type { ICertificateData, ICertificateFailure, ICertificateExpiring } from './types.js'; @@ -16,7 +16,7 @@ export interface Port80HandlerSubscribers { * Subscribes to Port80Handler events based on provided callbacks */ export function subscribeToPort80Handler( - handler: Port80Handler, + handler: any, subscribers: Port80HandlerSubscribers ): void { if (subscribers.onCertificateIssued) { diff --git a/ts/core/models/common-types.ts b/ts/core/models/common-types.ts index 02d5d10..1aeb997 100644 --- a/ts/core/models/common-types.ts +++ b/ts/core/models/common-types.ts @@ -34,7 +34,7 @@ export interface ICertificateData { } /** - * Events emitted by the Port80Handler + * @deprecated Events emitted by the Port80Handler - use SmartCertManager instead */ export enum Port80HandlerEvents { CERTIFICATE_ISSUED = 'certificate-issued', diff --git a/ts/core/utils/event-utils.ts b/ts/core/utils/event-utils.ts index 173ea19..93703ce 100644 --- a/ts/core/utils/event-utils.ts +++ b/ts/core/utils/event-utils.ts @@ -1,34 +1,25 @@ -import type { Port80Handler } from '../../http/port80/port80-handler.js'; +// Port80Handler has been removed - use SmartCertManager instead import { Port80HandlerEvents } from '../models/common-types.js'; -import type { ICertificateData, ICertificateFailure, ICertificateExpiring } from '../models/common-types.js'; + +// Re-export for backward compatibility +export { Port80HandlerEvents }; /** - * Subscribers callback definitions for Port80Handler events + * @deprecated Use SmartCertManager instead */ export interface IPort80HandlerSubscribers { - onCertificateIssued?: (data: ICertificateData) => void; - onCertificateRenewed?: (data: ICertificateData) => void; - onCertificateFailed?: (data: ICertificateFailure) => void; - onCertificateExpiring?: (data: ICertificateExpiring) => void; + onCertificateIssued?: (data: any) => void; + onCertificateRenewed?: (data: any) => void; + onCertificateFailed?: (data: any) => void; + onCertificateExpiring?: (data: any) => void; } /** - * Subscribes to Port80Handler events based on provided callbacks + * @deprecated Use SmartCertManager instead */ export function subscribeToPort80Handler( - handler: Port80Handler, + handler: any, subscribers: IPort80HandlerSubscribers ): 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); - } + console.warn('subscribeToPort80Handler is deprecated - use SmartCertManager instead'); } \ No newline at end of file diff --git a/ts/http/models/http-types.ts b/ts/http/models/http-types.ts index 76751c5..9e52171 100644 --- a/ts/http/models/http-types.ts +++ b/ts/http/models/http-types.ts @@ -1,8 +1,12 @@ import * as plugins from '../../plugins.js'; -import type { - IDomainOptions, - IAcmeOptions -} from '../../certificate/models/certificate-types.js'; +// Certificate types have been removed - use SmartCertManager instead +export interface IDomainOptions { + domainName: string; + sslRedirect: boolean; + acmeMaintenance: boolean; + forward?: { ip: string; port: number }; + acmeForward?: { ip: string; port: number }; +} /** * HTTP-specific event types diff --git a/ts/http/port80/acme-interfaces.ts b/ts/http/port80/acme-interfaces.ts deleted file mode 100644 index acaa4cf..0000000 --- a/ts/http/port80/acme-interfaces.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Type definitions for SmartAcme interfaces used by ChallengeResponder - * These reflect the actual SmartAcme API based on the documentation - * - * Also includes route-based interfaces for Port80Handler to extract domains - * that need certificate management from route configurations. - */ -import * as plugins from '../../plugins.js'; -import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js'; - -/** - * Structure for SmartAcme certificate result - */ -export interface ISmartAcmeCert { - id?: string; - domainName: string; - created?: number | Date | string; - privateKey: string; - publicKey: string; - csr?: string; - validUntil: number | Date | string; -} - -/** - * Structure for SmartAcme options - */ -export interface ISmartAcmeOptions { - accountEmail: string; - certManager: ICertManager; - environment: 'production' | 'integration'; - challengeHandlers: IChallengeHandler[]; - challengePriority?: string[]; - retryOptions?: { - retries?: number; - factor?: number; - minTimeoutMs?: number; - maxTimeoutMs?: number; - }; -} - -/** - * Interface for certificate manager - */ -export interface ICertManager { - init(): Promise; - get(domainName: string): Promise; - put(cert: ISmartAcmeCert): Promise; - delete(domainName: string): Promise; - close?(): Promise; -} - -/** - * Interface for challenge handler - */ -export interface IChallengeHandler { - getSupportedTypes(): string[]; - prepare(ch: T): Promise; - verify?(ch: T): Promise; - cleanup(ch: T): Promise; - checkWetherDomainIsSupported(domain: string): Promise; -} - -/** - * HTTP-01 challenge type - */ -export interface IHttp01Challenge { - type: string; // 'http-01' - token: string; - keyAuthorization: string; - webPath: string; -} - -/** - * HTTP-01 Memory Handler Interface - */ -export interface IHttp01MemoryHandler extends IChallengeHandler { - handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, next?: () => void): void; -} - -/** - * SmartAcme main class interface - */ -export interface ISmartAcme { - start(): Promise; - stop(): Promise; - getCertificateForDomain(domain: string): Promise; - on?(event: string, listener: (data: any) => void): void; - eventEmitter?: plugins.EventEmitter; -} - -/** - * Port80Handler route options - */ -export interface IPort80RouteOptions { - // The domain for the certificate - domain: string; - - // Whether to redirect HTTP to HTTPS - sslRedirect: boolean; - - // Whether to enable ACME certificate management - acmeMaintenance: boolean; - - // Optional target for forwarding HTTP requests - forward?: { - ip: string; - port: number; - }; - - // Optional target for forwarding ACME challenge requests - acmeForward?: { - ip: string; - port: number; - }; - - // Reference to the route that requested this certificate - routeReference?: { - routeId?: string; - routeName?: string; - }; -} - -/** - * Extract domains that need certificate management from routes - * @param routes Route configurations to extract domains from - * @returns Array of Port80RouteOptions for each domain - */ -export function extractPort80RoutesFromRoutes(routes: IRouteConfig[]): IPort80RouteOptions[] { - const result: IPort80RouteOptions[] = []; - - for (const route of routes) { - // Skip routes that don't have domains or TLS configuration - if (!route.match.domains || !route.action.tls) continue; - - // Skip routes that don't terminate TLS - if (route.action.tls.mode !== 'terminate' && route.action.tls.mode !== 'terminate-and-reencrypt') continue; - - // Only routes with automatic certificates need ACME - if (route.action.tls.certificate !== 'auto') continue; - - // Get domains from route - const domains = Array.isArray(route.match.domains) - ? route.match.domains - : [route.match.domains]; - - // Create Port80RouteOptions for each domain - for (const domain of domains) { - // Skip wildcards (we can't get certificates for them) - if (domain.includes('*')) continue; - - // Create Port80RouteOptions - const options: IPort80RouteOptions = { - domain, - sslRedirect: true, // Default to true for HTTPS routes - acmeMaintenance: true, // Default to true for auto certificates - - // Add route reference - routeReference: { - routeName: route.name - } - }; - - // Add domain to result - result.push(options); - } - } - - return result; -} \ No newline at end of file diff --git a/ts/http/port80/challenge-responder.ts b/ts/http/port80/challenge-responder.ts deleted file mode 100644 index 4c0a6f8..0000000 --- a/ts/http/port80/challenge-responder.ts +++ /dev/null @@ -1,246 +0,0 @@ -import * as plugins from '../../plugins.js'; -import { IncomingMessage, ServerResponse } from 'http'; -import { - CertificateEvents -} from '../../certificate/events/certificate-events.js'; -import type { - ICertificateData, - ICertificateFailure, - ICertificateExpiring -} from '../../certificate/models/certificate-types.js'; -import type { - ISmartAcme, - ISmartAcmeCert, - ISmartAcmeOptions, - IHttp01MemoryHandler -} from './acme-interfaces.js'; - -/** - * ChallengeResponder handles ACME HTTP-01 challenges by leveraging SmartAcme - * It acts as a bridge between the HTTP server and the ACME challenge verification process - */ -export class ChallengeResponder extends plugins.EventEmitter { - private smartAcme: ISmartAcme | null = null; - private http01Handler: IHttp01MemoryHandler | null = null; - - /** - * Creates a new challenge responder - * @param useProduction Whether to use production ACME servers - * @param email Account email for ACME - * @param certificateStore Directory to store certificates - */ - constructor( - private readonly useProduction: boolean = false, - private readonly email: string = 'admin@example.com', - private readonly certificateStore: string = './certs' - ) { - super(); - } - - /** - * Initialize the ACME client - */ - public async initialize(): Promise { - try { - // Create the HTTP-01 memory handler from SmartACME - this.http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler(); - - // Ensure certificate store directory exists - await this.ensureCertificateStore(); - - // Create a MemoryCertManager for certificate storage - const certManager = new plugins.smartacme.certmanagers.MemoryCertManager(); - - // Initialize the SmartACME client with appropriate options - this.smartAcme = new plugins.smartacme.SmartAcme({ - accountEmail: this.email, - certManager: certManager, - environment: this.useProduction ? 'production' : 'integration', - challengeHandlers: [this.http01Handler], - challengePriority: ['http-01'] - }); - - // Set up event forwarding from SmartAcme - this.setupEventListeners(); - - // Start the SmartACME client - await this.smartAcme.start(); - console.log('ACME client initialized successfully'); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to initialize ACME client: ${errorMessage}`); - } - } - - /** - * Ensure the certificate store directory exists - */ - private async ensureCertificateStore(): Promise { - try { - await plugins.fs.promises.mkdir(this.certificateStore, { recursive: true }); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to create certificate store: ${errorMessage}`); - } - } - - /** - * Setup event listeners to forward SmartACME events to our own event emitter - */ - private setupEventListeners(): void { - if (!this.smartAcme) return; - - const setupEvents = (emitter: { on: (event: string, listener: (data: any) => void) => void }) => { - // Forward certificate events - emitter.on('certificate', (data: any) => { - const isRenewal = !!data.isRenewal; - - const certData: ICertificateData = { - domain: data.domainName || data.domain, - certificate: data.publicKey || data.cert, - privateKey: data.privateKey || data.key, - expiryDate: new Date(data.validUntil || data.expiryDate || Date.now()), - source: 'http01', - isRenewal - }; - - const eventType = isRenewal - ? CertificateEvents.CERTIFICATE_RENEWED - : CertificateEvents.CERTIFICATE_ISSUED; - - this.emit(eventType, certData); - }); - - // Forward error events - emitter.on('error', (error: any) => { - const domain = error.domainName || error.domain || 'unknown'; - const failureData: ICertificateFailure = { - domain, - error: error.message || String(error), - isRenewal: !!error.isRenewal - }; - - this.emit(CertificateEvents.CERTIFICATE_FAILED, failureData); - }); - }; - - // Check for direct event methods on SmartAcme - if (typeof this.smartAcme.on === 'function') { - setupEvents(this.smartAcme as any); - } - // Check for eventEmitter property - else if (this.smartAcme.eventEmitter) { - setupEvents(this.smartAcme.eventEmitter); - } - // If no proper event handling, log a warning - else { - console.warn('SmartAcme instance does not support expected event interface - events may not be forwarded'); - } - } - - /** - * Handle HTTP request by checking if it's an ACME challenge - * @param req HTTP request object - * @param res HTTP response object - * @returns true if the request was handled, false otherwise - */ - public handleRequest(req: IncomingMessage, res: ServerResponse): boolean { - if (!this.http01Handler) return false; - - // Check if this is an ACME challenge request (/.well-known/acme-challenge/*) - const url = req.url || ''; - if (url.startsWith('/.well-known/acme-challenge/')) { - try { - // Delegate to the HTTP-01 memory handler, which knows how to serve challenges - this.http01Handler.handleRequest(req, res); - return true; - } catch (error) { - console.error('Error handling ACME challenge:', error); - // If there was an error, send a 404 response - res.writeHead(404); - res.end('Not found'); - return true; - } - } - - return false; - } - - /** - * Request a certificate for a domain - * @param domain Domain name to request a certificate for - * @param isRenewal Whether this is a renewal request - */ - public async requestCertificate(domain: string, isRenewal: boolean = false): Promise { - if (!this.smartAcme) { - throw new Error('ACME client not initialized'); - } - - try { - // Request certificate using SmartACME - const certObj = await this.smartAcme.getCertificateForDomain(domain); - - // Convert the certificate object to our CertificateData format - const certData: ICertificateData = { - domain, - certificate: certObj.publicKey, - privateKey: certObj.privateKey, - expiryDate: new Date(certObj.validUntil), - source: 'http01', - isRenewal - }; - - return certData; - } catch (error) { - // Create failure object - const failure: ICertificateFailure = { - domain, - error: error instanceof Error ? error.message : String(error), - isRenewal - }; - - // Emit failure event - this.emit(CertificateEvents.CERTIFICATE_FAILED, failure); - - // Rethrow with more context - throw new Error(`Failed to ${isRenewal ? 'renew' : 'obtain'} certificate for ${domain}: ${ - error instanceof Error ? error.message : String(error) - }`); - } - } - - /** - * Check if a certificate is expiring soon and trigger renewal if needed - * @param domain Domain name - * @param certificate Certificate data - * @param thresholdDays Days before expiry to trigger renewal - */ - public checkCertificateExpiry( - domain: string, - certificate: ICertificateData, - thresholdDays: number = 30 - ): void { - if (!certificate.expiryDate) return; - - const now = new Date(); - const expiryDate = certificate.expiryDate; - const daysDifference = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); - - if (daysDifference <= thresholdDays) { - const expiryInfo: ICertificateExpiring = { - domain, - expiryDate, - daysRemaining: daysDifference - }; - - this.emit(CertificateEvents.CERTIFICATE_EXPIRING, expiryInfo); - - // Automatically attempt renewal if expiring - if (this.smartAcme) { - this.requestCertificate(domain, true).catch(error => { - console.error(`Failed to auto-renew certificate for ${domain}:`, error); - }); - } - } - } -} \ No newline at end of file diff --git a/ts/http/port80/index.ts b/ts/http/port80/index.ts deleted file mode 100644 index 6c4c765..0000000 --- a/ts/http/port80/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Port 80 handling - */ - -// Export the main components -export { Port80Handler } from './port80-handler.js'; -export { ChallengeResponder } from './challenge-responder.js'; - -// Export backward compatibility interfaces and types -export { - HttpError as Port80HandlerError, - CertificateError as CertError -} from '../models/http-types.js'; diff --git a/ts/http/port80/port80-handler.ts b/ts/http/port80/port80-handler.ts deleted file mode 100644 index 539acdc..0000000 --- a/ts/http/port80/port80-handler.ts +++ /dev/null @@ -1,728 +0,0 @@ -import * as plugins from '../../plugins.js'; -import { IncomingMessage, ServerResponse } from 'http'; -import { CertificateEvents } from '../../certificate/events/certificate-events.js'; -import type { - IDomainOptions, // Kept for backward compatibility - ICertificateData, - ICertificateFailure, - ICertificateExpiring, - IAcmeOptions, - IRouteForwardConfig -} from '../../certificate/models/certificate-types.js'; -import { - HttpEvents, - HttpStatus, - HttpError, - CertificateError, - ServerError, -} from '../models/http-types.js'; -import type { IDomainCertificate } from '../models/http-types.js'; -import { ChallengeResponder } from './challenge-responder.js'; -import { extractPort80RoutesFromRoutes } from './acme-interfaces.js'; -import type { IPort80RouteOptions } from './acme-interfaces.js'; -import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js'; - -// Re-export for backward compatibility -export { - HttpError as Port80HandlerError, - CertificateError, - ServerError -} - -// Port80Handler events enum for backward compatibility -export const Port80HandlerEvents = CertificateEvents; - -/** - * Configuration options for the Port80Handler - */ -// Port80Handler options moved to common types - - -/** - * Port80Handler with ACME certificate management and request forwarding capabilities - * Now with glob pattern support for domain matching - */ -export class Port80Handler extends plugins.EventEmitter { - private domainCertificates: Map; - private challengeResponder: ChallengeResponder | null = null; - private server: plugins.http.Server | null = null; - - // Renewal scheduling is handled externally by SmartProxy - private isShuttingDown: boolean = false; - private options: Required; - - /** - * Creates a new Port80Handler - * @param options Configuration options - */ - constructor(options: IAcmeOptions = {}) { - super(); - this.domainCertificates = new Map(); - - // Default options - this.options = { - port: options.port ?? 80, - accountEmail: options.accountEmail ?? 'admin@example.com', - useProduction: options.useProduction ?? false, // Safer default: staging - httpsRedirectPort: options.httpsRedirectPort ?? 443, - 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, - routeForwards: options.routeForwards ?? [] - }; - - // Initialize challenge responder - if (this.options.enabled) { - this.challengeResponder = new ChallengeResponder( - this.options.useProduction, - this.options.accountEmail, - this.options.certificateStore - ); - - // Forward certificate events from the challenge responder - this.challengeResponder.on(CertificateEvents.CERTIFICATE_ISSUED, (data: ICertificateData) => { - this.emit(CertificateEvents.CERTIFICATE_ISSUED, data); - }); - - this.challengeResponder.on(CertificateEvents.CERTIFICATE_RENEWED, (data: ICertificateData) => { - this.emit(CertificateEvents.CERTIFICATE_RENEWED, data); - }); - - this.challengeResponder.on(CertificateEvents.CERTIFICATE_FAILED, (error: ICertificateFailure) => { - this.emit(CertificateEvents.CERTIFICATE_FAILED, error); - }); - - this.challengeResponder.on(CertificateEvents.CERTIFICATE_EXPIRING, (expiry: ICertificateExpiring) => { - this.emit(CertificateEvents.CERTIFICATE_EXPIRING, expiry); - }); - } - } - - /** - * Starts the HTTP server for ACME challenges - */ - public async start(): Promise { - if (this.server) { - throw new ServerError('Server is already running'); - } - - if (this.isShuttingDown) { - throw new ServerError('Server is shutting down'); - } - - // Skip if disabled - if (this.options.enabled === false) { - console.log('Port80Handler is disabled, skipping start'); - return; - } - - // Initialize the challenge responder if enabled - if (this.options.enabled && this.challengeResponder) { - try { - await this.challengeResponder.initialize(); - } catch (error) { - throw new ServerError(`Failed to initialize challenge responder: ${ - error instanceof Error ? error.message : String(error) - }`); - } - } - - return new Promise((resolve, reject) => { - try { - this.server = plugins.http.createServer((req, res) => this.handleRequest(req, res)); - - this.server.on('error', (error: NodeJS.ErrnoException) => { - if (error.code === 'EACCES') { - 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') { - reject(new ServerError(`Port ${this.options.port} is already in use.`, error.code)); - } else { - reject(new ServerError(error.message, error.code)); - } - }); - - this.server.listen(this.options.port, () => { - console.log(`Port80Handler is listening on port ${this.options.port}`); - this.emit(CertificateEvents.MANAGER_STARTED, this.options.port); - - // Start certificate process for domains with acmeMaintenance enabled - for (const [domain, domainInfo] of this.domainCertificates.entries()) { - // Skip glob patterns for certificate issuance - if (this.isGlobPattern(domain)) { - console.log(`Skipping initial certificate for glob pattern: ${domain}`); - continue; - } - - if (domainInfo.options.acmeMaintenance && !domainInfo.certObtained && !domainInfo.obtainingInProgress) { - this.obtainCertificate(domain).catch(err => { - console.error(`Error obtaining initial certificate for ${domain}:`, err); - }); - } - } - - resolve(); - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Unknown error starting server'; - reject(new ServerError(message)); - } - }); - } - - /** - * Stops the HTTP server and cleanup resources - */ - public async stop(): Promise { - if (!this.server) { - return; - } - - this.isShuttingDown = true; - - return new Promise((resolve) => { - if (this.server) { - this.server.close(() => { - this.server = null; - this.isShuttingDown = false; - this.emit(CertificateEvents.MANAGER_STOPPED); - resolve(); - }); - } else { - this.isShuttingDown = false; - resolve(); - } - }); - } - - /** - * Adds a domain with configuration options - * @param options Domain configuration options - */ - public addDomain(options: IDomainOptions | IPort80RouteOptions): void { - // Normalize options format (handle both IDomainOptions and IPort80RouteOptions) - const normalizedOptions: IDomainOptions = this.normalizeOptions(options); - - if (!normalizedOptions.domainName || typeof normalizedOptions.domainName !== 'string') { - throw new HttpError('Invalid domain name'); - } - - const domainName = normalizedOptions.domainName; - - if (!this.domainCertificates.has(domainName)) { - this.domainCertificates.set(domainName, { - options: normalizedOptions, - certObtained: false, - obtainingInProgress: false - }); - - console.log(`Domain added: ${domainName} with configuration:`, { - sslRedirect: normalizedOptions.sslRedirect, - acmeMaintenance: normalizedOptions.acmeMaintenance, - hasForward: !!normalizedOptions.forward, - hasAcmeForward: !!normalizedOptions.acmeForward, - routeReference: normalizedOptions.routeReference - }); - - // If acmeMaintenance is enabled and not a glob pattern, start certificate process immediately - if (normalizedOptions.acmeMaintenance && this.server && !this.isGlobPattern(domainName)) { - 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 = normalizedOptions; - console.log(`Domain ${domainName} configuration updated`); - } - } - - /** - * Add domains from route configurations - * @param routes Array of route configurations - */ - public addDomainsFromRoutes(routes: IRouteConfig[]): void { - // Extract Port80RouteOptions from routes - const routeOptions = extractPort80RoutesFromRoutes(routes); - - // Add each domain - for (const options of routeOptions) { - this.addDomain(options); - } - - console.log(`Added ${routeOptions.length} domains from routes for certificate management`); - } - - /** - * Normalize options from either IDomainOptions or IPort80RouteOptions - * @param options Options to normalize - * @returns Normalized IDomainOptions - * @private - */ - private normalizeOptions(options: IDomainOptions | IPort80RouteOptions): IDomainOptions { - // Handle IPort80RouteOptions format - if ('domain' in options) { - return { - domainName: options.domain, - sslRedirect: options.sslRedirect, - acmeMaintenance: options.acmeMaintenance, - forward: options.forward, - acmeForward: options.acmeForward, - routeReference: options.routeReference - }; - } - - // Already in IDomainOptions format - return options; - } - - /** - * Removes a domain from management - * @param domain The domain to remove - */ - public removeDomain(domain: string): void { - if (this.domainCertificates.delete(domain)) { - console.log(`Domain removed: ${domain}`); - } - } - - /** - * Gets the certificate for a domain if it exists - * @param domain The domain to get the certificate for - */ - public getCertificate(domain: string): ICertificateData | null { - // Can't get certificates for glob patterns - if (this.isGlobPattern(domain)) { - return null; - } - - const domainInfo = this.domainCertificates.get(domain); - - if (!domainInfo || !domainInfo.certObtained || !domainInfo.certificate || !domainInfo.privateKey) { - return null; - } - - return { - domain, - certificate: domainInfo.certificate, - privateKey: domainInfo.privateKey, - expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate() - }; - } - - - - /** - * Check if a domain is a glob pattern - * @param domain Domain to check - * @returns True if the domain is a glob pattern - */ - private isGlobPattern(domain: string): boolean { - return domain.includes('*'); - } - - /** - * Get domain info for a specific domain, using glob pattern matching if needed - * @param requestDomain The actual domain from the request - * @returns The domain info or null if not found - */ - private getDomainInfoForRequest(requestDomain: string): { domainInfo: IDomainCertificate, pattern: string } | null { - // Try direct match first - if (this.domainCertificates.has(requestDomain)) { - return { - domainInfo: this.domainCertificates.get(requestDomain)!, - pattern: requestDomain - }; - } - - // Then try glob patterns - for (const [pattern, domainInfo] of this.domainCertificates.entries()) { - if (this.isGlobPattern(pattern) && this.domainMatchesPattern(requestDomain, pattern)) { - return { domainInfo, pattern }; - } - } - - return null; - } - - /** - * Check if a domain matches a glob pattern - * @param domain The domain to check - * @param pattern The pattern to match against - * @returns True if the domain matches the pattern - */ - private domainMatchesPattern(domain: string, pattern: string): boolean { - // Handle different glob pattern styles - if (pattern.startsWith('*.')) { - // *.example.com matches any subdomain - const suffix = pattern.substring(2); - return domain.endsWith(suffix) && domain.includes('.') && domain !== suffix; - } else if (pattern.endsWith('.*')) { - // example.* matches any TLD - const prefix = pattern.substring(0, pattern.length - 2); - const domainParts = domain.split('.'); - return domain.startsWith(prefix + '.') && domainParts.length >= 2; - } else if (pattern === '*') { - // Wildcard matches everything - return true; - } else { - // Exact match (shouldn't reach here as we check exact matches first) - return domain === pattern; - } - } - - - /** - * Handles incoming HTTP requests - * @param req The HTTP request - * @param res The HTTP response - */ - private handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { - // Emit request received event with basic info - this.emit(HttpEvents.REQUEST_RECEIVED, { - url: req.url, - method: req.method, - headers: req.headers - }); - - const hostHeader = req.headers.host; - if (!hostHeader) { - res.statusCode = HttpStatus.BAD_REQUEST; - res.end('Bad Request: Host header is missing'); - return; - } - - // Extract domain (ignoring any port in the Host header) - const domain = hostHeader.split(':')[0]; - - // Check if this is an ACME challenge request that our ChallengeResponder can handle - if (this.challengeResponder && req.url?.startsWith('/.well-known/acme-challenge/')) { - // Handle ACME HTTP-01 challenge with the challenge responder - const domainMatch = this.getDomainInfoForRequest(domain); - - // If there's a specific ACME forwarding config for this domain, use that instead - if (domainMatch?.domainInfo.options.acmeForward) { - this.forwardRequest(req, res, domainMatch.domainInfo.options.acmeForward, 'ACME challenge'); - return; - } - - // If domain exists and has acmeMaintenance enabled, or we don't have the domain yet - // (for auto-provisioning), try to handle the ACME challenge - if (!domainMatch || domainMatch.domainInfo.options.acmeMaintenance) { - // Let the challenge responder try to handle this request - if (this.challengeResponder.handleRequest(req, res)) { - // Challenge was handled - return; - } - } - } - - // Dynamic provisioning: if domain not yet managed, register for ACME and return 503 - if (!this.domainCertificates.has(domain)) { - try { - this.addDomain({ domainName: domain, sslRedirect: false, acmeMaintenance: true }); - } catch (err) { - console.error(`Error registering domain for on-demand provisioning: ${err}`); - } - res.statusCode = HttpStatus.SERVICE_UNAVAILABLE; - res.end('Certificate issuance in progress'); - return; - } - - // Get domain config, using glob pattern matching if needed - const domainMatch = this.getDomainInfoForRequest(domain); - if (!domainMatch) { - res.statusCode = HttpStatus.NOT_FOUND; - res.end('Domain not configured'); - return; - } - - const { domainInfo, pattern } = domainMatch; - const options = domainInfo.options; - - // 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 - // (Skip for glob patterns as they won't have certificates) - if (!this.isGlobPattern(pattern) && domainInfo.certObtained && options.sslRedirect) { - const httpsPort = this.options.httpsRedirectPort; - const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`; - const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`; - - res.statusCode = HttpStatus.MOVED_PERMANENTLY; - res.setHeader('Location', redirectUrl); - res.end(`Redirecting to ${redirectUrl}`); - return; - } - - // Handle case where certificate maintenance is enabled but not yet obtained - // (Skip for glob patterns as they can't have certificates) - if (!this.isGlobPattern(pattern) && options.acmeMaintenance && !domainInfo.certObtained) { - // Trigger certificate issuance if not already running - if (!domainInfo.obtainingInProgress) { - this.obtainCertificate(domain).catch(err => { - const errorMessage = err instanceof Error ? err.message : 'Unknown error'; - this.emit(CertificateEvents.CERTIFICATE_FAILED, { - domain, - error: errorMessage, - isRenewal: false - }); - console.error(`Error obtaining certificate for ${domain}:`, err); - }); - } - - res.statusCode = HttpStatus.SERVICE_UNAVAILABLE; - res.end('Certificate issuance in progress, please try again later.'); - return; - } - - // Default response for unhandled request - res.statusCode = HttpStatus.NOT_FOUND; - res.end('No handlers configured for this request'); - - // Emit request handled event - this.emit(HttpEvents.REQUEST_HANDLED, { - domain, - url: req.url, - statusCode: res.statusCode - }); - } - - /** - * 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: { ip: string; port: number }, - 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 || HttpStatus.INTERNAL_SERVER_ERROR; - - // 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(HttpEvents.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); - - this.emit(HttpEvents.REQUEST_ERROR, { - domain, - error: error.message, - target: `${target.ip}:${target.port}` - }); - - if (!res.headersSent) { - res.statusCode = HttpStatus.INTERNAL_SERVER_ERROR; - res.end(`Proxy error: ${error.message}`); - } else { - res.end(); - } - }); - - // Pipe original request to proxy request - if (req.readable) { - req.pipe(proxyReq); - } else { - proxyReq.end(); - } - } - - - /** - * Obtains a certificate for a domain using ACME HTTP-01 challenge - * @param domain The domain to obtain a certificate for - * @param isRenewal Whether this is a renewal attempt - */ - private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise { - if (this.isGlobPattern(domain)) { - throw new CertificateError('Cannot obtain certificates for glob pattern domains', domain, isRenewal); - } - - const domainInfo = this.domainCertificates.get(domain)!; - - if (!domainInfo.options.acmeMaintenance) { - console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`); - return; - } - - if (domainInfo.obtainingInProgress) { - console.log(`Certificate issuance already in progress for ${domain}`); - return; - } - - if (!this.challengeResponder) { - throw new HttpError('Challenge responder is not initialized'); - } - - domainInfo.obtainingInProgress = true; - domainInfo.lastRenewalAttempt = new Date(); - - try { - // Request certificate via ChallengeResponder - // The ChallengeResponder handles all ACME client interactions and will emit events - const certData = await this.challengeResponder.requestCertificate(domain, isRenewal); - - // Update domain info with certificate data - domainInfo.certificate = certData.certificate; - domainInfo.privateKey = certData.privateKey; - domainInfo.certObtained = true; - domainInfo.expiryDate = certData.expiryDate; - - console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`); - } catch (error: any) { - const errorMsg = error instanceof Error ? error.message : String(error); - console.error(`Error during certificate issuance for ${domain}:`, error); - throw new CertificateError(errorMsg, domain, isRenewal); - } finally { - domainInfo.obtainingInProgress = false; - } - } - - - /** - * 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 - * @param eventType The event type to emit - * @param data The certificate data - */ - private emitCertificateEvent(eventType: CertificateEvents, data: ICertificateData): void { - this.emit(eventType, data); - } - - /** - * Gets all domains and their certificate status - * @returns Map of domains to certificate status - */ - public getDomainCertificateStatus(): Map { - const result = new Map(); - - const now = new Date(); - - for (const [domain, domainInfo] of this.domainCertificates.entries()) { - // Skip glob patterns - if (this.isGlobPattern(domain)) continue; - - const status: { - certObtained: boolean; - expiryDate?: Date; - daysRemaining?: number; - obtainingInProgress: boolean; - lastRenewalAttempt?: Date; - } = { - certObtained: domainInfo.certObtained, - expiryDate: domainInfo.expiryDate, - obtainingInProgress: domainInfo.obtainingInProgress, - lastRenewalAttempt: domainInfo.lastRenewalAttempt - }; - - // Calculate days remaining if expiry date is available - if (domainInfo.expiryDate) { - const daysRemaining = Math.ceil( - (domainInfo.expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000) - ); - status.daysRemaining = daysRemaining; - } - - result.set(domain, status); - } - - return result; - } - - /** - * Request a certificate renewal for a specific domain. - * @param domain The domain to renew. - */ - public async renewCertificate(domain: string): Promise { - if (!this.domainCertificates.has(domain)) { - throw new HttpError(`Domain not managed: ${domain}`); - } - // Trigger renewal via ACME - await this.obtainCertificate(domain, true); - } -} \ No newline at end of file diff --git a/ts/index.ts b/ts/index.ts index d3c0dab..e8c06c0 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -9,39 +9,36 @@ export * from './proxies/nftables-proxy/index.js'; // Export NetworkProxy elements selectively to avoid RouteManager ambiguity export { NetworkProxy, CertificateManager, ConnectionPool, RequestHandler, WebSocketHandler } from './proxies/network-proxy/index.js'; export type { IMetricsTracker, MetricsTracker } from './proxies/network-proxy/index.js'; -export * from './proxies/network-proxy/models/index.js'; +// Export models except IAcmeOptions to avoid conflict +export type { INetworkProxyOptions, ICertificateEntry, ILogger } from './proxies/network-proxy/models/types.js'; export { RouteManager as NetworkProxyRouteManager } from './proxies/network-proxy/models/types.js'; -// Export port80handler elements selectively to avoid conflicts -export { - Port80Handler, - Port80HandlerError as HttpError, - ServerError, - CertificateError -} from './http/port80/port80-handler.js'; -// Use re-export to control the names -export { Port80HandlerEvents } from './certificate/events/certificate-events.js'; +// Certificate and Port80 modules have been removed - use SmartCertManager instead export * from './redirect/classes.redirect.js'; // Export SmartProxy elements selectively to avoid RouteManager ambiguity export { SmartProxy, ConnectionManager, SecurityManager, TimeoutManager, TlsManager, NetworkProxyBridge, RouteConnectionHandler } from './proxies/smart-proxy/index.js'; export { RouteManager } from './proxies/smart-proxy/route-manager.js'; -export * from './proxies/smart-proxy/models/index.js'; +// Export smart-proxy models +export type { ISmartProxyOptions, IConnectionRecord, IRouteConfig, IRouteMatch, IRouteAction, IRouteTls, IRouteContext } from './proxies/smart-proxy/models/index.js'; +export type { TSmartProxyCertProvisionObject } from './proxies/smart-proxy/models/interfaces.js'; export * from './proxies/smart-proxy/utils/index.js'; // Original: export * from './smartproxy/classes.pp.snihandler.js' // Now we export from the new module export { SniHandler } from './tls/sni/sni-handler.js'; // Original: export * from './smartproxy/classes.pp.interfaces.js' -// Now we export from the new module -export * from './proxies/smart-proxy/models/interfaces.js'; +// Now we export from the new module (selectively to avoid conflicts) // Core types and utilities export * from './core/models/common-types.js'; +// Export IAcmeOptions from one place only +export type { IAcmeOptions } from './proxies/smart-proxy/models/interfaces.js'; + // Modular exports for new architecture export * as forwarding from './forwarding/index.js'; -export * as certificate from './certificate/index.js'; +// Certificate module has been removed - use SmartCertManager instead export * as tls from './tls/index.js'; export * as http from './http/index.js'; \ No newline at end of file diff --git a/ts/proxies/index.ts b/ts/proxies/index.ts index 372c78b..c2c40be 100644 --- a/ts/proxies/index.ts +++ b/ts/proxies/index.ts @@ -2,16 +2,19 @@ * Proxy implementations module */ -// Export NetworkProxy with selective imports to avoid RouteManager ambiguity +// Export NetworkProxy with selective imports to avoid conflicts export { NetworkProxy, CertificateManager, ConnectionPool, RequestHandler, WebSocketHandler } from './network-proxy/index.js'; export type { IMetricsTracker, MetricsTracker } from './network-proxy/index.js'; -export * from './network-proxy/models/index.js'; +// Export network-proxy models except IAcmeOptions +export type { INetworkProxyOptions, ICertificateEntry, ILogger } from './network-proxy/models/types.js'; +export { RouteManager as NetworkProxyRouteManager } from './network-proxy/models/types.js'; -// Export SmartProxy with selective imports to avoid RouteManager ambiguity +// Export SmartProxy with selective imports to avoid conflicts export { SmartProxy, ConnectionManager, SecurityManager, TimeoutManager, TlsManager, NetworkProxyBridge, RouteConnectionHandler } from './smart-proxy/index.js'; export { RouteManager as SmartProxyRouteManager } from './smart-proxy/route-manager.js'; export * from './smart-proxy/utils/index.js'; -export * from './smart-proxy/models/index.js'; +// Export smart-proxy models except IAcmeOptions +export type { ISmartProxyOptions, IConnectionRecord, IRouteConfig, IRouteMatch, IRouteAction, IRouteTls, IRouteContext } from './smart-proxy/models/index.js'; // Export NFTables proxy (no conflicts) export * from './nftables-proxy/index.js'; diff --git a/ts/proxies/network-proxy/certificate-manager.ts b/ts/proxies/network-proxy/certificate-manager.ts index 8bbab12..0a776b1 100644 --- a/ts/proxies/network-proxy/certificate-manager.ts +++ b/ts/proxies/network-proxy/certificate-manager.ts @@ -3,21 +3,17 @@ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { type INetworkProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './models/types.js'; -import { Port80Handler } from '../../http/port80/port80-handler.js'; -import { CertificateEvents } from '../../certificate/events/certificate-events.js'; -import { buildPort80Handler } from '../../certificate/acme/acme-factory.js'; -import { subscribeToPort80Handler } from '../../core/utils/event-utils.js'; -import type { IDomainOptions } from '../../certificate/models/certificate-types.js'; import type { IRouteConfig } from '../smart-proxy/models/route-types.js'; /** - * Manages SSL certificates for NetworkProxy including ACME integration + * @deprecated This class is deprecated. Use SmartCertManager instead. + * + * This is a stub implementation that maintains backward compatibility + * while the functionality has been moved to SmartCertManager. */ export class CertificateManager { private defaultCertificates: { key: string; cert: string }; private certificateCache: Map = new Map(); - private port80Handler: Port80Handler | null = null; - private externalPort80Handler: boolean = false; private certificateStoreDir: string; private logger: ILogger; private httpsServer: plugins.https.Server | null = null; @@ -26,6 +22,8 @@ export class CertificateManager { this.certificateStoreDir = path.resolve(options.acme?.certificateStore || './certs'); this.logger = createLogger(options.logLevel || 'info'); + this.logger.warn('CertificateManager is deprecated - use SmartCertManager instead'); + // Ensure certificate store directory exists try { if (!fs.existsSync(this.certificateStoreDir)) { @@ -44,7 +42,6 @@ export class CertificateManager { */ public loadDefaultCertificates(): void { const __dirname = path.dirname(fileURLToPath(import.meta.url)); - // Fix the path to look for certificates at the project root instead of inside ts directory const certPath = path.join(__dirname, '..', '..', '..', 'assets', 'certs'); try { @@ -52,467 +49,145 @@ export class CertificateManager { key: fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'), cert: fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8') }; - this.logger.info('Default certificates loaded successfully'); + this.logger.info('Loaded default certificates from filesystem'); } catch (error) { - this.logger.error('Error loading default certificates', error); - - // Generate self-signed fallback certificates - try { - // This is a placeholder for actual certificate generation code - // In a real implementation, you would use a library like selfsigned to generate certs - this.defaultCertificates = { - key: "FALLBACK_KEY_CONTENT", - cert: "FALLBACK_CERT_CONTENT" - }; - this.logger.warn('Using fallback self-signed certificates'); - } catch (fallbackError) { - this.logger.error('Failed to generate fallback certificates', fallbackError); - throw new Error('Could not load or generate SSL certificates'); - } + this.logger.error(`Failed to load default certificates: ${error}`); + this.generateSelfSignedCertificate(); } } - + /** - * Set the HTTPS server reference for context updates + * Generates self-signed certificates as fallback + */ + private generateSelfSignedCertificate(): void { + // Generate a self-signed certificate using forge or similar + // For now, just use a placeholder + const selfSignedCert = `-----BEGIN CERTIFICATE----- +MIIBkTCB+wIJAKHHIgIIA0/cMA0GCSqGSIb3DQEBBQUAMA0xCzAJBgNVBAYTAlVT +MB4XDTE0MDEwMTAwMDAwMFoXDTI0MDEwMTAwMDAwMFowDTELMAkGA1UEBhMCVVMw +gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMRiH0VwnOH3jCV7c6JFZWYrvuqy +-----END CERTIFICATE-----`; + + const selfSignedKey = `-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMRiH0VwnOH3jCV7 +c6JFZWYrvuqyALCLXj0pcr1iqNdHjegNXnkl5zjdaUjq4edNOKl7M1AlFiYjG2xk +-----END PRIVATE KEY-----`; + + this.defaultCertificates = { + key: selfSignedKey, + cert: selfSignedCert + }; + + this.logger.warn('Using self-signed certificate as fallback'); + } + + /** + * Gets the default certificates + */ + public getDefaultCertificates(): { key: string; cert: string } { + return this.defaultCertificates; + } + + /** + * @deprecated Use SmartCertManager instead + */ + public setExternalPort80Handler(handler: any): void { + this.logger.warn('setExternalPort80Handler is deprecated - use SmartCertManager instead'); + } + + /** + * @deprecated Use SmartCertManager instead + */ + public async updateRoutes(routes: IRouteConfig[]): Promise { + this.logger.warn('updateRoutes is deprecated - use SmartCertManager instead'); + } + + /** + * Handles SNI callback to provide appropriate certificate + */ + public handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void { + const certificate = this.getCachedCertificate(domain); + + if (certificate) { + const context = plugins.tls.createSecureContext({ + key: certificate.key, + cert: certificate.cert + }); + cb(null, context); + return; + } + + // Use default certificate if no domain-specific certificate found + const defaultContext = plugins.tls.createSecureContext({ + key: this.defaultCertificates.key, + cert: this.defaultCertificates.cert + }); + cb(null, defaultContext); + } + + /** + * Updates a certificate in the cache + */ + public updateCertificate(domain: string, cert: string, key: string): void { + this.certificateCache.set(domain, { + cert, + key, + expires: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days + }); + + this.logger.info(`Certificate updated for ${domain}`); + } + + /** + * Gets a cached certificate + */ + private getCachedCertificate(domain: string): ICertificateEntry | null { + return this.certificateCache.get(domain) || null; + } + + /** + * @deprecated Use SmartCertManager instead + */ + public async initializePort80Handler(): Promise { + this.logger.warn('initializePort80Handler is deprecated - use SmartCertManager instead'); + return null; + } + + /** + * @deprecated Use SmartCertManager instead + */ + public async stopPort80Handler(): Promise { + this.logger.warn('stopPort80Handler is deprecated - use SmartCertManager instead'); + } + + /** + * @deprecated Use SmartCertManager instead + */ + public registerDomainsWithPort80Handler(domains: string[]): void { + this.logger.warn('registerDomainsWithPort80Handler is deprecated - use SmartCertManager instead'); + } + + /** + * @deprecated Use SmartCertManager instead + */ + public registerRoutesWithPort80Handler(routes: IRouteConfig[]): void { + this.logger.warn('registerRoutesWithPort80Handler is deprecated - use SmartCertManager instead'); + } + + /** + * Sets the HTTPS server for certificate updates */ public setHttpsServer(server: plugins.https.Server): void { this.httpsServer = server; } - - /** - * Get default certificates - */ - public getDefaultCertificates(): { key: string; cert: string } { - return { ...this.defaultCertificates }; - } - - /** - * Sets an external Port80Handler for certificate management - */ - public setExternalPort80Handler(handler: Port80Handler): void { - if (this.port80Handler && !this.externalPort80Handler) { - this.logger.warn('Replacing existing internal Port80Handler with external handler'); - - // Clean up existing handler if needed - if (this.port80Handler !== handler) { - // Unregister event handlers to avoid memory leaks - this.port80Handler.removeAllListeners(CertificateEvents.CERTIFICATE_ISSUED); - this.port80Handler.removeAllListeners(CertificateEvents.CERTIFICATE_RENEWED); - this.port80Handler.removeAllListeners(CertificateEvents.CERTIFICATE_FAILED); - this.port80Handler.removeAllListeners(CertificateEvents.CERTIFICATE_EXPIRING); - } - } - - // Set the external handler - this.port80Handler = handler; - this.externalPort80Handler = true; - - // 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'); - - // Register domains with Port80Handler if we have any certificates cached - if (this.certificateCache.size > 0) { - const domains = Array.from(this.certificateCache.keys()) - .filter(domain => !domain.includes('*')); // Skip wildcard domains - - this.registerDomainsWithPort80Handler(domains); - } - } /** - * Update route configurations managed by this certificate manager - * This method is called when route configurations change - * - * @param routes Array of route configurations + * Gets statistics for metrics */ - public updateRouteConfigs(routes: IRouteConfig[]): void { - if (!this.port80Handler) { - this.logger.warn('Cannot update routes - Port80Handler is not initialized'); - return; - } - - // Register domains from routes with Port80Handler - this.registerRoutesWithPort80Handler(routes); - - // Process individual routes for certificate requirements - for (const route of routes) { - this.processRouteForCertificates(route); - } - - this.logger.info(`Updated certificate management for ${routes.length} routes`); - } - - /** - * Handle newly issued or renewed certificates from Port80Handler - */ - private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void { - const { domain, certificate, privateKey, expiryDate } = data; - - this.logger.info(`Certificate ${this.certificateCache.has(domain) ? 'renewed' : 'issued'} for ${domain}, valid until ${expiryDate.toISOString()}`); - - // Update certificate in HTTPS server - this.updateCertificateCache(domain, certificate, privateKey, expiryDate); - - // Save the certificate to the filesystem if not using external handler - if (!this.externalPort80Handler && this.options.acme?.certificateStore) { - this.saveCertificateToStore(domain, certificate, privateKey); - } - } - - /** - * Handle certificate issuance failures - */ - private handleCertificateFailed(data: { domain: string; error: string }): void { - this.logger.error(`Certificate issuance failed for ${data.domain}: ${data.error}`); - } - - /** - * Saves certificate and private key to the filesystem - */ - private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void { - try { - const certPath = path.join(this.certificateStoreDir, `${domain}.cert.pem`); - const keyPath = path.join(this.certificateStoreDir, `${domain}.key.pem`); - - fs.writeFileSync(certPath, certificate); - fs.writeFileSync(keyPath, privateKey); - - // Ensure private key has restricted permissions - try { - fs.chmodSync(keyPath, 0o600); - } catch (error) { - this.logger.warn(`Failed to set permissions on private key for ${domain}: ${error}`); - } - - this.logger.info(`Saved certificate for ${domain} to ${certPath}`); - } catch (error) { - this.logger.error(`Failed to save certificate for ${domain}: ${error}`); - } - } - - /** - * Handles SNI (Server Name Indication) for TLS connections - * Used by the HTTPS server to select the correct certificate for each domain - */ - public handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void { - this.logger.debug(`SNI request for domain: ${domain}`); - - // Check if we have a certificate for this domain - const certs = this.certificateCache.get(domain); - if (certs) { - try { - // Create TLS context with the cached certificate - const context = plugins.tls.createSecureContext({ - key: certs.key, - cert: certs.cert - }); - this.logger.debug(`Using cached certificate for ${domain}`); - cb(null, context); - return; - } catch (err) { - this.logger.error(`Error creating secure context for ${domain}:`, err); - } - } - // No existing certificate: trigger dynamic provisioning via Port80Handler - if (this.port80Handler) { - try { - this.logger.info(`Triggering on-demand certificate retrieval for ${domain}`); - this.port80Handler.addDomain({ - domainName: domain, - sslRedirect: false, - acmeMaintenance: true - }); - } catch (err) { - this.logger.error(`Error registering domain for on-demand certificate: ${domain}`, err); - } - } - - // Check if we should trigger certificate issuance - if (this.options.acme?.enabled && this.port80Handler && !domain.includes('*')) { - // Check if this domain is already registered - const certData = this.port80Handler.getCertificate(domain); - - if (!certData) { - this.logger.info(`No certificate found for ${domain}, registering for issuance`); - - // Register with new domain options format - const domainOptions: IDomainOptions = { - domainName: domain, - sslRedirect: true, - acmeMaintenance: true - }; - - this.port80Handler.addDomain(domainOptions); - } - } - - // Fall back to default certificate - try { - const context = plugins.tls.createSecureContext({ - key: this.defaultCertificates.key, - cert: this.defaultCertificates.cert - }); - - this.logger.debug(`Using default certificate for ${domain}`); - cb(null, context); - } catch (err) { - this.logger.error(`Error creating default secure context:`, err); - cb(new Error('Cannot create secure context'), null); - } - } - - /** - * Updates certificate in cache - */ - public updateCertificateCache(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void { - // Update certificate context in HTTPS server if it's running - if (this.httpsServer) { - try { - this.httpsServer.addContext(domain, { - key: privateKey, - cert: certificate - }); - this.logger.debug(`Updated SSL context for domain: ${domain}`); - } catch (error) { - this.logger.error(`Error updating SSL context for domain ${domain}:`, error); - } - } - - // Update certificate in cache - this.certificateCache.set(domain, { - key: privateKey, - cert: certificate, - expires: expiryDate - }); - } - - /** - * Gets a certificate for a domain - */ - public getCertificate(domain: string): ICertificateEntry | undefined { - return this.certificateCache.get(domain); - } - - /** - * Requests a new certificate for a domain - */ - public async requestCertificate(domain: string): Promise { - if (!this.options.acme?.enabled && !this.externalPort80Handler) { - this.logger.warn('ACME certificate management is not enabled'); - return false; - } - - if (!this.port80Handler) { - this.logger.error('Port80Handler is not initialized'); - return false; - } - - // Skip wildcard domains - can't get certs for these with HTTP-01 validation - if (domain.includes('*')) { - this.logger.error(`Cannot request certificate for wildcard domain: ${domain}`); - return false; - } - - try { - // Use the new domain options format - const domainOptions: IDomainOptions = { - domainName: domain, - sslRedirect: true, - acmeMaintenance: true - }; - - this.port80Handler.addDomain(domainOptions); - this.logger.info(`Certificate request submitted for domain: ${domain}`); - return true; - } catch (error) { - this.logger.error(`Error requesting certificate for domain ${domain}:`, error); - return false; - } - } - - /** - * Registers domains with Port80Handler for ACME certificate management - * @param domains String array of domains to register - */ - public registerDomainsWithPort80Handler(domains: string[]): void { - if (!this.port80Handler) { - this.logger.warn('Port80Handler is not initialized'); - return; - } - - for (const domain of domains) { - // Skip wildcard domains - can't get certs for these with HTTP-01 validation - if (domain.includes('*')) { - this.logger.info(`Skipping wildcard domain for ACME: ${domain}`); - continue; - } - - // Skip domains already with certificates if configured to do so - if (this.options.acme?.skipConfiguredCerts) { - const cachedCert = this.certificateCache.get(domain); - if (cachedCert) { - this.logger.info(`Skipping domain with existing certificate: ${domain}`); - continue; - } - } - - // Register the domain for certificate issuance with new domain options format - const domainOptions: IDomainOptions = { - domainName: domain, - sslRedirect: true, - acmeMaintenance: true - }; - - this.port80Handler.addDomain(domainOptions); - this.logger.info(`Registered domain for ACME certificate issuance: ${domain}`); - } - } - - /** - * Extract domains from route configurations and register with Port80Handler - * This method enables direct integration with route-based configuration - * - * @param routes Array of route configurations - */ - public registerRoutesWithPort80Handler(routes: IRouteConfig[]): void { - if (!this.port80Handler) { - this.logger.warn('Port80Handler is not initialized'); - return; - } - - // Extract domains from route configurations - const domains: Set = new Set(); - - for (const route of routes) { - // Skip disabled routes - if (route.enabled === false) { - continue; - } - - // Skip routes without HTTPS termination - if (route.action.type !== 'forward' || route.action.tls?.mode !== 'terminate') { - continue; - } - - // Extract domains from match criteria - if (route.match.domains) { - if (typeof route.match.domains === 'string') { - domains.add(route.match.domains); - } else if (Array.isArray(route.match.domains)) { - for (const domain of route.match.domains) { - domains.add(domain); - } - } - } - } - - // Register extracted domains - this.registerDomainsWithPort80Handler(Array.from(domains)); - } - - /** - * Process a route config to determine if it requires automatic certificate provisioning - * @param route Route configuration to process - */ - public processRouteForCertificates(route: IRouteConfig): void { - // Skip disabled routes - if (route.enabled === false) { - return; - } - - // Skip routes without HTTPS termination or auto certificate - if (route.action.type !== 'forward' || - route.action.tls?.mode !== 'terminate' || - route.action.tls?.certificate !== 'auto') { - return; - } - - // Extract domains from match criteria - const domains: string[] = []; - if (route.match.domains) { - if (typeof route.match.domains === 'string') { - domains.push(route.match.domains); - } else if (Array.isArray(route.match.domains)) { - domains.push(...route.match.domains); - } - } - - // Request certificates for the domains - for (const domain of domains) { - if (!domain.includes('*')) { // Skip wildcard domains - this.requestCertificate(domain).catch(err => { - this.logger.error(`Error requesting certificate for domain ${domain}:`, err); - }); - } - } - } - - /** - * Initialize internal Port80Handler - */ - public async initializePort80Handler(): Promise { - // Skip if using external handler - if (this.externalPort80Handler) { - this.logger.info('Using external Port80Handler, skipping initialization'); - return this.port80Handler; - } - - if (!this.options.acme?.enabled) { - return null; - } - - // Build and configure Port80Handler - this.port80Handler = buildPort80Handler({ - port: this.options.acme.port, - accountEmail: this.options.acme.accountEmail, - useProduction: this.options.acme.useProduction, - httpsRedirectPort: this.options.port, // Redirect to our HTTPS port - enabled: this.options.acme.enabled, - certificateStore: this.options.acme.certificateStore, - skipConfiguredCerts: this.options.acme.skipConfiguredCerts - }); - // 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 - try { - await this.port80Handler.start(); - this.logger.info(`Port80Handler started on port ${this.options.acme.port}`); - return this.port80Handler; - } catch (error) { - this.logger.error(`Failed to start Port80Handler: ${error}`); - this.port80Handler = null; - return null; - } - } - - /** - * Stop the Port80Handler if it was internally created - */ - public async stopPort80Handler(): Promise { - if (this.port80Handler && !this.externalPort80Handler) { - try { - await this.port80Handler.stop(); - this.logger.info('Port80Handler stopped'); - } catch (error) { - this.logger.error('Error stopping Port80Handler', error); - } - } + public getStats() { + return { + cachedCertificates: this.certificateCache.size, + defaultCertEnabled: true + }; } } \ No newline at end of file diff --git a/ts/proxies/network-proxy/models/types.ts b/ts/proxies/network-proxy/models/types.ts index 46ec077..4f58f2e 100644 --- a/ts/proxies/network-proxy/models/types.ts +++ b/ts/proxies/network-proxy/models/types.ts @@ -1,5 +1,17 @@ import * as plugins from '../../../plugins.js'; -import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js'; +// Certificate types removed - define IAcmeOptions locally +export interface IAcmeOptions { + enabled: boolean; + email?: string; + accountEmail?: string; + port?: number; + certificateStore?: string; + environment?: 'production' | 'staging'; + useProduction?: boolean; + renewThresholdDays?: number; + autoRenew?: boolean; + skipConfiguredCerts?: boolean; +} import type { IRouteConfig } from '../../smart-proxy/models/route-types.js'; import type { IRouteContext } from '../../../core/models/route-context.js'; @@ -22,7 +34,7 @@ export interface INetworkProxyOptions { // Settings for SmartProxy integration connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend portProxyIntegration?: boolean; // Flag to indicate this proxy is used by SmartProxy - useExternalPort80Handler?: boolean; // Flag to indicate using external Port80Handler + useExternalPort80Handler?: boolean; // @deprecated - use SmartCertManager instead // Protocol to use when proxying to backends: HTTP/1.x or HTTP/2 backendProtocol?: 'http1' | 'http2'; diff --git a/ts/proxies/network-proxy/network-proxy.ts b/ts/proxies/network-proxy/network-proxy.ts index 68db24d..cf7d6db 100644 --- a/ts/proxies/network-proxy/network-proxy.ts +++ b/ts/proxies/network-proxy/network-proxy.ts @@ -18,7 +18,6 @@ import { RequestHandler, type IMetricsTracker } from './request-handler.js'; import { WebSocketHandler } from './websocket-handler.js'; import { ProxyRouter } from '../../http/router/index.js'; import { RouteRouter } from '../../http/router/route-router.js'; -import { Port80Handler } from '../../http/port80/port80-handler.js'; import { FunctionCache } from './function-cache.js'; /** @@ -221,15 +220,10 @@ export class NetworkProxy implements IMetricsTracker { } /** - * Sets an external Port80Handler for certificate management - * This allows the NetworkProxy to use a centrally managed Port80Handler - * instead of creating its own - * - * @param handler The Port80Handler instance to use + * @deprecated Use SmartCertManager instead */ - public setExternalPort80Handler(handler: Port80Handler): void { - // Connect it to the certificate manager - this.certificateManager.setExternalPort80Handler(handler); + public setExternalPort80Handler(handler: any): void { + this.logger.warn('Port80Handler is deprecated - use SmartCertManager instead'); } /** @@ -238,10 +232,7 @@ export class NetworkProxy implements IMetricsTracker { public async start(): Promise { this.startTime = Date.now(); - // Initialize Port80Handler if enabled and not using external handler - if (this.options.acme?.enabled && !this.options.useExternalPort80Handler) { - await this.certificateManager.initializePort80Handler(); - } + // Certificate management is now handled by SmartCertManager // Create HTTP/2 server with HTTP/1 fallback this.httpsServer = plugins.http2.createSecureServer( @@ -385,7 +376,7 @@ export class NetworkProxy implements IMetricsTracker { // Directly update the certificate manager with the new routes // This will extract domains and handle certificate provisioning - this.certificateManager.updateRouteConfigs(routes); + this.certificateManager.updateRoutes(routes); // Collect all domains and certificates for configuration const currentHostnames = new Set(); @@ -425,7 +416,7 @@ export class NetworkProxy implements IMetricsTracker { // Update certificate cache with any static certificates for (const [domain, certData] of certificateUpdates.entries()) { try { - this.certificateManager.updateCertificateCache( + this.certificateManager.updateCertificate( domain, certData.cert, certData.key @@ -547,8 +538,7 @@ export class NetworkProxy implements IMetricsTracker { // Close all connection pool connections this.connectionPool.closeAllConnections(); - // Stop Port80Handler if internally managed - await this.certificateManager.stopPort80Handler(); + // Certificate management cleanup is handled by SmartCertManager // Close the HTTPS server return new Promise((resolve) => { @@ -566,7 +556,8 @@ export class NetworkProxy implements IMetricsTracker { * @returns A promise that resolves when the request is submitted (not when the certificate is issued) */ public async requestCertificate(domain: string): Promise { - return this.certificateManager.requestCertificate(domain); + this.logger.warn('requestCertificate is deprecated - use SmartCertManager instead'); + return false; } /** @@ -587,7 +578,7 @@ export class NetworkProxy implements IMetricsTracker { expiryDate?: Date ): void { this.logger.info(`Updating certificate for ${domain}`); - this.certificateManager.updateCertificateCache(domain, certificate, privateKey, expiryDate); + this.certificateManager.updateCertificate(domain, certificate, privateKey); } /** diff --git a/ts/proxies/smart-proxy/models/index.ts b/ts/proxies/smart-proxy/models/index.ts index 09bb8f6..4e31417 100644 --- a/ts/proxies/smart-proxy/models/index.ts +++ b/ts/proxies/smart-proxy/models/index.ts @@ -1,5 +1,6 @@ /** * SmartProxy models */ -export * from './interfaces.js'; +// Export everything except IAcmeOptions from interfaces +export type { ISmartProxyOptions, IConnectionRecord, TSmartProxyCertProvisionObject } from './interfaces.js'; export * from './route-types.js'; diff --git a/ts/proxies/smart-proxy/models/interfaces.ts b/ts/proxies/smart-proxy/models/interfaces.ts index 33793ec..a9d119c 100644 --- a/ts/proxies/smart-proxy/models/interfaces.ts +++ b/ts/proxies/smart-proxy/models/interfaces.ts @@ -1,5 +1,18 @@ import * as plugins from '../../../plugins.js'; -import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js'; +// Certificate types removed - define IAcmeOptions locally +export interface IAcmeOptions { + enabled?: boolean; + email?: string; + environment?: 'production' | 'staging'; + port?: number; + useProduction?: boolean; + renewThresholdDays?: number; + autoRenew?: boolean; + certificateStore?: string; + skipConfiguredCerts?: boolean; + renewCheckIntervalHours?: number; + routeForwards?: any[]; +} import type { IRouteConfig } from './route-types.js'; import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; diff --git a/ts/proxies/smart-proxy/models/route-types.ts b/ts/proxies/smart-proxy/models/route-types.ts index 17af275..ee6f81b 100644 --- a/ts/proxies/smart-proxy/models/route-types.ts +++ b/ts/proxies/smart-proxy/models/route-types.ts @@ -1,5 +1,5 @@ import * as plugins from '../../../plugins.js'; -import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js'; +// Certificate types removed - use local definition import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js'; diff --git a/ts/proxies/smart-proxy/smart-proxy.ts b/ts/proxies/smart-proxy/smart-proxy.ts index 2d73939..5d8b69d 100644 --- a/ts/proxies/smart-proxy/smart-proxy.ts +++ b/ts/proxies/smart-proxy/smart-proxy.ts @@ -121,13 +121,12 @@ export class SmartProxy extends plugins.EventEmitter { this.settings.acme = { enabled: false, port: 80, - accountEmail: 'admin@example.com', + email: 'admin@example.com', useProduction: false, renewThresholdDays: 30, autoRenew: true, certificateStore: './certs', skipConfiguredCerts: false, - httpsRedirectPort: 443, renewCheckIntervalHours: 24, routeForwards: [] };