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.
- package/README.md +1 -2
- package/index.js +142 -39
- 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
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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 =
|
|
748
|
+
err.code = errors.find((e) => e.code)?.code || dns.BADRESP;
|
|
667
749
|
if (!err.errno)
|
|
668
|
-
err.errno =
|
|
669
|
-
|
|
750
|
+
err.errno = errors.find((e) => e.errno)?.errno || undefined;
|
|
670
751
|
throw err;
|
|
671
752
|
}
|
|
753
|
+
*/
|
|
672
754
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
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, '',
|
|
922
|
+
const err = this.constructor.createError(ip, '', 'EINVAL');
|
|
923
|
+
err.message = `getHostByAddr EINVAL ${err.hostname}`;
|
|
839
924
|
err.syscall = 'getHostByAddr';
|
|
840
|
-
|
|
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
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
-
|
|
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
|
+
"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",
|