tangerine 1.4.9 → 1.5.1
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 +49 -4
- package/index.js +90 -13
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -51,11 +51,12 @@
|
|
|
51
51
|
* [`tangerine.resolveSoa(hostname[, options, abortController]))`](#tangerineresolvesoahostname-options-abortcontroller)
|
|
52
52
|
* [`tangerine.resolveSrv(hostname[, options, abortController]))`](#tangerineresolvesrvhostname-options-abortcontroller)
|
|
53
53
|
* [`tangerine.resolveTxt(hostname[, options, abortController]))`](#tangerineresolvetxthostname-options-abortcontroller)
|
|
54
|
-
* [`tangerine.resolveCert(hostname
|
|
55
|
-
* [`tangerine.resolveTlsa(hostname
|
|
54
|
+
* [`tangerine.resolveCert(hostname[, options, abortController]))`](#tangerineresolvecerthostname-options-abortcontroller)
|
|
55
|
+
* [`tangerine.resolveTlsa(hostname[, options, abortController]))`](#tangerineresolvetlsahostname-options-abortcontroller)
|
|
56
56
|
* [`tangerine.reverse(ip[, abortController, purgeCache])`](#tangerinereverseip-abortcontroller-purgecache)
|
|
57
57
|
* [`tangerine.setDefaultResultOrder(order)`](#tangerinesetdefaultresultorderorder)
|
|
58
58
|
* [`tangerine.setServers(servers)`](#tangerinesetserversservers)
|
|
59
|
+
* [`tangerine.spoofPacket(hostname, rrtype, answers[, json])`](#tangerinespoofpackethostname-rrtype-answers-json)
|
|
59
60
|
* [Options](#options)
|
|
60
61
|
* [Cache](#cache)
|
|
61
62
|
* [Compatibility](#compatibility)
|
|
@@ -273,7 +274,7 @@ Tangerine supports a new `ecsSubnet` property in the `options` Object argument.
|
|
|
273
274
|
|
|
274
275
|
### `tangerine.resolveTxt(hostname[, options, abortController]))`
|
|
275
276
|
|
|
276
|
-
### `tangerine.resolveCert(hostname
|
|
277
|
+
### `tangerine.resolveCert(hostname[, options, abortController]))`
|
|
277
278
|
|
|
278
279
|
This function returns a Promise that resolves with an Array with parsed values from results:
|
|
279
280
|
|
|
@@ -292,7 +293,7 @@ This function returns a Promise that resolves with an Array with parsed values f
|
|
|
292
293
|
|
|
293
294
|
This mirrors output from <https://github.com/rthalley/dnspython>.
|
|
294
295
|
|
|
295
|
-
### `tangerine.resolveTlsa(hostname
|
|
296
|
+
### `tangerine.resolveTlsa(hostname[, options, abortController]))`
|
|
296
297
|
|
|
297
298
|
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
299
|
|
|
@@ -331,6 +332,50 @@ This mirrors output from <https://github.com/rthalley/dnspython>.
|
|
|
331
332
|
|
|
332
333
|
### `tangerine.setServers(servers)`
|
|
333
334
|
|
|
335
|
+
### `tangerine.spoofPacket(hostname, rrtype, answers[, json])`
|
|
336
|
+
|
|
337
|
+
This method is useful for writing tests to spoof DNS packets in-memory.
|
|
338
|
+
|
|
339
|
+
The `rrtype` must be either `"TXT"` or `"MX"`, and `answers` must be an Array of DNS resource record answers.
|
|
340
|
+
|
|
341
|
+
If you pass `json` as `true`, then value returned will be converted to JSON via `JSON.stringify`.
|
|
342
|
+
|
|
343
|
+
For example, if you want to spoof TXT and MX records:
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
const Redis = require('ioredis-mock');
|
|
347
|
+
const Tangerine = require('tangerine');
|
|
348
|
+
const ip = require('ip');
|
|
349
|
+
|
|
350
|
+
const cache = new Redis();
|
|
351
|
+
const tangerine = new Tangerine({ cache });
|
|
352
|
+
|
|
353
|
+
const obj = {};
|
|
354
|
+
|
|
355
|
+
obj['txt:forwardmail.net'] = tangerine.spoofPacket('forwardmail.net', 'TXT', [
|
|
356
|
+
`v=spf1 ip4:${ip.address()} -all`
|
|
357
|
+
]);
|
|
358
|
+
|
|
359
|
+
obj['mx:forwardemail.net'] = tangerine.spoofPacket('forwardemail.net', 'MX', [
|
|
360
|
+
{ exchange: 'mx1.forwardemail.net', preference: 0 },
|
|
361
|
+
{ exchange: 'mx2.forwardemail.net', preference: 0 }
|
|
362
|
+
]);
|
|
363
|
+
|
|
364
|
+
await cache.mset(obj);
|
|
365
|
+
|
|
366
|
+
//
|
|
367
|
+
// NOTE: spoofed values are used below (this means no DNS query performed)
|
|
368
|
+
//
|
|
369
|
+
|
|
370
|
+
const txt = await tangerine.resolveTxt('forwardemail.net');
|
|
371
|
+
console.log('txt', txt);
|
|
372
|
+
|
|
373
|
+
const mx = await tangerine.resolveMx('forwardemail.net');
|
|
374
|
+
console.log('mx', mx);
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Pull requests are welcome to add support for other `rrtype` values for this method.**
|
|
378
|
+
|
|
334
379
|
|
|
335
380
|
## Options
|
|
336
381
|
|
package/index.js
CHANGED
|
@@ -6,9 +6,7 @@ const { Buffer } = require('node:buffer');
|
|
|
6
6
|
const { debuglog } = require('node:util');
|
|
7
7
|
const { getEventListeners, setMaxListeners } = require('node:events');
|
|
8
8
|
const { isIP, isIPv4, isIPv6 } = require('node:net');
|
|
9
|
-
|
|
10
9
|
const { toASCII } = require('punycode/');
|
|
11
|
-
|
|
12
10
|
const autoBind = require('auto-bind');
|
|
13
11
|
const getStream = require('get-stream');
|
|
14
12
|
const hostile = require('hostile');
|
|
@@ -21,7 +19,6 @@ const packet = require('dns-packet');
|
|
|
21
19
|
const semver = require('semver');
|
|
22
20
|
const structuredClone = require('@ungap/structured-clone').default;
|
|
23
21
|
const { getService } = require('port-numbers');
|
|
24
|
-
|
|
25
22
|
const pkg = require('./package.json');
|
|
26
23
|
|
|
27
24
|
const debug = debuglog('tangerine');
|
|
@@ -123,21 +120,21 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
123
120
|
|
|
124
121
|
// if all errors had `name` and they were all the same then preserve it
|
|
125
122
|
if (
|
|
126
|
-
|
|
123
|
+
errors[0].name !== undefined &&
|
|
127
124
|
errors.every((e) => e.name === errors[0].name)
|
|
128
125
|
)
|
|
129
126
|
err.name = errors[0].name;
|
|
130
127
|
|
|
131
128
|
// if all errors had `code` and they were all the same then preserve it
|
|
132
129
|
if (
|
|
133
|
-
|
|
130
|
+
errors[0].code !== undefined &&
|
|
134
131
|
errors.every((e) => e.code === errors[0].code)
|
|
135
132
|
)
|
|
136
133
|
err.code = errors[0].code;
|
|
137
134
|
|
|
138
135
|
// if all errors had `errno` and they were all the same then preserve it
|
|
139
136
|
if (
|
|
140
|
-
|
|
137
|
+
errors[0].errno !== undefined &&
|
|
141
138
|
errors.every((e) => e.errno === errors[0].errno)
|
|
142
139
|
)
|
|
143
140
|
err.errno = errors[0].errno;
|
|
@@ -607,7 +604,7 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
607
604
|
|
|
608
605
|
options = { family: options };
|
|
609
606
|
} else if (
|
|
610
|
-
|
|
607
|
+
options?.family !== undefined &&
|
|
611
608
|
![0, 4, 6, 'IPv4', 'IPv6'].includes(options.family)
|
|
612
609
|
) {
|
|
613
610
|
// validate family
|
|
@@ -761,7 +758,7 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
761
758
|
if (answers.length > 0)
|
|
762
759
|
answers =
|
|
763
760
|
answers[0].length > 0 &&
|
|
764
|
-
(
|
|
761
|
+
(options.family === undefined || options.family === 0)
|
|
765
762
|
? answers[0]
|
|
766
763
|
: answers.flat();
|
|
767
764
|
|
|
@@ -1428,6 +1425,76 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1428
1425
|
this.options.servers = new Set(servers);
|
|
1429
1426
|
}
|
|
1430
1427
|
|
|
1428
|
+
spoofPacket(name, rrtype, answers = [], json = false) {
|
|
1429
|
+
if (typeof name !== 'string') {
|
|
1430
|
+
const err = new TypeError('The "name" argument must be of type string.');
|
|
1431
|
+
err.code = 'ERR_INVALID_ARG_TYPE';
|
|
1432
|
+
throw err;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (typeof rrtype !== 'string') {
|
|
1436
|
+
const err = new TypeError(
|
|
1437
|
+
'The "rrtype" argument must be of type string.'
|
|
1438
|
+
);
|
|
1439
|
+
err.code = 'ERR_INVALID_ARG_TYPE';
|
|
1440
|
+
throw err;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
if (!this.constructor.TYPES.has(rrtype)) {
|
|
1444
|
+
const err = new TypeError("The argument 'rrtype' is invalid.");
|
|
1445
|
+
err.code = 'ERR_INVALID_ARG_VALUE';
|
|
1446
|
+
throw err;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
if (!Array.isArray(answers)) {
|
|
1450
|
+
const err = new TypeError("The argument 'answers' is invalid.");
|
|
1451
|
+
err.code = 'ERR_INVALID_ARG_VALUE';
|
|
1452
|
+
throw err;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
const obj = {
|
|
1456
|
+
id: 0,
|
|
1457
|
+
type: 'response',
|
|
1458
|
+
flags: 384,
|
|
1459
|
+
flag_qr: true,
|
|
1460
|
+
opcode: 'QUERY',
|
|
1461
|
+
flag_aa: false,
|
|
1462
|
+
flag_tc: false,
|
|
1463
|
+
flag_rd: true,
|
|
1464
|
+
flag_ra: true,
|
|
1465
|
+
flag_z: false,
|
|
1466
|
+
flag_ad: false,
|
|
1467
|
+
flag_cd: false,
|
|
1468
|
+
rcode: 'NOERROR',
|
|
1469
|
+
questions: [{ name, type: rrtype, class: 'IN' }],
|
|
1470
|
+
answers: answers.map((answer) => ({
|
|
1471
|
+
name,
|
|
1472
|
+
type: rrtype,
|
|
1473
|
+
ttl: 300,
|
|
1474
|
+
class: 'IN',
|
|
1475
|
+
flush: false,
|
|
1476
|
+
data: rrtype === 'TXT' ? [answer] : answer
|
|
1477
|
+
})),
|
|
1478
|
+
authorities: [],
|
|
1479
|
+
additionals: [
|
|
1480
|
+
{
|
|
1481
|
+
name: '.',
|
|
1482
|
+
type: 'OPT',
|
|
1483
|
+
udpPayloadSize: 1232,
|
|
1484
|
+
extendedRcode: 0,
|
|
1485
|
+
ednsVersion: 0,
|
|
1486
|
+
flags: 0,
|
|
1487
|
+
flag_do: false,
|
|
1488
|
+
options: [Array]
|
|
1489
|
+
}
|
|
1490
|
+
],
|
|
1491
|
+
ttl: 300,
|
|
1492
|
+
expires: Date.now() + 10000
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
return json ? JSON.stringify(obj) : obj;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1431
1498
|
// eslint-disable-next-line complexity
|
|
1432
1499
|
async resolve(name, rrtype = 'A', options = {}, abortController) {
|
|
1433
1500
|
if (typeof name !== 'string') {
|
|
@@ -1481,6 +1548,17 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1481
1548
|
// (this saves us from duplicating the same `...sort().filter(Number.isFinite)` logic)
|
|
1482
1549
|
//
|
|
1483
1550
|
data = await this.options.cache.get(key);
|
|
1551
|
+
//
|
|
1552
|
+
// if it's not an object then assume that
|
|
1553
|
+
// the cache implementation does not have JSON.parse built-in
|
|
1554
|
+
// and so we should try to parse it on our own (user-friendly)
|
|
1555
|
+
//
|
|
1556
|
+
if (typeof data === 'string') {
|
|
1557
|
+
try {
|
|
1558
|
+
data = JSON.parse(data);
|
|
1559
|
+
} catch {}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1484
1562
|
// safeguard in case cache pollution
|
|
1485
1563
|
if (data && typeof data === 'object') {
|
|
1486
1564
|
debug('cache retrieved', key);
|
|
@@ -1830,11 +1908,10 @@ class Tangerine extends dns.promises.Resolver {
|
|
|
1830
1908
|
algorithm: answer.data.subarray(4, 5).readUInt8(),
|
|
1831
1909
|
certificate: answer.data.subarray(5).toString('base64')
|
|
1832
1910
|
};
|
|
1833
|
-
|
|
1834
|
-
obj.certificate_type
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
: obj.certificate_type.toString();
|
|
1911
|
+
if (this.constructor.CTYPE_BY_VALUE[obj.certificate_type])
|
|
1912
|
+
obj.certificate_type =
|
|
1913
|
+
this.constructor.CTYPE_BY_VALUE[obj.certificate_type];
|
|
1914
|
+
else obj.certificate_type = obj.certificate_type.toString();
|
|
1838
1915
|
return obj;
|
|
1839
1916
|
} catch (err) {
|
|
1840
1917
|
this.options.logger.error(err, { name, rrtype, options, answer });
|
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
|
+
"version": "1.5.1",
|
|
5
5
|
"author": "Forward Email (https://forwardemail.net)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/forwardemail/tangerine/issues"
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
"Forward Email (https://forwardemail.net)"
|
|
11
11
|
],
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@ungap/structured-clone": "^1.0
|
|
13
|
+
"@ungap/structured-clone": "^1.2.0",
|
|
14
14
|
"auto-bind": "4",
|
|
15
|
-
"dns-packet": "^5.
|
|
15
|
+
"dns-packet": "^5.6.0",
|
|
16
16
|
"dohdec": "^5.0.3",
|
|
17
17
|
"get-stream": "6",
|
|
18
18
|
"hostile": "^1.3.3",
|
|
@@ -21,16 +21,16 @@
|
|
|
21
21
|
"p-map": "4",
|
|
22
22
|
"p-timeout": "4",
|
|
23
23
|
"p-wait-for": "3",
|
|
24
|
-
"port-numbers": "
|
|
24
|
+
"port-numbers": "6.0.1",
|
|
25
25
|
"private-ip": "^3.0.0",
|
|
26
26
|
"punycode": "^2.3.0",
|
|
27
|
-
"semver": "^7.
|
|
27
|
+
"semver": "^7.5.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@commitlint/cli": "^17.
|
|
31
|
-
"@commitlint/config-conventional": "^17.
|
|
30
|
+
"@commitlint/cli": "^17.6.3",
|
|
31
|
+
"@commitlint/config-conventional": "^17.6.3",
|
|
32
32
|
"ava": "^5.2.0",
|
|
33
|
-
"axios": "^1.
|
|
33
|
+
"axios": "^1.4.0",
|
|
34
34
|
"benchmark": "^2.1.4",
|
|
35
35
|
"cross-env": "^7.0.3",
|
|
36
36
|
"eslint": "^8.34.0",
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
"fixpack": "^4.0.0",
|
|
40
40
|
"got": "11",
|
|
41
41
|
"husky": "^8.0.3",
|
|
42
|
-
"ioredis": "^5.3.
|
|
43
|
-
"ioredis-mock": "^8.
|
|
42
|
+
"ioredis": "^5.3.2",
|
|
43
|
+
"ioredis-mock": "^8.7.0",
|
|
44
44
|
"is-ci": "^3.0.1",
|
|
45
|
-
"lint-staged": "^13.
|
|
45
|
+
"lint-staged": "^13.2.2",
|
|
46
46
|
"lodash": "^4.17.21",
|
|
47
|
-
"nock": "^13.3.
|
|
47
|
+
"nock": "^13.3.1",
|
|
48
48
|
"node-fetch": "2",
|
|
49
49
|
"nyc": "^15.1.0",
|
|
50
50
|
"phin": "^3.7.0",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"request": "^2.88.2",
|
|
54
54
|
"sort-keys": "4.2.0",
|
|
55
55
|
"superagent": "^8.0.9",
|
|
56
|
-
"undici": "^5.
|
|
57
|
-
"xo": "^0.
|
|
56
|
+
"undici": "^5.22.1",
|
|
57
|
+
"xo": "^0.54.2"
|
|
58
58
|
},
|
|
59
59
|
"engines": {
|
|
60
60
|
"node": ">=16"
|