UNPKG

dns-query

Version:

Node & Browser tested, Non-JSON DNS over HTTPS fetching with minimal dependencies.

348 lines (282 loc) 9.11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.loadJSON = loadJSON; exports.processResolvers = processResolvers; exports.queryDns = queryDns; exports.request = request; var _dns = require("dns"); var _dgram = require("dgram"); var _dnsSocket = require("@leichtgewicht/dns-socket"); var codec = _interopRequireWildcard(require("@leichtgewicht/ip-codec"), true); var _base64Codec = require("@leichtgewicht/base64-codec"); var _https = require("https"); var _http = require("http"); var common = _interopRequireWildcard(require("./common.js"), true); var _fs = require("fs"); var path = _interopRequireWildcard(require("path"), true); var _buffer = require("buffer"); 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 { AbortError, HTTPStatusError, TimeoutError, UDP4Endpoint, UDP6Endpoint, URL } = common; // Node 6 support const writeFile = (path, data) => new Promise((resolve, reject) => _fs.writeFile(path, data, err => { err ? reject(err) : resolve(); })); const readFile = (path, opts) => new Promise((resolve, reject) => _fs.readFile(path, opts, (err, data) => { err ? reject(err) : resolve(data); })); const mkdir = path => new Promise((resolve, reject) => _fs.mkdir(path, err => { err ? reject(err) : resolve(); })); const stat = path => new Promise((resolve, reject) => _fs.stat(path, (err, stats) => { err ? reject(err) : resolve(stats); })); const filename = __filename; const contentType = 'application/dns-message'; let socket4; let socket6; function clearSocketMaybe(socket) { if (socket.inflight === 0) { socket.destroy(); if (socket === socket4) { socket4 = null; } else { socket6 = null; } } } const MAX_32BIT_INT = 2147483647; function getSocket(protocol) { if (protocol === 'udp4:') { if (!socket4) { socket4 = new _dnsSocket.DNSSocket({ timeout: MAX_32BIT_INT, timeoutChecks: MAX_32BIT_INT, retries: 0, socket: _dgram.createSocket('udp4') }); } return socket4; } if (!socket6) { socket6 = new _dnsSocket.DNSSocket({ timeout: MAX_32BIT_INT, timeoutChecks: MAX_32BIT_INT, retries: 0, socket: _dgram.createSocket('udp6') }); } return socket6; } function queryDns(endpoint, query, timeout, signal) { return new Promise((resolve, reject) => { const socket = getSocket(endpoint.protocol); if (endpoint.pk) { // TODO: add dnscrypt support to @leichtgewicht/dns-socket return reject(new Error('dnscrypt servers currently not supported')); } const done = (err, res) => { if (signal) { signal.removeEventListener('abort', onAbort); } clearSocketMaybe(socket); clearTimeout(t); if (err) return reject(err); resolve(res); }; const requestId = socket.query(query, endpoint.port, endpoint.ipv4 || endpoint.ipv6, (err, res) => { // Done for sturdier tests, some DNS servers return very, very fast. setTimeout(done, 10, err, res); }); const t = setTimeout(onTimeout, timeout); if (signal) { signal.addEventListener('abort', onAbort); } function onAbort() { done(new AbortError()); socket.cancel(requestId); clearSocketMaybe(socket); } function onTimeout() { done(new TimeoutError(timeout)); socket.cancel(requestId); clearSocketMaybe(socket); } }); } function requestRaw(url, method, body, timeout, abortSignal, headers) { return new Promise((resolve, reject) => { let timer; const client = url.protocol === 'https:' ? _https : _http; let finish = (error, data, response) => { finish = null; clearTimeout(timer); if (abortSignal) { abortSignal.removeEventListener('abort', onabort); } if (error) { if (response) { resolve({ error, response }); } else { reject(error); } } else { resolve({ data, response }); } }; const target = new URL(url); if (method === 'GET' && body) { target.search = '?dns=' + _base64Codec.base64URL.decode(body); } const req = client.request({ hostname: target.hostname, port: target.port || (target.protocol === 'https:' ? 443 : 80), path: `${target.pathname}${target.search}`, method, headers }, onresponse); if (abortSignal) { abortSignal.addEventListener('abort', onabort); } req.on('error', onerror); if (method === 'POST') { req.end(_buffer.Buffer.from(body)); } else { req.end(); } resetTimeout(); function onabort() { req.destroy(new AbortError()); } function onresponse(res) { if (res.statusCode !== 200) { const error = new HTTPStatusError(target.toString(), res.statusCode, method); finish(error, null, res); res.destroy(error); return; } const result = []; res.on('error', onerror); res.on('data', data => { resetTimeout(); result.push(data); }); res.on('end', onclose); res.on('close', onclose); function onclose() { if (finish !== null) { finish(null, _buffer.Buffer.concat(result), res); } } } function onerror(error) { if (finish !== null) { if (error instanceof Error) { finish(error); } else { finish(error ? new Error(error) : new Error('Unknown Error.')); } } } function resetTimeout() { clearTimeout(timer); timer = setTimeout(ontimeout, timeout); } function ontimeout() { req.destroy(new TimeoutError(timeout)); } }); } function request(url, method, packet, timeout, abortSignal) { const headers = { Accept: contentType }; if (method === 'POST') { headers['Content-Type'] = contentType; headers['Content-Length'] = packet.byteLength; } return requestRaw(url, method, packet, timeout, abortSignal, headers); } function loadCache(cache, cachePath) { if (!cachePath) { return Promise.resolve(); } return stat(cachePath).then(function (stats) { const time = stats.mtime.getTime(); if (stats.isFile && time > cache.maxTime) { return readFile(cachePath, 'utf8').then(function (raw) { const data = JSON.parse(raw); return { time, data }; }); } }).catch(noop); } function storeCache(folder, cachePath, data) { if (!cachePath) { return Promise.resolve(null); } return mkdir(folder).catch(function () {}) // mkdir is okay to fail! .then(function () { return writeFile(cachePath, data); }).then(function () { return stat(cachePath); }).then(function (stat) { return stat.mtime.getTime(); }, function () { return null; }); } function noop() {} function loadJSON(url, cache, timeout, abortSignal) { const folder = path.join(filename, '..', '.cache'); const cachePath = cache ? path.join(folder, cache.name) : null; return loadCache(cache, cachePath).then(function (cached) { if (cached) { return cached; } return requestRaw(url, 'GET', null, timeout, abortSignal).then(function (response) { if (response.error) { return Promise.reject(response.error); } const data = response.data; return storeCache(folder, cachePath, data).then(function (time) { return { time, data: JSON.parse(data.toString()) }; }); }); }); } function processResolvers(resolvers) { return resolvers.concat(_dns.getServers().map((host, index) => { const name = `local#${index}`; return { name, endpoint: codec.familyOf(host) === 1 ? new UDP4Endpoint({ protocol: 'udp4:', ipv4: host }) : new UDP6Endpoint({ protocol: 'udp6:', ipv6: host }) }; })); }