tangerine 1.4.9 → 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 +79 -0
  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);
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.9",
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"