1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | 'use strict';
|
22 |
|
23 | var 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 |
|
33 | var 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 |
|
42 | var debug = function() {
|
43 |
|
44 |
|
45 | };
|
46 |
|
47 | var 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 | };
|
78 | util.inherits(Request, EventEmitter);
|
79 |
|
80 | Request.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 |
|
93 | Request.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 |
|
102 | Request.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 |
|
110 | Request.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 |
|
118 | Request.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 |
|
139 | Request.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 |
|
150 | Request.prototype.cancel = function() {
|
151 | debug('request cancelled', this.id, this.question);
|
152 | this.emit('cancelled');
|
153 | this.done();
|
154 | };
|
155 |
|
156 | var _queue = [];
|
157 |
|
158 | var sendQueued = function() {
|
159 | debug('platform ready sending queued requests');
|
160 | _queue.forEach(function(request) {
|
161 | request.start();
|
162 | });
|
163 | _queue = [];
|
164 | };
|
165 |
|
166 | platform.on('ready', function() {
|
167 | sendQueued();
|
168 | });
|
169 |
|
170 | if (platform.ready) {
|
171 | sendQueued();
|
172 | }
|
173 |
|
174 | var 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 |
|
195 | Resolve.prototype.cancel = function() {
|
196 | if (this._request) {
|
197 | this._request.cancel();
|
198 | }
|
199 | };
|
200 |
|
201 | Resolve.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 |
|
210 | Resolve.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 |
|
221 | Resolve.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 |
|
251 | Resolve.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 |
|
256 | Resolve.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 |
|
265 | Resolve.prototype._shouldContinue = function() {
|
266 | debug('resolve should continue', this._server_list.length, this._domain);
|
267 | return this._server_list.length;
|
268 | };
|
269 |
|
270 | Resolve.prototype._nextQuestion = function() {
|
271 | debug('resolve next question', this._domain);
|
272 | };
|
273 |
|
274 | Resolve.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 |
|
301 | var 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 |
|
306 | Resolve.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 |
|
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 |
|
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 |
|
365 | Resolve.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 |
|
379 | var 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 | };
|
452 | exports.resolve = resolve;
|
453 |
|
454 | var resolve4 = function(domain, callback) {
|
455 | return resolve(domain, 'A', function(err, results) {
|
456 | callback(err, results);
|
457 | });
|
458 | };
|
459 | exports.resolve4 = resolve4;
|
460 |
|
461 | var resolve6 = function(domain, callback) {
|
462 | return resolve(domain, 'AAAA', function(err, results) {
|
463 | callback(err, results);
|
464 | });
|
465 | };
|
466 | exports.resolve6 = resolve6;
|
467 |
|
468 | var resolveMx = function(domain, callback) {
|
469 | return resolve(domain, 'MX', function(err, results) {
|
470 | callback(err, results);
|
471 | });
|
472 | };
|
473 | exports.resolveMx = resolveMx;
|
474 |
|
475 | var resolveTxt = function(domain, callback) {
|
476 | return resolve(domain, 'TXT', function(err, results) {
|
477 | callback(err, results);
|
478 | });
|
479 | };
|
480 | exports.resolveTxt = resolveTxt;
|
481 |
|
482 | var resolveSrv = function(domain, callback) {
|
483 | return resolve(domain, 'SRV', function(err, results) {
|
484 | callback(err, results);
|
485 | });
|
486 | };
|
487 | exports.resolveSrv = resolveSrv;
|
488 |
|
489 | var resolveNs = function(domain, callback) {
|
490 | return resolve(domain, 'NS', function(err, results) {
|
491 | callback(err, results);
|
492 | });
|
493 | };
|
494 | exports.resolveNs = resolveNs;
|
495 |
|
496 | var resolveCname = function(domain, callback) {
|
497 | return resolve(domain, 'CNAME', function(err, results) {
|
498 | callback(err, results);
|
499 | });
|
500 | };
|
501 | exports.resolveCname = resolveCname;
|
502 |
|
503 | var 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 | };
|
539 | exports.reverse = reverse;
|
540 |
|
541 | var Lookup = function(opts) {
|
542 | Resolve.call(this, opts);
|
543 | this._type = 'getaddrinfo';
|
544 | };
|
545 | util.inherits(Lookup, Resolve);
|
546 |
|
547 | Lookup.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 |
|
569 | Lookup.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 |
|
575 | Lookup.prototype._nextQuestion = function() {
|
576 | debug('Lookup next question');
|
577 | this._buildQuestion([this._domain, this._search_path.pop()].join('.'));
|
578 | };
|
579 |
|
580 | var 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 | };
|
642 | exports.lookup = lookup;
|