import * as http from 'node:http'; /** * Verifies ACME challenges by making HTTP requests or DNS lookups. */ export class ChallengeVerifier { private verificationEnabled: boolean; constructor(verificationEnabled = true) { this.verificationEnabled = verificationEnabled; } /** * Verify an HTTP-01 challenge by fetching the token from the domain. */ async verifyHttp01(domain: string, token: string, expectedKeyAuth: string): Promise { if (!this.verificationEnabled) { return true; } try { const url = `http://${domain}/.well-known/acme-challenge/${token}`; const body = await this.httpGet(url); return body.trim() === expectedKeyAuth.trim(); } catch { return false; } } /** * Verify a DNS-01 challenge by looking up the TXT record. */ async verifyDns01(domain: string, expectedHash: string): Promise { if (!this.verificationEnabled) { return true; } try { const { promises: dns } = await import('node:dns'); const records = await dns.resolveTxt(`_acme-challenge.${domain}`); const flatRecords = records.map((r) => r.join('')); return flatRecords.some((r) => r === expectedHash); } catch { return false; } } private httpGet(url: string): Promise { return new Promise((resolve, reject) => { const req = http.get(url, { timeout: 10000 }, (res) => { const chunks: Buffer[] = []; res.on('data', (chunk: Buffer) => chunks.push(chunk)); res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8'))); }); req.on('error', reject); req.on('timeout', () => { req.destroy(new Error('HTTP-01 verification timeout')); }); }); } }