tangerine 1.4.6 → 1.4.7

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 +10 -0
  2. package/index.js +67 -46
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -58,6 +58,7 @@
58
58
  * [`tangerine.setServers(servers)`](#tangerinesetserversservers)
59
59
  * [Options](#options)
60
60
  * [Cache](#cache)
61
+ * [Compatibility](#compatibility)
61
62
  * [Debugging](#debugging)
62
63
  * [Benchmarks](#benchmarks)
63
64
  * [Tangerine Benchmarks](#tangerine-benchmarks)
@@ -430,6 +431,15 @@ await tangerine.resolve('forwardemail.net'); // uses cached value
430
431
  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/)).
431
432
 
432
433
 
434
+ ## Compatibility
435
+
436
+ The only known compatibility issue is for locally running DNS servers that have wildcard DNS matching.
437
+
438
+ If you are using `dnsmasq` with a wildcard match on "localhost" to "127.0.0.1", then the results may vary. For example, if your `dnsmasq` configuration has `address=/localhost/127.0.0.1`, then any match of `localhost` will resolve to `127.0.0.1`. This means that `dns.promises.lookup('foo.localhost')` will return `127.0.0.1` – however with :tangerine: Tangerine it will not return a value.
439
+
440
+ The reason is because :tangerine: Tangerine only looks at either `/etc/hosts` (macOS/Linux) and `C:/Windows/System32/drivers/etc/hosts` (Windows). It does not lookup BIND, dnsmasq, or other configurations running locally. We would welcome a PR to resolve this (see `isCI` usage in test folder) – however it is a non-issue, as the workaround is to simply append a new line to the hostfile of `127.0.0.1 foo.localhost`.
441
+
442
+
433
443
  ## Debugging
434
444
 
435
445
  If you run into issues while using :tangerine: Tangerine, then these recommendations may help:
package/index.js CHANGED
@@ -20,20 +20,12 @@ const pWaitFor = require('p-wait-for');
20
20
  const packet = require('dns-packet');
21
21
  const semver = require('semver');
22
22
  const structuredClone = require('@ungap/structured-clone').default;
23
- const { Hosts } = require('hosts-parser');
24
23
  const { getService } = require('port-numbers');
25
24
 
26
25
  const pkg = require('./package.json');
27
26
 
28
27
  const debug = debuglog('tangerine');
29
28
 
30
- const hosts = new Hosts(
31
- hostile
32
- .get()
33
- .map((arr) => arr.join(' '))
34
- .join('\n')
35
- );
36
-
37
29
  // dynamically import dohdec
38
30
  let dohdec;
39
31
  // eslint-disable-next-line unicorn/prefer-top-level-await
@@ -41,8 +33,32 @@ import('dohdec').then((obj) => {
41
33
  dohdec = obj;
42
34
  });
43
35
 
36
+ // dynamically import private-ip
37
+ let isPrivateIP;
38
+ // eslint-disable-next-line unicorn/prefer-top-level-await
39
+ import('private-ip').then((obj) => {
40
+ isPrivateIP = obj.default;
41
+ });
42
+
43
+ const HOSTFILE = hostile
44
+ .get(true)
45
+ .map((s) => (Array.isArray(s) ? s.join(' ') : s))
46
+ .join('\n');
47
+
48
+ const HOSTS = [];
49
+ const hosts = hostile.get();
50
+ for (const line of hosts) {
51
+ const [ip, str] = line;
52
+ const hosts = str.split(' ');
53
+ HOSTS.push({ ip, hosts });
54
+ }
55
+
44
56
  // <https://github.com/szmarczak/cacheable-lookup/pull/76>
45
57
  class Tangerine extends dns.promises.Resolver {
58
+ static HOSTFILE = HOSTFILE;
59
+
60
+ static HOSTS = HOSTS;
61
+
46
62
  static isValidPort(port) {
47
63
  return Number.isSafeInteger(port) && port >= 0 && port <= 65535;
48
64
  }
@@ -616,6 +632,16 @@ class Tangerine extends dns.promises.Resolver {
616
632
  throw err;
617
633
  }
618
634
 
635
+ if (name === '.') {
636
+ const err = this.constructor.createError(name, '', dns.NOTFOUND);
637
+ // remap and perform syscall
638
+ err.syscall = 'getaddrinfo';
639
+ err.message = err.message.replace('query', 'getaddrinfo');
640
+ err.errno = -3008; // <-- ?
641
+ // err.errno = -3007;
642
+ throw err;
643
+ }
644
+
619
645
  // purge cache support
620
646
  let purgeCache;
621
647
  if (options?.purgeCache) {
@@ -662,25 +688,22 @@ class Tangerine extends dns.promises.Resolver {
662
688
  let resolve4;
663
689
  let resolve6;
664
690
 
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;
691
+ const lower = name.toLowerCase();
692
+
693
+ for (const rule of this.constructor.HOSTS) {
694
+ if (rule.hosts.every((h) => h.toLowerCase() !== lower)) continue;
672
695
  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;
696
+ if (!resolve4 && type === 4) {
697
+ if (!Array.isArray(resolve4)) resolve4 = [rule.ip];
698
+ else if (!resolve4.includes(rule.ip)) resolve4.push([rule.ip]);
699
+ } else if (!resolve6 && type === 6) {
700
+ if (!Array.isArray(resolve6)) resolve6 = [rule.ip];
701
+ else if (!resolve6.includes(rule.ip)) resolve6.push(rule.ip);
702
+ }
676
703
  }
677
704
 
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
- ) {
705
+ // safeguard (matches c-ares)
706
+ if (lower === 'localhost' || lower === 'localhost.') {
684
707
  if (!resolve4) resolve4 = ['127.0.0.1'];
685
708
  if (!resolve6) resolve6 = ['::1'];
686
709
  }
@@ -733,25 +756,6 @@ class Tangerine extends dns.promises.Resolver {
733
756
  throw err;
734
757
  }
735
758
 
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);
743
- err.hostname = name;
744
- // remap and perform syscall
745
- err.syscall = 'getaddrinfo';
746
- err.message = err.message.replace('query', 'getaddrinfo');
747
- if (!err.code)
748
- err.code = errors.find((e) => e.code)?.code || dns.BADRESP;
749
- if (!err.errno)
750
- err.errno = errors.find((e) => e.errno)?.errno || undefined;
751
- throw err;
752
- }
753
- */
754
-
755
759
  // default node behavior seems to return IPv4 by default always regardless
756
760
  if (answers.length > 0)
757
761
  answers =
@@ -927,8 +931,25 @@ class Tangerine extends dns.promises.Resolver {
927
931
  throw err;
928
932
  }
929
933
 
930
- // edge case where localhost IP returns empty
931
- if (ip === '127.0.0.1' || ip === '::1') return [];
934
+ // edge case where localhost IP returns matches
935
+ if (!isPrivateIP) await pWaitFor(() => Boolean(isPrivateIP));
936
+
937
+ const answers = new Set();
938
+ let match = false;
939
+
940
+ for (const rule of this.constructor.HOSTS) {
941
+ if (rule.ip === ip) {
942
+ match = true;
943
+ for (const host of rule.hosts.slice(1)) {
944
+ answers.add(host);
945
+ }
946
+ }
947
+ }
948
+
949
+ if (answers.size > 0 || match) return [...answers];
950
+
951
+ // NOTE: we can prob remove this (?)
952
+ // if (ip === '::1' || ip === '127.0.0.1') return [];
932
953
 
933
954
  // reverse the IP address
934
955
  if (!dohdec) await pWaitFor(() => Boolean(dohdec));
@@ -1430,7 +1451,7 @@ class Tangerine extends dns.promises.Resolver {
1430
1451
 
1431
1452
  // edge case where c-ares detects "." as start of string
1432
1453
  // <https://github.com/c-ares/c-ares/blob/38b30bc922c21faa156939bde15ea35332c30e08/src/lib/ares_getaddrinfo.c#L829>
1433
- if (name.startsWith('.') || name.includes('..'))
1454
+ if (name !== '.' && (name.startsWith('.') || name.includes('..')))
1434
1455
  throw this.constructor.createError(name, rrtype, dns.BADNAME);
1435
1456
 
1436
1457
  // purge cache support
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.6",
4
+ "version": "1.4.7",
5
5
  "author": "Forward Email (https://forwardemail.net)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/forwardemail/tangerine/issues"
@@ -16,13 +16,13 @@
16
16
  "dohdec": "^5.0.3",
17
17
  "get-stream": "6",
18
18
  "hostile": "^1.3.3",
19
- "hosts-parser": "^0.3.2",
20
19
  "ipaddr.js": "^2.0.1",
21
20
  "merge-options": "3.0.4",
22
21
  "p-map": "4",
23
22
  "p-timeout": "4",
24
23
  "p-wait-for": "3",
25
24
  "port-numbers": "^6.0.1",
25
+ "private-ip": "^3.0.0",
26
26
  "punycode": "^2.3.0",
27
27
  "semver": "^7.3.8"
28
28
  },
@@ -41,6 +41,7 @@
41
41
  "husky": "^8.0.3",
42
42
  "ioredis": "^5.3.1",
43
43
  "ioredis-mock": "^8.2.6",
44
+ "is-ci": "^3.0.1",
44
45
  "lint-staged": "^13.1.2",
45
46
  "lodash": "^4.17.21",
46
47
  "nock": "^13.3.0",