// Copyright 2011 Timothy J Fontaine // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE 'use strict'; var ipaddr = require('ipaddr.js'), net = require('net'), util = require('util'), EventEmitter = require('events').EventEmitter, PendingRequests = require('./pending'), Packet = require('./packet'), consts = require('native-dns-packet').consts, utils = require('./utils'), platform = require('./platform'); var A = consts.NAME_TO_QTYPE.A, AAAA = consts.NAME_TO_QTYPE.AAAA, MX = consts.NAME_TO_QTYPE.MX, TXT = consts.NAME_TO_QTYPE.TXT, NS = consts.NAME_TO_QTYPE.NS, CNAME = consts.NAME_TO_QTYPE.CNAME, SRV = consts.NAME_TO_QTYPE.SRV, PTR = consts.NAME_TO_QTYPE.PTR, TLSA = consts.NAME_TO_QTYPE.TLSA; var debug = function() {}; if (process.env.NODE_DEBUG && process.env.NODE_DEBUG.match(/dns/)) { debug = function debug() { var args = Array.prototype.slice.call(arguments); console.error.apply(this, ['client', Date.now().toString()].concat(args)); }; } var Request = exports.Request = function(opts) { if (!(this instanceof Request)) return new Request(opts); this.question = opts.question; this.server = opts.server; if (typeof(this.server) === 'string' || this.server instanceof String) this.server = { address: this.server, port: 53, type: 'udp'}; if (!this.server || !this.server.address || !net.isIP(this.server.address)) throw new Error('Server object must be supplied with at least address'); if (!this.server.type || ['udp', 'tcp'].indexOf(this.server.type) === -1) this.server.type = 'udp'; if (!this.server.port) this.server.port = 53; this.timeout = opts.timeout || 4 * 1000; this.try_edns = opts.try_edns || false; this.fired = false; this.id = undefined; if (opts.cache || opts.cache === false) { this.cache = opts.cache; } else { this.cache = platform.cache; } debug('request created', this.question); }; util.inherits(Request, EventEmitter); Request.prototype.handle = function(err, answer, cached) { if (!this.fired) { debug('request handled', this.id, this.question); if (!cached && this.cache && this.cache.store && answer) { this.cache.store(answer); } this.emit('message', err, answer); this.done(); } }; Request.prototype.done = function() { debug('request finished', this.id, this.question); this.fired = true; clearTimeout(this.timer_); PendingRequests.remove(this); this.emit('end'); this.id = undefined; }; Request.prototype.handleTimeout = function() { if (!this.fired) { debug('request timedout', this.id, this.question); this.emit('timeout'); this.done(); } }; Request.prototype.error = function(err) { if (!this.fired) { debug('request error', err, this.id, this.question); this.emit('error', err); this.done(); } }; Request.prototype.send = function() { debug('request starting', this.question); var self = this; if (this.cache && this.cache.lookup) { this.cache.lookup(this.question, function(results) { var packet; if (!results) { self._send(); } else { packet = new Packet(); packet.answer = results.slice(); self.handle(null, packet, true); } }); } else { this._send(); } }; Request.prototype._send = function() { debug('request not in cache', this.question); var self = this; this.timer_ = setTimeout(function() { self.handleTimeout(); }, this.timeout); PendingRequests.send(self); }; Request.prototype.cancel = function() { debug('request cancelled', this.id, this.question); this.emit('cancelled'); this.done(); }; var _queue = []; var sendQueued = function() { debug('platform ready sending queued requests'); _queue.forEach(function(request) { request.start(); }); _queue = []; }; platform.on('ready', function() { sendQueued(); }); if (platform.ready) { sendQueued(); } var Resolve = function Resolve(opts, cb) { if (!(this instanceof Resolve)) return new Resolve(opts, cb); this.opts = util._extend({ retryOnTruncate: true, }, opts); this._domain = opts.domain; this._rrtype = opts.rrtype; this._buildQuestion(this._domain); this._started = false; this._current_server = undefined; this._server_list = []; if (opts.remote) { this._server_list.push({ address: opts.remote, port: 53, type: 'tcp', }); this._server_list.push({ address: opts.remote, port: 53, type: 'udp', }); } this._request = undefined; this._type = 'getHostByName'; this._cb = cb; if (!platform.ready) { _queue.push(this); } else { this.start(); } }; util.inherits(Resolve, EventEmitter); Resolve.prototype.cancel = function() { if (this._request) { this._request.cancel(); } }; Resolve.prototype._buildQuestion = function(name) { debug('building question', name); this.question = { type: this._rrtype, class: consts.NAME_TO_QCLASS.IN, name: name }; }; exports.Resolve = Resolve; Resolve.prototype._emit = function(err, answer) { debug('resolve end', this._domain); var self = this; process.nextTick(function() { if (err) { err.syscall = self._type; } self._cb(err, answer); }); }; Resolve.prototype._fillServers = function() { debug('resolve filling servers', this._domain); var tries = 0, s, t, u, slist; slist = platform.name_servers; while (this._server_list.length < platform.attempts) { s = slist[tries % slist.length]; u = { address: s.address, port: s.port, type: 'udp' }; t = { address: s.address, port: s.port, type: 'tcp' }; this._server_list.push(u); this._server_list.push(t); tries += 1; } this._server_list.reverse(); }; Resolve.prototype._popServer = function() { debug('resolve pop server', this._current_server, this._domain); this._server_list.splice(0, 1, this._current_server); }; Resolve.prototype._preStart = function() { if (!this._started) { this._started = new Date().getTime(); this.try_edns = platform.edns; if (!this._server_list.length) this._fillServers(); } }; Resolve.prototype._shouldContinue = function() { debug('resolve should continue', this._server_list.length, this._domain); return this._server_list.length; }; Resolve.prototype._nextQuestion = function() { debug('resolve next question', this._domain); }; Resolve.prototype.start = function() { if (!this._started) { this._preStart(); } if (this._server_list.length === 0) { debug('resolve no more servers', this._domain); this.handleTimeout(); } else { this._current_server = this._server_list.pop(); debug('resolve start', this._current_server, this._domain); this._request = Request({ question: this.question, server: this._current_server, timeout: platform.timeout, try_edns: this.try_edns }); this._request.on('timeout', this._handleTimeout.bind(this)); this._request.on('message', this._handle.bind(this)); this._request.on('error', this._handle.bind(this)); this._request.send(); } }; var NOERROR = consts.NAME_TO_RCODE.NOERROR, SERVFAIL = consts.NAME_TO_RCODE.SERVFAIL, NOTFOUND = consts.NAME_TO_RCODE.NOTFOUND, FORMERR = consts.NAME_TO_RCODE.FORMERR; Resolve.prototype._handle = function(err, answer) { var rcode, errno; if (answer) { rcode = answer.header.rcode; } debug('resolve handle', rcode, this._domain); switch (rcode) { case NOERROR: // answer trucated retry with tcp //console.log(answer); if (answer.header.tc && this.opts.retryOnTruncate && this._shouldContinue()) { debug('truncated', this._domain, answer); this.emit('truncated', err, answer); // remove udp servers this._server_list = this._server_list.filter(function(server) { return server.type === 'tcp'; }); answer = undefined; } break; case SERVFAIL: if (this._shouldContinue()) { this._nextQuestion(); //this._popServer(); } else { errno = consts.SERVFAIL; } answer = undefined; break; case NOTFOUND: if (this._shouldContinue()) { this._nextQuestion(); } else { errno = consts.NOTFOUND; } answer = undefined; break; case FORMERR: if (this.try_edns) { this.try_edns = false; //this._popServer(); } else { errno = consts.FORMERR; } answer = undefined; break; default: if (!err) { errno = consts.RCODE_TO_NAME[rcode]; answer = undefined; } else { errno = consts.NOTFOUND; } break; } if (errno || answer) { if (errno) { err = new Error(this._type + ' ' + errno); err.errno = err.code = errno; } this._emit(err, answer); } else { this.start(); } }; Resolve.prototype._handleTimeout = function() { var err; if (this._server_list.length === 0) { debug('resolve timeout no more servers', this._domain); err = new Error(this._type + ' ' + consts.TIMEOUT); err.errno = consts.TIMEOUT; this._emit(err, undefined); } else { debug('resolve timeout continue', this._domain); this.start(); } }; var resolve = function(domain, rrtype, ip, callback) { var res; if (!callback) { callback = ip; ip = undefined; } if (!callback) { callback = rrtype; rrtype = undefined; } rrtype = consts.NAME_TO_QTYPE[rrtype || 'A']; if (rrtype === PTR) { return reverse(domain, callback); } var opts = { domain: domain, rrtype: rrtype, remote: ip, }; res = new Resolve(opts); res._cb = function(err, response) { var ret = [], i, a; if (err) { callback(err, response); return; } for (i = 0; i < response.answer.length; i++) { a = response.answer[i]; if (a.type === rrtype) { switch (rrtype) { case A: case AAAA: ret.push(a.address); break; case consts.NAME_TO_QTYPE.MX: ret.push({ priority: a.priority, exchange: a.exchange }); break; case TXT: case NS: case CNAME: case PTR: ret.push(a.data); break; case SRV: ret.push({ priority: a.priority, weight: a.weight, port: a.port, name: a.target }); break; default: ret.push(a); break; } } } if (ret.length === 0) { ret = undefined; } callback(err, ret); }; return res; }; exports.resolve = resolve; var resolve4 = function(domain, callback) { return resolve(domain, 'A', function(err, results) { callback(err, results); }); }; exports.resolve4 = resolve4; var resolve6 = function(domain, callback) { return resolve(domain, 'AAAA', function(err, results) { callback(err, results); }); }; exports.resolve6 = resolve6; var resolveMx = function(domain, callback) { return resolve(domain, 'MX', function(err, results) { callback(err, results); }); }; exports.resolveMx = resolveMx; var resolveTxt = function(domain, callback) { return resolve(domain, 'TXT', function(err, results) { callback(err, results); }); }; exports.resolveTxt = resolveTxt; var resolveSrv = function(domain, callback) { return resolve(domain, 'SRV', function(err, results) { callback(err, results); }); }; exports.resolveSrv = resolveSrv; var resolveNs = function(domain, callback) { return resolve(domain, 'NS', function(err, results) { callback(err, results); }); }; exports.resolveNs = resolveNs; var resolveCname = function(domain, callback) { return resolve(domain, 'CNAME', function(err, results) { callback(err, results); }); }; exports.resolveCname = resolveCname; var resolveTlsa = function(domain, callback) { return resolve(domain, 'TLSA', function(err, results) { callback(err, results); }); }; exports.resolveTlsa = resolveTlsa; var reverse = function(ip, callback) { var error, opts, res; if (!net.isIP(ip)) { error = new Error('getHostByAddr ENOTIMP'); error.errno = error.code = 'ENOTIMP'; throw error; } opts = { domain: utils.reverseIP(ip), rrtype: PTR }; res = new Lookup(opts); res._cb = function(err, response) { var results = []; if (response) { response.answer.forEach(function(a) { if (a.type === PTR) { results.push(a.data); } }); } if (results.length === 0) { results = undefined; } callback(err, results); }; return res; }; exports.reverse = reverse; var Lookup = function(opts) { Resolve.call(this, opts); this._type = 'getaddrinfo'; }; util.inherits(Lookup, Resolve); Lookup.prototype.start = function() { var self = this; if (!this._started) { this._search_path = platform.search_path.slice(0); this._preStart(); } platform.hosts.lookup(this.question, function(results) { var packet; if (results && results.length) { debug('Lookup in hosts', results); packet = new Packet(); packet.answer = results.slice(); self._emit(null, packet); } else { debug('Lookup not in hosts'); Resolve.prototype.start.call(self); } }); }; Lookup.prototype._shouldContinue = function() { debug('Lookup should continue', this._server_list.length, this._search_path.length); return this._server_list.length && this._search_path.length; }; Lookup.prototype._nextQuestion = function() { debug('Lookup next question'); this._buildQuestion([this._domain, this._search_path.pop()].join('.')); }; var lookup = function(domain, family, callback) { var rrtype, revip, res; if (!callback) { callback = family; family = undefined; } if (!family) { family = 4; } revip = net.isIP(domain); if (revip === 4 || revip === 6) { process.nextTick(function() { callback(null, domain, revip); }); return {}; } if (!domain) { process.nextTick(function() { callback(null, null, family); }); return {}; } rrtype = consts.FAMILY_TO_QTYPE[family]; var opts = { domain: domain, rrtype: rrtype }; res = new Lookup(opts); res._cb = function(err, response) { var i, afamily, address, a, all; if (err) { callback(err, undefined, undefined); return; } all = response.answer.concat(response.additional); for (i = 0; i < all.length; i++) { a = all[i]; if (a.type === A || a.type === AAAA) { afamily = consts.QTYPE_TO_FAMILY[a.type]; address = a.address; break; } } callback(err, address, afamily); }; return res; }; exports.lookup = lookup;