62 lines
1.7 KiB
TypeScript
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'));
|
|
});
|
|
});
|
|
}
|
|
}
|