tangerine 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -27
- package/index.js +163 -63
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
</div>
|
|
12
12
|
<br />
|
|
13
13
|
<div align="center">
|
|
14
|
-
🍊 <a href="https://github.com/forwardemail/tangerine" target="_blank">Tangerine</a> is the best <a href="https://nodejs.org" target="_blank">Node.js</a> drop-in replacement for <a href="https://nodejs.org/api/dns.html#resolveroptions" target="_blank">dns.promises.Resolver</a> using <a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank">DNS over HTTPS</a> ("DoH") via <a href="https://github.com/nodejs/undici" target="_blank">undici</a> with built-in retries, timeouts, smart server rotation, <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController" target="_blank">AbortControllers</a>, and caching support for multiple backends (with TTL support).
|
|
14
|
+
🍊 <a href="https://github.com/forwardemail/tangerine" target="_blank">Tangerine</a> is the best <a href="https://nodejs.org" target="_blank">Node.js</a> drop-in replacement for <a href="https://nodejs.org/api/dns.html#resolveroptions" target="_blank">dns.promises.Resolver</a> using <a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank">DNS over HTTPS</a> ("DoH") via <a href="https://github.com/nodejs/undici" target="_blank">undici</a> with built-in retries, timeouts, smart server rotation, <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController" target="_blank">AbortControllers</a>, and caching support for multiple backends (with TTL and purge support).
|
|
15
15
|
</div>
|
|
16
16
|
<hr />
|
|
17
17
|
<div align="center">
|
|
@@ -37,21 +37,21 @@
|
|
|
37
37
|
* [`tangerine.cancel()`](#tangerinecancel)
|
|
38
38
|
* [`tangerine.getServers()`](#tangerinegetservers)
|
|
39
39
|
* [`tangerine.lookup(hostname[, options])`](#tangerinelookuphostname-options)
|
|
40
|
-
* [`tangerine.lookupService(address, port, abortController)`](#tangerinelookupserviceaddress-port-abortcontroller)
|
|
40
|
+
* [`tangerine.lookupService(address, port[, abortController, purgeCache])`](#tangerinelookupserviceaddress-port-abortcontroller-purgecache)
|
|
41
41
|
* [`tangerine.resolve(hostname[, rrtype, options, abortController])`](#tangerineresolvehostname-rrtype-options-abortcontroller)
|
|
42
42
|
* [`tangerine.resolve4(hostname[, options, abortController])`](#tangerineresolve4hostname-options-abortcontroller)
|
|
43
43
|
* [`tangerine.resolve6(hostname[, options, abortController])`](#tangerineresolve6hostname-options-abortcontroller)
|
|
44
|
-
* [`tangerine.resolveAny(hostname[, abortController])`](#tangerineresolveanyhostname-abortcontroller)
|
|
45
|
-
* [`tangerine.resolveCaa(hostname[, abortController]))`](#tangerineresolvecaahostname-abortcontroller)
|
|
46
|
-
* [`tangerine.resolveCname(hostname[, abortController]))`](#tangerineresolvecnamehostname-abortcontroller)
|
|
47
|
-
* [`tangerine.resolveMx(hostname[, abortController]))`](#tangerineresolvemxhostname-abortcontroller)
|
|
48
|
-
* [`tangerine.resolveNaptr(hostname[, abortController]))`](#tangerineresolvenaptrhostname-abortcontroller)
|
|
49
|
-
* [`tangerine.resolveNs(hostname[, abortController]))`](#tangerineresolvenshostname-abortcontroller)
|
|
50
|
-
* [`tangerine.resolvePtr(hostname[, abortController]))`](#tangerineresolveptrhostname-abortcontroller)
|
|
51
|
-
* [`tangerine.resolveSoa(hostname[, abortController]))`](#tangerineresolvesoahostname-abortcontroller)
|
|
52
|
-
* [`tangerine.resolveSrv(hostname[, abortController]))`](#tangerineresolvesrvhostname-abortcontroller)
|
|
53
|
-
* [`tangerine.resolveTxt(hostname[, abortController]))`](#tangerineresolvetxthostname-abortcontroller)
|
|
54
|
-
* [`tangerine.reverse(ip[, abortController])`](#tangerinereverseip-abortcontroller)
|
|
44
|
+
* [`tangerine.resolveAny(hostname[, options, abortController])`](#tangerineresolveanyhostname-options-abortcontroller)
|
|
45
|
+
* [`tangerine.resolveCaa(hostname[, options, abortController]))`](#tangerineresolvecaahostname-options-abortcontroller)
|
|
46
|
+
* [`tangerine.resolveCname(hostname[, options, abortController]))`](#tangerineresolvecnamehostname-options-abortcontroller)
|
|
47
|
+
* [`tangerine.resolveMx(hostname[, options, abortController]))`](#tangerineresolvemxhostname-options-abortcontroller)
|
|
48
|
+
* [`tangerine.resolveNaptr(hostname[, options, abortController]))`](#tangerineresolvenaptrhostname-options-abortcontroller)
|
|
49
|
+
* [`tangerine.resolveNs(hostname[, options, abortController]))`](#tangerineresolvenshostname-options-abortcontroller)
|
|
50
|
+
* [`tangerine.resolvePtr(hostname[, options, abortController]))`](#tangerineresolveptrhostname-options-abortcontroller)
|
|
51
|
+
* [`tangerine.resolveSoa(hostname[, options, abortController]))`](#tangerineresolvesoahostname-options-abortcontroller)
|
|
52
|
+
* [`tangerine.resolveSrv(hostname[, options, abortController]))`](#tangerineresolvesrvhostname-options-abortcontroller)
|
|
53
|
+
* [`tangerine.resolveTxt(hostname[, options, abortController]))`](#tangerineresolvetxthostname-options-abortcontroller)
|
|
54
|
+
* [`tangerine.reverse(ip[, abortController, purgeCache])`](#tangerinereverseip-abortcontroller-purgecache)
|
|
55
55
|
* [`tangerine.setDefaultResultOrder(order)`](#tangerinesetdefaultresultorderorder)
|
|
56
56
|
* [`tangerine.setServers(servers)`](#tangerinesetserversservers)
|
|
57
57
|
* [Options](#options)
|
|
@@ -97,7 +97,7 @@ Our team at [Forward Email](https://forwardemail.net) (100% open-source and priv
|
|
|
97
97
|
* Once popular packages such as [native-dns](https://github.com/tjfontaine/node-dns/issues/111) and [dnscached](https://github.com/yahoo/dnscache/issues/28) are archived or deprecated.
|
|
98
98
|
* [Other packages](https://www.npmjs.com/search?q=dns%20cache) only provide `lookup` functions, have a limited sub-set of methods such as [@zeit/dns-cached-resolver](https://github.com/vercel/dns-cached-resolve), or are unmaintained.
|
|
99
99
|
* Act as a 1:1 drop-in replacement for `dns.promises.Resolver` with DNS over HTTPS ("DoH").
|
|
100
|
-
* Support caching for multiple backends (with TTL support), retries, smart server rotation, and [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) usage.
|
|
100
|
+
* Support caching for multiple backends (with TTL and purge support), retries, smart server rotation, and [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) usage.
|
|
101
101
|
* Provide out of the box support for both ECMAScript modules (ESM) **and** CommonJS (CJS) (see discussions [for](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) and [against](https://gist.github.com/joepie91/bca2fda868c1e8b2c2caf76af7dfcad3)).
|
|
102
102
|
* The native Node.js `dns` module does not support caching out of the box – which is a [highly requested feature](https://github.com/nodejs/node/issues/5893) (but belongs in userland).
|
|
103
103
|
* Writing tests against DNS-related infrastructure requires either hacky DNS mocking or a DNS server (manipulating cache is much easier).
|
|
@@ -225,8 +225,11 @@ tangerine.resolve('forwardemail.net').then(console.log);
|
|
|
225
225
|
* Specify default request options based off the library under `requestOptions` below
|
|
226
226
|
* Instance methods of [dns.promises.Resolver](https://nodejs.org/api/dns.html) are mirrored to :tangerine: Tangerine.
|
|
227
227
|
* Resolver methods accept an optional `abortController` argument, which is an instance of [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). Note that :tangerine: Tangerine manages `AbortController` usage internally – so you most likely won't need to pass your own (see [index.js](https://github.com/forwardemail/tangerine/blob/main/index.js) for more insight).
|
|
228
|
-
*
|
|
228
|
+
* Resolver methods that accept `options` argument also accept an optional `options.purgeCache` option.
|
|
229
|
+
* Resolver methods support a `purgeCache` option as either `options.purgeCache` (Boolean) via `options` argument or `purgeCache` (Boolean) argument – see [API](#api) and [Cache](#cache) for more insight.
|
|
230
|
+
* If set to `true`, then the result will be re-queried and re-cached – see [Cache](#cache) documentation for more insight.
|
|
229
231
|
* Instances of `new Tangerine()` are instances of `dns.promises.Resolver` via `class Tangerine extends dns.promises.Resolver { ... }` (namely for compatibility with projects such as [cacheable-lookup](https://github.com/szmarczak/cacheable-lookup)).
|
|
232
|
+
* See the complete list of [Options](#options) below.
|
|
230
233
|
|
|
231
234
|
### `tangerine.cancel()`
|
|
232
235
|
|
|
@@ -234,7 +237,7 @@ tangerine.resolve('forwardemail.net').then(console.log);
|
|
|
234
237
|
|
|
235
238
|
### `tangerine.lookup(hostname[, options])`
|
|
236
239
|
|
|
237
|
-
### `tangerine.lookupService(address, port, abortController)`
|
|
240
|
+
### `tangerine.lookupService(address, port[, abortController, purgeCache])`
|
|
238
241
|
|
|
239
242
|
### `tangerine.resolve(hostname[, rrtype, options, abortController])`
|
|
240
243
|
|
|
@@ -246,27 +249,27 @@ Tangerine supports a new `ecsSubnet` property in the `options` Object argument.
|
|
|
246
249
|
|
|
247
250
|
Tangerine supports a new `ecsSubnet` property in the `options` Object argument.
|
|
248
251
|
|
|
249
|
-
### `tangerine.resolveAny(hostname[, abortController])`
|
|
252
|
+
### `tangerine.resolveAny(hostname[, options, abortController])`
|
|
250
253
|
|
|
251
|
-
### `tangerine.resolveCaa(hostname[, abortController]))`
|
|
254
|
+
### `tangerine.resolveCaa(hostname[, options, abortController]))`
|
|
252
255
|
|
|
253
|
-
### `tangerine.resolveCname(hostname[, abortController]))`
|
|
256
|
+
### `tangerine.resolveCname(hostname[, options, abortController]))`
|
|
254
257
|
|
|
255
|
-
### `tangerine.resolveMx(hostname[, abortController]))`
|
|
258
|
+
### `tangerine.resolveMx(hostname[, options, abortController]))`
|
|
256
259
|
|
|
257
|
-
### `tangerine.resolveNaptr(hostname[, abortController]))`
|
|
260
|
+
### `tangerine.resolveNaptr(hostname[, options, abortController]))`
|
|
258
261
|
|
|
259
|
-
### `tangerine.resolveNs(hostname[, abortController]))`
|
|
262
|
+
### `tangerine.resolveNs(hostname[, options, abortController]))`
|
|
260
263
|
|
|
261
|
-
### `tangerine.resolvePtr(hostname[, abortController]))`
|
|
264
|
+
### `tangerine.resolvePtr(hostname[, options, abortController]))`
|
|
262
265
|
|
|
263
|
-
### `tangerine.resolveSoa(hostname[, abortController]))`
|
|
266
|
+
### `tangerine.resolveSoa(hostname[, options, abortController]))`
|
|
264
267
|
|
|
265
|
-
### `tangerine.resolveSrv(hostname[, abortController]))`
|
|
268
|
+
### `tangerine.resolveSrv(hostname[, options, abortController]))`
|
|
266
269
|
|
|
267
|
-
### `tangerine.resolveTxt(hostname[, abortController]))`
|
|
270
|
+
### `tangerine.resolveTxt(hostname[, options, abortController]))`
|
|
268
271
|
|
|
269
|
-
### `tangerine.reverse(ip[, abortController])`
|
|
272
|
+
### `tangerine.reverse(ip[, abortController, purgeCache])`
|
|
270
273
|
|
|
271
274
|
### `tangerine.setDefaultResultOrder(order)`
|
|
272
275
|
|
|
@@ -358,6 +361,19 @@ without cache: 98.25ms
|
|
|
358
361
|
with cache: 0.091ms
|
|
359
362
|
```
|
|
360
363
|
|
|
364
|
+
You can also force the cache to be purged and reset to a new value:
|
|
365
|
+
|
|
366
|
+
```js
|
|
367
|
+
await tangerine.resolve('forwardemail.net'); // cached
|
|
368
|
+
await tangerine.resolve('forwardemail.net'); // uses cached value
|
|
369
|
+
await tangerine.resolve('forwardemail.net'); // uses cached value
|
|
370
|
+
await tangerine.resolve('forwardemail.net', { purgeCache: true }); // re-cached
|
|
371
|
+
await tangerine.resolve('forwardemail.net'); // uses cached value
|
|
372
|
+
await tangerine.resolve('forwardemail.net'); // uses cached value
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
This purge cache feature is useful for DNS records that have recently changed and have had their caches purged at the relevant DNS provider (e.g. [Cloudflare's Purge Cache tool](https://1.1.1.1/purge-cache/)).
|
|
376
|
+
|
|
361
377
|
|
|
362
378
|
## Debugging
|
|
363
379
|
|
package/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const { debuglog } = require('node:util');
|
|
|
7
7
|
const { getEventListeners, setMaxListeners } = require('node:events');
|
|
8
8
|
const { isIP, isIPv4, isIPv6 } = require('node:net');
|
|
9
9
|
|
|
10
|
+
const { toASCII } = require('punycode/');
|
|
10
11
|
const getStream = require('get-stream');
|
|
11
12
|
const ipaddr = require('ipaddr.js');
|
|
12
13
|
const mergeOptions = require('merge-options');
|
|
@@ -464,7 +465,7 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
464
465
|
|
|
465
466
|
options = { family: options };
|
|
466
467
|
} else if (
|
|
467
|
-
typeof options
|
|
468
|
+
typeof options?.family !== 'undefined' &&
|
|
468
469
|
![0, 4, 6, 'IPv4', 'IPv6'].includes(options.family)
|
|
469
470
|
) {
|
|
470
471
|
// validate family
|
|
@@ -475,12 +476,12 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
475
476
|
throw err;
|
|
476
477
|
}
|
|
477
478
|
|
|
478
|
-
if (options
|
|
479
|
-
else if (options
|
|
479
|
+
if (options?.family === 'IPv4') options.family = 4;
|
|
480
|
+
else if (options?.family === 'IPv6') options.family = 6;
|
|
480
481
|
|
|
481
482
|
// validate hints
|
|
482
483
|
// eslint-disable-next-line no-bitwise
|
|
483
|
-
if ((options
|
|
484
|
+
if ((options?.hints & ~(dns.ADDRCONFIG | dns.ALL | dns.V4MAPPED)) !== 0) {
|
|
484
485
|
const err = new TypeError(
|
|
485
486
|
`The argument 'hints' is invalid. Received ${options.hints}`
|
|
486
487
|
);
|
|
@@ -488,16 +489,55 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
488
489
|
throw err;
|
|
489
490
|
}
|
|
490
491
|
|
|
491
|
-
//
|
|
492
|
+
// purge cache support
|
|
493
|
+
let purgeCache;
|
|
494
|
+
if (options?.purgeCache) {
|
|
495
|
+
purgeCache = true;
|
|
496
|
+
delete options.purgeCache;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (options.hints) {
|
|
500
|
+
switch (options.hints) {
|
|
501
|
+
case dns.ADDRCONFIG: {
|
|
502
|
+
options.family = this.constructor.getAddrConfigTypes();
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// eslint-disable-next-line no-bitwise
|
|
507
|
+
case dns.ADDRCONFIG | dns.V4MAPPED: {
|
|
508
|
+
options.family = this.constructor.getAddrConfigTypes();
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// eslint-disable-next-line no-bitwise
|
|
513
|
+
case dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL: {
|
|
514
|
+
options.family = this.constructor.getAddrConfigTypes();
|
|
515
|
+
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
default: {
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// resolve the first A or AAAA record (conditionally)
|
|
492
526
|
let answers = [];
|
|
493
527
|
|
|
494
528
|
try {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
529
|
+
// `any` or `all` is based off !options.family || options.family === 0
|
|
530
|
+
// (according to official nodejs dns.lookup docs)
|
|
531
|
+
answers = await Promise[
|
|
532
|
+
typeof options.family === 'undefined' || options.family === 0
|
|
533
|
+
? 'all'
|
|
534
|
+
: 'any'
|
|
535
|
+
]([
|
|
536
|
+
// the only downside here is that if one succeeds the other won't be aborted (iff "any")
|
|
537
|
+
this.resolve4(name, { purgeCache, noThrowOnNODATA: true }),
|
|
538
|
+
this.resolve6(name, { purgeCache, noThrowOnNODATA: true })
|
|
500
539
|
]);
|
|
540
|
+
answers = answers.flat();
|
|
501
541
|
} catch (_err) {
|
|
502
542
|
debug(_err);
|
|
503
543
|
|
|
@@ -522,9 +562,18 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
522
562
|
throw err;
|
|
523
563
|
}
|
|
524
564
|
|
|
565
|
+
// if no results then throw ENODATA
|
|
566
|
+
if (answers.length === 0) {
|
|
567
|
+
const err = this.constructor.createError(name, '', dns.NODATA);
|
|
568
|
+
// remap and perform syscall
|
|
569
|
+
err.syscall = 'getaddrinfo';
|
|
570
|
+
// err.errno = -3008;
|
|
571
|
+
throw err;
|
|
572
|
+
}
|
|
573
|
+
|
|
525
574
|
// respect options from dns module
|
|
526
575
|
// <https://nodejs.org/api/dns.html#dnspromiseslookuphostname-options>
|
|
527
|
-
// - [x]
|
|
576
|
+
// - [x] `family` (4, 6, or 0, default is 0)
|
|
528
577
|
// - [x] `hints` multiple flags may be passed by bitwise OR'ing values
|
|
529
578
|
// - [x] `all` (iff true, then return all results, otherwise single result)
|
|
530
579
|
// - [x] `verbatim` - if `true` then return as-is, otherwise use dns order
|
|
@@ -541,14 +590,8 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
541
590
|
// dns.ALL:
|
|
542
591
|
// If dns.V4MAPPED is specified, return resolved IPv6 addresses as well as IPv4 mapped IPv6 addresses.
|
|
543
592
|
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
switch (hints) {
|
|
547
|
-
case dns.ADDRCONFIG: {
|
|
548
|
-
options.family = this.constructor.getAddrConfigTypes();
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
|
|
593
|
+
if (options.hints) {
|
|
594
|
+
switch (options.hints) {
|
|
552
595
|
case dns.V4MAPPED: {
|
|
553
596
|
if (options.family === 6 && !answers.some((answer) => isIPv6(answer)))
|
|
554
597
|
answers = answers.map((answer) =>
|
|
@@ -564,7 +607,6 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
564
607
|
|
|
565
608
|
// eslint-disable-next-line no-bitwise
|
|
566
609
|
case dns.ADDRCONFIG | dns.V4MAPPED: {
|
|
567
|
-
options.family = this.constructor.getAddrConfigTypes();
|
|
568
610
|
if (options.family === 6 && !answers.some((answer) => isIPv6(answer)))
|
|
569
611
|
answers = answers.map((answer) =>
|
|
570
612
|
ipaddr.parse(answer).toIPv4MappedAddress().toString()
|
|
@@ -584,7 +626,6 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
584
626
|
|
|
585
627
|
// eslint-disable-next-line no-bitwise
|
|
586
628
|
case dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL: {
|
|
587
|
-
options.family = this.constructor.getAddrConfigTypes();
|
|
588
629
|
if (options.family === 6 && !answers.some((answer) => isIPv6(answer)))
|
|
589
630
|
answers = answers.map((answer) =>
|
|
590
631
|
ipaddr.parse(answer).toIPv4MappedAddress().toString()
|
|
@@ -605,9 +646,20 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
605
646
|
else if (options.family === 6)
|
|
606
647
|
answers = answers.filter((answer) => isIPv6(answer));
|
|
607
648
|
|
|
649
|
+
//
|
|
608
650
|
// respect sort order from `setDefaultResultOrder` method
|
|
609
|
-
|
|
610
|
-
|
|
651
|
+
//
|
|
652
|
+
// NOTE: we need to optimize this sort logic at some point
|
|
653
|
+
//
|
|
654
|
+
if (options.verbatim !== true && this.options.dnsOrder === 'ipv4first') {
|
|
655
|
+
answers = answers.sort((a, b) => {
|
|
656
|
+
const aFamily = isIP(a);
|
|
657
|
+
const bFamily = isIP(b);
|
|
658
|
+
if (aFamily < bFamily) return -1;
|
|
659
|
+
if (aFamily > bFamily) return 1;
|
|
660
|
+
return 0;
|
|
661
|
+
});
|
|
662
|
+
}
|
|
611
663
|
|
|
612
664
|
return options.all === true
|
|
613
665
|
? answers.map((answer) => ({
|
|
@@ -618,7 +670,7 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
618
670
|
}
|
|
619
671
|
|
|
620
672
|
// <https://man7.org/linux/man-pages/man3/getnameinfo.3.html>
|
|
621
|
-
async lookupService(address, port, abortController) {
|
|
673
|
+
async lookupService(address, port, abortController, purgeCache = false) {
|
|
622
674
|
if (!address || !port) {
|
|
623
675
|
const err = new TypeError(
|
|
624
676
|
'The "address" and "port" arguments must be specified.'
|
|
@@ -647,7 +699,11 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
647
699
|
|
|
648
700
|
// reverse lookup
|
|
649
701
|
try {
|
|
650
|
-
const [hostname] = await this.reverse(
|
|
702
|
+
const [hostname] = await this.reverse(
|
|
703
|
+
address,
|
|
704
|
+
abortController,
|
|
705
|
+
purgeCache
|
|
706
|
+
);
|
|
651
707
|
return { hostname, service: name };
|
|
652
708
|
} catch (err) {
|
|
653
709
|
err.syscall = 'getnameinfo';
|
|
@@ -655,7 +711,7 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
655
711
|
}
|
|
656
712
|
}
|
|
657
713
|
|
|
658
|
-
async reverse(ip, abortController) {
|
|
714
|
+
async reverse(ip, abortController, purgeCache = false) {
|
|
659
715
|
// basically reverse the IP and then perform PTR lookup
|
|
660
716
|
if (typeof ip !== 'string') {
|
|
661
717
|
const err = new TypeError('The "ip" argument must be of type string.');
|
|
@@ -677,7 +733,12 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
677
733
|
|
|
678
734
|
// perform resolvePTR
|
|
679
735
|
try {
|
|
680
|
-
const answers = await this.resolve(
|
|
736
|
+
const answers = await this.resolve(
|
|
737
|
+
name,
|
|
738
|
+
'PTR',
|
|
739
|
+
{ purgeCache },
|
|
740
|
+
abortController
|
|
741
|
+
);
|
|
681
742
|
return answers;
|
|
682
743
|
} catch (err) {
|
|
683
744
|
// remap syscall
|
|
@@ -699,40 +760,40 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
699
760
|
return this.resolve(name, 'AAAA', options, abortController);
|
|
700
761
|
}
|
|
701
762
|
|
|
702
|
-
resolveCaa(name, abortController) {
|
|
703
|
-
return this.resolve(name, 'CAA',
|
|
763
|
+
resolveCaa(name, options, abortController) {
|
|
764
|
+
return this.resolve(name, 'CAA', options, abortController);
|
|
704
765
|
}
|
|
705
766
|
|
|
706
|
-
resolveCname(name, abortController) {
|
|
707
|
-
return this.resolve(name, 'CNAME',
|
|
767
|
+
resolveCname(name, options, abortController) {
|
|
768
|
+
return this.resolve(name, 'CNAME', options, abortController);
|
|
708
769
|
}
|
|
709
770
|
|
|
710
|
-
resolveMx(name, abortController) {
|
|
711
|
-
return this.resolve(name, 'MX',
|
|
771
|
+
resolveMx(name, options, abortController) {
|
|
772
|
+
return this.resolve(name, 'MX', options, abortController);
|
|
712
773
|
}
|
|
713
774
|
|
|
714
|
-
resolveNaptr(name, abortController) {
|
|
715
|
-
return this.resolve(name, 'NAPTR',
|
|
775
|
+
resolveNaptr(name, options, abortController) {
|
|
776
|
+
return this.resolve(name, 'NAPTR', options, abortController);
|
|
716
777
|
}
|
|
717
778
|
|
|
718
|
-
resolveNs(name, abortController) {
|
|
719
|
-
return this.resolve(name, 'NS',
|
|
779
|
+
resolveNs(name, options, abortController) {
|
|
780
|
+
return this.resolve(name, 'NS', options, abortController);
|
|
720
781
|
}
|
|
721
782
|
|
|
722
|
-
resolvePtr(name, abortController) {
|
|
723
|
-
return this.resolve(name, 'PTR',
|
|
783
|
+
resolvePtr(name, options, abortController) {
|
|
784
|
+
return this.resolve(name, 'PTR', options, abortController);
|
|
724
785
|
}
|
|
725
786
|
|
|
726
|
-
resolveSoa(name, abortController) {
|
|
727
|
-
return this.resolve(name, 'SOA',
|
|
787
|
+
resolveSoa(name, options, abortController) {
|
|
788
|
+
return this.resolve(name, 'SOA', options, abortController);
|
|
728
789
|
}
|
|
729
790
|
|
|
730
|
-
resolveSrv(name, abortController) {
|
|
731
|
-
return this.resolve(name, 'SRV',
|
|
791
|
+
resolveSrv(name, options, abortController) {
|
|
792
|
+
return this.resolve(name, 'SRV', options, abortController);
|
|
732
793
|
}
|
|
733
794
|
|
|
734
|
-
resolveTxt(name, abortController) {
|
|
735
|
-
return this.resolve(name, 'TXT',
|
|
795
|
+
resolveTxt(name, options, abortController) {
|
|
796
|
+
return this.resolve(name, 'TXT', options, abortController);
|
|
736
797
|
}
|
|
737
798
|
|
|
738
799
|
// 1:1 mapping with node's official dns.promises API
|
|
@@ -789,7 +850,13 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
789
850
|
// eslint-disable-next-line complexity
|
|
790
851
|
async #query(name, rrtype = 'A', ecsSubnet, abortController) {
|
|
791
852
|
if (!dohdec) await pWaitFor(() => Boolean(dohdec));
|
|
792
|
-
debug('query', {
|
|
853
|
+
debug('query', {
|
|
854
|
+
name,
|
|
855
|
+
nameToASCII: toASCII(name),
|
|
856
|
+
rrtype,
|
|
857
|
+
ecsSubnet,
|
|
858
|
+
abortController
|
|
859
|
+
});
|
|
793
860
|
// <https://github.com/hildjj/dohdec/blob/43564118c40f2127af871bdb4d40f615409d4b9c/pkg/dohdec/lib/dnsUtils.js#L161>
|
|
794
861
|
const pkt = dohdec.DNSoverHTTPS.makePacket({
|
|
795
862
|
id:
|
|
@@ -797,7 +864,8 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
797
864
|
? await this.options.id()
|
|
798
865
|
: this.options.id,
|
|
799
866
|
rrtype,
|
|
800
|
-
|
|
867
|
+
// mirrors dns module behavior
|
|
868
|
+
name: toASCII(name),
|
|
801
869
|
// <https://github.com/mafintosh/dns-packet/pull/47#issuecomment-1435818437>
|
|
802
870
|
ecsSubnet
|
|
803
871
|
});
|
|
@@ -932,7 +1000,7 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
932
1000
|
}
|
|
933
1001
|
}
|
|
934
1002
|
|
|
935
|
-
#resolveByType(name, parentAbortController) {
|
|
1003
|
+
#resolveByType(name, options = {}, parentAbortController) {
|
|
936
1004
|
return async (type) => {
|
|
937
1005
|
const abortController = new AbortController();
|
|
938
1006
|
this.abortControllers.add(abortController);
|
|
@@ -956,7 +1024,7 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
956
1024
|
case 'A': {
|
|
957
1025
|
const result = await this.resolve4(
|
|
958
1026
|
name,
|
|
959
|
-
{ ttl: true },
|
|
1027
|
+
{ ...options, ttl: true },
|
|
960
1028
|
abortController
|
|
961
1029
|
);
|
|
962
1030
|
return result.map((r) => ({ type, ...r }));
|
|
@@ -965,49 +1033,73 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
965
1033
|
case 'AAAA': {
|
|
966
1034
|
const result = await this.resolve6(
|
|
967
1035
|
name,
|
|
968
|
-
{ ttl: true },
|
|
1036
|
+
{ ...options, ttl: true },
|
|
969
1037
|
abortController
|
|
970
1038
|
);
|
|
971
1039
|
return result.map((r) => ({ type, ...r }));
|
|
972
1040
|
}
|
|
973
1041
|
|
|
974
1042
|
case 'CNAME': {
|
|
975
|
-
const result = await this.resolveCname(
|
|
1043
|
+
const result = await this.resolveCname(
|
|
1044
|
+
name,
|
|
1045
|
+
options,
|
|
1046
|
+
abortController
|
|
1047
|
+
);
|
|
976
1048
|
return result.map((value) => ({ type, value }));
|
|
977
1049
|
}
|
|
978
1050
|
|
|
979
1051
|
case 'MX': {
|
|
980
|
-
const result = await this.resolveMx(name, abortController);
|
|
1052
|
+
const result = await this.resolveMx(name, options, abortController);
|
|
981
1053
|
return result.map((r) => ({ type, ...r }));
|
|
982
1054
|
}
|
|
983
1055
|
|
|
984
1056
|
case 'NAPTR': {
|
|
985
|
-
const result = await this.resolveNaptr(
|
|
1057
|
+
const result = await this.resolveNaptr(
|
|
1058
|
+
name,
|
|
1059
|
+
options,
|
|
1060
|
+
abortController
|
|
1061
|
+
);
|
|
986
1062
|
return result.map((value) => ({ type, value }));
|
|
987
1063
|
}
|
|
988
1064
|
|
|
989
1065
|
case 'NS': {
|
|
990
|
-
const result = await this.resolveNs(name, abortController);
|
|
1066
|
+
const result = await this.resolveNs(name, options, abortController);
|
|
991
1067
|
return result.map((value) => ({ type, value }));
|
|
992
1068
|
}
|
|
993
1069
|
|
|
994
1070
|
case 'PTR': {
|
|
995
|
-
const result = await this.resolvePtr(
|
|
1071
|
+
const result = await this.resolvePtr(
|
|
1072
|
+
name,
|
|
1073
|
+
options,
|
|
1074
|
+
abortController
|
|
1075
|
+
);
|
|
996
1076
|
return result.map((value) => ({ type, value }));
|
|
997
1077
|
}
|
|
998
1078
|
|
|
999
1079
|
case 'SOA': {
|
|
1000
|
-
const result = await this.resolveSoa(
|
|
1080
|
+
const result = await this.resolveSoa(
|
|
1081
|
+
name,
|
|
1082
|
+
options,
|
|
1083
|
+
abortController
|
|
1084
|
+
);
|
|
1001
1085
|
return { type, ...result };
|
|
1002
1086
|
}
|
|
1003
1087
|
|
|
1004
1088
|
case 'SRV': {
|
|
1005
|
-
const result = await this.resolveSrv(
|
|
1089
|
+
const result = await this.resolveSrv(
|
|
1090
|
+
name,
|
|
1091
|
+
options,
|
|
1092
|
+
abortController
|
|
1093
|
+
);
|
|
1006
1094
|
return result.map((value) => ({ type, value }));
|
|
1007
1095
|
}
|
|
1008
1096
|
|
|
1009
1097
|
case 'TXT': {
|
|
1010
|
-
const result = await this.resolveTxt(
|
|
1098
|
+
const result = await this.resolveTxt(
|
|
1099
|
+
name,
|
|
1100
|
+
options,
|
|
1101
|
+
abortController
|
|
1102
|
+
);
|
|
1011
1103
|
return result.map((entries) => ({ type, entries }));
|
|
1012
1104
|
}
|
|
1013
1105
|
|
|
@@ -1025,7 +1117,7 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1025
1117
|
}
|
|
1026
1118
|
|
|
1027
1119
|
// <https://nodejs.org/api/dns.html#dnspromisesresolveanyhostname>
|
|
1028
|
-
async resolveAny(name, abortController) {
|
|
1120
|
+
async resolveAny(name, options = {}, abortController) {
|
|
1029
1121
|
if (typeof name !== 'string') {
|
|
1030
1122
|
const err = new TypeError('The "name" argument must be of type string.');
|
|
1031
1123
|
err.code = 'ERR_INVALID_ARG_TYPE';
|
|
@@ -1056,7 +1148,7 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1056
1148
|
|
|
1057
1149
|
const results = await pMap(
|
|
1058
1150
|
this.constructor.ANY_TYPES,
|
|
1059
|
-
this.#resolveByType(name, abortController),
|
|
1151
|
+
this.#resolveByType(name, options, abortController),
|
|
1060
1152
|
// <https://developers.cloudflare.com/fundamentals/api/reference/limits/>
|
|
1061
1153
|
{ concurrency: this.options.concurrency, signal: abortController.signal }
|
|
1062
1154
|
);
|
|
@@ -1114,9 +1206,16 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1114
1206
|
throw err;
|
|
1115
1207
|
}
|
|
1116
1208
|
|
|
1209
|
+
// purge cache support
|
|
1210
|
+
let purgeCache;
|
|
1211
|
+
if (options?.purgeCache) {
|
|
1212
|
+
purgeCache = true;
|
|
1213
|
+
delete options.purgeCache;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1117
1216
|
// ecsSubnet support
|
|
1118
1217
|
let ecsSubnet;
|
|
1119
|
-
if (options
|
|
1218
|
+
if (options?.ecsSubnet) {
|
|
1120
1219
|
ecsSubnet = options.ecsSubnet;
|
|
1121
1220
|
delete options.ecsSubnet;
|
|
1122
1221
|
}
|
|
@@ -1127,7 +1226,7 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1127
1226
|
|
|
1128
1227
|
let result;
|
|
1129
1228
|
let data;
|
|
1130
|
-
if (this.options.cache) {
|
|
1229
|
+
if (this.options.cache && !purgeCache) {
|
|
1131
1230
|
//
|
|
1132
1231
|
// NOTE: we store `result.ttl` which was the lowest TTL determined
|
|
1133
1232
|
// (this saves us from duplicating the same `...sort().filter(Number.isFinite)` logic)
|
|
@@ -1309,7 +1408,8 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1309
1408
|
}
|
|
1310
1409
|
|
|
1311
1410
|
// if no results then throw ENODATA
|
|
1312
|
-
|
|
1411
|
+
// (hidden option for `lookup` to prevent errors being thrown)
|
|
1412
|
+
if (result.answers.length === 0 && !options.noThrowOnNODATA)
|
|
1313
1413
|
throw this.constructor.createError(name, rrtype, dns.NODATA);
|
|
1314
1414
|
|
|
1315
1415
|
// filter the answers for the same type
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tangerine",
|
|
3
|
-
"description": "Tangerine is the best Node.js drop-in replacement for dns.promises.Resolver using DNS over HTTPS (\"DoH\") via undici with built-in retries, timeouts, smart server rotation, AbortControllers, and caching support for multiple backends (with TTL support).",
|
|
4
|
-
"version": "1.
|
|
3
|
+
"description": "Tangerine is the best Node.js drop-in replacement for dns.promises.Resolver using DNS over HTTPS (\"DoH\") via undici with built-in retries, timeouts, smart server rotation, AbortControllers, and caching support for multiple backends (with TTL and purge support).",
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"author": "Forward Email (https://forwardemail.net)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/forwardemail/tangerine/issues"
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"p-timeout": "4",
|
|
21
21
|
"p-wait-for": "3",
|
|
22
22
|
"port-numbers": "^6.0.1",
|
|
23
|
+
"punycode": "^2.3.0",
|
|
23
24
|
"semver": "^7.3.8"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|