274 lines
7.9 KiB
JavaScript
274 lines
7.9 KiB
JavaScript
/**
|
|
* DNS Propagation Checker
|
|
* Verifies DNS record propagation by querying multiple resolvers.
|
|
* Runs as background jobs with configurable timeout and interval.
|
|
*
|
|
* @module dns-propagation
|
|
*/
|
|
|
|
const dns = require('dns').promises;
|
|
const EventEmitter = require('events');
|
|
|
|
/** Default verification options */
|
|
const DEFAULT_OPTIONS = {
|
|
timeout: 300000, // 5 minutes
|
|
interval: 10000, // 10 seconds
|
|
resolvers: ['1.1.1.1', '8.8.8.8', '9.9.9.9']
|
|
};
|
|
|
|
/** Maximum age for stored verification results (1 hour) */
|
|
const MAX_RESULT_AGE_MS = 3600000;
|
|
|
|
class DNSPropagationChecker extends EventEmitter {
|
|
/**
|
|
* Create a DNSPropagationChecker instance.
|
|
* @param {Object} ctx - Shared application context
|
|
* @param {Object} ctx.notification - NotificationManager instance
|
|
* @param {Object} ctx.log - Logger instance
|
|
*/
|
|
constructor(ctx) {
|
|
super();
|
|
this.ctx = ctx;
|
|
this.log = ctx.log || console;
|
|
|
|
/** @type {Map<string, Object>} domain → verification status */
|
|
this.verifications = new Map();
|
|
}
|
|
|
|
/**
|
|
* Verify that a DNS record has propagated by querying multiple resolvers.
|
|
* Retries every `interval` ms until `timeout` is reached.
|
|
*
|
|
* @param {string} domain - The domain to check (e.g., 'test.sami')
|
|
* @param {string} expectedIp - The expected IP address
|
|
* @param {Object} [options={}] - Verification options
|
|
* @param {number} [options.timeout=300000] - Maximum time to wait (ms)
|
|
* @param {number} [options.interval=10000] - Time between retries (ms)
|
|
* @param {string[]} [options.resolvers] - DNS resolvers to query
|
|
* @returns {Promise<Object>} Verification result
|
|
*/
|
|
async verifyRecord(domain, expectedIp, options = {}) {
|
|
const startTime = Date.now();
|
|
const {
|
|
timeout = DEFAULT_OPTIONS.timeout,
|
|
interval = DEFAULT_OPTIONS.interval,
|
|
resolvers = DEFAULT_OPTIONS.resolvers
|
|
} = options;
|
|
|
|
const allResults = [];
|
|
let propagated = false;
|
|
|
|
while (Date.now() - startTime < timeout) {
|
|
const roundResults = [];
|
|
|
|
for (const resolver of resolvers) {
|
|
const checkStart = Date.now();
|
|
try {
|
|
// Use dns.resolve4 with a custom resolver
|
|
const resolverInstance = new dns.Resolver();
|
|
resolverInstance.setServers([resolver]);
|
|
resolverInstance.setTimeout(5000);
|
|
|
|
const addresses = await resolverInstance.resolve4(domain);
|
|
const matched = addresses.includes(expectedIp);
|
|
|
|
const result = {
|
|
resolver,
|
|
ips: addresses,
|
|
matched,
|
|
checkedAt: new Date().toISOString(),
|
|
responseTime: Date.now() - checkStart
|
|
};
|
|
|
|
roundResults.push(result);
|
|
|
|
if (matched) {
|
|
propagated = true;
|
|
}
|
|
} catch (err) {
|
|
roundResults.push({
|
|
resolver,
|
|
ips: [],
|
|
matched: false,
|
|
checkedAt: new Date().toISOString(),
|
|
error: err.code || err.message,
|
|
responseTime: Date.now() - checkStart
|
|
});
|
|
}
|
|
}
|
|
|
|
allResults.push(...roundResults);
|
|
|
|
// Emit progress event
|
|
this.emit('propagation-check', {
|
|
domain,
|
|
expectedIp,
|
|
roundResults,
|
|
elapsed: Date.now() - startTime,
|
|
propagated
|
|
});
|
|
|
|
if (propagated) {
|
|
break;
|
|
}
|
|
|
|
// Wait before next attempt
|
|
await new Promise(resolve => setTimeout(resolve, interval));
|
|
}
|
|
|
|
const totalTime = Date.now() - startTime;
|
|
|
|
return {
|
|
domain,
|
|
expectedIp,
|
|
propagated,
|
|
results: allResults,
|
|
totalTime,
|
|
checkedAt: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Start a background DNS propagation verification.
|
|
* Does not block — returns immediately with the job reference.
|
|
*
|
|
* @param {string} domain - The domain to verify
|
|
* @param {string} expectedIp - The expected IP address
|
|
* @param {Object} [options={}] - Verification options
|
|
* @returns {Object} Job status object
|
|
*/
|
|
startVerification(domain, expectedIp, options = {}) {
|
|
// If there's already a running verification for this domain, return it
|
|
const existing = this.verifications.get(domain);
|
|
if (existing && existing.status === 'running') {
|
|
return existing;
|
|
}
|
|
|
|
const job = {
|
|
domain,
|
|
expectedIp,
|
|
status: 'running',
|
|
startedAt: new Date().toISOString(),
|
|
progress: [],
|
|
result: null
|
|
};
|
|
|
|
this.verifications.set(domain, job);
|
|
|
|
// Run verification in background (non-blocking)
|
|
this.verifyRecord(domain, expectedIp, options)
|
|
.then(result => {
|
|
job.status = 'completed';
|
|
job.result = result;
|
|
job.completedAt = new Date().toISOString();
|
|
|
|
if (result.propagated) {
|
|
this.emit('propagation-complete', result);
|
|
|
|
if (this.ctx.notification) {
|
|
this.ctx.notification.send('dns-propagation', {
|
|
text: `✅ DNS record for ${domain} propagated successfully to ${expectedIp}`,
|
|
domain,
|
|
expectedIp,
|
|
totalTime: result.totalTime
|
|
}, 'success').catch(err => {
|
|
this.log.error('dns-propagation', 'Failed to send propagation notification', {
|
|
error: err.message
|
|
});
|
|
});
|
|
}
|
|
} else {
|
|
this.emit('propagation-timeout', result);
|
|
|
|
if (this.ctx.notification) {
|
|
this.ctx.notification.send('dns-propagation', {
|
|
text: `⏱️ DNS propagation timeout for ${domain} — expected ${expectedIp} not found after ${Math.round(result.totalTime / 1000)}s`,
|
|
domain,
|
|
expectedIp,
|
|
totalTime: result.totalTime
|
|
}, 'warning').catch(err => {
|
|
this.log.error('dns-propagation', 'Failed to send timeout notification', {
|
|
error: err.message
|
|
});
|
|
});
|
|
}
|
|
}
|
|
})
|
|
.catch(err => {
|
|
job.status = 'error';
|
|
job.error = err.message;
|
|
job.completedAt = new Date().toISOString();
|
|
|
|
this.log.error('dns-propagation', `Verification failed for ${domain}`, {
|
|
error: err.message
|
|
});
|
|
});
|
|
|
|
return job;
|
|
}
|
|
|
|
/**
|
|
* Get the current verification status for a domain.
|
|
*
|
|
* @param {string} domain - The domain to look up
|
|
* @returns {Object|null} Verification status or null if not found
|
|
*/
|
|
getVerificationStatus(domain) {
|
|
const job = this.verifications.get(domain);
|
|
if (!job) return null;
|
|
return {
|
|
domain: job.domain,
|
|
expectedIp: job.expectedIp,
|
|
status: job.status,
|
|
startedAt: job.startedAt,
|
|
completedAt: job.completedAt || null,
|
|
result: job.result || null,
|
|
error: job.error || null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get all recent verifications.
|
|
*
|
|
* @returns {Object[]} Array of verification statuses
|
|
*/
|
|
getAllVerifications() {
|
|
const results = [];
|
|
for (const [domain, job] of this.verifications.entries()) {
|
|
results.push({
|
|
domain,
|
|
expectedIp: job.expectedIp,
|
|
status: job.status,
|
|
startedAt: job.startedAt,
|
|
completedAt: job.completedAt || null,
|
|
propagated: job.result?.propagated || null,
|
|
totalTime: job.result?.totalTime || null,
|
|
error: job.error || null
|
|
});
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Remove verifications older than 1 hour.
|
|
*/
|
|
cleanup() {
|
|
const now = Date.now();
|
|
for (const [domain, job] of this.verifications.entries()) {
|
|
const completedAt = job.completedAt ? new Date(job.completedAt).getTime() : null;
|
|
const startedAt = new Date(job.startedAt).getTime();
|
|
|
|
// Clean up completed/error jobs older than 1 hour
|
|
// Also clean up stale running jobs that started over 2 hours ago
|
|
const age = completedAt ? (now - completedAt) : (now - startedAt);
|
|
const maxAge = job.status === 'running' ? MAX_RESULT_AGE_MS * 2 : MAX_RESULT_AGE_MS;
|
|
|
|
if (age > maxAge) {
|
|
this.verifications.delete(domain);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = DNSPropagationChecker;
|