feat(acme): Add ACME interfaces for Port80Handler and refactor ChallengeResponder to use new acme-interfaces, enhancing event subscription and certificate workflows.
This commit is contained in:
		| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '12.1.0', | ||||
|   version: '12.2.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.' | ||||
| } | ||||
|   | ||||
							
								
								
									
										85
									
								
								ts/http/port80/acme-interfaces.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								ts/http/port80/acme-interfaces.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| /** | ||||
|  * Type definitions for SmartAcme interfaces used by ChallengeResponder | ||||
|  * These reflect the actual SmartAcme API based on the documentation | ||||
|  */ | ||||
| import * as plugins from '../../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * Structure for SmartAcme certificate result | ||||
|  */ | ||||
| export interface SmartAcmeCert { | ||||
|   id?: string; | ||||
|   domainName: string; | ||||
|   created?: number | Date | string; | ||||
|   privateKey: string; | ||||
|   publicKey: string; | ||||
|   csr?: string; | ||||
|   validUntil: number | Date | string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Structure for SmartAcme options | ||||
|  */ | ||||
| export interface SmartAcmeOptions { | ||||
|   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<SmartAcmeCert | null>; | ||||
|   put(cert: SmartAcmeCert): Promise<SmartAcmeCert>; | ||||
|   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 Http01Challenge { | ||||
|   type: string; // 'http-01' | ||||
|   token: string; | ||||
|   keyAuthorization: string; | ||||
|   webPath: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * HTTP-01 Memory Handler Interface | ||||
|  */ | ||||
| export interface Http01MemoryHandler extends IChallengeHandler<Http01Challenge> { | ||||
|   handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, next?: () => void): void; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * SmartAcme main class interface | ||||
|  */ | ||||
| export interface SmartAcme { | ||||
|   start(): Promise<void>; | ||||
|   stop(): Promise<void>; | ||||
|   getCertificateForDomain(domain: string): Promise<SmartAcmeCert>; | ||||
|   on?(event: string, listener: (data: any) => void): void; | ||||
|   eventEmitter?: plugins.EventEmitter; | ||||
| } | ||||
| @@ -1,20 +1,27 @@ | ||||
| import * as plugins from '../../plugins.js'; | ||||
| import { IncomingMessage, ServerResponse } from 'http'; | ||||
| import {  | ||||
|   CertificateEvents  | ||||
| import { | ||||
|   CertificateEvents | ||||
| } from '../../certificate/events/certificate-events.js'; | ||||
| import type { | ||||
|   CertificateData, | ||||
|   CertificateFailure, | ||||
|   CertificateExpiring | ||||
| } from '../../certificate/models/certificate-types.js'; | ||||
| import type { | ||||
|   SmartAcme, | ||||
|   SmartAcmeCert, | ||||
|   SmartAcmeOptions, | ||||
|   Http01MemoryHandler | ||||
| } from './acme-interfaces.js'; | ||||
|  | ||||
| /** | ||||
|  * Handles ACME HTTP-01 challenge responses | ||||
|  * 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: plugins.smartacme.SmartAcme | null = null; | ||||
|   private http01Handler: plugins.smartacme.handlers.Http01MemoryHandler | null = null; | ||||
|   private smartAcme: SmartAcme | null = null; | ||||
|   private http01Handler: Http01MemoryHandler | null = null; | ||||
|  | ||||
|   /** | ||||
|    * Creates a new challenge responder | ||||
| @@ -35,198 +42,134 @@ export class ChallengeResponder extends plugins.EventEmitter { | ||||
|    */ | ||||
|   public async initialize(): Promise<void> { | ||||
|     try { | ||||
|       // Initialize HTTP-01 challenge handler | ||||
|       // Create the HTTP-01 memory handler from SmartACME | ||||
|       this.http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler(); | ||||
|        | ||||
|       // 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(); | ||||
|  | ||||
|       // 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.setupEventForwarding(); | ||||
|        | ||||
|       // Start SmartAcme | ||||
|       this.setupEventListeners(); | ||||
|  | ||||
|       // Start the SmartACME client | ||||
|       await this.smartAcme.start(); | ||||
|       console.log('ACME client initialized successfully'); | ||||
|     } catch (error) { | ||||
|       throw new Error(`Failed to initialize ACME client: ${error instanceof Error ? error.message : String(error)}`); | ||||
|       const errorMessage = error instanceof Error ? error.message : String(error); | ||||
|       throw new Error(`Failed to initialize ACME client: ${errorMessage}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * 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 | ||||
|    * Ensure the certificate store directory exists | ||||
|    */ | ||||
|   private async ensureCertificateStore(): Promise<void> { | ||||
|     try { | ||||
|       await plugins.fs.promises.mkdir(this.certificateStore, { recursive: true }); | ||||
|     } catch (error) { | ||||
|       throw new Error(`Failed to create certificate store: ${error instanceof Error ? error.message : String(error)}`); | ||||
|       const errorMessage = error instanceof Error ? error.message : String(error); | ||||
|       throw new Error(`Failed to create certificate store: ${errorMessage}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Handle HTTP request and check if it's an ACME challenge | ||||
|    * @param req HTTP request | ||||
|    * @param res HTTP response | ||||
|    * @returns true if the request was handled as an ACME challenge | ||||
|    * 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: CertificateData = { | ||||
|           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: CertificateFailure = { | ||||
|           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; | ||||
|     } | ||||
|     if (!this.http01Handler) return false; | ||||
|  | ||||
|     const url = req.url || '/'; | ||||
|  | ||||
|     // Check if this is an ACME challenge request | ||||
|     // Check if this is an ACME challenge request (/.well-known/acme-challenge/*) | ||||
|     const url = req.url || ''; | ||||
|     if (url.startsWith('/.well-known/acme-challenge/')) { | ||||
|       const token = url.split('/').pop() || ''; | ||||
|  | ||||
|       if (token && this.http01Handler) { | ||||
|         try { | ||||
|           // Try to delegate to the handler - casting to any for flexibility | ||||
|           const handler = this.http01Handler as any; | ||||
|  | ||||
|           // 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 | ||||
|         } | ||||
|       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; | ||||
|       } | ||||
|  | ||||
|       // Invalid ACME challenge | ||||
|       res.writeHead(404); | ||||
|       res.end('Not found'); | ||||
|       return true; | ||||
|     } | ||||
|  | ||||
|      | ||||
|     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 | ||||
|    * @param isRenewal Whether this is a renewal | ||||
|    * @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<CertificateData> { | ||||
|     if (!this.smartAcme) { | ||||
| @@ -234,9 +177,10 @@ export class ChallengeResponder extends plugins.EventEmitter { | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       // Request certificate via SmartAcme | ||||
|       // Request certificate using SmartACME | ||||
|       const certObj = await this.smartAcme.getCertificateForDomain(domain); | ||||
|        | ||||
|       // Convert the certificate object to our CertificateData format | ||||
|       const certData: CertificateData = { | ||||
|         domain, | ||||
|         certificate: certObj.publicKey, | ||||
| @@ -246,26 +190,19 @@ export class ChallengeResponder extends plugins.EventEmitter { | ||||
|         isRenewal | ||||
|       }; | ||||
|        | ||||
|       // 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 { | ||||
|         this.emit(CertificateEvents.CERTIFICATE_ISSUED, certData); | ||||
|       } | ||||
|        | ||||
|       return certData; | ||||
|     } catch (error) { | ||||
|       // Construct failure object | ||||
|       // Create failure object | ||||
|       const failure: CertificateFailure = { | ||||
|         domain, | ||||
|         error: error instanceof Error ? error.message : String(error), | ||||
|         isRenewal, | ||||
|         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) | ||||
|       }`); | ||||
| @@ -273,19 +210,17 @@ export class ChallengeResponder extends plugins.EventEmitter { | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Check if a certificate is expiring soon | ||||
|    * 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 a renewal | ||||
|    * @param thresholdDays Days before expiry to trigger renewal | ||||
|    */ | ||||
|   public checkCertificateExpiry( | ||||
|     domain: string, | ||||
|     certificate: CertificateData, | ||||
|     thresholdDays: number = 30 | ||||
|   ): void { | ||||
|     if (!certificate.expiryDate) { | ||||
|       return; | ||||
|     } | ||||
|     if (!certificate.expiryDate) return; | ||||
|      | ||||
|     const now = new Date(); | ||||
|     const expiryDate = certificate.expiryDate; | ||||
| @@ -295,7 +230,7 @@ export class ChallengeResponder extends plugins.EventEmitter { | ||||
|       const expiryInfo: CertificateExpiring = { | ||||
|         domain, | ||||
|         expiryDate, | ||||
|         daysRemaining: daysDifference, | ||||
|         daysRemaining: daysDifference | ||||
|       }; | ||||
|        | ||||
|       this.emit(CertificateEvents.CERTIFICATE_EXPIRING, expiryInfo); | ||||
|   | ||||
| @@ -550,6 +550,7 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|  | ||||
|     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 | ||||
| @@ -559,13 +560,9 @@ export class Port80Handler extends plugins.EventEmitter { | ||||
|       domainInfo.expiryDate = certData.expiryDate; | ||||
|  | ||||
|       console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`); | ||||
|  | ||||
|       // The event will be emitted by the ChallengeResponder, we just store the certificate | ||||
|     } catch (error: any) { | ||||
|       const errorMsg = error instanceof Error ? error.message : String(error); | ||||
|       console.error(`Error during certificate issuance for ${domain}:`, error); | ||||
|  | ||||
|       // The failure event will be emitted by the ChallengeResponder | ||||
|       throw new CertificateError(errorMsg, domain, isRenewal); | ||||
|     } finally { | ||||
|       domainInfo.obtainingInProgress = false; | ||||
|   | ||||
| @@ -1,3 +1,8 @@ | ||||
| /** | ||||
|  * NetworkProxy implementation | ||||
|  */ | ||||
| // Re-export models | ||||
| export * from './models/index.js'; | ||||
|  | ||||
| // Core NetworkProxy will be added later: | ||||
| // export { NetworkProxy } from './network-proxy.js'; | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| /** | ||||
|  * NetworkProxy models | ||||
|  */ | ||||
| export * from './types.js'; | ||||
|   | ||||
							
								
								
									
										130
									
								
								ts/proxies/network-proxy/models/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								ts/proxies/network-proxy/models/types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| import * as plugins from '../../../plugins.js'; | ||||
| import type { AcmeOptions } from '../../../certificate/models/certificate-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Configuration options for NetworkProxy | ||||
|  */ | ||||
| export interface NetworkProxyOptions { | ||||
|   port: number; | ||||
|   maxConnections?: number; | ||||
|   keepAliveTimeout?: number; | ||||
|   headersTimeout?: number; | ||||
|   logLevel?: 'error' | 'warn' | 'info' | 'debug'; | ||||
|   cors?: { | ||||
|     allowOrigin?: string; | ||||
|     allowMethods?: string; | ||||
|     allowHeaders?: string; | ||||
|     maxAge?: number; | ||||
|   }; | ||||
|    | ||||
|   // Settings for SmartProxy integration | ||||
|   connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend | ||||
|   portProxyIntegration?: boolean; // Flag to indicate this proxy is used by SmartProxy | ||||
|   useExternalPort80Handler?: boolean; // Flag to indicate using external Port80Handler | ||||
|   // Protocol to use when proxying to backends: HTTP/1.x or HTTP/2 | ||||
|   backendProtocol?: 'http1' | 'http2'; | ||||
|    | ||||
|   // ACME certificate management options | ||||
|   acme?: AcmeOptions; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Interface for a certificate entry in the cache | ||||
|  */ | ||||
| export interface CertificateEntry { | ||||
|   key: string; | ||||
|   cert: string; | ||||
|   expires?: Date; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Interface for reverse proxy configuration | ||||
|  */ | ||||
| export interface ReverseProxyConfig { | ||||
|   destinationIps: string[]; | ||||
|   destinationPorts: number[]; | ||||
|   hostName: string; | ||||
|   privateKey: string; | ||||
|   publicKey: string; | ||||
|   authentication?: { | ||||
|     type: 'Basic'; | ||||
|     user: string; | ||||
|     pass: string; | ||||
|   }; | ||||
|   rewriteHostHeader?: boolean; | ||||
|   /** | ||||
|    * Protocol to use when proxying to this backend: 'http1' or 'http2'. | ||||
|    * Overrides the global backendProtocol option if set. | ||||
|    */ | ||||
|   backendProtocol?: 'http1' | 'http2'; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Interface for connection tracking in the pool | ||||
|  */ | ||||
| export interface ConnectionEntry { | ||||
|   socket: plugins.net.Socket; | ||||
|   lastUsed: number; | ||||
|   isIdle: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * WebSocket with heartbeat interface | ||||
|  */ | ||||
| export interface WebSocketWithHeartbeat extends plugins.wsDefault { | ||||
|   lastPong: number; | ||||
|   isAlive: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Logger interface for consistent logging across components | ||||
|  */ | ||||
| export interface Logger { | ||||
|   debug(message: string, data?: any): void; | ||||
|   info(message: string, data?: any): void; | ||||
|   warn(message: string, data?: any): void; | ||||
|   error(message: string, data?: any): void; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Creates a logger based on the specified log level | ||||
|  */ | ||||
| export function createLogger(logLevel: string = 'info'): Logger { | ||||
|   const logLevels = { | ||||
|     error: 0, | ||||
|     warn: 1, | ||||
|     info: 2, | ||||
|     debug: 3 | ||||
|   }; | ||||
|    | ||||
|   return { | ||||
|     debug: (message: string, data?: any) => { | ||||
|       if (logLevels[logLevel] >= logLevels.debug) { | ||||
|         console.log(`[DEBUG] ${message}`, data || ''); | ||||
|       } | ||||
|     }, | ||||
|     info: (message: string, data?: any) => { | ||||
|       if (logLevels[logLevel] >= logLevels.info) { | ||||
|         console.log(`[INFO] ${message}`, data || ''); | ||||
|       } | ||||
|     }, | ||||
|     warn: (message: string, data?: any) => { | ||||
|       if (logLevels[logLevel] >= logLevels.warn) { | ||||
|         console.warn(`[WARN] ${message}`, data || ''); | ||||
|       } | ||||
|     }, | ||||
|     error: (message: string, data?: any) => { | ||||
|       if (logLevels[logLevel] >= logLevels.error) { | ||||
|         console.error(`[ERROR] ${message}`, data || ''); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
| } | ||||
|  | ||||
| // Backward compatibility interfaces | ||||
| export interface INetworkProxyOptions extends NetworkProxyOptions {} | ||||
| export interface ICertificateEntry extends CertificateEntry {} | ||||
| export interface IReverseProxyConfig extends ReverseProxyConfig {} | ||||
| export interface IConnectionEntry extends ConnectionEntry {} | ||||
| export interface IWebSocketWithHeartbeat extends WebSocketWithHeartbeat {} | ||||
| export interface ILogger extends Logger {} | ||||
| @@ -1,3 +1,5 @@ | ||||
| /** | ||||
|  * NfTablesProxy implementation | ||||
|  */ | ||||
| // Core NfTablesProxy will be added later: | ||||
| // export { NfTablesProxy } from './nftables-proxy.js'; | ||||
|   | ||||
| @@ -1,3 +1,8 @@ | ||||
| /** | ||||
|  * SmartProxy implementation | ||||
|  */ | ||||
| // Re-export models | ||||
| export * from './models/index.js'; | ||||
|  | ||||
| // Core SmartProxy will be added later: | ||||
| // export { SmartProxy } from './smart-proxy.js'; | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| /** | ||||
|  * SmartProxy models | ||||
|  */ | ||||
| export * from './interfaces.js'; | ||||
|   | ||||
							
								
								
									
										142
									
								
								ts/proxies/smart-proxy/models/interfaces.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								ts/proxies/smart-proxy/models/interfaces.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| import * as plugins from '../../../plugins.js'; | ||||
| import type { ForwardConfig } from '../../../forwarding/config/forwarding-types.js'; | ||||
|  | ||||
| /** | ||||
|  * Provision object for static or HTTP-01 certificate | ||||
|  */ | ||||
| export type SmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01'; | ||||
|  | ||||
| /** | ||||
|  * Domain configuration with forwarding configuration | ||||
|  */ | ||||
| export interface DomainConfig { | ||||
|   domains: string[]; // Glob patterns for domain(s) | ||||
|   forwarding: ForwardConfig; // Unified forwarding configuration | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Configuration options for the SmartProxy | ||||
|  */ | ||||
| import type { AcmeOptions } from '../../../certificate/models/certificate-types.js'; | ||||
| export interface SmartProxyOptions { | ||||
|   fromPort: number; | ||||
|   toPort: number; | ||||
|   targetIP?: string; // Global target host to proxy to, defaults to 'localhost' | ||||
|   domainConfigs: DomainConfig[]; | ||||
|   sniEnabled?: boolean; | ||||
|   defaultAllowedIPs?: string[]; | ||||
|   defaultBlockedIPs?: string[]; | ||||
|   preserveSourceIP?: boolean; | ||||
|  | ||||
|   // TLS options | ||||
|   pfx?: Buffer; | ||||
|   key?: string | Buffer | Array<Buffer | string>; | ||||
|   passphrase?: string; | ||||
|   cert?: string | Buffer | Array<string | Buffer>; | ||||
|   ca?: string | Buffer | Array<string | Buffer>; | ||||
|   ciphers?: string; | ||||
|   honorCipherOrder?: boolean; | ||||
|   rejectUnauthorized?: boolean; | ||||
|   secureProtocol?: string; | ||||
|   servername?: string; | ||||
|   minVersion?: string; | ||||
|   maxVersion?: string; | ||||
|  | ||||
|   // Timeout settings | ||||
|   initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s) | ||||
|   socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h) | ||||
|   inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s) | ||||
|   maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 86400000 (24h) | ||||
|   inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h) | ||||
|  | ||||
|   gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown | ||||
|   globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges | ||||
|   forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP | ||||
|  | ||||
|   // Socket optimization settings | ||||
|   noDelay?: boolean; // Disable Nagle's algorithm (default: true) | ||||
|   keepAlive?: boolean; // Enable TCP keepalive (default: true) | ||||
|   keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms) | ||||
|   maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup | ||||
|  | ||||
|   // Enhanced features | ||||
|   disableInactivityCheck?: boolean; // Disable inactivity checking entirely | ||||
|   enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes | ||||
|   enableDetailedLogging?: boolean; // Enable detailed connection logging | ||||
|   enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging | ||||
|   enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd | ||||
|   allowSessionTicket?: boolean; // Allow TLS session ticket for reconnection (default: true) | ||||
|  | ||||
|   // Rate limiting and security | ||||
|   maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP | ||||
|   connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP | ||||
|  | ||||
|   // Enhanced keep-alive settings | ||||
|   keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections | ||||
|   keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections | ||||
|   extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms) | ||||
|  | ||||
|   // NetworkProxy integration | ||||
|   useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy | ||||
|   networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443) | ||||
|  | ||||
|   // ACME configuration options for SmartProxy | ||||
|   acme?: AcmeOptions; | ||||
|    | ||||
|   /** | ||||
|    * Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges, | ||||
|    * or a static certificate object for immediate provisioning. | ||||
|    */ | ||||
|   certProvisionFunction?: (domain: string) => Promise<SmartProxyCertProvisionObject>; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Enhanced connection record | ||||
|  */ | ||||
| export interface ConnectionRecord { | ||||
|   id: string; // Unique connection identifier | ||||
|   incoming: plugins.net.Socket; | ||||
|   outgoing: plugins.net.Socket | null; | ||||
|   incomingStartTime: number; | ||||
|   outgoingStartTime?: number; | ||||
|   outgoingClosedTime?: number; | ||||
|   lockedDomain?: string; // Used to lock this connection to the initial SNI | ||||
|   connectionClosed: boolean; // Flag to prevent multiple cleanup attempts | ||||
|   cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity | ||||
|   alertFallbackTimeout?: NodeJS.Timeout; // Timer for fallback after alert | ||||
|   lastActivity: number; // Last activity timestamp for inactivity detection | ||||
|   pendingData: Buffer[]; // Buffer to hold data during connection setup | ||||
|   pendingDataSize: number; // Track total size of pending data | ||||
|  | ||||
|   // Enhanced tracking fields | ||||
|   bytesReceived: number; // Total bytes received | ||||
|   bytesSent: number; // Total bytes sent | ||||
|   remoteIP: string; // Remote IP (cached for logging after socket close) | ||||
|   localPort: number; // Local port (cached for logging) | ||||
|   isTLS: boolean; // Whether this connection is a TLS connection | ||||
|   tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete | ||||
|   hasReceivedInitialData: boolean; // Whether initial data has been received | ||||
|   domainConfig?: DomainConfig; // Associated domain config for this connection | ||||
|  | ||||
|   // Keep-alive tracking | ||||
|   hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection | ||||
|   inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued | ||||
|   incomingTerminationReason?: string | null; // Reason for incoming termination | ||||
|   outgoingTerminationReason?: string | null; // Reason for outgoing termination | ||||
|  | ||||
|   // NetworkProxy tracking | ||||
|   usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy | ||||
|  | ||||
|   // Renegotiation handler | ||||
|   renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection | ||||
|  | ||||
|   // Browser connection tracking | ||||
|   isBrowserConnection?: boolean; // Whether this connection appears to be from a browser | ||||
|   domainSwitches?: number; // Number of times the domain has been switched on this connection | ||||
| } | ||||
|  | ||||
| // Backward compatibility types | ||||
| export type ISmartProxyCertProvisionObject = SmartProxyCertProvisionObject; | ||||
| export interface IDomainConfig extends DomainConfig {} | ||||
| export interface ISmartProxyOptions extends SmartProxyOptions {} | ||||
| export interface IConnectionRecord extends ConnectionRecord {} | ||||
		Reference in New Issue
	
	Block a user