tangerine 1.4.4 → 1.4.6

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.
Files changed (3) hide show
  1. package/README.md +1 -2
  2. package/index.js +142 -39
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -158,7 +158,6 @@ Thanks to the authors of [dohdec](https://github.com/hildjj/dohdec), [dns-packet
158
158
  * `resolveNs` → `queryNs`
159
159
  * `resolveNs` → `queryNs`
160
160
  * `resolveTxt` → `queryTxt`
161
- * `resolveTsla` → `queryTsla`
162
161
  * `resolveSrv` → `querySrv`
163
162
  * `resolvePtr` → `queryPtr`
164
163
  * `resolveNaptr` → `queryNaptr`
@@ -294,7 +293,7 @@ This mirrors output from <https://github.com/rthalley/dnspython>.
294
293
 
295
294
  ### `tangerine.resolveTlsa(hostname, [, options, abortController]))`
296
295
 
297
- This method was added for DANE and TSLA support. See this [excellent article](https://www.mailhardener.com/kb/dane), [index.js](https://github.com/forwardemail/tangerine/blob/main/index.js), and <https://github.com/nodejs/node/issues/39569> for more insight.
296
+ This method was added for DANE and TLSA support. See this [excellent article](https://www.mailhardener.com/kb/dane), [index.js](https://github.com/forwardemail/tangerine/blob/main/index.js), and <https://github.com/nodejs/node/issues/39569> for more insight.
298
297
 
299
298
  This function returns a Promise that resolves with an Array with parsed values from results:
300
299
 
package/index.js CHANGED
@@ -7,8 +7,11 @@ 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/');
11
+
10
12
  const autoBind = require('auto-bind');
11
13
  const getStream = require('get-stream');
14
+ const hostile = require('hostile');
12
15
  const ipaddr = require('ipaddr.js');
13
16
  const mergeOptions = require('merge-options');
14
17
  const pMap = require('p-map');
@@ -17,14 +20,20 @@ const pWaitFor = require('p-wait-for');
17
20
  const packet = require('dns-packet');
18
21
  const semver = require('semver');
19
22
  const structuredClone = require('@ungap/structured-clone').default;
23
+ const { Hosts } = require('hosts-parser');
20
24
  const { getService } = require('port-numbers');
21
- // eslint-disable-next-line import/order
22
- const { toASCII } = require('punycode/');
23
25
 
24
26
  const pkg = require('./package.json');
25
27
 
26
28
  const debug = debuglog('tangerine');
27
29
 
30
+ const hosts = new Hosts(
31
+ hostile
32
+ .get()
33
+ .map((arr) => arr.join(' '))
34
+ .join('\n')
35
+ );
36
+
28
37
  // dynamically import dohdec
29
38
  let dohdec;
30
39
  // eslint-disable-next-line unicorn/prefer-top-level-await
@@ -148,7 +157,8 @@ class Tangerine extends dns.promises.Resolver {
148
157
  dns.NOTINITIALIZED,
149
158
  dns.REFUSED,
150
159
  dns.SERVFAIL,
151
- dns.TIMEOUT
160
+ dns.TIMEOUT,
161
+ 'EINVAL'
152
162
  ]);
153
163
 
154
164
  static DNS_TYPES = new Set([
@@ -639,50 +649,124 @@ class Tangerine extends dns.promises.Resolver {
639
649
  }
640
650
  }
641
651
 
652
+ // <https://github.com/c-ares/c-ares/blob/38b30bc922c21faa156939bde15ea35332c30e08/src/lib/ares_getaddrinfo.c#L407>
653
+ // <https://www.rfc-editor.org/rfc/rfc6761.html#section-6.3>
654
+ //
655
+ // > 'localhost and any domains falling within .localhost'
656
+ //
657
+ // if no system loopback match, then revert to the default
658
+ // <https://github.com/c-ares/c-ares/blob/38b30bc922c21faa156939bde15ea35332c30e08/src/lib/ares__addrinfo_localhost.c#L224-L229>
659
+ // - IPv4 = '127.0.0.1"
660
+ // - IPv6 = "::1"
661
+ //
662
+ let resolve4;
663
+ let resolve6;
664
+
665
+ // sorted in reverse to match behavior of lookup
666
+ for (const rule of hosts._origin.reverse()) {
667
+ if (
668
+ rule.hostname.toLowerCase() !== name.toLowerCase() &&
669
+ rule.ip !== name
670
+ )
671
+ continue;
672
+ const type = isIP(rule.ip);
673
+ if (!resolve4 && type === 4) resolve4 = [rule.ip];
674
+ else if (!resolve6 && type === 6) resolve6 = [rule.ip];
675
+ if (resolve4 && resolve6) break;
676
+ }
677
+
678
+ // if no matches found for resolve4 and resolve6 and it was localhost
679
+ // (this is a safeguard in case host file is missing these)
680
+ if (
681
+ name.toLowerCase() === 'localhost' ||
682
+ name.toLowerCase() === 'localhost.'
683
+ ) {
684
+ if (!resolve4) resolve4 = ['127.0.0.1'];
685
+ if (!resolve6) resolve6 = ['::1'];
686
+ }
687
+
688
+ if (isIPv4(name)) {
689
+ resolve4 = [name];
690
+ resolve6 = [];
691
+ } else if (isIPv6(name)) {
692
+ resolve6 = [name];
693
+ resolve4 = [];
694
+ }
695
+
642
696
  // resolve the first A or AAAA record (conditionally)
697
+ const results = await Promise.all(
698
+ [
699
+ Array.isArray(resolve4)
700
+ ? Promise.resolve(resolve4)
701
+ : this.resolve4(name, { purgeCache, noThrowOnNODATA: true }),
702
+ Array.isArray(resolve6)
703
+ ? Promise.resolve(resolve6)
704
+ : this.resolve6(name, { purgeCache, noThrowOnNODATA: true })
705
+ ].map((p) => p.catch((err) => err))
706
+ );
707
+
708
+ const errors = [];
643
709
  let answers = [];
644
710
 
645
- try {
646
- answers = await Promise.all([
647
- this.resolve4(name, { purgeCache, noThrowOnNODATA: true }),
648
- this.resolve6(name, { purgeCache, noThrowOnNODATA: true })
649
- ]);
650
- // default node behavior seems to return IPv4 by default always regardless
651
- answers =
652
- answers[0].length > 0 &&
653
- (typeof options.family === 'undefined' || options.family === 0)
654
- ? answers[0]
655
- : answers.flat();
656
- } catch (_err) {
657
- debug(_err);
711
+ for (const result of results) {
712
+ if (result instanceof Error) {
713
+ errors.push(result);
714
+ } else {
715
+ answers.push(result);
716
+ }
717
+ }
658
718
 
659
- // this will most likely be instanceof AggregateError
660
- if (_err instanceof AggregateError) {
661
- const err = this.constructor.combineErrors(_err.errors);
719
+ if (
720
+ answers.length === 0 &&
721
+ errors.length > 0 &&
722
+ errors.every((e) => e.code === errors[0].code)
723
+ ) {
724
+ const err = this.constructor.createError(
725
+ name,
726
+ '',
727
+ errors[0].code === dns.BADNAME ? dns.NOTFOUND : errors[0].code
728
+ );
729
+ // remap and perform syscall
730
+ err.syscall = 'getaddrinfo';
731
+ err.message = err.message.replace('query', 'getaddrinfo');
732
+ err.errno = -3008;
733
+ throw err;
734
+ }
735
+
736
+ /*
737
+ //
738
+ // NOTE: we probably should handle this differently (?)
739
+ // (not sure what native nodejs dns module does for different errors - haven't checked yet)
740
+ //
741
+ if (errors.every((e) => e.code !== 'ENODATA')) {
742
+ const err = this.constructor.combineErrors(errors);
662
743
  err.hostname = name;
663
744
  // remap and perform syscall
664
745
  err.syscall = 'getaddrinfo';
746
+ err.message = err.message.replace('query', 'getaddrinfo');
665
747
  if (!err.code)
666
- err.code = _err.errors.find((e) => e.code)?.code || dns.BADRESP;
748
+ err.code = errors.find((e) => e.code)?.code || dns.BADRESP;
667
749
  if (!err.errno)
668
- err.errno = _err.errors.find((e) => e.errno)?.errno || undefined;
669
-
750
+ err.errno = errors.find((e) => e.errno)?.errno || undefined;
670
751
  throw err;
671
752
  }
753
+ */
672
754
 
673
- const err = this.constructor.createError(name, '', _err.code, _err.errno);
674
- // remap and perform syscall
675
- err.syscall = 'getaddrinfo';
676
- err.error = _err;
677
- throw err;
678
- }
755
+ // default node behavior seems to return IPv4 by default always regardless
756
+ if (answers.length > 0)
757
+ answers =
758
+ answers[0].length > 0 &&
759
+ (typeof options.family === 'undefined' || options.family === 0)
760
+ ? answers[0]
761
+ : answers.flat();
679
762
 
680
763
  // if no results then throw ENODATA
681
764
  if (answers.length === 0) {
682
765
  const err = this.constructor.createError(name, '', dns.NODATA);
683
766
  // remap and perform syscall
684
767
  err.syscall = 'getaddrinfo';
685
- // err.errno = -3008;
768
+ err.message = err.message.replace('query', 'getaddrinfo');
769
+ err.errno = -3008;
686
770
  throw err;
687
771
  }
688
772
 
@@ -835,15 +919,20 @@ class Tangerine extends dns.promises.Resolver {
835
919
  }
836
920
 
837
921
  if (!isIP(ip)) {
838
- const err = this.constructor.createError(ip, '', dns.EINVAL);
922
+ const err = this.constructor.createError(ip, '', 'EINVAL');
923
+ err.message = `getHostByAddr EINVAL ${err.hostname}`;
839
924
  err.syscall = 'getHostByAddr';
840
- // err.errno = -22;
925
+ err.errno = -22;
841
926
  if (!ip) delete err.hostname;
842
927
  throw err;
843
928
  }
844
929
 
930
+ // edge case where localhost IP returns empty
931
+ if (ip === '127.0.0.1' || ip === '::1') return [];
932
+
845
933
  // reverse the IP address
846
934
  if (!dohdec) await pWaitFor(() => Boolean(dohdec));
935
+
847
936
  const name = dohdec.DNSoverHTTPS.reverse(ip);
848
937
 
849
938
  // perform resolvePTR
@@ -1270,13 +1359,22 @@ class Tangerine extends dns.promises.Resolver {
1270
1359
  );
1271
1360
  }
1272
1361
 
1273
- const results = await pMap(
1274
- this.constructor.ANY_TYPES,
1275
- this.#resolveByType(name, options, abortController),
1276
- // <https://developers.cloudflare.com/fundamentals/api/reference/limits/>
1277
- { concurrency: this.options.concurrency, signal: abortController.signal }
1278
- );
1279
- return results.flat().filter(Boolean);
1362
+ try {
1363
+ const results = await pMap(
1364
+ this.constructor.ANY_TYPES,
1365
+ this.#resolveByType(name, options, abortController),
1366
+ // <https://developers.cloudflare.com/fundamentals/api/reference/limits/>
1367
+ {
1368
+ concurrency: this.options.concurrency,
1369
+ signal: abortController.signal
1370
+ }
1371
+ );
1372
+ return results.flat().filter(Boolean);
1373
+ } catch (err) {
1374
+ err.syscall = 'queryAny';
1375
+ err.message = `queryAny ${err.code} ${name}`;
1376
+ throw err;
1377
+ }
1280
1378
  }
1281
1379
 
1282
1380
  setDefaultResultOrder(dnsOrder) {
@@ -1330,6 +1428,11 @@ class Tangerine extends dns.promises.Resolver {
1330
1428
  throw err;
1331
1429
  }
1332
1430
 
1431
+ // edge case where c-ares detects "." as start of string
1432
+ // <https://github.com/c-ares/c-ares/blob/38b30bc922c21faa156939bde15ea35332c30e08/src/lib/ares_getaddrinfo.c#L829>
1433
+ if (name.startsWith('.') || name.includes('..'))
1434
+ throw this.constructor.createError(name, rrtype, dns.BADNAME);
1435
+
1333
1436
  // purge cache support
1334
1437
  let purgeCache;
1335
1438
  if (options?.purgeCache) {
@@ -1712,7 +1815,7 @@ class Tangerine extends dns.promises.Resolver {
1712
1815
  : obj.certificate_type.toString();
1713
1816
  return obj;
1714
1817
  } catch (err) {
1715
- console.error(err);
1818
+ this.options.logger.error(err, { name, rrtype, options, answer });
1716
1819
  throw err;
1717
1820
  }
1718
1821
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tangerine",
3
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.4",
4
+ "version": "1.4.6",
5
5
  "author": "Forward Email (https://forwardemail.net)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/forwardemail/tangerine/issues"
@@ -15,6 +15,8 @@
15
15
  "dns-packet": "^5.4.0",
16
16
  "dohdec": "^5.0.3",
17
17
  "get-stream": "6",
18
+ "hostile": "^1.3.3",
19
+ "hosts-parser": "^0.3.2",
18
20
  "ipaddr.js": "^2.0.1",
19
21
  "merge-options": "3.0.4",
20
22
  "p-map": "4",