dns-over-http-resolver
Version:
DNS over HTTP resolver
204 lines • 7.69 kB
JavaScript
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