288 lines
7.0 KiB
JavaScript
288 lines
7.0 KiB
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
|
||
|
|
}
|
||
|
|
}
|