feat(smartproxy): Migrate internal module paths and update HTTP/ACME components for SmartProxy
This commit is contained in:
		
							
								
								
									
										11
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								changelog.md
									
									
									
									
									
								
							| @@ -1,5 +1,16 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-05-09 - 12.1.0 - feat(smartproxy) | ||||
| Migrate internal module paths and update HTTP/ACME components for SmartProxy | ||||
|  | ||||
| - Mark migration tasks as complete in readme.plan.md (checkboxes updated to ✅) | ||||
| - Moved Port80Handler from ts/port80handler to ts/http/port80 (and extracted challenge responder) | ||||
| - Migrated redirect handlers and router components to ts/http/redirects and ts/http/router respectively | ||||
| - Updated re-exports in ts/index.ts and ts/plugins.ts to expose new module paths and additional exports | ||||
| - Refactored CertificateEvents to include deprecation notes on Port80HandlerEvents | ||||
| - Adjusted internal module organization for TLS, ACME, and forwarding (SNI extraction, client-hello parsing, etc.) | ||||
| - Added minor logging and formatting improvements in several modules | ||||
|  | ||||
| ## 2025-05-09 - 12.0.0 - BREAKING CHANGE(forwarding) | ||||
| Rename 'sniPassthrough' export to 'httpsPassthrough' for consistent naming and remove outdated forwarding example | ||||
|  | ||||
|   | ||||
| @@ -136,19 +136,19 @@ This component has the cleanest design, so we'll start migration here: | ||||
|   - [x] Extract SNI extraction to `ts/tls/sni/sni-extraction.ts` | ||||
|   - [x] Extract ClientHello parsing to `ts/tls/sni/client-hello-parser.ts` | ||||
|  | ||||
| ### Phase 5: HTTP Component Migration (Week 3) | ||||
| ### Phase 5: HTTP Component Migration (Week 3) ✅ | ||||
|  | ||||
| - [ ] Migrate Port80Handler | ||||
|   - [ ] Move `ts/port80handler/classes.port80handler.ts` → `ts/http/port80/port80-handler.ts` | ||||
|   - [ ] Extract ACME challenge handling to `ts/http/port80/challenge-responder.ts` | ||||
| - [x] Migrate Port80Handler | ||||
|   - [x] Move `ts/port80handler/classes.port80handler.ts` → `ts/http/port80/port80-handler.ts` | ||||
|   - [x] Extract ACME challenge handling to `ts/http/port80/challenge-responder.ts` | ||||
|  | ||||
| - [ ] Migrate redirect handlers | ||||
|   - [ ] Move `ts/redirect/classes.redirect.ts` → `ts/http/redirects/redirect-handler.ts` | ||||
|   - [ ] Create `ts/http/redirects/ssl-redirect.ts` for specialized redirects | ||||
| - [x] Migrate redirect handlers | ||||
|   - [x] Move `ts/redirect/classes.redirect.ts` → `ts/http/redirects/redirect-handler.ts` | ||||
|   - [x] Create `ts/http/redirects/ssl-redirect.ts` for specialized redirects | ||||
|  | ||||
| - [ ] Migrate router components | ||||
|   - [ ] Move `ts/classes.router.ts` → `ts/http/router/proxy-router.ts` | ||||
|   - [ ] Extract route matching to `ts/http/router/route-matcher.ts` | ||||
| - [x] Migrate router components | ||||
|   - [x] Move `ts/classes.router.ts` → `ts/http/router/proxy-router.ts` | ||||
|   - [x] Extract route matching to `ts/http/router/route-matcher.ts` | ||||
|  | ||||
| ### Phase 6: Proxy Implementation Migration (Weeks 3-4) | ||||
|  | ||||
| @@ -259,9 +259,9 @@ This component has the cleanest design, so we'll start migration here: | ||||
| | (new) | ts/tls/sni/sni-extraction.ts | ✅ | | ||||
| | (new) | ts/tls/sni/client-hello-parser.ts | ✅ | | ||||
| | **HTTP Components** | | | | ||||
| | ts/port80handler/classes.port80handler.ts | ts/http/port80/port80-handler.ts | ❌ | | ||||
| | ts/redirect/classes.redirect.ts | ts/http/redirects/redirect-handler.ts | ❌ | | ||||
| | ts/classes.router.ts | ts/http/router/proxy-router.ts | ❌ | | ||||
| | ts/port80handler/classes.port80handler.ts | ts/http/port80/port80-handler.ts | ✅ | | ||||
| | ts/redirect/classes.redirect.ts | ts/http/redirects/redirect-handler.ts | ✅ | | ||||
| | ts/classes.router.ts | ts/http/router/proxy-router.ts | ✅ | | ||||
| | **SmartProxy Components** | | | | ||||
| | ts/smartproxy/classes.smartproxy.ts | ts/proxies/smart-proxy/smart-proxy.ts | ❌ | | ||||
| | ts/smartproxy/classes.pp.interfaces.ts | ts/proxies/smart-proxy/models/interfaces.ts | ❌ | | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '12.0.0', | ||||
|   version: '12.1.0', | ||||
|   description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.' | ||||
| } | ||||
|   | ||||
| @@ -7,10 +7,14 @@ export enum CertificateEvents { | ||||
|   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', | ||||
|   | ||||
| @@ -2,7 +2,18 @@ | ||||
|  * HTTP functionality module | ||||
|  */ | ||||
|  | ||||
| // Export types and models | ||||
| export * from './models/http-types.js'; | ||||
|  | ||||
| // Export submodules | ||||
| export * from './port80/index.js'; | ||||
| export * from './router/index.js'; | ||||
| export * from './redirects/index.js'; | ||||
|  | ||||
| // Convenience namespace exports | ||||
| export const Http = { | ||||
|   Port80: { | ||||
|     Handler: require('./port80/port80-handler.js').Port80Handler, | ||||
|     ChallengeResponder: require('./port80/challenge-responder.js').ChallengeResponder | ||||
|   } | ||||
| }; | ||||
|   | ||||
| @@ -35,58 +35,92 @@ export class ChallengeResponder extends plugins.EventEmitter { | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     try { | ||||
|       // Initialize SmartAcme | ||||
|       this.smartAcme = new plugins.smartacme.SmartAcme({ | ||||
|         useProduction: this.useProduction, | ||||
|         accountEmail: this.email, | ||||
|         directoryUrl: this.useProduction | ||||
|           ? 'https://acme-v02.api.letsencrypt.org/directory' // Production | ||||
|           : 'https://acme-staging-v02.api.letsencrypt.org/directory', // Staging | ||||
|       }); | ||||
|  | ||||
|       // Initialize HTTP-01 challenge handler | ||||
|       this.http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler(); | ||||
|       this.smartAcme.useHttpChallenge(this.http01Handler); | ||||
|        | ||||
|       // Initialize SmartAcme with proper options | ||||
|       this.smartAcme = new plugins.smartacme.SmartAcme({ | ||||
|         accountEmail: this.email, | ||||
|         certManager: new plugins.smartacme.certmanagers.MemoryCertManager(), | ||||
|         environment: this.useProduction ? 'production' : 'integration', | ||||
|         challengeHandlers: [this.http01Handler], | ||||
|         challengePriority: ['http-01'], | ||||
|       }); | ||||
|  | ||||
|       // Ensure certificate store directory exists | ||||
|       await this.ensureCertificateStore(); | ||||
|  | ||||
|       // Subscribe to SmartAcme events | ||||
|       this.smartAcme.on('certificate-issued', (data: any) => { | ||||
|         const certData: CertificateData = { | ||||
|           domain: data.domain, | ||||
|           certificate: data.cert, | ||||
|           privateKey: data.key, | ||||
|           expiryDate: new Date(data.expiryDate), | ||||
|         }; | ||||
|         this.emit(CertificateEvents.CERTIFICATE_ISSUED, certData); | ||||
|       }); | ||||
|       // Set up event forwarding from SmartAcme | ||||
|       this.setupEventForwarding(); | ||||
|        | ||||
|       this.smartAcme.on('certificate-renewed', (data: any) => { | ||||
|         const certData: CertificateData = { | ||||
|           domain: data.domain, | ||||
|           certificate: data.cert, | ||||
|           privateKey: data.key, | ||||
|           expiryDate: new Date(data.expiryDate), | ||||
|         }; | ||||
|         this.emit(CertificateEvents.CERTIFICATE_RENEWED, certData); | ||||
|       }); | ||||
|  | ||||
|       this.smartAcme.on('certificate-error', (data: any) => { | ||||
|         const error: CertificateFailure = { | ||||
|           domain: data.domain, | ||||
|           error: data.error instanceof Error ? data.error.message : String(data.error), | ||||
|           isRenewal: data.isRenewal || false, | ||||
|         }; | ||||
|         this.emit(CertificateEvents.CERTIFICATE_FAILED, error); | ||||
|       }); | ||||
|  | ||||
|       await this.smartAcme.initialize(); | ||||
|       // Start SmartAcme | ||||
|       await this.smartAcme.start(); | ||||
|     } catch (error) { | ||||
|       throw new Error(`Failed to initialize ACME client: ${error instanceof Error ? error.message : String(error)}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Sets up event forwarding from SmartAcme to this component | ||||
|    */ | ||||
|   private setupEventForwarding(): void { | ||||
|     if (!this.smartAcme) return; | ||||
|  | ||||
|     // Cast smartAcme to any since different versions have different event APIs | ||||
|     const smartAcmeAny = this.smartAcme as any; | ||||
|  | ||||
|     // Forward certificate events to our own emitter | ||||
|     if (typeof smartAcmeAny.on === 'function') { | ||||
|       smartAcmeAny.on('certificate', (data: any) => { | ||||
|         const certData: CertificateData = { | ||||
|           domain: data.domain, | ||||
|           certificate: data.cert || data.publicKey, | ||||
|           privateKey: data.key || data.privateKey, | ||||
|           expiryDate: new Date(data.expiryDate || data.validUntil), | ||||
|           source: 'http01' | ||||
|         }; | ||||
|         // Emit as issued or renewed based on the renewal flag | ||||
|         const eventType = data.isRenewal | ||||
|           ? CertificateEvents.CERTIFICATE_RENEWED | ||||
|           : CertificateEvents.CERTIFICATE_ISSUED; | ||||
|         this.emit(eventType, certData); | ||||
|       }); | ||||
|  | ||||
|       smartAcmeAny.on('error', (data: any) => { | ||||
|         const failure: CertificateFailure = { | ||||
|           domain: data.domain || 'unknown', | ||||
|           error: data.message || data.toString(), | ||||
|           isRenewal: false | ||||
|         }; | ||||
|         this.emit(CertificateEvents.CERTIFICATE_FAILED, failure); | ||||
|       }); | ||||
|     } else if (smartAcmeAny.eventEmitter && typeof smartAcmeAny.eventEmitter.on === 'function') { | ||||
|       // Alternative event emitter approach for newer versions | ||||
|       smartAcmeAny.eventEmitter.on('certificate', (data: any) => { | ||||
|         const certData: CertificateData = { | ||||
|           domain: data.domain, | ||||
|           certificate: data.cert || data.publicKey, | ||||
|           privateKey: data.key || data.privateKey, | ||||
|           expiryDate: new Date(data.expiryDate || data.validUntil), | ||||
|           source: 'http01' | ||||
|         }; | ||||
|         const eventType = data.isRenewal | ||||
|           ? CertificateEvents.CERTIFICATE_RENEWED | ||||
|           : CertificateEvents.CERTIFICATE_ISSUED; | ||||
|         this.emit(eventType, certData); | ||||
|       }); | ||||
|  | ||||
|       smartAcmeAny.eventEmitter.on('error', (data: any) => { | ||||
|         const failure: CertificateFailure = { | ||||
|           domain: data.domain || 'unknown', | ||||
|           error: data.message || data.toString(), | ||||
|           isRenewal: false | ||||
|         }; | ||||
|         this.emit(CertificateEvents.CERTIFICATE_FAILED, failure); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Ensure certificate store directory exists | ||||
|    */ | ||||
| @@ -115,16 +149,32 @@ export class ChallengeResponder extends plugins.EventEmitter { | ||||
|     if (url.startsWith('/.well-known/acme-challenge/')) { | ||||
|       const token = url.split('/').pop() || ''; | ||||
|  | ||||
|       if (token) { | ||||
|         const response = this.http01Handler.getResponse(token); | ||||
|       if (token && this.http01Handler) { | ||||
|         try { | ||||
|           // Try to delegate to the handler - casting to any for flexibility | ||||
|           const handler = this.http01Handler as any; | ||||
|  | ||||
|         if (response) { | ||||
|           // This is a valid ACME challenge | ||||
|           res.setHeader('Content-Type', 'text/plain'); | ||||
|           res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0'); | ||||
|           res.writeHead(200); | ||||
|           res.end(response); | ||||
|           return true; | ||||
|           // Different versions may have different handler methods | ||||
|           if (typeof handler.handleChallenge === 'function') { | ||||
|             handler.handleChallenge(req, res); | ||||
|             return true; | ||||
|           } else if (typeof handler.handleRequest === 'function') { | ||||
|             // Some versions use handleRequest instead | ||||
|             handler.handleRequest(req, res); | ||||
|             return true; | ||||
|           } else { | ||||
|             // Fall back to manual response | ||||
|             const resp = this.getTokenResponse(token); | ||||
|             if (resp) { | ||||
|               res.setHeader('Content-Type', 'text/plain'); | ||||
|               res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0'); | ||||
|               res.writeHead(200); | ||||
|               res.end(resp); | ||||
|               return true; | ||||
|             } | ||||
|           } | ||||
|         } catch (err) { | ||||
|           // Challenge not found | ||||
|         } | ||||
|       } | ||||
|  | ||||
| @@ -137,6 +187,42 @@ export class ChallengeResponder extends plugins.EventEmitter { | ||||
|     return false; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get the response for a specific token if available | ||||
|    * This is a fallback method in case direct handler access isn't available | ||||
|    */ | ||||
|   private getTokenResponse(token: string): string | null { | ||||
|     if (!this.http01Handler) return null; | ||||
|  | ||||
|     try { | ||||
|       // Cast to any to handle different versions of the API | ||||
|       const handler = this.http01Handler as any; | ||||
|  | ||||
|       // Try different methods that might be available in different versions | ||||
|       if (typeof handler.getResponse === 'function') { | ||||
|         return handler.getResponse(token); | ||||
|       } | ||||
|  | ||||
|       if (typeof handler.getChallengeVerification === 'function') { | ||||
|         return handler.getChallengeVerification(token); | ||||
|       } | ||||
|  | ||||
|       // Try to access the challenges directly from the handler's internal state | ||||
|       if (handler.challenges && typeof handler.challenges === 'object' && handler.challenges[token]) { | ||||
|         return handler.challenges[token]; | ||||
|       } | ||||
|  | ||||
|       // Try the token map if it exists (another common pattern) | ||||
|       if (handler.tokenMap && typeof handler.tokenMap === 'object' && handler.tokenMap[token]) { | ||||
|         return handler.tokenMap[token]; | ||||
|       } | ||||
|     } catch (err) { | ||||
|       console.error('Error getting token response:', err); | ||||
|     } | ||||
|  | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Request a certificate for a domain | ||||
|    * @param domain Domain name | ||||
| @@ -148,16 +234,20 @@ export class ChallengeResponder extends plugins.EventEmitter { | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       const result = await this.smartAcme.getCertificate(domain); | ||||
|       // Request certificate via SmartAcme | ||||
|       const certObj = await this.smartAcme.getCertificateForDomain(domain); | ||||
|        | ||||
|       const certData: CertificateData = { | ||||
|         domain, | ||||
|         certificate: result.cert, | ||||
|         privateKey: result.key, | ||||
|         expiryDate: new Date(result.expiryDate), | ||||
|         certificate: certObj.publicKey, | ||||
|         privateKey: certObj.privateKey, | ||||
|         expiryDate: new Date(certObj.validUntil), | ||||
|         source: 'http01', | ||||
|         isRenewal | ||||
|       }; | ||||
|        | ||||
|       // Emit appropriate event | ||||
|       // SmartACME will emit its own events, but we'll emit our own too | ||||
|       // for consistency with the rest of the system | ||||
|       if (isRenewal) { | ||||
|         this.emit(CertificateEvents.CERTIFICATE_RENEWED, certData); | ||||
|       } else { | ||||
|   | ||||
| @@ -1,3 +1,13 @@ | ||||
| /** | ||||
|  * 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'; | ||||
|   | ||||
| @@ -15,8 +15,8 @@ import { | ||||
|   HttpError, | ||||
|   CertificateError, | ||||
|   ServerError, | ||||
|   DomainCertificate | ||||
| } from '../models/http-types.js'; | ||||
| import type { DomainCertificate } from '../models/http-types.js'; | ||||
| import { ChallengeResponder } from './challenge-responder.js'; | ||||
|  | ||||
| // Re-export for backward compatibility | ||||
| @@ -115,22 +115,20 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|       console.log('Port80Handler is disabled, skipping start'); | ||||
|       return; | ||||
|     } | ||||
|     // Initialize SmartAcme with in-memory HTTP-01 challenge handler | ||||
|     if (this.options.enabled) { | ||||
|       this.smartAcmeHttp01Handler = new plugins.smartacme.handlers.Http01MemoryHandler(); | ||||
|       this.smartAcme = new plugins.smartacme.SmartAcme({ | ||||
|         accountEmail: this.options.accountEmail, | ||||
|         certManager: new plugins.smartacme.certmanagers.MemoryCertManager(), | ||||
|         environment: this.options.useProduction ? 'production' : 'integration', | ||||
|         challengeHandlers: [ this.smartAcmeHttp01Handler ], | ||||
|         challengePriority: ['http-01'], | ||||
|       }); | ||||
|       await this.smartAcme.start(); | ||||
|  | ||||
|     // 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) => { | ||||
| @@ -145,7 +143,7 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|  | ||||
|         this.server.listen(this.options.port, () => { | ||||
|           console.log(`Port80Handler is listening on port ${this.options.port}`); | ||||
|           this.emit(Port80HandlerEvents.MANAGER_STARTED, 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()) { | ||||
| @@ -172,7 +170,7 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Stops the HTTP server and renewal timer | ||||
|    * Stops the HTTP server and cleanup resources | ||||
|    */ | ||||
|   public async stop(): Promise<void> { | ||||
|     if (!this.server) { | ||||
| @@ -181,13 +179,12 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|  | ||||
|     this.isShuttingDown = true; | ||||
|  | ||||
|  | ||||
|     return new Promise<void>((resolve) => { | ||||
|       if (this.server) { | ||||
|         this.server.close(() => { | ||||
|           this.server = null; | ||||
|           this.isShuttingDown = false; | ||||
|           this.emit(Port80HandlerEvents.MANAGER_STOPPED); | ||||
|           this.emit(CertificateEvents.MANAGER_STOPPED); | ||||
|           resolve(); | ||||
|         }); | ||||
|       } else { | ||||
| @@ -201,9 +198,9 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|    * Adds a domain with configuration options | ||||
|    * @param options Domain configuration options | ||||
|    */ | ||||
|   public addDomain(options: IDomainOptions): void { | ||||
|   public addDomain(options: DomainOptions): void { | ||||
|     if (!options.domainName || typeof options.domainName !== 'string') { | ||||
|       throw new Port80HandlerError('Invalid domain name'); | ||||
|       throw new HttpError('Invalid domain name'); | ||||
|     } | ||||
|  | ||||
|     const domainName = options.domainName; | ||||
| @@ -250,7 +247,7 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|    * Gets the certificate for a domain if it exists | ||||
|    * @param domain The domain to get the certificate for | ||||
|    */ | ||||
|   public getCertificate(domain: string): ICertificateData | null { | ||||
|   public getCertificate(domain: string): CertificateData | null { | ||||
|     // Can't get certificates for glob patterns | ||||
|     if (this.isGlobPattern(domain)) { | ||||
|       return null; | ||||
| @@ -286,7 +283,7 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|    * @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 { | ||||
|   private getDomainInfoForRequest(requestDomain: string): { domainInfo: DomainCertificate, pattern: string } | null { | ||||
|     // Try direct match first | ||||
|     if (this.domainCertificates.has(requestDomain)) { | ||||
|       return { | ||||
| @@ -338,9 +335,16 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|    * @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 = 400; | ||||
|       res.statusCode = HttpStatus.BAD_REQUEST; | ||||
|       res.end('Bad Request: Host header is missing'); | ||||
|       return; | ||||
|     } | ||||
| @@ -348,6 +352,28 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|     // 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 { | ||||
| @@ -355,14 +381,15 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|       } catch (err) { | ||||
|         console.error(`Error registering domain for on-demand provisioning: ${err}`); | ||||
|       } | ||||
|       res.statusCode = 503; | ||||
|       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 = 404; | ||||
|       res.statusCode = HttpStatus.NOT_FOUND; | ||||
|       res.end('Domain not configured'); | ||||
|       return; | ||||
|     } | ||||
| @@ -370,29 +397,6 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|     const { domainInfo, pattern } = domainMatch; | ||||
|     const options = domainInfo.options; | ||||
|  | ||||
|     // Handle ACME HTTP-01 challenge requests or forwarding | ||||
|     if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) { | ||||
|       // Forward ACME requests if configured | ||||
|       if (options.acmeForward) { | ||||
|         this.forwardRequest(req, res, options.acmeForward, 'ACME challenge'); | ||||
|         return; | ||||
|       } | ||||
|       // If not managing ACME for this domain, return 404 | ||||
|       if (!options.acmeMaintenance) { | ||||
|         res.statusCode = 404; | ||||
|         res.end('Not found'); | ||||
|         return; | ||||
|       } | ||||
|       // Delegate to Http01MemoryHandler | ||||
|       if (this.smartAcmeHttp01Handler) { | ||||
|         this.smartAcmeHttp01Handler.handleRequest(req, res); | ||||
|       } else { | ||||
|         res.statusCode = 500; | ||||
|         res.end('ACME HTTP-01 handler not initialized'); | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Check if we should forward non-ACME requests | ||||
|     if (options.forward) { | ||||
|       this.forwardRequest(req, res, options.forward, 'HTTP'); | ||||
| @@ -406,7 +410,7 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|       const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`; | ||||
|       const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`; | ||||
|  | ||||
|       res.statusCode = 301; | ||||
|       res.statusCode = HttpStatus.MOVED_PERMANENTLY; | ||||
|       res.setHeader('Location', redirectUrl); | ||||
|       res.end(`Redirecting to ${redirectUrl}`); | ||||
|       return; | ||||
| @@ -419,7 +423,7 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|       if (!domainInfo.obtainingInProgress) { | ||||
|         this.obtainCertificate(domain).catch(err => { | ||||
|           const errorMessage = err instanceof Error ? err.message : 'Unknown error'; | ||||
|           this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, { | ||||
|           this.emit(CertificateEvents.CERTIFICATE_FAILED, { | ||||
|             domain, | ||||
|             error: errorMessage, | ||||
|             isRenewal: false | ||||
| @@ -428,14 +432,21 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       res.statusCode = 503; | ||||
|       res.statusCode = HttpStatus.SERVICE_UNAVAILABLE; | ||||
|       res.end('Certificate issuance in progress, please try again later.'); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Default response for unhandled request | ||||
|     res.statusCode = 404; | ||||
|     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 | ||||
|     }); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
| @@ -448,7 +459,7 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|   private forwardRequest( | ||||
|     req: plugins.http.IncomingMessage, | ||||
|     res: plugins.http.ServerResponse, | ||||
|     target: IForwardConfig, | ||||
|     target: ForwardConfig, | ||||
|     requestType: string | ||||
|   ): void { | ||||
|     const options = { | ||||
| @@ -464,7 +475,7 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|  | ||||
|     const proxyReq = plugins.http.request(options, (proxyRes) => { | ||||
|       // Copy status code | ||||
|       res.statusCode = proxyRes.statusCode || 500; | ||||
|       res.statusCode = proxyRes.statusCode || HttpStatus.INTERNAL_SERVER_ERROR; | ||||
|  | ||||
|       // Copy headers | ||||
|       for (const [key, value] of Object.entries(proxyRes.headers)) { | ||||
| @@ -474,7 +485,7 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|       // Pipe response data | ||||
|       proxyRes.pipe(res); | ||||
|  | ||||
|       this.emit(Port80HandlerEvents.REQUEST_FORWARDED, { | ||||
|       this.emit(HttpEvents.REQUEST_FORWARDED, { | ||||
|         domain, | ||||
|         requestType, | ||||
|         target: `${target.ip}:${target.port}`, | ||||
| @@ -484,8 +495,15 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|  | ||||
|     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 = 502; | ||||
|         res.statusCode = HttpStatus.INTERNAL_SERVER_ERROR; | ||||
|         res.end(`Proxy error: ${error.message}`); | ||||
|       } else { | ||||
|         res.end(); | ||||
| @@ -506,59 +524,48 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|    * @param domain The domain to obtain a certificate for | ||||
|    * @param isRenewal Whether this is a renewal attempt | ||||
|    */ | ||||
|   /** | ||||
|    * Obtains a certificate for a domain using SmartAcme HTTP-01 challenges | ||||
|    * @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.smartAcme) { | ||||
|       throw new Port80HandlerError('SmartAcme is not initialized'); | ||||
|  | ||||
|     if (!this.challengeResponder) { | ||||
|       throw new HttpError('Challenge responder is not initialized'); | ||||
|     } | ||||
|  | ||||
|     domainInfo.obtainingInProgress = true; | ||||
|     domainInfo.lastRenewalAttempt = new Date(); | ||||
|  | ||||
|     try { | ||||
|       // Request certificate via SmartAcme | ||||
|       const certObj = await this.smartAcme.getCertificateForDomain(domain); | ||||
|       const certificate = certObj.publicKey; | ||||
|       const privateKey = certObj.privateKey; | ||||
|       const expiryDate = new Date(certObj.validUntil); | ||||
|       domainInfo.certificate = certificate; | ||||
|       domainInfo.privateKey = privateKey; | ||||
|       // Request certificate via ChallengeResponder | ||||
|       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 = expiryDate; | ||||
|       domainInfo.expiryDate = certData.expiryDate; | ||||
|  | ||||
|       console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`); | ||||
|       // Persistence moved to CertProvisioner | ||||
|       const eventType = isRenewal | ||||
|         ? Port80HandlerEvents.CERTIFICATE_RENEWED | ||||
|         : Port80HandlerEvents.CERTIFICATE_ISSUED; | ||||
|       this.emitCertificateEvent(eventType, { | ||||
|         domain, | ||||
|         certificate, | ||||
|         privateKey, | ||||
|         expiryDate: expiryDate || this.getDefaultExpiryDate() | ||||
|       }); | ||||
|  | ||||
|       // The event will be emitted by the ChallengeResponder, we just store the certificate | ||||
|     } catch (error: any) { | ||||
|       const errorMsg = error?.message || 'Unknown error'; | ||||
|       const errorMsg = error instanceof Error ? error.message : String(error); | ||||
|       console.error(`Error during certificate issuance for ${domain}:`, error); | ||||
|       this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, { | ||||
|         domain, | ||||
|         error: errorMsg, | ||||
|         isRenewal | ||||
|       } as ICertificateFailure); | ||||
|  | ||||
|       // The failure event will be emitted by the ChallengeResponder | ||||
|       throw new CertificateError(errorMsg, domain, isRenewal); | ||||
|     } finally { | ||||
|       domainInfo.obtainingInProgress = false; | ||||
| @@ -608,7 +615,7 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|    * @param eventType The event type to emit | ||||
|    * @param data The certificate data | ||||
|    */ | ||||
|   private emitCertificateEvent(eventType: Port80HandlerEvents, data: ICertificateData): void { | ||||
|   private emitCertificateEvent(eventType: CertificateEvents, data: CertificateData): void { | ||||
|     this.emit(eventType, data); | ||||
|   } | ||||
|    | ||||
| @@ -670,7 +677,7 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|    */ | ||||
|   public async renewCertificate(domain: string): Promise<void> { | ||||
|     if (!this.domainCertificates.has(domain)) { | ||||
|       throw new Port80HandlerError(`Domain not managed: ${domain}`); | ||||
|       throw new HttpError(`Domain not managed: ${domain}`); | ||||
|     } | ||||
|     // Trigger renewal via ACME | ||||
|     await this.obtainCertificate(domain, true); | ||||
|   | ||||
							
								
								
									
										13
									
								
								ts/index.ts
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								ts/index.ts
									
									
									
									
									
								
							| @@ -5,7 +5,17 @@ | ||||
| // Legacy exports (to maintain backward compatibility) | ||||
| export * from './nfttablesproxy/classes.nftablesproxy.js'; | ||||
| export * from './networkproxy/index.js'; | ||||
| export * from './port80handler/classes.port80handler.js'; | ||||
| // Export port80handler elements selectively to avoid conflicts | ||||
| export { | ||||
|   Port80Handler, | ||||
|   default as Port80HandlerDefault, | ||||
|   HttpError, | ||||
|   ServerError, | ||||
|   CertificateError | ||||
| } from './port80handler/classes.port80handler.js'; | ||||
| // Use re-export to control the names | ||||
| export { Port80HandlerEvents } from './certificate/events/certificate-events.js'; | ||||
|  | ||||
| export * from './redirect/classes.redirect.js'; | ||||
| export * from './smartproxy/classes.smartproxy.js'; | ||||
| // Original: export * from './smartproxy/classes.pp.snihandler.js' | ||||
| @@ -20,3 +30,4 @@ export * from './core/models/common-types.js'; | ||||
| export * as forwarding from './forwarding/index.js'; | ||||
| export * as certificate from './certificate/index.js'; | ||||
| export * as tls from './tls/index.js'; | ||||
| export * as http from './http/index.js'; | ||||
| @@ -1,5 +1,6 @@ | ||||
| // node native scope | ||||
| import { EventEmitter } from 'events'; | ||||
| import * as fs from 'fs'; | ||||
| import * as http from 'http'; | ||||
| import * as https from 'https'; | ||||
| import * as net from 'net'; | ||||
| @@ -7,7 +8,7 @@ import * as tls from 'tls'; | ||||
| import * as url from 'url'; | ||||
| import * as http2 from 'http2'; | ||||
|  | ||||
| export { EventEmitter, http, https, net, tls, url, http2 }; | ||||
| export { EventEmitter, fs, http, https, net, tls, url, http2 }; | ||||
|  | ||||
| // tsclass scope | ||||
| import * as tsclass from '@tsclass/tsclass'; | ||||
|   | ||||
							
								
								
									
										24
									
								
								ts/port80handler/classes.port80handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								ts/port80handler/classes.port80handler.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| /** | ||||
|  * TEMPORARY FILE FOR BACKWARD COMPATIBILITY | ||||
|  * This will be removed in a future version when all imports are updated | ||||
|  * @deprecated Use the new HTTP module instead | ||||
|  */ | ||||
|  | ||||
| // Re-export the Port80Handler from its new location | ||||
| export * from '../http/port80/port80-handler.js'; | ||||
|  | ||||
| // Re-export HTTP error types for backward compatibility | ||||
| export * from '../http/models/http-types.js'; | ||||
|  | ||||
| // Re-export selected events to avoid name conflicts | ||||
| export { | ||||
|   CertificateEvents, | ||||
|   Port80HandlerEvents, | ||||
|   CertProvisionerEvents | ||||
| } from '../certificate/events/certificate-events.js'; | ||||
|  | ||||
| // Import the new Port80Handler | ||||
| import { Port80Handler } from '../http/port80/port80-handler.js'; | ||||
|  | ||||
| // Export it as the default export for backward compatibility | ||||
| export default Port80Handler; | ||||
		Reference in New Issue
	
	Block a user