dns-socket
Version:
Make low-level DNS requests with retry and timeout support.
288 lines (241 loc) • 7.17 kB
JavaScript
'use strict'
const dgram = require('dgram')
const util = require('util')
const packet = require('dns-packet')
const events = require('events')
module.exports = DNS
function DNS (opts) {
if (!(this instanceof DNS)) {
return new DNS(opts)
}
if (!opts) {
opts = {}
}
events.EventEmitter.call(this)
const self = this
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.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', onerror)
this.socket.on('message', onmessage)
if (isListening(this.socket)) onlistening()
else this.socket.on('listening', onlistening)
this.socket.on('close', onclose)
function onerror (err) {
if (err.code === 'EACCES' || err.code === 'EADDRINUSE') {
self.emit('error', err)
} else {
self.emit('warning', err)
}
}
function onmessage (message, rinfo) {
self._onmessage(message, rinfo)
}
function ontimeoutCheck () {
self._ontimeoutCheck()
}
function onlistening () {
self._interval = setInterval(ontimeoutCheck, self.timeoutChecks)
self.emit('listening')
}
function onclose () {
self.emit('close')
}
}
util.inherits(DNS, events.EventEmitter)
DNS.RECURSION_DESIRED = DNS.prototype.RECURSION_DESIRED = packet.RECURSION_DESIRED
DNS.RECURSION_AVAILABLE = DNS.prototype.RECURSION_AVAILABLE = packet.RECURSION_AVAILABLE
DNS.TRUNCATED_RESPONSE = DNS.prototype.TRUNCATED_RESPONSE = packet.TRUNCATED_RESPONSE
DNS.AUTHORITATIVE_ANSWER = DNS.prototype.AUTHORITATIVE_ANSWER = packet.AUTHORITATIVE_ANSWER
DNS.AUTHENTIC_DATA = DNS.prototype.AUTHENTIC_DATA = packet.AUTHENTIC_DATA
DNS.CHECKING_DISABLED = DNS.prototype.CHECKING_DISABLED = packet.CHECKING_DISABLED
DNS.prototype.address = function () {
return this.socket.address()
}
DNS.prototype.bind = function (...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)
}
}
DNS.prototype.destroy = function (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
}
DNS.prototype._ontimeoutCheck = function () {
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')
}
}
DNS.prototype._shouldRedirect = function (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 = packet.encode(q.query)
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
}
DNS.prototype._onmessage = function (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)
}
DNS.prototype.unref = function () {
this.socket.unref()
}
DNS.prototype.ref = function () {
this.socket.ref()
}
DNS.prototype.response = function (query, response, port, host) {
if (this.destroyed) {
return
}
response.type = 'response'
response.id = query.id
const buffer = packet.encode(response)
this.socket.send(buffer, 0, buffer.length, port, host)
}
DNS.prototype.cancel = function (id) {
const q = this._queries[id]
if (!q) return
this._queries[id] = null
this.inflight--
q.callback(new Error('Query cancelled'))
}
DNS.prototype.setRetries = function (id, retries) {
const q = this._queries[id]
if (!q) return
q.firstTry = q.firstTry - this.timeout * (retries - q.retries)
q.retries = this.retries - retries
}
DNS.prototype._getNextEmptyId = function () {
// 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
}
DNS.prototype.query = function (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 : DNS.RECURSION_DESIRED
const id = this._getNextEmptyId()
if (id === -1) {
cb(new Error('Query array is full!'))
return 0
}
query.id = id + 1
const buffer = packet.encode(query)
this._queries[id] = {
callback: cb || noop,
redirects: 0,
firstTry: Date.now(),
query: query,
tries: 0,
buffer: buffer,
port: port,
host: 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
}
function noop () {
}
function isListening (socket) {
try {
return socket.address().port !== 0
} catch (err) {
return false
}
}