tangerine 1.4.2 → 1.4.4

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 +56 -0
  2. package/index.js +228 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,6 +51,8 @@
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, [, options, abortController]))`](#tangerineresolvecerthostname--options-abortcontroller)
55
+ * [`tangerine.resolveTlsa(hostname, [, options, abortController]))`](#tangerineresolvetlsahostname--options-abortcontroller)
54
56
  * [`tangerine.reverse(ip[, abortController, purgeCache])`](#tangerinereverseip-abortcontroller-purgecache)
55
57
  * [`tangerine.setDefaultResultOrder(order)`](#tangerinesetdefaultresultorderorder)
56
58
  * [`tangerine.setServers(servers)`](#tangerinesetserversservers)
@@ -156,6 +158,7 @@ Thanks to the authors of [dohdec](https://github.com/hildjj/dohdec), [dns-packet
156
158
  * `resolveNs` → `queryNs`
157
159
  * `resolveNs` → `queryNs`
158
160
  * `resolveTxt` → `queryTxt`
161
+ * `resolveTsla` → `queryTsla`
159
162
  * `resolveSrv` → `querySrv`
160
163
  * `resolvePtr` → `queryPtr`
161
164
  * `resolveNaptr` → `queryNaptr`
@@ -230,6 +233,7 @@ tangerine.resolve('forwardemail.net').then(console.log);
230
233
  * If set to `true`, then the result will be re-queried and re-cached – see [Cache](#cache) documentation for more insight.
231
234
  * Instances of `new Tangerine()` are instances of `dns.promises.Resolver` via `class Tangerine extends dns.promises.Resolver { ... }` (namely for compatibility with projects such as [cacheable-lookup](https://github.com/szmarczak/cacheable-lookup)).
232
235
  * See the complete list of [Options](#options) below.
236
+ * Any `rrtype` from the list at <https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4> is supported (unlike the native Node.js DNS module which only supports a limited set).
233
237
 
234
238
  ### `tangerine.cancel()`
235
239
 
@@ -269,6 +273,58 @@ Tangerine supports a new `ecsSubnet` property in the `options` Object argument.
269
273
 
270
274
  ### `tangerine.resolveTxt(hostname[, options, abortController]))`
271
275
 
276
+ ### `tangerine.resolveCert(hostname, [, options, abortController]))`
277
+
278
+ This function returns a Promise that resolves with an Array with parsed values from results:
279
+
280
+ ```js
281
+ [
282
+ {
283
+ algorithm: 0,
284
+ certificate: 'MIIEoTCCA4mgAwIBAgICAacwDQYJKoZIhvcNAQELBQAwgY0xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNRDEOMAwGA1UEBwwFQm95ZHMxEzARBgNVBAoMCkRyYWplciBMTEMxIjAgBgNVBAMMGWludGVybWVkaWF0ZS5oZWFsdGhpdC5nb3YxKDAmBgkqhkiG9w0BCQEWGWludGVybWVkaWF0ZS5oZWFsdGhpdC5nb3YwHhcNMTgwOTI1MTgyNDIzWhcNMjgwOTIyMTgyNDIzWjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUQxDjAMBgNVBAcMBUJveWRzMRMwEQYDVQQKDApEcmFqZXIgTExDMRkwFwYDVQQDDBBldHQuaGVhbHRoaXQuZ292MR8wHQYJKoZIhvcNAQkBFhBldHQuaGVhbHRoaXQuZ292MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxaA2MIuaqpvP2Id85KIhUVA6zlj+CgZh/3prgJ1q4leP3T5F1tSSgrQ/WYTFglEwN7FJx4yJ324NaKncaMPDBIg3IUgC3Q5nrPUbIJAUgM5+67pXnGgt6s9bQelEsTdbyA/JlLC7Hsv184mqo0yrueC9NJEea4/yTV51G9S4jLjnKhr0XUTw0Fb/PFNL9ZwaEdFgQfUaE1maleazKGDyLLuEGvpXsRNs1Ju/kdHkOUVLf741Cq8qLlqOKN2v5jQkUdFUKHbYIF5KXt4ToV9mvxTaz6Mps1UbS+a73Xr+VqmBqmEQnXA5DZ7ucikzv9DLokDwtmPzhdqye2msgDpw0QIDAQABo4IBGjCCARYwCQYDVR0TBAIwADAbBgNVHREEFDASghBldHQuaGVhbHRoaXQuZ292MB0GA1UdDgQWBBQ6E22jc99mm+WraUj93IvQcw6JHDAfBgNVHSMEGDAWgBRfW20fzencvG+Attm1rcvQV+3rOTALBgNVHQ8EBAMCBaAwSQYDVR0fBEIwQDA+oDygOoY4aHR0cDovL2NhLmRpcmVjdGNhLm9yZy9jcmwvaW50ZXJtZWRpYXRlLmhlYWx0aGl0Lmdvdi5jcmwwVAYIKwYBBQUHAQEESDBGMEQGCCsGAQUFBzAChjhodHRwOi8vY2EuZGlyZWN0Y2Eub3JnL2FpYS9pbnRlcm1lZGlhdGUuaGVhbHRoaXQuZ292LmRlcjANBgkqhkiG9w0BAQsFAAOCAQEAhCASLubdxWp+XzXO4a8zMgWOMpjft+ilIy2ROVKOKslbB7lKx0NR7chrTPxCmK+YTL2ttLaTpOniw/vTGrZgeFPyXzJCNtpnx8fFipPE18OAlKMc2nyy7RfUscf28UAEmFo2cEJfpsZjyynkBsTnQ5rQVNgM7TbXXfboxwWwhg4HnWIcmlTs2YM1a9v+idK6LSfX9y/Nvhf9pl0DQflc9ym4z/XCq87erCce+11kxH1+36N6rRqeiHVBYnoYIGMH690r4cgE8cW5B4eK7kaD3iCbmpChO0gZSa5Lex49WLXeFfM+ukd9y3AB00KMZcsUV5bCgwShH053ZQa+FMON8w==',
285
+ certificate_type: 'PKIX',
286
+ key_tag: 0,
287
+ name: 'ett.healthit.gov',
288
+ ttl: 19045,
289
+ },
290
+ ]
291
+ ```
292
+
293
+ This mirrors output from <https://github.com/rthalley/dnspython>.
294
+
295
+ ### `tangerine.resolveTlsa(hostname, [, options, abortController]))`
296
+
297
+ This method was added for DANE and TSLA 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
+ This function returns a Promise that resolves with an Array with parsed values from results:
300
+
301
+ ```js
302
+ [
303
+ {
304
+ cert: Buffer @Uint8Array [
305
+ e1ae9c3d e848ece1 ba72e0d9 91ae4d0d 9ec547c6 bad1ddda b9d6beb0 a7e0e0d8
306
+ ],
307
+ mtype: 1,
308
+ name: 'proloprod.mail._dane.internet.nl',
309
+ selector: 1,
310
+ ttl: 622,
311
+ usage: 2,
312
+ },
313
+ {
314
+ cert: Buffer @Uint8Array [
315
+ d6fea64d 4e68caea b7cbb2e0 f905d7f3 ca3308b1 2fd88c5b 469f08ad 7e05c7c7
316
+ ],
317
+ mtype: 1,
318
+ name: 'proloprod.mail._dane.internet.nl',
319
+ selector: 1,
320
+ ttl: 622,
321
+ usage: 3,
322
+ },
323
+ ]
324
+ ```
325
+
326
+ This mirrors output from <https://github.com/rthalley/dnspython>.
327
+
272
328
  ### `tangerine.reverse(ip[, abortController, purgeCache])`
273
329
 
274
330
  ### `tangerine.setDefaultResultOrder(order)`
package/index.js CHANGED
@@ -38,6 +38,19 @@ class Tangerine extends dns.promises.Resolver {
38
38
  return Number.isSafeInteger(port) && port >= 0 && port <= 65535;
39
39
  }
40
40
 
41
+ static CTYPE_BY_VALUE = {
42
+ 1: 'PKIX',
43
+ 2: 'SPKI',
44
+ 3: 'PGP',
45
+ 4: 'IPKIX',
46
+ 5: 'ISPKI',
47
+ 6: 'IPGP',
48
+ 7: 'ACPKIX',
49
+ 8: 'IACPKIX',
50
+ 253: 'URI',
51
+ 254: 'OID'
52
+ };
53
+
41
54
  static getAddrConfigTypes() {
42
55
  const networkInterfaces = os.networkInterfaces();
43
56
  let hasIPv4 = false;
@@ -138,7 +151,7 @@ class Tangerine extends dns.promises.Resolver {
138
151
  dns.TIMEOUT
139
152
  ]);
140
153
 
141
- static TYPES = new Set([
154
+ static DNS_TYPES = new Set([
142
155
  'A',
143
156
  'AAAA',
144
157
  'CAA',
@@ -152,6 +165,99 @@ class Tangerine extends dns.promises.Resolver {
152
165
  'TXT'
153
166
  ]);
154
167
 
168
+ // <https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4>
169
+ static TYPES = new Set([
170
+ 'A',
171
+ 'A6',
172
+ 'AAAA',
173
+ 'AFSDB',
174
+ 'AMTRELAY',
175
+ 'APL',
176
+ 'ATMA',
177
+ 'AVC',
178
+ 'AXFR',
179
+ 'CAA',
180
+ 'CDNSKEY',
181
+ 'CDS',
182
+ 'CERT',
183
+ 'CNAME',
184
+ 'CSYNC',
185
+ 'DHCID',
186
+ 'DLV',
187
+ 'DNAME',
188
+ 'DNSKEY',
189
+ 'DOA',
190
+ 'DS',
191
+ 'EID',
192
+ 'EUI48',
193
+ 'EUI64',
194
+ 'GID',
195
+ 'GPOS',
196
+ 'HINFO',
197
+ 'HIP',
198
+ 'HTTPS',
199
+ 'IPSECKEY',
200
+ 'ISDN',
201
+ 'IXFR',
202
+ 'KEY',
203
+ 'KX',
204
+ 'L32',
205
+ 'L64',
206
+ 'LOC',
207
+ 'LP',
208
+ 'MAILA',
209
+ 'MAILB',
210
+ 'MB',
211
+ 'MD',
212
+ 'MF',
213
+ 'MG',
214
+ 'MINFO',
215
+ 'MR',
216
+ 'MX',
217
+ 'NAPTR',
218
+ 'NID',
219
+ 'NIMLOC',
220
+ 'NINFO',
221
+ 'NS',
222
+ 'NSAP',
223
+ 'NSAP-PTR',
224
+ 'NSEC',
225
+ 'NSEC3',
226
+ 'NSEC3PARAM',
227
+ 'NULL',
228
+ 'NXT',
229
+ 'OPENPGPKEY',
230
+ 'OPT',
231
+ 'PTR',
232
+ 'PX',
233
+ 'RKEY',
234
+ 'RP',
235
+ 'RRSIG',
236
+ 'RT',
237
+ 'Reserved',
238
+ 'SIG',
239
+ 'SINK',
240
+ 'SMIMEA',
241
+ 'SOA',
242
+ 'SPF',
243
+ 'SRV',
244
+ 'SSHFP',
245
+ 'SVCB',
246
+ 'TA',
247
+ 'TALINK',
248
+ 'TKEY',
249
+ 'TLSA',
250
+ 'TSIG',
251
+ 'TXT',
252
+ 'UID',
253
+ 'UINFO',
254
+ 'UNSPEC',
255
+ 'URI',
256
+ 'WKS',
257
+ 'X25',
258
+ 'ZONEMD'
259
+ ]);
260
+
155
261
  static ANY_TYPES = [
156
262
  'A',
157
263
  'AAAA',
@@ -805,6 +911,15 @@ class Tangerine extends dns.promises.Resolver {
805
911
  return this.resolve(name, 'TXT', options, abortController);
806
912
  }
807
913
 
914
+ resolveCert(name, options, abortController) {
915
+ return this.resolve(name, 'CERT', options, abortController);
916
+ }
917
+
918
+ // NOTE: parse this properly according to spec (see below default case)
919
+ resolveTlsa(name, options, abortController) {
920
+ return this.resolve(name, 'TLSA', options, abortController);
921
+ }
922
+
808
923
  // 1:1 mapping with node's official dns.promises API
809
924
  // (this means it's a drop-in replacement for `dns`)
810
925
  // <https://github.com/nodejs/node/blob/9bbde3d7baef584f14569ef79f116e9d288c7aaa/lib/internal/dns/utils.js#L87-L95>
@@ -1243,7 +1358,7 @@ class Tangerine extends dns.promises.Resolver {
1243
1358
  data = await this.options.cache.get(key);
1244
1359
  // safeguard in case cache pollution
1245
1360
  if (data && typeof data === 'object') {
1246
- debug('cache retrieved', data);
1361
+ debug('cache retrieved', key);
1247
1362
  const now = Date.now();
1248
1363
  // safeguard in case cache pollution
1249
1364
  if (
@@ -1252,6 +1367,7 @@ class Tangerine extends dns.promises.Resolver {
1252
1367
  !Number.isFinite(data.ttl) ||
1253
1368
  data.ttl < 1
1254
1369
  ) {
1370
+ debug('cache expired', key);
1255
1371
  data = undefined;
1256
1372
  } else if (options?.ttl) {
1257
1373
  // clone the data so that we don't mutate cache (e.g. if it's in-memory)
@@ -1271,6 +1387,7 @@ class Tangerine extends dns.promises.Resolver {
1271
1387
 
1272
1388
  // eslint-disable-next-line max-depth
1273
1389
  if (data.answers[i].ttl <= 0) {
1390
+ debug('answer cache expired', key);
1274
1391
  data = undefined;
1275
1392
  break;
1276
1393
  }
@@ -1384,7 +1501,7 @@ class Tangerine extends dns.promises.Resolver {
1384
1501
  // this supports both redis-based key/value/ttl and simple key/value implementations
1385
1502
  result.expires = Date.now() + Math.round(result.ttl * 1000);
1386
1503
  const args = [key, result, ...this.options.setCacheArgs(key, result)];
1387
- debug('setting cache', [key, result, ...args]);
1504
+ debug('setting cache', { args });
1388
1505
  await this.options.cache.set(...args);
1389
1506
  }
1390
1507
 
@@ -1517,17 +1634,117 @@ class Tangerine extends dns.promises.Resolver {
1517
1634
 
1518
1635
  case 'TXT': {
1519
1636
  // text records `dnsPromises.resolveTxt()`
1520
- return result.answers.flatMap((a) => [
1521
- Buffer.isBuffer(a.data)
1522
- ? a.data.toString()
1523
- : Array.isArray(a.data)
1524
- ? a.data.map((d) => (Buffer.isBuffer(d) ? d.toString() : d))
1525
- : a.data
1526
- ]);
1637
+ return result.answers.flatMap((a) => {
1638
+ //
1639
+ // NOTE: we need to support buffer conversion
1640
+ // (e.g. JSON.stringify from most cache stores will convert this as such below)
1641
+ //
1642
+ // a {
1643
+ // name: 'forwardemail.net',
1644
+ // type: 'TXT',
1645
+ // ttl: 3600,
1646
+ // class: 'IN',
1647
+ // flush: false,
1648
+ // data: [ { type: 'Buffer', data: [Array] } ]
1649
+ // }
1650
+ //
1651
+ // (or)
1652
+ //
1653
+ // a {
1654
+ // name: 'forwardemail.net',
1655
+ // type: 'TXT',
1656
+ // ttl: 3600,
1657
+ // class: 'IN',
1658
+ // flush: false,
1659
+ // data: { type: 'Buffer', data: [Array] }
1660
+ // }
1661
+ //
1662
+ if (Array.isArray(a.data)) {
1663
+ a.data = a.data.map((d) => {
1664
+ if (
1665
+ typeof d === 'object' &&
1666
+ d.type === 'Buffer' &&
1667
+ Array.isArray(d.data)
1668
+ )
1669
+ return Buffer.from(d.data);
1670
+ return d;
1671
+ });
1672
+ } else if (
1673
+ typeof a.data === 'object' &&
1674
+ a.data.type === 'Buffer' &&
1675
+ Array.isArray(a.data.data)
1676
+ ) {
1677
+ a.data = Buffer.from(a.data.data);
1678
+ }
1679
+
1680
+ return [
1681
+ Buffer.isBuffer(a.data)
1682
+ ? a.data.toString()
1683
+ : Array.isArray(a.data)
1684
+ ? a.data.map((d) => (Buffer.isBuffer(d) ? d.toString() : d))
1685
+ : a.data
1686
+ ];
1687
+ });
1688
+ }
1689
+
1690
+ case 'CERT': {
1691
+ // CERT records `tangerine.resolveCert`
1692
+ // <https://github.com/jpnarkinsky/tangerine/commit/5f70954875aa93ef4acf076172d7540298b0a16b>
1693
+ // <https://www.rfc-editor.org/rfc/rfc4398.html>
1694
+ return result.answers.map((answer) => {
1695
+ if (!Buffer.isBuffer(answer.data))
1696
+ throw new Error('Buffer was not available');
1697
+
1698
+ try {
1699
+ // <https://github.com/rthalley/dnspython/blob/98b12e9e43847dac615bb690355d2fabaff969d2/dns/rdtypes/ANY/CERT.py#L69>
1700
+ const obj = {
1701
+ name: answer.name,
1702
+ ttl: answer.ttl,
1703
+ certificate_type: answer.data.subarray(0, 2).readUInt16BE(),
1704
+ key_tag: answer.data.subarray(2, 4).readUInt16BE(),
1705
+ algorithm: answer.data.subarray(4, 5).readUInt8(),
1706
+ certificate: answer.data.subarray(5).toString('base64')
1707
+ };
1708
+ obj.certificate_type = this.constructor.CTYPE_BY_VALUE[
1709
+ obj.certificate_type
1710
+ ]
1711
+ ? this.constructor.CTYPE_BY_VALUE[obj.certificate_type]
1712
+ : obj.certificate_type.toString();
1713
+ return obj;
1714
+ } catch (err) {
1715
+ console.error(err);
1716
+ throw err;
1717
+ }
1718
+ });
1719
+ }
1720
+
1721
+ case 'TLSA': {
1722
+ // if it returns answers with `type: TLSA` then recursively lookup
1723
+ // 3 1 1 D6FEA64D4E68CAEAB7CBB2E0F905D7F3CA3308B12FD88C5B469F08AD 7E05C7C7
1724
+ return result.answers.map((answer) => {
1725
+ if (!Buffer.isBuffer(answer.data))
1726
+ throw new Error('Buffer was not available');
1727
+
1728
+ // <https://www.mailhardener.com/kb/dane>
1729
+ return {
1730
+ name: answer.name,
1731
+ ttl: answer.ttl,
1732
+ // <https://github.com/rthalley/dnspython/blob/98b12e9e43847dac615bb690355d2fabaff969d2/dns/rdtypes/tlsabase.py#L35>
1733
+ usage: answer.data.subarray(0, 1).readUInt8(),
1734
+ selector: answer.data.subarray(1, 2).readUInt8(),
1735
+ mtype: answer.data.subarray(2, 3).readUInt8(),
1736
+ cert: answer.data.subarray(3)
1737
+ };
1738
+ });
1527
1739
  }
1528
1740
 
1529
1741
  default: {
1530
- throw new Error(`Unknown type of ${rrtype}`);
1742
+ this.options.logger.error(
1743
+ new Error(
1744
+ `Submit a PR at <https://github.com/forwardemail/tangerine> with proper parsing for ${rrtype} records. You can reference <https://github.com/rthalley/dnspython/tree/master/dns/rdtypes/ANY> for inspiration.`
1745
+ )
1746
+ );
1747
+ return result.answers;
1531
1748
  }
1532
1749
  }
1533
1750
  }
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.2",
4
+ "version": "1.4.4",
5
5
  "author": "Forward Email (https://forwardemail.net)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/forwardemail/tangerine/issues"