UNPKG

14.8 kBJavaScriptView Raw
1// Copyright 2011 Timothy J Fontaine <tjfontaine@gmail.com>
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19// THE SOFTWARE
20
21'use strict';
22
23var ipaddr = require('ipaddr.js'),
24 net = require('net'),
25 util = require('util'),
26 EventEmitter = require('events').EventEmitter,
27 PendingRequests = require('./pending'),
28 Packet = require('./packet'),
29 consts = require('./consts'),
30 utils = require('./utils'),
31 platform = require('./platform');
32
33var A = consts.NAME_TO_QTYPE.A,
34 AAAA = consts.NAME_TO_QTYPE.AAAA,
35 MX = consts.NAME_TO_QTYPE.MX,
36 TXT = consts.NAME_TO_QTYPE.TXT,
37 NS = consts.NAME_TO_QTYPE.NS,
38 CNAME = consts.NAME_TO_QTYPE.CNAME,
39 SRV = consts.NAME_TO_QTYPE.SRV,
40 PTR = consts.NAME_TO_QTYPE.PTR;
41
42var debug = function() {
43 //var args = Array.prototype.slice.call(arguments);
44 //console.log.apply(this, ['client', Date.now().toString()].concat(args));
45};
46
47var Request = exports.Request = function(opts) {
48 if (!(this instanceof Request)) return new Request(opts);
49
50 this.question = opts.question;
51 this.server = opts.server;
52
53 if (typeof(this.server) === 'string' || this.server instanceof String)
54 this.server = { address: this.server, port: 53, type: 'udp'};
55
56 if (!this.server || !this.server.address || !net.isIP(this.server.address))
57 throw new Error('Server object must be supplied with at least address');
58
59 if (!this.server.type || ['udp', 'tcp'].indexOf(this.server.type) === -1)
60 this.server.type = 'udp';
61
62 if (!this.server.port)
63 this.server.port = 53;
64
65 this.timeout = opts.timeout || 4 * 1000;
66 this.try_edns = opts.try_edns || false;
67
68 this.fired = false;
69 this.id = undefined;
70
71 if (opts.cache || opts.cache === false) {
72 this.cache = opts.cache;
73 } else {
74 this.cache = platform.cache;
75 }
76 debug('request created', this.question);
77};
78util.inherits(Request, EventEmitter);
79
80Request.prototype.handle = function(err, answer, cached) {
81 if (!this.fired) {
82 debug('request handled', this.id, this.question);
83
84 if (!cached && this.cache && this.cache.store && answer) {
85 this.cache.store(answer);
86 }
87
88 this.emit('message', err, answer);
89 this.done();
90 }
91};
92
93Request.prototype.done = function() {
94 debug('request finished', this.id, this.question);
95 this.fired = true;
96 clearTimeout(this.timer_);
97 PendingRequests.remove(this);
98 this.emit('end');
99 this.id = undefined;
100};
101
102Request.prototype.handleTimeout = function() {
103 if (!this.fired) {
104 debug('request timedout', this.id, this.question);
105 this.emit('timeout');
106 this.done();
107 }
108};
109
110Request.prototype.error = function(err) {
111 if (!this.fired) {
112 debug('request error', this.id, this.question);
113 this.emit('error', err);
114 this.done();
115 }
116};
117
118Request.prototype.send = function() {
119 debug('request starting', this.question);
120 var self = this;
121
122 if (this.cache && this.cache.lookup) {
123 this.cache.lookup(this.question, function(results) {
124 var packet;
125
126 if (!results) {
127 self._send();
128 } else {
129 packet = new Packet();
130 packet.answer = results.slice();
131 self.handle(null, packet, true);
132 }
133 });
134 } else {
135 this._send();
136 }
137};
138
139Request.prototype._send = function() {
140 debug('request not in cache', this.question);
141 var self = this;
142
143 this.timer_ = setTimeout(function() {
144 self.handleTimeout();
145 }, this.timeout);
146
147 PendingRequests.send(self);
148};
149
150Request.prototype.cancel = function() {
151 debug('request cancelled', this.id, this.question);
152 this.emit('cancelled');
153 this.done();
154};
155
156var _queue = [];
157
158var sendQueued = function() {
159 debug('platform ready sending queued requests');
160 _queue.forEach(function(request) {
161 request.start();
162 });
163 _queue = [];
164};
165
166platform.on('ready', function() {
167 sendQueued();
168});
169
170if (platform.ready) {
171 sendQueued();
172}
173
174var Resolve = function(opts) {
175 this._domain = opts.domain;
176 this._rrtype = opts.rrtype;
177
178 this._buildQuestion(this._domain);
179
180 this._started = false;
181 this._current_server = undefined;
182 this._server_list = [];
183
184 this._request = undefined;
185 this._type = 'getHostByName';
186 this._cb = undefined;
187
188 if (!platform.ready) {
189 _queue.push(this);
190 } else {
191 this.start();
192 }
193};
194
195Resolve.prototype.cancel = function() {
196 if (this._request) {
197 this._request.cancel();
198 }
199};
200
201Resolve.prototype._buildQuestion = function(name) {
202 debug('building question', name);
203 this.question = {
204 type: this._rrtype,
205 class: consts.NAME_TO_QCLASS.IN,
206 name: name
207 };
208};
209
210Resolve.prototype._emit = function(err, answer) {
211 debug('resolve end', this._domain);
212 var self = this;
213 process.nextTick(function() {
214 if (err) {
215 err.syscall = self._type;
216 }
217 self._cb(err, answer);
218 });
219};
220
221Resolve.prototype._fillServers = function() {
222 debug('resolve filling servers', this._domain);
223 var tries = 0, s, t, u, slist;
224
225 slist = platform.name_servers;
226
227 while (this._server_list.length < platform.attempts) {
228 s = slist[tries % slist.length];
229
230 u = {
231 address: s.address,
232 port: s.port,
233 type: 'udp'
234 };
235
236 t = {
237 address: s.address,
238 port: s.port,
239 type: 'tcp'
240 };
241
242 this._server_list.push(u);
243 this._server_list.push(t);
244
245 tries += 1;
246 }
247
248 this._server_list.reverse();
249};
250
251Resolve.prototype._popServer = function() {
252 debug('resolve pop server', this._current_server, this._domain);
253 this._server_list.splice(0, 1, this._current_server);
254};
255
256Resolve.prototype._preStart = function() {
257 if (!this._started) {
258 this._started = new Date().getTime();
259 this.try_edns = platform.edns;
260
261 this._fillServers();
262 }
263};
264
265Resolve.prototype._shouldContinue = function() {
266 debug('resolve should continue', this._server_list.length, this._domain);
267 return this._server_list.length;
268};
269
270Resolve.prototype._nextQuestion = function() {
271 debug('resolve next question', this._domain);
272};
273
274Resolve.prototype.start = function() {
275 if (!this._started) {
276 this._preStart();
277 }
278
279 if (this._server_list.length === 0) {
280 debug('resolve no more servers', this._domain);
281 this.handleTimeout();
282 } else {
283 this._current_server = this._server_list.pop();
284 debug('resolve start', this._current_server, this._domain);
285
286 this._request = Request({
287 question: this.question,
288 server: this._current_server,
289 timeout: platform.timeout,
290 try_edns: this.try_edns
291 });
292
293 this._request.on('timeout', this._handleTimeout.bind(this));
294 this._request.on('message', this._handle.bind(this));
295 this._request.on('error', this._handle.bind(this));
296
297 this._request.send();
298 }
299};
300
301var NOERROR = consts.NAME_TO_RCODE.NOERROR,
302 SERVFAIL = consts.NAME_TO_RCODE.SERVFAIL,
303 NOTFOUND = consts.NAME_TO_RCODE.NOTFOUND,
304 FORMERR = consts.NAME_TO_RCODE.FORMERR;
305
306Resolve.prototype._handle = function(err, answer) {
307 var rcode, errno;
308
309 if (answer) {
310 rcode = answer.header.rcode;
311 }
312
313 debug('resolve handle', rcode, this._domain);
314
315 switch (rcode) {
316 case NOERROR:
317 break;
318 case SERVFAIL:
319 if (this._shouldContinue()) {
320 this._nextQuestion();
321 //this._popServer();
322 } else {
323 errno = consts.SERVFAIL;
324 }
325 answer = undefined;
326 break;
327 case NOTFOUND:
328 if (this._shouldContinue()) {
329 this._nextQuestion();
330 } else {
331 errno = consts.NOTFOUND;
332 }
333 answer = undefined;
334 break;
335 case FORMERR:
336 if (this.try_edns) {
337 this.try_edns = false;
338 //this._popServer();
339 } else {
340 errno = consts.FORMERR;
341 }
342 answer = undefined;
343 break;
344 default:
345 if (!err) {
346 errno = consts.RCODE_TO_NAME[rcode];
347 answer = undefined;
348 } else {
349 errno = consts.NOTFOUND;
350 }
351 break;
352 }
353
354 if (errno || answer) {
355 if (errno) {
356 err = new Error(this._type + ' ' + errno);
357 err.errno = err.code = errno;
358 }
359 this._emit(err, answer);
360 } else {
361 this.start();
362 }
363};
364
365Resolve.prototype._handleTimeout = function() {
366 var err;
367
368 if (this._server_list.length === 0) {
369 debug('resolve timeout no more servers', this._domain);
370 err = new Error(this._type + ' ' + consts.TIMEOUT);
371 err.errno = consts.TIMEOUT;
372 this._emit(err, undefined);
373 } else {
374 debug('resolve timeout continue', this._domain);
375 this.start();
376 }
377};
378
379var resolve = function(domain, rrtype, callback) {
380 var res;
381
382 if (!callback) {
383 callback = rrtype;
384 rrtype = undefined;
385 }
386
387 rrtype = consts.NAME_TO_QTYPE[rrtype || A];
388
389 if (rrtype === PTR) {
390 return reverse(domain, callback);
391 }
392
393 var opts = {
394 domain: domain,
395 rrtype: rrtype
396 };
397
398 res = new Resolve(opts);
399
400 res._cb = function(err, response) {
401 var ret = [], i, a;
402
403 if (err) {
404 callback(err, response);
405 return;
406 }
407
408 for (i = 0; i < response.answer.length; i++) {
409 a = response.answer[i];
410 if (a.type === rrtype) {
411 switch (rrtype) {
412 case A:
413 case AAAA:
414 ret.push(a.address);
415 break;
416 case consts.NAME_TO_QTYPE.MX:
417 ret.push({
418 priority: a.priority,
419 exchange: a.exchange
420 });
421 break;
422 case TXT:
423 case NS:
424 case CNAME:
425 case PTR:
426 ret.push(a.data);
427 break;
428 case SRV:
429 ret.push({
430 priority: a.priority,
431 weight: a.weight,
432 port: a.port,
433 name: a.target
434 });
435 break;
436 default:
437 ret.push(a);
438 break;
439 }
440 }
441 }
442
443 if (ret.length === 0) {
444 ret = undefined;
445 }
446
447 callback(err, ret);
448 };
449
450 return res;
451};
452exports.resolve = resolve;
453
454var resolve4 = function(domain, callback) {
455 return resolve(domain, 'A', function(err, results) {
456 callback(err, results);
457 });
458};
459exports.resolve4 = resolve4;
460
461var resolve6 = function(domain, callback) {
462 return resolve(domain, 'AAAA', function(err, results) {
463 callback(err, results);
464 });
465};
466exports.resolve6 = resolve6;
467
468var resolveMx = function(domain, callback) {
469 return resolve(domain, 'MX', function(err, results) {
470 callback(err, results);
471 });
472};
473exports.resolveMx = resolveMx;
474
475var resolveTxt = function(domain, callback) {
476 return resolve(domain, 'TXT', function(err, results) {
477 callback(err, results);
478 });
479};
480exports.resolveTxt = resolveTxt;
481
482var resolveSrv = function(domain, callback) {
483 return resolve(domain, 'SRV', function(err, results) {
484 callback(err, results);
485 });
486};
487exports.resolveSrv = resolveSrv;
488
489var resolveNs = function(domain, callback) {
490 return resolve(domain, 'NS', function(err, results) {
491 callback(err, results);
492 });
493};
494exports.resolveNs = resolveNs;
495
496var resolveCname = function(domain, callback) {
497 return resolve(domain, 'CNAME', function(err, results) {
498 callback(err, results);
499 });
500};
501exports.resolveCname = resolveCname;
502
503var reverse = function(ip, callback) {
504 var error, opts, res;
505
506 if (!net.isIP(ip)) {
507 error = new Error('getHostByAddr ENOTIMP');
508 error.errno = error.code = 'ENOTIMP';
509 throw error;
510 }
511
512 opts = {
513 domain: utils.reverseIP(ip),
514 rrtype: PTR
515 };
516
517 res = new Lookup(opts);
518
519 res._cb = function(err, response) {
520 var results = [];
521
522 if (response) {
523 response.answer.forEach(function(a) {
524 if (a.type === PTR) {
525 results.push(a.data);
526 }
527 });
528 }
529
530 if (results.length === 0) {
531 results = undefined;
532 }
533
534 callback(err, results);
535 };
536
537 return res;
538};
539exports.reverse = reverse;
540
541var Lookup = function(opts) {
542 Resolve.call(this, opts);
543 this._type = 'getaddrinfo';
544};
545util.inherits(Lookup, Resolve);
546
547Lookup.prototype.start = function() {
548 var self = this;
549
550 if (!this._started) {
551 this._search_path = platform.search_path.slice(0);
552 this._preStart();
553 }
554
555 platform.hosts.lookup(this.question, function(results) {
556 var packet;
557 if (results && results.length) {
558 debug('Lookup in hosts', results);
559 packet = new Packet();
560 packet.answer = results.slice();
561 self._emit(null, packet);
562 } else {
563 debug('Lookup not in hosts');
564 Resolve.prototype.start.call(self);
565 }
566 });
567};
568
569Lookup.prototype._shouldContinue = function() {
570 debug('Lookup should continue', this._server_list.length,
571 this._search_path.length);
572 return this._server_list.length && this._search_path.length;
573};
574
575Lookup.prototype._nextQuestion = function() {
576 debug('Lookup next question');
577 this._buildQuestion([this._domain, this._search_path.pop()].join('.'));
578};
579
580var lookup = function(domain, family, callback) {
581 var rrtype, revip, res;
582
583 if (!callback) {
584 callback = family;
585 family = undefined;
586 }
587
588 if (!family) {
589 family = 4;
590 }
591
592 revip = net.isIP(domain);
593
594 if (revip === 4 || revip === 6) {
595 process.nextTick(function() {
596 callback(null, domain, revip);
597 });
598 return {};
599 }
600
601 if (!domain) {
602 process.nextTick(function() {
603 callback(null, null, family);
604 });
605 return {};
606 }
607
608 rrtype = consts.FAMILY_TO_QTYPE[family];
609
610 var opts = {
611 domain: domain,
612 rrtype: rrtype
613 };
614
615 res = new Lookup(opts);
616
617 res._cb = function(err, response) {
618 var i, afamily, address, a, all;
619
620 if (err) {
621 callback(err, undefined, undefined);
622 return;
623 }
624
625 all = response.answer.concat(response.additional);
626
627 for (i = 0; i < all.length; i++) {
628 a = all[i];
629
630 if (a.type === A || a.type === AAAA) {
631 afamily = consts.QTYPE_TO_FAMILY[a.type];
632 address = a.address;
633 break;
634 }
635 }
636
637 callback(err, address, afamily);
638 };
639
640 return res;
641};
642exports.lookup = lookup;