Files
smartacme/ts_server/server.classes.challenge.verifier.ts

62 lines
1.7 KiB
TypeScript

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<boolean> {
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<boolean> {
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<string> {
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'));
});
});
}
}