tangerine 1.0.3 → 1.1.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 +23 -21
  2. package/index.js +25 -19
  3. package/package.json +1 -2
package/README.md CHANGED
@@ -251,27 +251,29 @@ Tangerine supports a new `ecsSubnet` property in the `options` Object argument.
251
251
 
252
252
  Similar to the `options` argument from `new dns.promises.Resolver(options)` invocation – :tangerine: Tangerine also has its own options with default `dns` behavior mirrored. See [index.js](https://github.com/forwardemail/tangerine/blob/main/index.js) for more insight into how these options work.
253
253
 
254
- | Property | Type | Default Value | Description |
255
- | ------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
256
- | `timeout` | `Number` | `5000` | Number of milliseconds for requests to timeout. |
257
- | `tries` | `Number` | `4` | Number of tries per `server` in `servers` to attempt. |
258
- | `servers` | `Set` or `Array` | `new Set(['1.1.1.1', '1.0.0.1'])` | A Set or Array of [RFC 5952](https://tools.ietf.org/html/rfc5952#section-6) formatted addresses for DNS queries (matches default Node.js dns module behavior). Duplicates will be removed as this is converted to a `Set` internally. Defaults to Cloudflare's of `1.1.1.1` and `1.0.0.1`. If an `Array` is passed, then it will be converted to a `Set`. |
259
- | `undici` | `Object` | Defaults to an Object with `undici.method` and `undici.headers` properties and values below | Default options to pass to [undici](https://github.com/nodejs/undici). |
260
- | `undici.method` | `String` | Defaults to `"GET"` (must be either `"GET"` or `"POST"`). | Default HTTP method to use for DNS over HTTP ("DoH") requests. |
261
- | `undici.headers` | `Object` | Defaults to `{ 'content-type': 'application/dns-message', 'user-agent': pkg.name + "/" + pkg.version, accept: 'application/dns-message' }`. | Default HTTP headers to use for DNS over HTTP ("DoH") requests. |
262
- | `protocol` | `String` | Defaults to `"https"`. | Default HTTP protocol to use for DNS over HTTPS ("DoH") requests. |
263
- | `dnsOrder` | `String` | Defaults to `"verbatim"` for Node.js v17.0.0+ and `"ipv4first"` for older versions. | Sets the default result order of `lookup` invocations (see [dns.setDefaultResultOrder](https://nodejs.org/api/dns.html#dnssetdefaultresultorderorder) for more insight). |
264
- | `logger` | `Object` | `false` | This is the default logger. We recommend using [Cabin](https://github.com/cabinjs) instead of using `console` as your default logger. Set this value to `false` to disable logging entirely (uses noop function). |
265
- | `id` | `Number` or `Function` | `0` | Default `id` to be passed for DNS packet creation. This could alternatively be a synchronous or asynchronous function that returns a `Number` (e.g. `id: () => Tangerine.getRandomInt(1, 65534)`). |
266
- | `concurrency` | `Number` | `os.cpus().length` | Default concurrency to use for `resolveAny` lookup via [p-map](https://github.com/sindresorhus/p-map). The default value is the number of CPU's available to the system using the Node.js `os` module [os.cpus()](https://nodejs.org/api/os.html#oscpus) method. |
267
- | `ipv4` | `String` | `"0.0.0.0"` | Default IPv4 address to use for HTTP agent `localAddress` if DNS `server` was an IPv4 address. |
268
- | `ipv6` | `String` | `"::0"` | Default IPv6 address to use for HTTP agent `localAddress` if DNS `server` was an IPv6 address. |
269
- | `ipv4Port` | `Number` | `undefined` | Default port to use for HTTP agent `localPort` if DNS `server` was an IPv4 address. |
270
- | `ipv6Port` | `Number` | `undefined` | Default port to use for HTTP agent `localPort` if DNS `server` was an IPv6 address. |
271
- | `cache` | `Map` or `Boolean` | `new Map()` | Set this to `false` in order to disable caching. Default `Map` instance to use for caching. Entries are by type, e.g. `map.set('TXT', new Keyv({})`). If cache set values are not provided, then they will default to a new instance of `Keyv`. See cache setup and usage in [index.js](https://github.com/forwardemail/tangerine/blob/main/index.js) for more insight. You can iterate over `Tangerine.TYPES` if necessary to create a similar cache setup. |
272
- | `returnHTTPErrors` | `Boolean` | `false` | Whether to return HTTP errors instead of mapping them to corresponding DNS errors. |
273
- | `smartRotate` | `Boolean` | `true` | Whether to do smart server rotation if servers fail. |
274
- | `defaultHTTPErrorMessage` | `String` | `"Unsuccessful HTTP response"` | Default fallback message if `statusCode` returned from HTTP request was not found in [http.STATUS_CODES](https://nodejs.org/api/http.html#httpstatus_codes). |
254
+ | Property | Type | Default Value | Description |
255
+ | ------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
256
+ | `timeout` | `Number` | `5000` | Number of milliseconds for requests to timeout. |
257
+ | `tries` | `Number` | `4` | Number of tries per `server` in `servers` to attempt. |
258
+ | `servers` | `Set` or `Array` | `new Set(['1.1.1.1', '1.0.0.1'])` | A Set or Array of [RFC 5952](https://tools.ietf.org/html/rfc5952#section-6) formatted addresses for DNS queries (matches default Node.js dns module behavior). Duplicates will be removed as this is converted to a `Set` internally. Defaults to Cloudflare's of `1.1.1.1` and `1.0.0.1`. If an `Array` is passed, then it will be converted to a `Set`. |
259
+ | `request` | `Function` | [undici.request](https://undici.nodejs.org/#/?id=undicirequesturl-options-promise) | HTTP library request async or Promise returning function to be used for making request (defaults to undici's request method). You could alternatively use `got` or any other HTTP library of your choice that accepts `fn(url, options)`. It should return an object with `body`, `headers`, and either a `status` or `statusCode` property. The `body` property returned should be either a `Buffer` or `Stream`. Specify default request options based off the library under `requestOptions` below (e.g. for `got` you'd set `responseType: 'buffer', retry: { limit: 0 }`) since this library already has built-in retries. See `requestTimeout` function below, as it is required to be set properly if you are using a custom HTTP library function. |
260
+ | `requestOptions` | `Object` | Defaults to an Object with `requestOptions.method` and `requestOptions.headers` properties and values below | Default options to pass to [undici](https://github.com/nodejs/undici) (or your custom HTTP library function passed as `request`). |
261
+ | `requestOptions.method` | `String` | Defaults to `"GET"` (must be either `"GET"` or `"POST"`, case-insensitive depending on library you use). | Default HTTP method to use for DNS over HTTP ("DoH") requests. |
262
+ | `requestOptions.headers` | `Object` | Defaults to `{ 'content-type': 'application/dns-message', 'user-agent': pkg.name + "/" + pkg.version, accept: 'application/dns-message', bodyTimeout: timeout }`. | Default HTTP headers to use for DNS over HTTP ("DoH") requests. |
263
+ | `requestTimeout` | `Function` | Defaults to `(ms) => ({ bodyTimeout })` for setting undici timeout properly. | This function accepts an argument `ms` which is the number of milliseconds to wait for the request to timeout (since we use a back-off strategy that mirrors the Node.js DNS module). This function is required to be passed and customized if you are using a custom HTTP library. If you're using a custom HTTP library such as `got`, you'd set this to `requestTimeout: (ms) => ({ timeout: { request: ms } })` |
264
+ | `protocol` | `String` | Defaults to `"https"`. | Default HTTP protocol to use for DNS over HTTPS ("DoH") requests. |
265
+ | `dnsOrder` | `String` | Defaults to `"verbatim"` for Node.js v17.0.0+ and `"ipv4first"` for older versions. | Sets the default result order of `lookup` invocations (see [dns.setDefaultResultOrder](https://nodejs.org/api/dns.html#dnssetdefaultresultorderorder) for more insight). |
266
+ | `logger` | `Object` | `false` | This is the default logger. We recommend using [Cabin](https://github.com/cabinjs) instead of using `console` as your default logger. Set this value to `false` to disable logging entirely (uses noop function). |
267
+ | `id` | `Number` or `Function` | `0` | Default `id` to be passed for DNS packet creation. This could alternatively be a synchronous or asynchronous function that returns a `Number` (e.g. `id: () => Tangerine.getRandomInt(1, 65534)`). |
268
+ | `concurrency` | `Number` | `os.cpus().length` | Default concurrency to use for `resolveAny` lookup via [p-map](https://github.com/sindresorhus/p-map). The default value is the number of CPU's available to the system using the Node.js `os` module [os.cpus()](https://nodejs.org/api/os.html#oscpus) method. |
269
+ | `ipv4` | `String` | `"0.0.0.0"` | Default IPv4 address to use for HTTP agent `localAddress` if DNS `server` was an IPv4 address. |
270
+ | `ipv6` | `String` | `"::0"` | Default IPv6 address to use for HTTP agent `localAddress` if DNS `server` was an IPv6 address. |
271
+ | `ipv4Port` | `Number` | `undefined` | Default port to use for HTTP agent `localPort` if DNS `server` was an IPv4 address. |
272
+ | `ipv6Port` | `Number` | `undefined` | Default port to use for HTTP agent `localPort` if DNS `server` was an IPv6 address. |
273
+ | `cache` | `Map` or `Boolean` | `new Map()` | Set this to `false` in order to disable caching. Default `Map` instance to use for caching. Entries are by type, e.g. `map.set('TXT', new Keyv({})`). If cache set values are not provided, then they will default to a new instance of `Keyv`. See cache setup and usage in [index.js](https://github.com/forwardemail/tangerine/blob/main/index.js) for more insight. You can iterate over `Tangerine.TYPES` if necessary to create a similar cache setup. |
274
+ | `returnHTTPErrors` | `Boolean` | `false` | Whether to return HTTP errors instead of mapping them to corresponding DNS errors. |
275
+ | `smartRotate` | `Boolean` | `true` | Whether to do smart server rotation if servers fail. |
276
+ | `defaultHTTPErrorMessage` | `String` | `"Unsuccessful HTTP response"` | Default fallback message if `statusCode` returned from HTTP request was not found in [http.STATUS_CODES](https://nodejs.org/api/http.html#httpstatus_codes). |
275
277
 
276
278
 
277
279
  ## Debugging
package/index.js CHANGED
@@ -12,7 +12,6 @@ const getStream = require('get-stream');
12
12
  const ipaddr = require('ipaddr.js');
13
13
  const mergeOptions = require('merge-options');
14
14
  const pMap = require('p-map');
15
- const pTimeout = require('p-timeout');
16
15
  const pWaitFor = require('p-wait-for');
17
16
  const packet = require('dns-packet');
18
17
  const semver = require('semver');
@@ -31,7 +30,7 @@ import('dohdec').then((obj) => {
31
30
  });
32
31
 
33
32
  // <https://github.com/szmarczak/cacheable-lookup/pull/76>
34
- class Resolver extends dns.promises.Resolver {
33
+ class Tangerine extends dns.promises.Resolver {
35
34
  static isValidPort(port) {
36
35
  return Number.isSafeInteger(port) && port >= 0 && port <= 65535;
37
36
  }
@@ -238,7 +237,9 @@ class Resolver extends dns.promises.Resolver {
238
237
  // dns servers will optionally retry in series
239
238
  // and servers that error will get shifted to the end of list
240
239
  servers: new Set(['1.1.1.1', '1.0.0.1']),
241
- undici: {
240
+ // HTTP library function to use
241
+ request,
242
+ requestOptions: {
242
243
  method: 'GET',
243
244
  headers: {
244
245
  'content-type': 'application/dns-message',
@@ -246,6 +247,7 @@ class Resolver extends dns.promises.Resolver {
246
247
  accept: 'application/dns-message'
247
248
  }
248
249
  },
250
+ requestTimeout: (ms) => ({ bodyTimeout: ms }),
249
251
  //
250
252
  // NOTE: we set the default to "get" since it is faster from `benchmark` results
251
253
  //
@@ -291,6 +293,14 @@ class Resolver extends dns.promises.Resolver {
291
293
  if (!Number.isFinite(this.options.tries) || this.options.tries < 1)
292
294
  throw new Error('Tries must be >= 1');
293
295
 
296
+ // request option method must be either GET or POST
297
+ if (
298
+ !['get', 'post'].includes(
299
+ this.options.requestOptions.method.toLowerCase()
300
+ )
301
+ )
302
+ throw new Error('Request options method must be either GET or POST');
303
+
294
304
  // perform validation by re-using `setServers` method
295
305
  this.setServers([...this.options.servers]);
296
306
 
@@ -725,12 +735,7 @@ class Resolver extends dns.promises.Resolver {
725
735
  // was too confusing and the documentation was lacking, misleading, or incredibly complex
726
736
  // <https://github.com/sindresorhus/got/issues/2226>
727
737
  //
728
- async #request(
729
- pkt,
730
- server,
731
- abortController,
732
- requestTimeout = this.options.timeout
733
- ) {
738
+ async #request(pkt, server, abortController, timeout = this.options.timeout) {
734
739
  // safeguard in case aborted
735
740
  if (abortController.signal.aborted) return;
736
741
 
@@ -746,25 +751,24 @@ class Resolver extends dns.promises.Resolver {
746
751
  }
747
752
 
748
753
  const options = {
749
- signal: abortController.signal,
750
- ...this.options.undici
754
+ ...this.options.requestOptions,
755
+ ...this.options.requestTimeout(timeout), // returns `{ bodyTimeout: requestTimeout }`
756
+ signal: abortController.signal
751
757
  };
752
758
 
753
759
  if (localAddress !== '0.0.0.0') options.localAddress = localAddress;
754
760
  if (localPort) options.localPort = localPort;
755
761
 
756
762
  // <https://github.com/hildjj/dohdec/blob/43564118c40f2127af871bdb4d40f615409d4b9c/pkg/dohdec/lib/doh.js#L117-L120>
757
- if (this.options.undici.method === 'GET') {
763
+ if (this.options.requestOptions.method.toLowerCase() === 'get') {
758
764
  if (!dohdec) await pWaitFor(() => Boolean(dohdec));
759
765
  url += `?dns=${dohdec.DNSoverHTTPS.base64urlEncode(pkt)}`;
760
766
  } else {
761
767
  options.body = pkt;
762
768
  }
763
769
 
764
- debug('request', { url, options, requestTimeout });
765
- const response = await pTimeout(request(url, options), requestTimeout, {
766
- signal: abortController.signal
767
- });
770
+ debug('request', { url, options });
771
+ const response = await this.options.request(url, options);
768
772
  return response;
769
773
  }
770
774
 
@@ -812,8 +816,10 @@ class Resolver extends dns.promises.Resolver {
812
816
 
813
817
  // eslint-disable-next-line max-depth
814
818
  if (body && statusCode >= 200 && statusCode < 300) {
815
- // eslint-disable-next-line no-await-in-loop
816
- buffer = await getStream.buffer(body);
819
+ buffer = Buffer.isBuffer(body)
820
+ ? body
821
+ : // eslint-disable-next-line no-await-in-loop
822
+ await getStream.buffer(body);
817
823
  // eslint-disable-next-line max-depth
818
824
  if (!abortController.signal.aborted) abortController.abort();
819
825
  break;
@@ -1388,4 +1394,4 @@ class Resolver extends dns.promises.Resolver {
1388
1394
  }
1389
1395
  }
1390
1396
 
1391
- module.exports = Resolver;
1397
+ module.exports = Tangerine;
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 via Keyv.",
4
- "version": "1.0.3",
4
+ "version": "1.1.0",
5
5
  "author": "Forward Email (https://forwardemail.net)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/forwardemail/tangerine/issues"
@@ -17,7 +17,6 @@
17
17
  "keyv": "^4.5.2",
18
18
  "merge-options": "3.0.4",
19
19
  "p-map": "4",
20
- "p-timeout": "4",
21
20
  "p-wait-for": "3",
22
21
  "port-numbers": "^6.0.1",
23
22
  "semver": "^7.3.8",