dns-suite.js/node_modules/native-dns/lib/client.js

698 lines
16 KiB
JavaScript

// Copyright 2011 Timothy J Fontaine <tjfontaine@gmail.com>
//
// 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;