dns-query
Version:
Node & Browser tested, Non-JSON DNS over HTTPS fetching with minimal dependencies.
438 lines (372 loc) • 12.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "AbortError", {
enumerable: true,
get: function () {
return _common.AbortError;
}
});
Object.defineProperty(exports, "BaseEndpoint", {
enumerable: true,
get: function () {
return _common.BaseEndpoint;
}
});
exports.DNS_RCODE_MESSAGE = exports.DNS_RCODE_ERROR = exports.DNSRcodeError = void 0;
Object.defineProperty(exports, "HTTPEndpoint", {
enumerable: true,
get: function () {
return _common.HTTPEndpoint;
}
});
Object.defineProperty(exports, "HTTPStatusError", {
enumerable: true,
get: function () {
return _common.HTTPStatusError;
}
});
Object.defineProperty(exports, "ResponseError", {
enumerable: true,
get: function () {
return _common.ResponseError;
}
});
Object.defineProperty(exports, "TimeoutError", {
enumerable: true,
get: function () {
return _common.TimeoutError;
}
});
Object.defineProperty(exports, "UDP4Endpoint", {
enumerable: true,
get: function () {
return _common.UDP4Endpoint;
}
});
Object.defineProperty(exports, "UDP6Endpoint", {
enumerable: true,
get: function () {
return _common.UDP6Endpoint;
}
});
exports.backup = exports.Wellknown = void 0;
exports.combineTXT = combineTXT;
exports.lookupTxt = lookupTxt;
Object.defineProperty(exports, "parseEndpoint", {
enumerable: true,
get: function () {
return _common.parseEndpoint;
}
});
exports.query = query;
Object.defineProperty(exports, "toEndpoint", {
enumerable: true,
get: function () {
return _common.toEndpoint;
}
});
exports.validateResponse = validateResponse;
exports.wellknown = void 0;
var packet = _interopRequireWildcard(require("@leichtgewicht/dns-packet"), true);
var _rcodes = require("@leichtgewicht/dns-packet/rcodes.js");
var _utf8Codec = require("utf8-codec");
var lib = _interopRequireWildcard(require("./lib.js"), true);
var _resolvers = require("./resolvers.js");
var _common = require("./common.js");
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
const DNS_RCODE_ERROR = {
1: 'FormErr',
2: 'ServFail',
3: 'NXDomain',
4: 'NotImp',
5: 'Refused',
6: 'YXDomain',
7: 'YXRRSet',
8: 'NXRRSet',
9: 'NotAuth',
10: 'NotZone',
11: 'DSOTYPENI'
};
exports.DNS_RCODE_ERROR = DNS_RCODE_ERROR;
const DNS_RCODE_MESSAGE = {
// https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6
1: 'The name server was unable to interpret the query.',
2: 'The name server was unable to process this query due to a problem with the name server.',
3: 'Non-Existent Domain.',
4: 'The name server does not support the requested kind of query.',
5: 'The name server refuses to perform the specified operation for policy reasons.',
6: 'Name Exists when it should not.',
7: 'RR Set Exists when it should not.',
8: 'RR Set that should exist does not.',
9: 'Server Not Authoritative for zone / Not Authorized.',
10: 'Name not contained in zone.',
11: 'DSO-TYPE Not Implemented.'
};
exports.DNS_RCODE_MESSAGE = DNS_RCODE_MESSAGE;
class DNSRcodeError extends Error {
constructor(rcode, question) {
super(`${DNS_RCODE_MESSAGE[rcode] || 'Undefined error.'} (rcode=${rcode}${DNS_RCODE_ERROR[rcode] ? `, error=${DNS_RCODE_ERROR[rcode]}` : ''}, question=${JSON.stringify(question)})`);
this.rcode = rcode;
this.code = `DNS_RCODE_${rcode}`;
this.error = DNS_RCODE_ERROR[rcode];
this.question = question;
}
toJSON() {
return {
code: this.code,
error: this.error,
question: this.question,
endpoint: this.endpoint
};
}
}
exports.DNSRcodeError = DNSRcodeError;
function validateResponse(data, question) {
const rcode = (0, _rcodes.toRcode)(data.rcode);
if (rcode !== 0) {
const err = new DNSRcodeError(rcode, question);
err.endpoint = data.endpoint;
throw err;
}
return data;
}
function processResolvers(res) {
const time = res.time === null || res.time === undefined ? Date.now() : res.time;
const resolvers = lib.processResolvers(res.data.map(resolver => {
resolver.endpoint = (0, _common.toEndpoint)(Object.assign({
name: resolver.name
}, resolver.endpoint));
return resolver;
}));
const endpoints = resolvers.map(resolver => resolver.endpoint);
return {
data: {
resolvers,
resolverByName: resolvers.reduce((byName, resolver) => {
byName[resolver.name] = resolver;
return byName;
}, {}),
endpoints,
endpointByName: endpoints.reduce((byName, endpoint) => {
byName[endpoint.name] = endpoint;
return byName;
}, {})
},
time
};
}
const backup = processResolvers(_resolvers.resolvers);
exports.backup = backup;
function toMultiQuery(singleQuery) {
const query = Object.assign({
type: 'query'
}, singleQuery);
delete query.question;
query.questions = [];
if (singleQuery.question) {
query.questions.push(singleQuery.question);
}
return query;
}
function queryOne(endpoint, query, timeout, abortSignal) {
if (abortSignal && abortSignal.aborted) {
return Promise.reject(new _common.AbortError());
}
if (endpoint.protocol === 'udp4:' || endpoint.protocol === 'udp6:') {
return lib.queryDns(endpoint, query, timeout, abortSignal);
}
return queryDoh(endpoint, query, timeout, abortSignal);
}
function queryDoh(endpoint, query, timeout, abortSignal) {
return lib.request(endpoint.url, endpoint.method, packet.encode(Object.assign({
flags: packet.RECURSION_DESIRED
}, query)), timeout, abortSignal).then(function (res) {
const data = res.data;
const response = res.response;
let error = res.error;
if (error === undefined) {
if (data.length === 0) {
error = new _common.ResponseError('Empty.');
} else {
try {
const decoded = packet.decode(data);
decoded.response = response;
return decoded;
} catch (err) {
error = new _common.ResponseError('Invalid packet (cause=' + err.message + ')', err);
}
}
}
throw Object.assign(error, {
response
});
});
}
const UPDATE_URL = new _common.URL('https://martinheidegger.github.io/dns-query/resolvers.json');
function concatUint8(arrs) {
const res = new Uint8Array(arrs.reduce((len, arr) => len + arr.length, 0));
let pos = 0;
for (const arr of arrs) {
res.set(arr, pos);
pos += arr.length;
}
return res;
}
function combineTXT(inputs) {
return (0, _utf8Codec.decode)(concatUint8(inputs));
}
function isNameString(entry) {
return /^@/.test(entry);
}
class Wellknown {
constructor(opts) {
this.opts = Object.assign({
timeout: 5000,
update: true,
updateURL: UPDATE_URL,
persist: false,
localStoragePrefix: 'dnsquery_',
maxAge: 300000 // 5 minutes
}, opts);
this._dataP = null;
}
_data(force, outdated) {
if (!force && this._dataP !== null) {
return this._dataP.then(res => {
if (res.time < Date.now() - this.opts.maxAge) {
return this._data(true, res);
}
return res;
});
}
this._dataP = !this.opts.update ? Promise.resolve(backup) : lib.loadJSON(this.opts.updateURL, this.opts.persist ? {
name: 'resolvers.json',
localStoragePrefix: this.opts.localStoragePrefix,
maxTime: Date.now() - this.opts.maxAge
} : null, this.opts.timeout).then(res => processResolvers({
data: res.data.resolvers,
time: res.time
})).catch(() => outdated || backup);
return this._dataP;
}
data() {
return this._data(false).then(data => data.data);
}
endpoints(input) {
if (input === null || input === undefined) {
return this.data().then(data => data.endpoints);
}
if (input === 'doh') {
input = filterDoh;
}
if (input === 'dns') {
input = filterDns;
}
if (typeof input === 'function') {
return this.data().then(data => data.endpoints.filter(input));
}
if (typeof input === 'string' || typeof input[Symbol.iterator] !== 'function') {
return Promise.reject(new Error(`Endpoints (${input}) needs to be iterable (array).`));
}
input = Array.from(input).filter(Boolean);
if (input.findIndex(isNameString) === -1) {
try {
return Promise.resolve(input.map(_common.toEndpoint));
} catch (err) {
return Promise.reject(err);
}
}
return this.data().then(data => input.map(entry => {
if (isNameString(entry)) {
const found = data.endpointByName[entry.substring(1)];
if (!found) {
throw new Error(`Endpoint ${entry} is not known.`);
}
return found;
}
return (0, _common.toEndpoint)(entry);
}));
}
}
exports.Wellknown = Wellknown;
const wellknown = new Wellknown();
exports.wellknown = wellknown;
function isPromise(input) {
if (input === null) {
return false;
}
if (typeof input !== 'object') {
return false;
}
return typeof input.then === 'function';
}
function toPromise(input) {
return isPromise(input) ? input : Promise.resolve(input);
}
function query(q, opts) {
opts = Object.assign({
retries: 5,
timeout: 30000 // 30 seconds
}, opts);
if (!q.question) return Promise.reject(new Error('To request data you need to specify a .question!'));
return toPromise(opts.endpoints).then(endpoints => {
if (!Array.isArray(endpoints) || endpoints.length === 0) {
throw new Error('No endpoints defined to lookup dns records.');
}
return queryN(endpoints.map(_common.toEndpoint), toMultiQuery(q), opts);
}).then(data => {
data.question = data.questions[0];
delete data.questions;
return data;
});
}
function lookupTxt(domain, opts) {
const q = Object.assign({
question: {
type: 'TXT',
name: domain
}
}, opts.query);
return query(q, opts).then(data => {
validateResponse(data, q);
return {
entries: (data.answers || []).filter(answer => answer.type === 'TXT' && answer.data).map(answer => {
return {
data: combineTXT(answer.data),
ttl: answer.ttl
};
}).sort((a, b) => {
if (a.data > b.data) return 1;
if (a.data < b.data) return -1;
return 0;
}),
endpoint: data.endpoint
};
});
}
function queryN(endpoints, q, opts) {
const endpoint = endpoints.length === 1 ? endpoints[0] : endpoints[Math.floor(Math.random() * endpoints.length) % endpoints.length];
return queryOne(endpoint, q, opts.timeout, opts.signal).then(data => {
// Add the endpoint to give a chance to identify which endpoint returned the result
data.endpoint = endpoint.toString();
return data;
}, err => {
if (err.name === 'AbortError' || opts.retries === 0) {
err.endpoint = endpoint.toString();
throw err;
}
if (opts.retries > 0) {
opts.retries -= 1;
}
return queryN(endpoints, q, opts);
});
}
function filterDoh(endpoint) {
return endpoint.protocol === 'https:' || endpoint.protocol === 'http:';
}
function filterDns(endpoint) {
return endpoint.protocol === 'udp4:' || endpoint.protocol === 'udp6:';
}