UNPKG

@leichtgewicht/dns-socket

Version:

Make low-level DNS requests with retry and timeout support.

295 lines (233 loc) 8.64 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DNSSocket = void 0; var _dgram = require("dgram"); var packet = _interopRequireWildcard(require("@leichtgewicht/dns-packet"), true); var _events = require("events"); 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; } class DNSSocket extends _events.EventEmitter { constructor(opts = {}) { super(); this.retries = opts.retries !== undefined ? opts.retries : 5; this.timeout = opts.timeout || 7500; this.timeoutChecks = opts.timeoutChecks || this.timeout / 10; this.destroyed = false; this.inflight = 0; this.raw = opts.raw === true; this.maxQueries = opts.maxQueries || 10000; this.maxRedirects = opts.maxRedirects || 0; this.socket = opts.socket || _dgram.createSocket('udp4'); this._id = Math.ceil(Math.random() * this.maxQueries); this._queries = new Array(this.maxQueries).fill(null); this._interval = null; this.socket.on('error', err => { if (err.code === 'EACCES' || err.code === 'EADDRINUSE') { this.emit('error', err); } else { this.emit('warning', err); } }); this.socket.on('message', (message, rinfo) => { this._onmessage(message, rinfo); }); const onlistening = () => { this._interval = setInterval(() => this._ontimeoutCheck(), this.timeoutChecks); this.emit('listening'); }; if (isListening(this.socket)) onlistening();else this.socket.on('listening', onlistening); this.socket.on('close', () => this.emit('close')); } address() { return this.socket.address(); } bind(...args) { const onlistening = args.length > 0 && args[args.length - 1]; if (typeof onlistening === 'function') { this.once('listening', onlistening); this.socket.bind(...args.slice(0, -1)); } else { this.socket.bind(...args); } } destroy(onclose) { if (onclose) { this.once('close', onclose); } if (this.destroyed) { return; } this.destroyed = true; clearInterval(this._interval); this.socket.close(); for (let i = 0; i < this.maxQueries; i++) { const q = this._queries[i]; if (q) { q.callback(new Error('Socket destroyed')); this._queries[i] = null; } } this.inflight = 0; } _ontimeoutCheck() { const now = Date.now(); for (let i = 0; i < this.maxQueries; i++) { const q = this._queries[i]; if (!q || now - q.firstTry < (q.tries + 1) * this.timeout) { continue; } if (q.tries > this.retries) { this._queries[i] = null; this.inflight--; this.emit('timeout', q.query, q.port, q.host); q.callback(new Error('Query timed out')); continue; } q.tries++; this.socket.send(q.buffer, 0, q.buffer.length, q.port, Array.isArray(q.host) ? q.host[Math.floor(q.host.length * Math.random())] : q.host || '127.0.0.1'); } } _shouldRedirect(q, result) { // no redirects, no query, more than 1 questions, has any A record answer if (this.maxRedirects <= 0 || !q || q.query.questions.length !== 1 || result.answers.filter(e => e.type === 'A').length > 0) { return false; } // no more redirects left if (q.redirects > this.maxRedirects) { return false; } const cnameresults = result.answers.filter(e => e.type === 'CNAME'); if (cnameresults.length === 0) { return false; } const id = this._getNextEmptyId(); if (id === -1) { q.callback(new Error('Query array is full!')); return true; } // replace current query with a new one q.query = { id: id + 1, flags: packet.RECURSION_DESIRED, questions: [{ type: 'A', name: cnameresults[0].data }] }; q.redirects++; q.firstTry = Date.now(); q.tries = 0; q.buffer = _buffer.Buffer.alloc(packet.encodingLength(q.query)); packet.encode(q.query, q.buffer); this._queries[id] = q; this.socket.send(q.buffer, 0, q.buffer.length, q.port, Array.isArray(q.host) ? q.host[Math.floor(q.host.length * Math.random())] : q.host || '127.0.0.1'); return true; } _onmessage(buffer, rinfo) { let message; try { message = packet.decode(buffer); } catch (err) { this.emit('warning', err); return; } if (message.type === 'response' && message.id) { const q = this._queries[message.id - 1]; if (q) { this._queries[message.id - 1] = null; this.inflight--; if (!this._shouldRedirect(q, message)) { q.callback(null, message); } } } this.emit(message.type, message, rinfo.port, rinfo.address); } unref() { this.socket.unref(); } ref() { this.socket.ref(); } response(query, response, port, host) { if (this.destroyed) { return; } response.type = 'response'; response.id = query.id; const buffer = _buffer.Buffer.alloc(packet.encodingLength(response)); packet.encode(response, buffer); this.socket.send(buffer, 0, buffer.length, port, host); } cancel(id) { const q = this._queries[id]; if (!q) return; this._queries[id] = null; this.inflight--; q.callback(new Error('Query cancelled')); } setRetries(id, retries) { const q = this._queries[id]; if (!q) return; q.firstTry = q.firstTry - this.timeout * (retries - q.retries); q.retries = this.retries - retries; } _getNextEmptyId() { // try to find the next unused id let id = -1; for (let idtries = this.maxQueries; idtries > 0; idtries--) { const normalizedId = (this._id + idtries) % this.maxQueries; if (this._queries[normalizedId] === null) { id = normalizedId; this._id = (normalizedId + 1) % this.maxQueries; break; } } return id; } query(query, port, host, cb) { if (this.destroyed) { cb(new Error('Socket destroyed')); return 0; } this.inflight++; query.type = 'query'; query.flags = typeof query.flags === 'number' ? query.flags : DNSSocket.RECURSION_DESIRED; const id = this._getNextEmptyId(); if (id === -1) { cb(new Error('Query array is full!')); return 0; } query.id = id + 1; const buffer = _buffer.Buffer.alloc(packet.encodingLength(query)); packet.encode(query, buffer); this._queries[id] = { callback: cb || noop, redirects: 0, firstTry: Date.now(), query, tries: 0, buffer, port, host }; this.socket.send(buffer, 0, buffer.length, port, Array.isArray(host) ? host[Math.floor(host.length * Math.random())] : host || '127.0.0.1'); return id; } } exports.DNSSocket = DNSSocket; DNSSocket.RECURSION_DESIRED = DNSSocket.prototype.RECURSION_DESIRED = packet.RECURSION_DESIRED; DNSSocket.RECURSION_AVAILABLE = DNSSocket.prototype.RECURSION_AVAILABLE = packet.RECURSION_AVAILABLE; DNSSocket.TRUNCATED_RESPONSE = DNSSocket.prototype.TRUNCATED_RESPONSE = packet.TRUNCATED_RESPONSE; DNSSocket.AUTHORITATIVE_ANSWER = DNSSocket.prototype.AUTHORITATIVE_ANSWER = packet.AUTHORITATIVE_ANSWER; DNSSocket.AUTHENTIC_DATA = DNSSocket.prototype.AUTHENTIC_DATA = packet.AUTHENTIC_DATA; DNSSocket.CHECKING_DISABLED = DNSSocket.prototype.CHECKING_DISABLED = packet.CHECKING_DISABLED; function noop() {} function isListening(socket) { try { return socket.address().port !== 0; } catch (err) { return false; } }