BREAKING CHANGE(certificates): Remove legacy certificate modules and Port80Handler; update documentation and route configurations to use SmartCertManager for certificate management.
This commit is contained in:
		| @@ -1,5 +1,13 @@ | |||||||
| # Changelog | # 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) | ## 2025-05-18 - 18.2.0 - feat(smartproxy/certificate) | ||||||
| Integrate HTTP-01 challenge handler into ACME certificate provisioning workflow | Integrate HTTP-01 challenge handler into ACME certificate provisioning workflow | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										217
									
								
								docs/certificate-management.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								docs/certificate-management.md
									
									
									
									
									
										Normal file
									
								
							| @@ -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** | ||||||
							
								
								
									
										18
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								readme.md
									
									
									
									
									
								
							| @@ -21,10 +21,10 @@ SmartProxy has been restructured using a modern, modular architecture with a uni | |||||||
| │   ├── /models               # Data models and interfaces | │   ├── /models               # Data models and interfaces | ||||||
| │   ├── /utils                # Shared utilities (IP validation, logging, etc.) | │   ├── /utils                # Shared utilities (IP validation, logging, etc.) | ||||||
| │   └── /events               # Common event definitions | │   └── /events               # Common event definitions | ||||||
| ├── /certificate              # Certificate management | ├── /certificate              # Certificate management (deprecated in v18+) | ||||||
| │   ├── /acme                 # ACME-specific functionality | │   ├── /acme                 # Moved to SmartCertManager | ||||||
| │   ├── /providers            # Certificate providers (static, ACME) | │   ├── /providers            # Now integrated in route configuration  | ||||||
| │   └── /storage              # Certificate storage mechanisms | │   └── /storage              # Now uses CertStore | ||||||
| ├── /forwarding               # Forwarding system | ├── /forwarding               # Forwarding system | ||||||
| │   ├── /handlers             # Various forwarding handlers | │   ├── /handlers             # Various forwarding handlers | ||||||
| │   │   ├── base-handler.ts   # Abstract base handler | │   │   ├── 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 | │   │   ├── /models           # SmartProxy-specific interfaces | ||||||
| │   │   │   ├── route-types.ts # Route-based configuration types | │   │   │   ├── route-types.ts # Route-based configuration types | ||||||
| │   │   │   └── interfaces.ts  # SmartProxy interfaces | │   │   │   └── 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-helpers.ts  # Helper functions for creating routes | ||||||
| │   │   ├── route-manager.ts  # Route management system | │   │   ├── route-manager.ts  # Route management system | ||||||
| │   │   ├── smart-proxy.ts    # Main SmartProxy class | │   │   ├── 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 | │   ├── /sni                  # SNI handling components | ||||||
| │   └── /alerts               # TLS alerts system | │   └── /alerts               # TLS alerts system | ||||||
| └── /http                     # HTTP-specific functionality | └── /http                     # HTTP-specific functionality | ||||||
|     ├── /port80               # Port80Handler components |     ├── /port80               # Port80Handler (removed in v18+) | ||||||
|     ├── /router               # HTTP routing system |     ├── /router               # HTTP routing system | ||||||
|     └── /redirects            # Redirect handlers |     └── /redirects            # Redirect handlers | ||||||
| ``` | ``` | ||||||
| @@ -1411,6 +1413,12 @@ NetworkProxy now supports full route-based configuration including: | |||||||
| - `useIPSets` (boolean, default true) | - `useIPSets` (boolean, default true) | ||||||
| - `qos`, `netProxyIntegration` (objects) | - `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 | ## Troubleshooting | ||||||
|  |  | ||||||
| ### SmartProxy | ### SmartProxy | ||||||
|   | |||||||
| @@ -1,5 +1,22 @@ | |||||||
| # ACME/Certificate Simplification Plan for SmartProxy | # 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 | ## Command to reread CLAUDE.md | ||||||
| `reread /home/philkunz/.claude/CLAUDE.md` | `reread /home/philkunz/.claude/CLAUDE.md` | ||||||
|  |  | ||||||
| @@ -71,14 +88,13 @@ ts/proxies/smart-proxy/ | |||||||
|  |  | ||||||
| ### Phase 1: Create SmartCertManager | ### Phase 1: Create SmartCertManager | ||||||
|  |  | ||||||
| #### 1.1 Create certificate-manager.ts | #### 1.1 Create certificate-manager.ts ✅ COMPLETED | ||||||
| ```typescript | ```typescript | ||||||
| // ts/proxies/smart-proxy/certificate-manager.ts | // ts/proxies/smart-proxy/certificate-manager.ts | ||||||
| import * as plugins from '../../plugins.js'; | import * as plugins from '../../plugins.js'; | ||||||
| import { NetworkProxy } from '../network-proxy/index.js'; | import { NetworkProxy } from '../network-proxy/index.js'; | ||||||
| import type { IRouteConfig, IRouteTls } from './models/route-types.js'; | import type { IRouteConfig, IRouteTls } from './models/route-types.js'; | ||||||
| import { CertStore } from './cert-store.js'; | import { CertStore } from './cert-store.js'; | ||||||
| import { AcmeClient } from './acme-client.js'; |  | ||||||
|  |  | ||||||
| export interface ICertStatus { | export interface ICertStatus { | ||||||
|   domain: string; |   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 | ```typescript | ||||||
| // ts/proxies/smart-proxy/cert-store.ts | // ts/proxies/smart-proxy/cert-store.ts | ||||||
| import * as plugins from '../../plugins.js'; | import * as plugins from '../../plugins.js'; | ||||||
| @@ -675,7 +691,7 @@ export class CertStore { | |||||||
|  |  | ||||||
| ### Phase 2: Update Route Types and Handler | ### Phase 2: Update Route Types and Handler | ||||||
|  |  | ||||||
| #### 2.1 Update route-types.ts | #### 2.1 Update route-types.ts ✅ COMPLETED | ||||||
| ```typescript | ```typescript | ||||||
| // Add to ts/proxies/smart-proxy/models/route-types.ts | // 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 | ```typescript | ||||||
| // Add to ts/proxies/smart-proxy/route-connection-handler.ts | // Add to ts/proxies/smart-proxy/route-connection-handler.ts | ||||||
|  |  | ||||||
| @@ -839,7 +855,7 @@ function getStatusText(status: number): string { | |||||||
|  |  | ||||||
| ### Phase 3: SmartProxy Integration | ### Phase 3: SmartProxy Integration | ||||||
|  |  | ||||||
| #### 3.1 Update SmartProxy class | #### 3.1 Update SmartProxy class ✅ COMPLETED | ||||||
| ```typescript | ```typescript | ||||||
| // Changes to ts/proxies/smart-proxy/smart-proxy.ts | // 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 | ```typescript | ||||||
| // Simplified ts/proxies/smart-proxy/network-proxy-bridge.ts | // 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 | ### Phase 5: Update HTTP Module | ||||||
|  |  | ||||||
| #### 5.1 Update http/index.ts | #### 5.1 Update http/index.ts ✅ COMPLETED | ||||||
| ```typescript | ```typescript | ||||||
| // ts/http/index.ts | // ts/http/index.ts | ||||||
| /** | /** | ||||||
| @@ -1388,25 +1404,26 @@ The simplification leverages SmartProxy's existing capabilities rather than rein | |||||||
|  |  | ||||||
| ## Implementation Sequence | ## Implementation Sequence | ||||||
|  |  | ||||||
| 1. **Day 1: Core Implementation** | 1. **Day 1: Core Implementation** ✅ COMPLETED | ||||||
|    - Create SmartCertManager class |    - Create SmartCertManager class | ||||||
|    - Create CertStore and AcmeClient |    - Create CertStore | ||||||
|    - Update route types |    - Update route types | ||||||
|  |    - Integrated with SmartAcme's built-in handlers | ||||||
|  |  | ||||||
| 2. **Day 2: Integration** | 2. **Day 2: Integration** ✅ COMPLETED | ||||||
|    - Update SmartProxy to use SmartCertManager |    - Update SmartProxy to use SmartCertManager | ||||||
|    - Simplify NetworkProxyBridge |    - Simplify NetworkProxyBridge | ||||||
|    - Remove old certificate system |    - Update HTTP index.ts | ||||||
|  |  | ||||||
| 3. **Day 3: Testing** | 3. **Day 3: Testing** ✅ COMPLETED | ||||||
|    - Create new tests using new format only |    - Created test.smartacme-integration.ts | ||||||
|    - No migration testing needed |    - Verified SmartAcme handler access | ||||||
|    - Test all new functionality |    - Verified certificate manager creation | ||||||
|  |  | ||||||
| 4. **Day 4: Documentation & Cleanup** | 4. **Day 4: Documentation & Cleanup** 🔄 IN PROGRESS | ||||||
|    - Update all documentation |    - ❌ Update all documentation   | ||||||
|    - Clean up old files |    - ❌ Clean up old files (certificate/ and port80/) | ||||||
|    - Final testing and validation |    - ❌ Final testing and validation | ||||||
|  |  | ||||||
| ## Risk Mitigation | ## Risk Mitigation | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@push.rocks/smartproxy', |   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.' |   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.' | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
| @@ -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<string, any>; |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * 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<void> { |  | ||||||
|     // 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; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| /** |  | ||||||
|  * ACME certificate provisioning |  | ||||||
|  */ |  | ||||||
| @@ -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' |  | ||||||
| } |  | ||||||
| @@ -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<TCertProvisionObject> |  | ||||||
| ): 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 |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
| @@ -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 |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @@ -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<TCertProvisionObject>; |  | ||||||
|   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<string, { type: 'http01' | 'dns01' | 'static', routeRef?: ICertRoute }>; |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * 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<TCertProvisionObject>, |  | ||||||
|     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<void> { |  | ||||||
|     // 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<void> { |  | ||||||
|     for (const certRoute of this.certRoutes) { |  | ||||||
|       await this.provisionCertificateForRoute(certRoute); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Provision a certificate for a route |  | ||||||
|    */ |  | ||||||
|   private async provisionCertificateForRoute(certRoute: ICertRoute): Promise<void> { |  | ||||||
|     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<void> { |  | ||||||
|     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<void> { |  | ||||||
|     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<void> { |  | ||||||
|     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<void> { |  | ||||||
|     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<void> { |  | ||||||
|     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<void> { |  | ||||||
|     // 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; |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Certificate providers |  | ||||||
|  */ |  | ||||||
| @@ -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<void> { |  | ||||||
|     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<ICertificateData | null> { |  | ||||||
|     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<boolean> { |  | ||||||
|     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<string[]> { |  | ||||||
|     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<Array<{ domain: string; expiryDate: Date; daysRemaining: number }>> { |  | ||||||
|     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<void> { |  | ||||||
|     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, '_'); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,3 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Certificate storage mechanisms |  | ||||||
|  */ |  | ||||||
| @@ -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 }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| import type { Port80Handler } from '../http/port80/port80-handler.js'; | // Port80Handler removed - use SmartCertManager instead | ||||||
| import { Port80HandlerEvents } from './types.js'; | import { Port80HandlerEvents } from './types.js'; | ||||||
| import type { ICertificateData, ICertificateFailure, ICertificateExpiring } 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 |  * Subscribes to Port80Handler events based on provided callbacks | ||||||
|  */ |  */ | ||||||
| export function subscribeToPort80Handler( | export function subscribeToPort80Handler( | ||||||
|   handler: Port80Handler, |   handler: any, | ||||||
|   subscribers: Port80HandlerSubscribers |   subscribers: Port80HandlerSubscribers | ||||||
| ): void { | ): void { | ||||||
|   if (subscribers.onCertificateIssued) { |   if (subscribers.onCertificateIssued) { | ||||||
|   | |||||||
| @@ -34,7 +34,7 @@ export interface ICertificateData { | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Events emitted by the Port80Handler |  * @deprecated Events emitted by the Port80Handler - use SmartCertManager instead | ||||||
|  */ |  */ | ||||||
| export enum Port80HandlerEvents { | export enum Port80HandlerEvents { | ||||||
|   CERTIFICATE_ISSUED = 'certificate-issued', |   CERTIFICATE_ISSUED = 'certificate-issued', | ||||||
|   | |||||||
| @@ -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 { 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 { | export interface IPort80HandlerSubscribers { | ||||||
|   onCertificateIssued?: (data: ICertificateData) => void; |   onCertificateIssued?: (data: any) => void; | ||||||
|   onCertificateRenewed?: (data: ICertificateData) => void; |   onCertificateRenewed?: (data: any) => void; | ||||||
|   onCertificateFailed?: (data: ICertificateFailure) => void; |   onCertificateFailed?: (data: any) => void; | ||||||
|   onCertificateExpiring?: (data: ICertificateExpiring) => void; |   onCertificateExpiring?: (data: any) => void; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Subscribes to Port80Handler events based on provided callbacks |  * @deprecated Use SmartCertManager instead | ||||||
|  */ |  */ | ||||||
| export function subscribeToPort80Handler( | export function subscribeToPort80Handler( | ||||||
|   handler: Port80Handler, |   handler: any, | ||||||
|   subscribers: IPort80HandlerSubscribers |   subscribers: IPort80HandlerSubscribers | ||||||
| ): void { | ): void { | ||||||
|   if (subscribers.onCertificateIssued) { |   console.warn('subscribeToPort80Handler is deprecated - use SmartCertManager instead'); | ||||||
|     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); |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| @@ -1,8 +1,12 @@ | |||||||
| import * as plugins from '../../plugins.js'; | import * as plugins from '../../plugins.js'; | ||||||
| import type { | // Certificate types have been removed - use SmartCertManager instead | ||||||
|   IDomainOptions, | export interface IDomainOptions { | ||||||
|   IAcmeOptions |   domainName: string; | ||||||
| } from '../../certificate/models/certificate-types.js'; |   sslRedirect: boolean; | ||||||
|  |   acmeMaintenance: boolean; | ||||||
|  |   forward?: { ip: string; port: number }; | ||||||
|  |   acmeForward?: { ip: string; port: number }; | ||||||
|  | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * HTTP-specific event types |  * HTTP-specific event types | ||||||
|   | |||||||
| @@ -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<any>[]; |  | ||||||
|   challengePriority?: string[]; |  | ||||||
|   retryOptions?: { |  | ||||||
|     retries?: number; |  | ||||||
|     factor?: number; |  | ||||||
|     minTimeoutMs?: number; |  | ||||||
|     maxTimeoutMs?: number; |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Interface for certificate manager |  | ||||||
|  */ |  | ||||||
| export interface ICertManager { |  | ||||||
|   init(): Promise<void>; |  | ||||||
|   get(domainName: string): Promise<ISmartAcmeCert | null>; |  | ||||||
|   put(cert: ISmartAcmeCert): Promise<ISmartAcmeCert>; |  | ||||||
|   delete(domainName: string): Promise<void>; |  | ||||||
|   close?(): Promise<void>; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Interface for challenge handler |  | ||||||
|  */ |  | ||||||
| export interface IChallengeHandler<T> { |  | ||||||
|   getSupportedTypes(): string[]; |  | ||||||
|   prepare(ch: T): Promise<void>; |  | ||||||
|   verify?(ch: T): Promise<void>; |  | ||||||
|   cleanup(ch: T): Promise<void>; |  | ||||||
|   checkWetherDomainIsSupported(domain: string): Promise<boolean>; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * 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<IHttp01Challenge> { |  | ||||||
|   handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, next?: () => void): void; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * SmartAcme main class interface |  | ||||||
|  */ |  | ||||||
| export interface ISmartAcme { |  | ||||||
|   start(): Promise<void>; |  | ||||||
|   stop(): Promise<void>; |  | ||||||
|   getCertificateForDomain(domain: string): Promise<ISmartAcmeCert>; |  | ||||||
|   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; |  | ||||||
| } |  | ||||||
| @@ -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<void> { |  | ||||||
|     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<void> { |  | ||||||
|     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<ICertificateData> { |  | ||||||
|     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); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -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'; |  | ||||||
| @@ -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<string, IDomainCertificate>; |  | ||||||
|   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<IAcmeOptions>; |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Creates a new Port80Handler |  | ||||||
|    * @param options Configuration options |  | ||||||
|    */ |  | ||||||
|   constructor(options: IAcmeOptions = {}) { |  | ||||||
|     super(); |  | ||||||
|     this.domainCertificates = new Map<string, IDomainCertificate>(); |  | ||||||
|  |  | ||||||
|     // 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<void> { |  | ||||||
|     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<void> { |  | ||||||
|     if (!this.server) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.isShuttingDown = true; |  | ||||||
|  |  | ||||||
|     return new Promise<void>((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<void> { |  | ||||||
|     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<string, { |  | ||||||
|     certObtained: boolean; |  | ||||||
|     expiryDate?: Date; |  | ||||||
|     daysRemaining?: number; |  | ||||||
|     obtainingInProgress: boolean; |  | ||||||
|     lastRenewalAttempt?: Date; |  | ||||||
|   }> { |  | ||||||
|     const result = new Map<string, { |  | ||||||
|       certObtained: boolean; |  | ||||||
|       expiryDate?: Date; |  | ||||||
|       daysRemaining?: number; |  | ||||||
|       obtainingInProgress: boolean; |  | ||||||
|       lastRenewalAttempt?: Date; |  | ||||||
|     }>(); |  | ||||||
|      |  | ||||||
|     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<void> { |  | ||||||
|     if (!this.domainCertificates.has(domain)) { |  | ||||||
|       throw new HttpError(`Domain not managed: ${domain}`); |  | ||||||
|     } |  | ||||||
|     // Trigger renewal via ACME |  | ||||||
|     await this.obtainCertificate(domain, true); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										25
									
								
								ts/index.ts
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								ts/index.ts
									
									
									
									
									
								
							| @@ -9,39 +9,36 @@ export * from './proxies/nftables-proxy/index.js'; | |||||||
| // Export NetworkProxy elements selectively to avoid RouteManager ambiguity | // Export NetworkProxy elements selectively to avoid RouteManager ambiguity | ||||||
| export { NetworkProxy, CertificateManager, ConnectionPool, RequestHandler, WebSocketHandler } from './proxies/network-proxy/index.js'; | export { NetworkProxy, CertificateManager, ConnectionPool, RequestHandler, WebSocketHandler } from './proxies/network-proxy/index.js'; | ||||||
| export type { IMetricsTracker, MetricsTracker } 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 { RouteManager as NetworkProxyRouteManager } from './proxies/network-proxy/models/types.js'; | ||||||
|  |  | ||||||
| // Export port80handler elements selectively to avoid conflicts | // Certificate and Port80 modules have been removed - use SmartCertManager instead | ||||||
| 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'; |  | ||||||
|  |  | ||||||
| export * from './redirect/classes.redirect.js'; | export * from './redirect/classes.redirect.js'; | ||||||
|  |  | ||||||
| // Export SmartProxy elements selectively to avoid RouteManager ambiguity | // Export SmartProxy elements selectively to avoid RouteManager ambiguity | ||||||
| export { SmartProxy, ConnectionManager, SecurityManager, TimeoutManager, TlsManager, NetworkProxyBridge, RouteConnectionHandler } from './proxies/smart-proxy/index.js'; | export { SmartProxy, ConnectionManager, SecurityManager, TimeoutManager, TlsManager, NetworkProxyBridge, RouteConnectionHandler } from './proxies/smart-proxy/index.js'; | ||||||
| export { RouteManager } from './proxies/smart-proxy/route-manager.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'; | export * from './proxies/smart-proxy/utils/index.js'; | ||||||
|  |  | ||||||
| // Original: export * from './smartproxy/classes.pp.snihandler.js' | // Original: export * from './smartproxy/classes.pp.snihandler.js' | ||||||
| // Now we export from the new module | // Now we export from the new module | ||||||
| export { SniHandler } from './tls/sni/sni-handler.js'; | export { SniHandler } from './tls/sni/sni-handler.js'; | ||||||
| // Original: export * from './smartproxy/classes.pp.interfaces.js' | // Original: export * from './smartproxy/classes.pp.interfaces.js' | ||||||
| // Now we export from the new module | // Now we export from the new module (selectively to avoid conflicts) | ||||||
| export * from './proxies/smart-proxy/models/interfaces.js'; |  | ||||||
|  |  | ||||||
| // Core types and utilities | // Core types and utilities | ||||||
| export * from './core/models/common-types.js'; | 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 | // Modular exports for new architecture | ||||||
| export * as forwarding from './forwarding/index.js'; | 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 tls from './tls/index.js'; | ||||||
| export * as http from './http/index.js'; | export * as http from './http/index.js'; | ||||||
| @@ -2,16 +2,19 @@ | |||||||
|  * Proxy implementations module |  * 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 { NetworkProxy, CertificateManager, ConnectionPool, RequestHandler, WebSocketHandler } from './network-proxy/index.js'; | ||||||
| export type { IMetricsTracker, MetricsTracker } 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 { SmartProxy, ConnectionManager, SecurityManager, TimeoutManager, TlsManager, NetworkProxyBridge, RouteConnectionHandler } from './smart-proxy/index.js'; | ||||||
| export { RouteManager as SmartProxyRouteManager } from './smart-proxy/route-manager.js'; | export { RouteManager as SmartProxyRouteManager } from './smart-proxy/route-manager.js'; | ||||||
| export * from './smart-proxy/utils/index.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 NFTables proxy (no conflicts) | ||||||
| export * from './nftables-proxy/index.js'; | export * from './nftables-proxy/index.js'; | ||||||
|   | |||||||
| @@ -3,21 +3,17 @@ import * as fs from 'fs'; | |||||||
| import * as path from 'path'; | import * as path from 'path'; | ||||||
| import { fileURLToPath } from 'url'; | import { fileURLToPath } from 'url'; | ||||||
| import { type INetworkProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './models/types.js'; | 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'; | 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 { | export class CertificateManager { | ||||||
|   private defaultCertificates: { key: string; cert: string }; |   private defaultCertificates: { key: string; cert: string }; | ||||||
|   private certificateCache: Map<string, ICertificateEntry> = new Map(); |   private certificateCache: Map<string, ICertificateEntry> = new Map(); | ||||||
|   private port80Handler: Port80Handler | null = null; |  | ||||||
|   private externalPort80Handler: boolean = false; |  | ||||||
|   private certificateStoreDir: string; |   private certificateStoreDir: string; | ||||||
|   private logger: ILogger; |   private logger: ILogger; | ||||||
|   private httpsServer: plugins.https.Server | null = null; |   private httpsServer: plugins.https.Server | null = null; | ||||||
| @@ -26,6 +22,8 @@ export class CertificateManager { | |||||||
|     this.certificateStoreDir = path.resolve(options.acme?.certificateStore || './certs'); |     this.certificateStoreDir = path.resolve(options.acme?.certificateStore || './certs'); | ||||||
|     this.logger = createLogger(options.logLevel || 'info'); |     this.logger = createLogger(options.logLevel || 'info'); | ||||||
|      |      | ||||||
|  |     this.logger.warn('CertificateManager is deprecated - use SmartCertManager instead'); | ||||||
|  |      | ||||||
|     // Ensure certificate store directory exists |     // Ensure certificate store directory exists | ||||||
|     try { |     try { | ||||||
|       if (!fs.existsSync(this.certificateStoreDir)) { |       if (!fs.existsSync(this.certificateStoreDir)) { | ||||||
| @@ -44,7 +42,6 @@ export class CertificateManager { | |||||||
|    */ |    */ | ||||||
|   public loadDefaultCertificates(): void { |   public loadDefaultCertificates(): void { | ||||||
|     const __dirname = path.dirname(fileURLToPath(import.meta.url)); |     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'); |     const certPath = path.join(__dirname, '..', '..', '..', 'assets', 'certs'); | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
| @@ -52,467 +49,145 @@ export class CertificateManager { | |||||||
|         key: fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'), |         key: fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'), | ||||||
|         cert: fs.readFileSync(path.join(certPath, 'cert.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) { |     } catch (error) { | ||||||
|       this.logger.error('Error loading default certificates', error); |       this.logger.error(`Failed to load default certificates: ${error}`); | ||||||
|  |       this.generateSelfSignedCertificate(); | ||||||
|       // 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'); |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * 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<void> { | ||||||
|  |     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<any> { | ||||||
|  |     this.logger.warn('initializePort80Handler is deprecated - use SmartCertManager instead'); | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * @deprecated Use SmartCertManager instead | ||||||
|  |    */ | ||||||
|  |   public async stopPort80Handler(): Promise<void> { | ||||||
|  |     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 { |   public setHttpsServer(server: plugins.https.Server): void { | ||||||
|     this.httpsServer = server; |     this.httpsServer = server; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Get default certificates |    * Gets statistics for metrics | ||||||
|    */ |    */ | ||||||
|   public getDefaultCertificates(): { key: string; cert: string } { |   public getStats() { | ||||||
|     return { ...this.defaultCertificates }; |     return { | ||||||
|   } |       cachedCertificates: this.certificateCache.size, | ||||||
|    |       defaultCertEnabled: true | ||||||
|   /** |     }; | ||||||
|    * 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 |  | ||||||
|    */ |  | ||||||
|   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<boolean> { |  | ||||||
|     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<string> = 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<Port80Handler | null> { |  | ||||||
|     // 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<void> { |  | ||||||
|     if (this.port80Handler && !this.externalPort80Handler) { |  | ||||||
|       try { |  | ||||||
|         await this.port80Handler.stop(); |  | ||||||
|         this.logger.info('Port80Handler stopped'); |  | ||||||
|       } catch (error) { |  | ||||||
|         this.logger.error('Error stopping Port80Handler', error); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -1,5 +1,17 @@ | |||||||
| import * as plugins from '../../../plugins.js'; | 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 { IRouteConfig } from '../../smart-proxy/models/route-types.js'; | ||||||
| import type { IRouteContext } from '../../../core/models/route-context.js'; | import type { IRouteContext } from '../../../core/models/route-context.js'; | ||||||
|  |  | ||||||
| @@ -22,7 +34,7 @@ export interface INetworkProxyOptions { | |||||||
|   // Settings for SmartProxy integration |   // Settings for SmartProxy integration | ||||||
|   connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend |   connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend | ||||||
|   portProxyIntegration?: boolean; // Flag to indicate this proxy is used by SmartProxy |   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 |   // Protocol to use when proxying to backends: HTTP/1.x or HTTP/2 | ||||||
|   backendProtocol?: 'http1' | 'http2'; |   backendProtocol?: 'http1' | 'http2'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -18,7 +18,6 @@ import { RequestHandler, type IMetricsTracker } from './request-handler.js'; | |||||||
| import { WebSocketHandler } from './websocket-handler.js'; | import { WebSocketHandler } from './websocket-handler.js'; | ||||||
| import { ProxyRouter } from '../../http/router/index.js'; | import { ProxyRouter } from '../../http/router/index.js'; | ||||||
| import { RouteRouter } from '../../http/router/route-router.js'; | import { RouteRouter } from '../../http/router/route-router.js'; | ||||||
| import { Port80Handler } from '../../http/port80/port80-handler.js'; |  | ||||||
| import { FunctionCache } from './function-cache.js'; | import { FunctionCache } from './function-cache.js'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -221,15 +220,10 @@ export class NetworkProxy implements IMetricsTracker { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Sets an external Port80Handler for certificate management |    * @deprecated Use SmartCertManager instead | ||||||
|    * This allows the NetworkProxy to use a centrally managed Port80Handler |  | ||||||
|    * instead of creating its own |  | ||||||
|    *  |  | ||||||
|    * @param handler The Port80Handler instance to use |  | ||||||
|    */ |    */ | ||||||
|   public setExternalPort80Handler(handler: Port80Handler): void { |   public setExternalPort80Handler(handler: any): void { | ||||||
|     // Connect it to the certificate manager |     this.logger.warn('Port80Handler is deprecated - use SmartCertManager instead'); | ||||||
|     this.certificateManager.setExternalPort80Handler(handler); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -238,10 +232,7 @@ export class NetworkProxy implements IMetricsTracker { | |||||||
|   public async start(): Promise<void> { |   public async start(): Promise<void> { | ||||||
|     this.startTime = Date.now(); |     this.startTime = Date.now(); | ||||||
|      |      | ||||||
|     // Initialize Port80Handler if enabled and not using external handler |     // Certificate management is now handled by SmartCertManager | ||||||
|     if (this.options.acme?.enabled && !this.options.useExternalPort80Handler) { |  | ||||||
|       await this.certificateManager.initializePort80Handler(); |  | ||||||
|     } |  | ||||||
|      |      | ||||||
|     // Create HTTP/2 server with HTTP/1 fallback |     // Create HTTP/2 server with HTTP/1 fallback | ||||||
|     this.httpsServer = plugins.http2.createSecureServer( |     this.httpsServer = plugins.http2.createSecureServer( | ||||||
| @@ -385,7 +376,7 @@ export class NetworkProxy implements IMetricsTracker { | |||||||
|  |  | ||||||
|     // Directly update the certificate manager with the new routes |     // Directly update the certificate manager with the new routes | ||||||
|     // This will extract domains and handle certificate provisioning |     // This will extract domains and handle certificate provisioning | ||||||
|     this.certificateManager.updateRouteConfigs(routes); |     this.certificateManager.updateRoutes(routes); | ||||||
|  |  | ||||||
|     // Collect all domains and certificates for configuration |     // Collect all domains and certificates for configuration | ||||||
|     const currentHostnames = new Set<string>(); |     const currentHostnames = new Set<string>(); | ||||||
| @@ -425,7 +416,7 @@ export class NetworkProxy implements IMetricsTracker { | |||||||
|     // Update certificate cache with any static certificates |     // Update certificate cache with any static certificates | ||||||
|     for (const [domain, certData] of certificateUpdates.entries()) { |     for (const [domain, certData] of certificateUpdates.entries()) { | ||||||
|       try { |       try { | ||||||
|         this.certificateManager.updateCertificateCache( |         this.certificateManager.updateCertificate( | ||||||
|           domain, |           domain, | ||||||
|           certData.cert, |           certData.cert, | ||||||
|           certData.key |           certData.key | ||||||
| @@ -547,8 +538,7 @@ export class NetworkProxy implements IMetricsTracker { | |||||||
|     // Close all connection pool connections |     // Close all connection pool connections | ||||||
|     this.connectionPool.closeAllConnections(); |     this.connectionPool.closeAllConnections(); | ||||||
|      |      | ||||||
|     // Stop Port80Handler if internally managed |     // Certificate management cleanup is handled by SmartCertManager | ||||||
|     await this.certificateManager.stopPort80Handler(); |  | ||||||
|      |      | ||||||
|     // Close the HTTPS server |     // Close the HTTPS server | ||||||
|     return new Promise((resolve) => { |     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) |    * @returns A promise that resolves when the request is submitted (not when the certificate is issued) | ||||||
|    */ |    */ | ||||||
|   public async requestCertificate(domain: string): Promise<boolean> { |   public async requestCertificate(domain: string): Promise<boolean> { | ||||||
|     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 |     expiryDate?: Date | ||||||
|   ): void { |   ): void { | ||||||
|     this.logger.info(`Updating certificate for ${domain}`); |     this.logger.info(`Updating certificate for ${domain}`); | ||||||
|     this.certificateManager.updateCertificateCache(domain, certificate, privateKey, expiryDate); |     this.certificateManager.updateCertificate(domain, certificate, privateKey); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| /** | /** | ||||||
|  * SmartProxy models |  * 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'; | export * from './route-types.js'; | ||||||
|   | |||||||
| @@ -1,5 +1,18 @@ | |||||||
| import * as plugins from '../../../plugins.js'; | 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 { IRouteConfig } from './route-types.js'; | ||||||
| import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; | import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import * as plugins from '../../../plugins.js'; | 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 { TForwardingType } from '../../../forwarding/config/forwarding-types.js'; | ||||||
| import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js'; | import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -121,13 +121,12 @@ export class SmartProxy extends plugins.EventEmitter { | |||||||
|       this.settings.acme = { |       this.settings.acme = { | ||||||
|         enabled: false, |         enabled: false, | ||||||
|         port: 80, |         port: 80, | ||||||
|         accountEmail: 'admin@example.com', |         email: 'admin@example.com', | ||||||
|         useProduction: false, |         useProduction: false, | ||||||
|         renewThresholdDays: 30, |         renewThresholdDays: 30, | ||||||
|         autoRenew: true, |         autoRenew: true, | ||||||
|         certificateStore: './certs', |         certificateStore: './certs', | ||||||
|         skipConfiguredCerts: false, |         skipConfiguredCerts: false, | ||||||
|         httpsRedirectPort: 443, |  | ||||||
|         renewCheckIntervalHours: 24, |         renewCheckIntervalHours: 24, | ||||||
|         routeForwards: [] |         routeForwards: [] | ||||||
|       }; |       }; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user