/** * 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} 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} 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;