feat(smartproxy/certificate): Integrate HTTP-01 challenge handler into ACME certificate provisioning workflow
This commit is contained in:
		| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartproxy', | ||||
|   version: '18.1.1', | ||||
|   version: '18.2.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.' | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,7 @@ export class SmartCertManager { | ||||
|   private networkProxy: NetworkProxy | null = null; | ||||
|   private renewalTimer: NodeJS.Timeout | null = null; | ||||
|   private pendingChallenges: Map<string, string> = new Map(); | ||||
|   private challengeRoute: IRouteConfig | null = null; | ||||
|    | ||||
|   // Track certificate status by route name | ||||
|   private certStatus: Map<string, ICertStatus> = new Map(); | ||||
| @@ -69,11 +70,18 @@ export class SmartCertManager { | ||||
|     ); | ||||
|      | ||||
|     if (hasAcmeRoutes && this.acmeOptions?.email) { | ||||
|       // Create SmartAcme instance with built-in MemoryCertManager | ||||
|       // Create HTTP-01 challenge handler | ||||
|       const http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler(); | ||||
|        | ||||
|       // Set up challenge handler integration with our routing | ||||
|       this.setupChallengeHandler(http01Handler); | ||||
|        | ||||
|       // Create SmartAcme instance with built-in MemoryCertManager and HTTP-01 handler | ||||
|       this.smartAcme = new plugins.smartacme.SmartAcme({ | ||||
|         accountEmail: this.acmeOptions.email, | ||||
|         environment: this.acmeOptions.useProduction ? 'production' : 'integration', | ||||
|         certManager: new plugins.smartacme.certmanagers.MemoryCertManager() | ||||
|         certManager: new plugins.smartacme.certmanagers.MemoryCertManager(), | ||||
|         challengeHandlers: [http01Handler] | ||||
|       }); | ||||
|        | ||||
|       await this.smartAcme.start(); | ||||
| @@ -157,30 +165,43 @@ export class SmartCertManager { | ||||
|     this.updateCertStatus(routeName, 'pending', 'acme'); | ||||
|      | ||||
|     try { | ||||
|       // Use smartacme to get certificate | ||||
|       const cert = await this.smartAcme.getCertificateForDomain(primaryDomain); | ||||
|       // Add challenge route before requesting certificate | ||||
|       await this.addChallengeRoute(); | ||||
|        | ||||
|       try { | ||||
|         // Use smartacme to get certificate | ||||
|         const cert = await this.smartAcme.getCertificateForDomain(primaryDomain); | ||||
|        | ||||
|       // SmartAcme's Cert object has these properties: | ||||
|       // - certPem: The certificate PEM string | ||||
|       // - privateKeyPem: The private key PEM string | ||||
|       // - publicKey: The certificate PEM string   | ||||
|       // - privateKey: The private key PEM string | ||||
|       // - csr: Certificate signing request | ||||
|       // - validUntil: Expiry date as Date object | ||||
|       // - validUntil: Timestamp in milliseconds | ||||
|       // - domainName: The domain name | ||||
|       const certData: ICertificateData = { | ||||
|         cert: cert.certPem, | ||||
|         key: cert.privateKeyPem, | ||||
|         ca: cert.certPem, // Use same as cert for now | ||||
|         expiryDate: cert.validUntil, | ||||
|         issueDate: new Date() // SmartAcme doesn't provide issue date | ||||
|         cert: cert.publicKey, | ||||
|         key: cert.privateKey,  | ||||
|         ca: cert.publicKey, // Use same as cert for now | ||||
|         expiryDate: new Date(cert.validUntil), | ||||
|         issueDate: new Date(cert.created) | ||||
|       }; | ||||
|        | ||||
|       await this.certStore.saveCertificate(routeName, certData); | ||||
|       await this.applyCertificate(primaryDomain, certData); | ||||
|       this.updateCertStatus(routeName, 'valid', 'acme', certData); | ||||
|        | ||||
|       console.log(`Successfully provisioned ACME certificate for ${primaryDomain}`); | ||||
|         console.log(`Successfully provisioned ACME certificate for ${primaryDomain}`); | ||||
|       } catch (error) { | ||||
|         console.error(`Failed to provision ACME certificate for ${primaryDomain}: ${error}`); | ||||
|         this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message); | ||||
|         throw error; | ||||
|       } finally { | ||||
|         // Always remove challenge route after provisioning | ||||
|         await this.removeChallengeRoute(); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       console.error(`Failed to provision ACME certificate for ${primaryDomain}: ${error}`); | ||||
|       // Handle outer try-catch from adding challenge route | ||||
|       console.error(`Failed to setup ACME challenge for ${primaryDomain}: ${error}`); | ||||
|       this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message); | ||||
|       throw error; | ||||
|     } | ||||
| @@ -287,40 +308,6 @@ export class SmartCertManager { | ||||
|     return cert.expiryDate > expiryThreshold; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Create ACME challenge route | ||||
|    * NOTE: SmartProxy already handles path-based routing and priority | ||||
|    */ | ||||
|   private createChallengeRoute(): IRouteConfig { | ||||
|     return { | ||||
|       name: 'acme-challenge', | ||||
|       priority: 1000,  // High priority to ensure it's checked first | ||||
|       match: { | ||||
|         ports: 80, | ||||
|         path: '/.well-known/acme-challenge/*' | ||||
|       }, | ||||
|       action: { | ||||
|         type: 'static', | ||||
|         handler: async (context) => { | ||||
|           const token = context.path?.split('/').pop(); | ||||
|           const keyAuth = token ? this.pendingChallenges.get(token) : undefined; | ||||
|            | ||||
|           if (keyAuth) { | ||||
|             return { | ||||
|               status: 200, | ||||
|               headers: { 'Content-Type': 'text/plain' }, | ||||
|               body: keyAuth | ||||
|             }; | ||||
|           } else { | ||||
|             return { | ||||
|               status: 404, | ||||
|               body: 'Not found' | ||||
|             }; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Add challenge route to SmartProxy | ||||
| @@ -330,9 +317,12 @@ export class SmartCertManager { | ||||
|       throw new Error('No route update callback set'); | ||||
|     } | ||||
|      | ||||
|     const challengeRoute = this.createChallengeRoute(); | ||||
|     const updatedRoutes = [...this.routes, challengeRoute]; | ||||
|     if (!this.challengeRoute) { | ||||
|       throw new Error('Challenge route not initialized'); | ||||
|     } | ||||
|     const challengeRoute = this.challengeRoute; | ||||
|      | ||||
|     const updatedRoutes = [...this.routes, challengeRoute]; | ||||
|     await this.updateRoutesCallback(updatedRoutes); | ||||
|   } | ||||
|    | ||||
| @@ -424,27 +414,66 @@ export class SmartCertManager { | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Handle ACME challenge | ||||
|    * Setup challenge handler integration with SmartProxy routing | ||||
|    */ | ||||
|   private async handleChallenge(token: string, keyAuth: string): Promise<void> { | ||||
|     this.pendingChallenges.set(token, keyAuth); | ||||
|   private setupChallengeHandler(http01Handler: plugins.smartacme.handlers.Http01MemoryHandler): void { | ||||
|     // Create a challenge route that delegates to SmartAcme's HTTP-01 handler | ||||
|     const challengeRoute: IRouteConfig = { | ||||
|       name: 'acme-challenge', | ||||
|       priority: 1000,  // High priority | ||||
|       match: { | ||||
|         ports: 80, | ||||
|         path: '/.well-known/acme-challenge/*' | ||||
|       }, | ||||
|       action: { | ||||
|         type: 'static', | ||||
|         handler: async (context) => { | ||||
|           // Extract the token from the path | ||||
|           const token = context.path?.split('/').pop(); | ||||
|           if (!token) { | ||||
|             return { status: 404, body: 'Not found' }; | ||||
|           } | ||||
|            | ||||
|           // Create mock request/response objects for SmartAcme | ||||
|           const mockReq = { | ||||
|             url: context.path, | ||||
|             method: 'GET', | ||||
|             headers: context.headers || {} | ||||
|           }; | ||||
|            | ||||
|           let responseData: any = null; | ||||
|           const mockRes = { | ||||
|             statusCode: 200, | ||||
|             setHeader: (name: string, value: string) => {}, | ||||
|             end: (data: any) => { | ||||
|               responseData = data; | ||||
|             } | ||||
|           }; | ||||
|            | ||||
|           // Use SmartAcme's handler | ||||
|           const handled = await new Promise<boolean>((resolve) => { | ||||
|             http01Handler.handleRequest(mockReq as any, mockRes as any, () => { | ||||
|               resolve(false); | ||||
|             }); | ||||
|             // Give it a moment to process | ||||
|             setTimeout(() => resolve(true), 100); | ||||
|           }); | ||||
|            | ||||
|           if (handled && responseData) { | ||||
|             return { | ||||
|               status: mockRes.statusCode, | ||||
|               headers: { 'Content-Type': 'text/plain' }, | ||||
|               body: responseData | ||||
|             }; | ||||
|           } else { | ||||
|             return { status: 404, body: 'Not found' }; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
|      | ||||
|     // Add challenge route if it's the first challenge | ||||
|     if (this.pendingChallenges.size === 1) { | ||||
|       await this.addChallengeRoute(); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Cleanup ACME challenge | ||||
|    */ | ||||
|   private async cleanupChallenge(token: string): Promise<void> { | ||||
|     this.pendingChallenges.delete(token); | ||||
|      | ||||
|     // Remove challenge route if no more challenges | ||||
|     if (this.pendingChallenges.size === 0) { | ||||
|       await this.removeChallengeRoute(); | ||||
|     } | ||||
|     // Store the challenge route to add it when needed | ||||
|     this.challengeRoute = challengeRoute; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|   | ||||
		Reference in New Issue
	
	Block a user