UNPKG

dns-over-http-resolver

Version:
204 lines 7.69 kB
import QuickLRU from 'quick-lru'; import debug from 'weald'; import * as utils from './utils.js'; const log = Object.assign(debug('dns-over-http-resolver'), { error: debug('dns-over-http-resolver:error') }); /** * DNS over HTTP resolver. * Uses a list of servers to resolve DNS records with HTTP requests. */ class Resolver { _cache; _TXTcache; _servers; _request; _abortControllers; /** * @class * @param {object} [options] * @param {number} [options.maxCache = 100] - maximum number of cached dns records * @param {Request} [options.request] - function to return DNSJSON */ constructor(options = {}) { this._cache = new QuickLRU({ maxSize: options?.maxCache ?? 100 }); this._TXTcache = new QuickLRU({ maxSize: options?.maxCache ?? 100 }); this._servers = [ 'https://cloudflare-dns.com/dns-query', 'https://dns.google/resolve' ]; this._request = options.request ?? utils.request; this._abortControllers = []; } /** * Cancel all outstanding DNS queries made by this resolver. Any outstanding * requests will be aborted and promises rejected. */ cancel() { this._abortControllers.forEach(controller => { controller.abort(); }); } /** * Get an array of the IP addresses currently configured for DNS resolution. * These addresses are formatted according to RFC 5952. It can include a custom port. */ getServers() { return this._servers; } /** * Get a shuffled array of the IP addresses currently configured for DNS resolution. * These addresses are formatted according to RFC 5952. It can include a custom port. */ _getShuffledServers() { const newServers = [...this._servers]; for (let i = newServers.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * i); const temp = newServers[i]; newServers[i] = newServers[j]; newServers[j] = temp; } return newServers; } /** * Sets the IP address and port of servers to be used when performing DNS resolution. * * @param {string[]} servers - array of RFC 5952 formatted addresses. */ setServers(servers) { this._servers = servers; } async resolve(hostname, rrType = 'A') { switch (rrType) { case 'A': return this.resolve4(hostname); case 'AAAA': return this.resolve6(hostname); case 'TXT': return this.resolveTxt(hostname); default: throw new Error(`${rrType} is not supported`); } } /** * Uses the DNS protocol to resolve the given host name into IPv4 addresses * * @param {string} hostname - host name to resolve */ async resolve4(hostname) { const recordType = 'A'; const cached = this._cache.get(utils.getCacheKey(hostname, recordType)); if (cached != null) { return cached; } let aborted = false; for (const server of this._getShuffledServers()) { const controller = new AbortController(); this._abortControllers.push(controller); try { const response = await this._request(utils.buildResource(server, hostname, recordType), controller.signal); const data = response.Answer.map(a => a.data); const ttl = Math.min(...response.Answer.map(a => a.TTL)); this._cache.set(utils.getCacheKey(hostname, recordType), data, { maxAge: ttl }); return data; } catch (err) { if (controller.signal.aborted) { aborted = true; } log.error(`${server} could not resolve ${hostname} record ${recordType}`); } finally { this._abortControllers = this._abortControllers.filter(c => c !== controller); } } if (aborted) { throw Object.assign(new Error('queryA ECANCELLED'), { code: 'ECANCELLED' }); } throw new Error(`Could not resolve ${hostname} record ${recordType}`); } /** * Uses the DNS protocol to resolve the given host name into IPv6 addresses * * @param {string} hostname - host name to resolve */ async resolve6(hostname) { const recordType = 'AAAA'; const cached = this._cache.get(utils.getCacheKey(hostname, recordType)); if (cached != null) { return cached; } let aborted = false; for (const server of this._getShuffledServers()) { const controller = new AbortController(); this._abortControllers.push(controller); try { const response = await this._request(utils.buildResource(server, hostname, recordType), controller.signal); const data = response.Answer.map(a => a.data); const ttl = Math.min(...response.Answer.map(a => a.TTL)); this._cache.set(utils.getCacheKey(hostname, recordType), data, { maxAge: ttl }); return data; } catch (err) { if (controller.signal.aborted) { aborted = true; } log.error(`${server} could not resolve ${hostname} record ${recordType}`); } finally { this._abortControllers = this._abortControllers.filter(c => c !== controller); } } if (aborted) { throw Object.assign(new Error('queryAaaa ECANCELLED'), { code: 'ECANCELLED' }); } throw new Error(`Could not resolve ${hostname} record ${recordType}`); } /** * Uses the DNS protocol to resolve the given host name into a Text record * * @param {string} hostname - host name to resolve */ async resolveTxt(hostname) { const recordType = 'TXT'; const cached = this._TXTcache.get(utils.getCacheKey(hostname, recordType)); if (cached != null) { return cached; } let aborted = false; for (const server of this._getShuffledServers()) { const controller = new AbortController(); this._abortControllers.push(controller); try { const response = await this._request(utils.buildResource(server, hostname, recordType), controller.signal); const data = response.Answer.map(a => [a.data.replace(/['"]+/g, '')]); const ttl = Math.min(...response.Answer.map(a => a.TTL)); this._TXTcache.set(utils.getCacheKey(hostname, recordType), data, { maxAge: ttl }); return data; } catch (err) { if (controller.signal.aborted) { aborted = true; } log.error(`${server} could not resolve ${hostname} record ${recordType}`); } finally { this._abortControllers = this._abortControllers.filter(c => c !== controller); } } if (aborted) { throw Object.assign(new Error('queryTxt ECANCELLED'), { code: 'ECANCELLED' }); } throw new Error(`Could not resolve ${hostname} record ${recordType}`); } clearCache() { this._cache.clear(); this._TXTcache.clear(); } } export default Resolver; //# sourceMappingURL=index.js.map