tangerine 1.4.8 → 1.5.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.
Files changed (3) hide show
  1. package/README.md +43 -0
  2. package/index.js +101 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,6 +56,7 @@
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)`](#tangerinespoofpackethostname-rrtype-answers)
59
60
  * [Options](#options)
60
61
  * [Cache](#cache)
61
62
  * [Compatibility](#compatibility)
@@ -331,6 +332,48 @@ This mirrors output from <https://github.com/rthalley/dnspython>.
331
332
 
332
333
  ### `tangerine.setServers(servers)`
333
334
 
335
+ ### `tangerine.spoofPacket(hostname, rrtype, answers)`
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
+ For example, if you want to spoof TXT and MX records:
342
+
343
+ ```js
344
+ const Redis = require('ioredis-mock');
345
+ const Tangerine = require('tangerine');
346
+ const ip = require('ip');
347
+
348
+ const cache = new Redis();
349
+ const tangerine = new Tangerine({ cache });
350
+
351
+ const obj = {};
352
+
353
+ obj['txt:forwardmail.net'] = tangerine.spoofPacket('forwardmail.net', 'TXT', [
354
+ `v=spf1 ip4:${ip.address()} -all`
355
+ ]);
356
+
357
+ obj['mx:forwardemail.net'] = tangerine.spoofPacket('forwardemail.net', 'MX', [
358
+ { exchange: 'mx1.forwardemail.net', preference: 0 },
359
+ { exchange: 'mx2.forwardemail.net', preference: 0 }
360
+ ]);
361
+
362
+ await cache.mset(obj);
363
+
364
+ //
365
+ // NOTE: spoofed values are used below (this means no DNS query performed)
366
+ //
367
+
368
+ const txt = await tangerine.resolveTxt('forwardemail.net');
369
+ console.log('txt', txt);
370
+
371
+ const mx = await tangerine.resolveMx('forwardemail.net');
372
+ console.log('mx', mx);
373
+ ```
374
+
375
+ **Pull requests are welcome to add support for other `rrtype` values for this method.**
376
+
334
377
 
335
378
  ## Options
336
379
 
package/index.js CHANGED
@@ -1428,6 +1428,74 @@ class Tangerine extends dns.promises.Resolver {
1428
1428
  this.options.servers = new Set(servers);
1429
1429
  }
1430
1430
 
1431
+ spoofPacket(name, rrtype, answers = []) {
1432
+ if (typeof name !== 'string') {
1433
+ const err = new TypeError('The "name" argument must be of type string.');
1434
+ err.code = 'ERR_INVALID_ARG_TYPE';
1435
+ throw err;
1436
+ }
1437
+
1438
+ if (typeof rrtype !== 'string') {
1439
+ const err = new TypeError(
1440
+ 'The "rrtype" argument must be of type string.'
1441
+ );
1442
+ err.code = 'ERR_INVALID_ARG_TYPE';
1443
+ throw err;
1444
+ }
1445
+
1446
+ if (!this.constructor.TYPES.has(rrtype)) {
1447
+ const err = new TypeError("The argument 'rrtype' is invalid.");
1448
+ err.code = 'ERR_INVALID_ARG_VALUE';
1449
+ throw err;
1450
+ }
1451
+
1452
+ if (!Array.isArray(answers)) {
1453
+ const err = new TypeError("The argument 'answers' is invalid.");
1454
+ err.code = 'ERR_INVALID_ARG_VALUE';
1455
+ throw err;
1456
+ }
1457
+
1458
+ return {
1459
+ id: 0,
1460
+ type: 'response',
1461
+ flags: 384,
1462
+ flag_qr: true,
1463
+ opcode: 'QUERY',
1464
+ flag_aa: false,
1465
+ flag_tc: false,
1466
+ flag_rd: true,
1467
+ flag_ra: true,
1468
+ flag_z: false,
1469
+ flag_ad: false,
1470
+ flag_cd: false,
1471
+ rcode: 'NOERROR',
1472
+ questions: [{ name, type: rrtype, class: 'IN' }],
1473
+ answers: answers.map((answer) => ({
1474
+ name,
1475
+ type: rrtype,
1476
+ ttl: 300,
1477
+ class: 'IN',
1478
+ flush: false,
1479
+ data: rrtype === 'TXT' ? [answer] : answer
1480
+ })),
1481
+ authorities: [],
1482
+ additionals: [
1483
+ {
1484
+ name: '.',
1485
+ type: 'OPT',
1486
+ udpPayloadSize: 1232,
1487
+ extendedRcode: 0,
1488
+ ednsVersion: 0,
1489
+ flags: 0,
1490
+ flag_do: false,
1491
+ options: [Array]
1492
+ }
1493
+ ],
1494
+ ttl: 300,
1495
+ expires: Date.now() + 10000
1496
+ };
1497
+ }
1498
+
1431
1499
  // eslint-disable-next-line complexity
1432
1500
  async resolve(name, rrtype = 'A', options = {}, abortController) {
1433
1501
  if (typeof name !== 'string') {
@@ -1481,6 +1549,17 @@ class Tangerine extends dns.promises.Resolver {
1481
1549
  // (this saves us from duplicating the same `...sort().filter(Number.isFinite)` logic)
1482
1550
  //
1483
1551
  data = await this.options.cache.get(key);
1552
+ //
1553
+ // if it's not an object then assume that
1554
+ // the cache implementation does not have JSON.parse built-in
1555
+ // and so we should try to parse it on our own (user-friendly)
1556
+ //
1557
+ if (typeof data === 'string') {
1558
+ try {
1559
+ data = JSON.parse(data);
1560
+ } catch {}
1561
+ }
1562
+
1484
1563
  // safeguard in case cache pollution
1485
1564
  if (data && typeof data === 'object') {
1486
1565
  debug('cache retrieved', key);
@@ -1847,19 +1926,30 @@ class Tangerine extends dns.promises.Resolver {
1847
1926
  // if it returns answers with `type: TLSA` then recursively lookup
1848
1927
  // 3 1 1 D6FEA64D4E68CAEAB7CBB2E0F905D7F3CA3308B12FD88C5B469F08AD 7E05C7C7
1849
1928
  return result.answers.map((answer) => {
1850
- if (!Buffer.isBuffer(answer.data))
1851
- throw new Error('Buffer was not available');
1852
-
1853
- // <https://www.mailhardener.com/kb/dane>
1854
- return {
1929
+ const obj = {
1855
1930
  name: answer.name,
1856
- ttl: answer.ttl,
1857
- // <https://github.com/rthalley/dnspython/blob/98b12e9e43847dac615bb690355d2fabaff969d2/dns/rdtypes/tlsabase.py#L35>
1858
- usage: answer.data.subarray(0, 1).readUInt8(),
1859
- selector: answer.data.subarray(1, 2).readUInt8(),
1860
- mtype: answer.data.subarray(2, 3).readUInt8(),
1861
- cert: answer.data.subarray(3)
1931
+ ttl: answer.ttl
1862
1932
  };
1933
+
1934
+ // <https://www.mailhardener.com/kb/dane>
1935
+ // <https://github.com/rthalley/dnspython/blob/98b12e9e43847dac615bb690355d2fabaff969d2/dns/rdtypes/tlsabase.py#L35>
1936
+ if (Buffer.isBuffer(answer.data)) {
1937
+ obj.usage = answer.data.subarray(0, 1).readUInt8();
1938
+ obj.selector = answer.data.subarray(1, 2).readUInt8();
1939
+ obj.mtype = answer.data.subarray(2, 3).readUInt8();
1940
+ obj.cert = answer.data.subarray(3);
1941
+ } else {
1942
+ obj.usage = answer.data.usage;
1943
+ obj.selector = answer.data.selector;
1944
+ obj.mtype = answer.data.matchingType;
1945
+ obj.cert = answer.data.certificate;
1946
+ }
1947
+
1948
+ // aliases to match Cloudflare DNS response
1949
+ obj.matchingType = obj.mtype;
1950
+ obj.certificate = obj.cert;
1951
+
1952
+ return obj;
1863
1953
  });
1864
1954
  }
1865
1955
 
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.8",
4
+ "version": "1.5.0",
5
5
  "author": "Forward Email (https://forwardemail.net)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/forwardemail/tangerine/issues"