pax_global_header 0000666 0000000 0000000 00000000064 14133234510 0014506 g ustar 00root root 0000000 0000000 52 comment=45b18daf9f0c8406691fad81188e956641e2309d
cacheable-lookup-6.0.4/ 0000775 0000000 0000000 00000000000 14133234510 0014713 5 ustar 00root root 0000000 0000000 cacheable-lookup-6.0.4/.editorconfig 0000664 0000000 0000000 00000000323 14133234510 0017366 0 ustar 00root root 0000000 0000000 root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[crlfHosts.txt]
end_of_line = crlf
[*.yml]
indent_style = space
indent_size = 2
cacheable-lookup-6.0.4/.gitattributes 0000664 0000000 0000000 00000000064 14133234510 0017606 0 ustar 00root root 0000000 0000000 * text=auto eol=lf
crlfHosts.txt text=auto eol=crlf
cacheable-lookup-6.0.4/.github/ 0000775 0000000 0000000 00000000000 14133234510 0016253 5 ustar 00root root 0000000 0000000 cacheable-lookup-6.0.4/.github/workflows/ 0000775 0000000 0000000 00000000000 14133234510 0020310 5 ustar 00root root 0000000 0000000 cacheable-lookup-6.0.4/.github/workflows/nodejs.yml 0000664 0000000 0000000 00000001077 14133234510 0022322 0 ustar 00root root 0000000 0000000 name: Node CI
on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [10.x, 12.x, 13.x]
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: npm install, build, and test
run: |
npm install
npm run build --if-present
npm test
- name: Codecov
uses: codecov/codecov-action@v1
cacheable-lookup-6.0.4/.gitignore 0000664 0000000 0000000 00000000064 14133234510 0016703 0 ustar 00root root 0000000 0000000 .nyc_output
node_modules
package-lock.json
coverage
cacheable-lookup-6.0.4/.npmrc 0000664 0000000 0000000 00000000023 14133234510 0016026 0 ustar 00root root 0000000 0000000 package-lock=false
cacheable-lookup-6.0.4/LICENSE 0000664 0000000 0000000 00000002057 14133234510 0015724 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2019 Szymon Marczak
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
cacheable-lookup-6.0.4/README.md 0000664 0000000 0000000 00000017077 14133234510 0016206 0 ustar 00root root 0000000 0000000 # cacheable-lookup
> A cacheable [`dns.lookup(…)`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) that respects TTL :tada:
[](https://github.com/szmarczak/cacheable-lookup/actions)
[](https://codecov.io/gh/szmarczak/cacheable-lookup)
[](https://www.npmjs.com/package/cacheable-lookup)
[](https://packagephobia.now.sh/result?p=cacheable-lookup)
Making lots of HTTP requests? You can save some time by caching DNS lookups :zap:
## Usage
### Using the `lookup` option
```js
const http = require('http');
const CacheableLookup = require('cacheable-lookup');
const cacheable = new CacheableLookup();
http.get('http://example.com', {lookup: cacheable.lookup}, response => {
// Handle the response here
});
```
### Attaching CacheableLookup to an Agent
```js
const http = require('http');
const https = require('https');
const CacheableLookup = require('cacheable-lookup');
const cacheable = new CacheableLookup();
cacheable.install(http.globalAgent);
cacheable.install(https.globalAgent);
http.get('http://example.com', response => {
// Handle the response here
});
```
## API
### new CacheableLookup(options)
Returns a new instance of `CacheableLookup`.
#### options
Type: `object`
Default: `{}`
Options used to cache the DNS lookups.
##### cache
Type: `Map` | [`Keyv`](https://github.com/lukechilds/keyv/)
Default: `new Map()`
Custom cache instance. If `undefined`, it will create a new one.
**Note**: If you decide to use Keyv instead of the native implementation, the performance will drop by 10x. Memory leaks may occur as it doesn't provide any way to remove all the deprecated values at once.
**Tip**: [`QuickLRU`](https://github.com/sindresorhus/quick-lru) is fully compatible with the Map API, you can use it to limit the amount of cached entries. Example:
```js
const http = require('http');
const CacheableLookup = require('cacheable-lookup');
const QuickLRU = require('quick-lru');
const cacheable = new CacheableLookup({
cache: new QuickLRU({maxSize: 1000})
});
http.get('http://example.com', {lookup: cacheable.lookup}, response => {
// Handle the response here
});
```
##### options.maxTtl
Type: `number`
Default: `Infinity`
The maximum lifetime of the entries received from the specifed DNS server (TTL in seconds).
If set to `0`, it will make a new DNS query each time.
**Pro Tip**: This shouldn't be lower than your DNS server response time in order to prevent bottlenecks. For example, if you use Cloudflare, this value should be greater than `0.01`.
##### options.fallbackDuration
Type: `number`
Default: `3600` (1 hour)
When the DNS server responds with `ENOTFOUND` or `ENODATA` and the OS reports that the entry is available, it will use `dns.lookup(...)` directly for the requested hostnames for the specified amount of time (in seconds).
**Note**: You should avoid setting this to `0` unless the provided DNS servers' database is limited to few domains.
##### options.errorTtl
Type: `number`
Default: `0.15`
The time how long it needs to remember queries that threw `ENOTFOUND` or `ENODATA` (TTL in seconds).
**Note**: This option is independent, `options.maxTtl` does not affect this.
**Pro Tip**: This shouldn't be lower than your DNS server response time in order to prevent bottlenecks. For example, if you use Cloudflare, this value should be greater than `0.01`.
##### options.resolver
Type: `dns.Resolver | dns.promises.Resolver`
Default: [`new dns.promises.Resolver()`](https://nodejs.org/api/dns.html#dns_class_dns_resolver)
An instance of [DNS Resolver](https://nodejs.org/api/dns.html#dns_class_dns_resolver) used to make DNS queries.
##### options.lookup
Type: `Function`
Default: [`dns.lookup`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback)
The fallback function to use when the DNS server responds with `ENOTFOUND` or `ENODATA`.
If you don't query internal hostnames (such as `localhost`, `database.local` etc.), it is strongly recommended to set this to `false`.
### Entry object
Type: `object`
#### address
Type: `string`
The IP address (can be an IPv4 or IPv6 address).
#### family
Type: `number`
The IP family (`4` or `6`).
##### expires
Type: `number`
**Note**: This is not present when falling back to `dns.lookup(...)`!
The timestamp (`Date.now() + ttl * 1000`) when the entry expires.
#### ttl
**Note**: This is not present when falling back to `dns.lookup(...)`!
The time in seconds for its lifetime.
### Entry object (callback-style)
When `options.all` is `false`, then `callback(error, address, family, expires, ttl)` is called.
When `options.all` is `true`, then `callback(error, entries)` is called.
### CacheableLookup instance
#### servers
Type: `Array`
The DNS servers used to make queries. Can be overridden - doing so will clear the cache.
#### [lookup(hostname, options, callback)](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback)
#### lookupAsync(hostname, options)
The asynchronous version of `dns.lookup(…)`.
Returns an [entry object](#entry-object).
If `options.all` is true, returns an array of entry objects.
##### hostname
Type: `string`
##### options
Type: `object`
The same as the [`dns.lookup(…)`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) options.
#### query(hostname)
An asynchronous function which returns cached DNS lookup entries.
This is the base for `lookupAsync(hostname, options)` and `lookup(hostname, options, callback)`.
**Note**: This function has no options.
Returns an array of objects with `address`, `family`, `ttl` and `expires` properties.
#### queryAndCache(hostname)
An asynchronous function which makes two DNS queries: A and AAAA. The result is cached.
This is used by `query(hostname)` if no entry in the database is present.
Returns an array of objects with `address`, `family`, `ttl` and `expires` properties.
#### updateInterfaceInfo()
Updates interface info. For example, you need to run this when you plug or unplug your WiFi driver.
**Note:** Running `updateInterfaceInfo()` will trigger `clear()` only on network interface removal.
#### clear(hostname?)
Clears the cache for the given hostname. If the hostname argument is not present, the entire cache will be emptied.
## High performance
Performed on:
- Query: `example.com`
- CPU: i7-7700k
- CPU governor: performance
```
CacheableLookup#lookupAsync x 2,896,251 ops/sec ±1.07% (85 runs sampled)
CacheableLookup#lookupAsync.all x 2,842,664 ops/sec ±1.11% (88 runs sampled)
CacheableLookup#lookupAsync.all.ADDRCONFIG x 2,598,283 ops/sec ±1.21% (88 runs sampled)
CacheableLookup#lookup x 2,565,913 ops/sec ±1.56% (85 runs sampled)
CacheableLookup#lookup.all x 2,609,039 ops/sec ±1.01% (86 runs sampled)
CacheableLookup#lookup.all.ADDRCONFIG x 2,416,242 ops/sec ±0.89% (85 runs sampled)
dns#lookup x 7,272 ops/sec ±0.36% (86 runs sampled)
dns#lookup.all x 7,249 ops/sec ±0.40% (86 runs sampled)
dns#lookup.all.ADDRCONFIG x 5,693 ops/sec ±0.28% (85 runs sampled)
Fastest is CacheableLookup#lookupAsync.all
```
## Related
- [cacheable-request](https://github.com/lukechilds/cacheable-request) - Wrap native HTTP requests with RFC compliant cache support
## License
MIT
cacheable-lookup-6.0.4/benchmark.js 0000664 0000000 0000000 00000003734 14133234510 0017212 0 ustar 00root root 0000000 0000000 'use strict';
const dns = require('dns');
const Benchmark = require('benchmark');
const CacheableLookup = require('.');
const cacheable = new CacheableLookup();
const suite = new Benchmark.Suite();
const options = {
defer: true
};
const lookupOptions = {
all: true
};
const lookupOptionsADDRCONFIG = {
...lookupOptions,
hints: dns.ADDRCONFIG
};
const query = 'example.com';
suite.add('CacheableLookup#lookupAsync', deferred => {
// eslint-disable-next-line promise/prefer-await-to-then
cacheable.lookupAsync(query).then(() => deferred.resolve());
}, options).add('CacheableLookup#lookupAsync.all', deferred => {
// eslint-disable-next-line promise/prefer-await-to-then
cacheable.lookupAsync(query, lookupOptions).then(() => deferred.resolve());
}, options).add('CacheableLookup#lookupAsync.all.ADDRCONFIG', deferred => {
// eslint-disable-next-line promise/prefer-await-to-then
cacheable.lookupAsync(query, lookupOptionsADDRCONFIG).then(() => deferred.resolve());
}, options).add('CacheableLookup#lookup', deferred => {
cacheable.lookup(query, lookupOptions, () => deferred.resolve());
}, options).add('CacheableLookup#lookup.all', deferred => {
cacheable.lookup(query, lookupOptions, () => deferred.resolve());
}, options).add('CacheableLookup#lookup.all.ADDRCONFIG', deferred => {
cacheable.lookup(query, lookupOptionsADDRCONFIG, () => deferred.resolve());
}, options).add('dns#lookup', deferred => {
dns.lookup(query, () => deferred.resolve());
}, options).add('dns#lookup.all', deferred => {
dns.lookup(query, lookupOptions, () => deferred.resolve());
}, options).add('dns#lookup.all.ADDRCONFIG', deferred => {
dns.lookup(query, lookupOptionsADDRCONFIG, () => deferred.resolve());
}, options).on('cycle', event => {
console.log(String(event.target));
}).on('complete', function () {
console.log(`Fastest is ${this.filter('fastest').map('name')}`);
});
(async () => {
await cacheable.lookupAsync(query);
await new Promise(resolve => setTimeout(resolve, 150));
suite.run();
})();
cacheable-lookup-6.0.4/index.d.ts 0000664 0000000 0000000 00000011516 14133234510 0016620 0 ustar 00root root 0000000 0000000 import {Resolver, promises as dnsPromises, lookup} from 'dns';
import {Agent} from 'http';
type AsyncResolver = dnsPromises.Resolver;
export type IPFamily = 4 | 6;
type TPromise = T | Promise;
export interface CacheInstance {
set(hostname: string, entries: EntryObject[], ttl: number): TPromise;
get(hostname: string): TPromise;
delete(hostname: string): TPromise;
clear(): TPromise;
}
export interface Options {
/**
* Custom cache instance. If `undefined`, it will create a new one.
* @default undefined
*/
cache?: CacheInstance;
/**
* Limits the cache time (TTL). If set to `0`, it will make a new DNS query each time.
* @default Infinity
*/
maxTtl?: number;
/**
* DNS Resolver used to make DNS queries.
* @default new dns.promises.Resolver()
*/
resolver?: Resolver | AsyncResolver;
/**
* When the DNS server responds with `ENOTFOUND` or `ENODATA` and the OS reports that the entry is available,
* it will use `dns.lookup(...)` directly for the requested hostnames for the specified amount of time (in seconds).
*
* If you don't query internal hostnames (such as `localhost`, `database.local` etc.),
* it is strongly recommended to set this value to `0`.
* @default 3600
*/
fallbackDuration?: number;
/**
* The time how long it needs to remember failed queries (TTL in seconds).
*
* **Note**: This option is independent, `options.maxTtl` does not affect this.
* @default 0.15
*/
errorTtl?: number;
/**
* The fallback function to use when the DNS server responds with `ENOTFOUND` or `ENODATA`.
*
* **Note**: This has no effect if the `fallbackDuration` option is less than `1`.
* @default dns.lookup
*/
lookup?: typeof lookup | false;
}
export interface EntryObject {
/**
* The IP address (can be an IPv4 or IPv5 address).
*/
readonly address: string;
/**
* The IP family.
*/
readonly family: IPFamily;
/**
* The original TTL.
*/
readonly ttl?: number;
/**
* The expiration timestamp.
*/
readonly expires?: number;
}
export interface LookupOptions {
/**
* One or more supported getaddrinfo flags. Multiple flags may be passed by bitwise ORing their values.
*/
hints?: number;
/**
* The record family. Must be `4` or `6`. IPv4 and IPv6 addresses are both returned by default.
*/
family?: IPFamily;
/**
* When `true`, the callback returns all resolved addresses in an array. Otherwise, returns a single address.
* @default false
*/
all?: boolean;
}
export default class CacheableLookup {
constructor(options?: Options);
/**
* The DNS servers used to make queries. Can be overridden - doing so will clear the cache.
*/
servers: string[];
/**
* @see https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback
*/
lookup(hostname: string, family: IPFamily, callback: (error: NodeJS.ErrnoException | null, address: string, family: IPFamily) => void): void;
lookup(hostname: string, callback: (error: NodeJS.ErrnoException | null, address: string, family: IPFamily) => void): void;
lookup(hostname: string, options: LookupOptions & {all: true}, callback: (error: NodeJS.ErrnoException | null, result: ReadonlyArray) => void): void;
lookup(hostname: string, options: LookupOptions, callback: (error: NodeJS.ErrnoException | null, address: string, family: IPFamily) => void): void;
/**
* The asynchronous version of `dns.lookup(…)`.
*/
lookupAsync(hostname: string, options: LookupOptions & {all: true}): Promise>;
lookupAsync(hostname: string, options: LookupOptions): Promise;
lookupAsync(hostname: string): Promise;
lookupAsync(hostname: string, family: IPFamily): Promise;
/**
* An asynchronous function which returns cached DNS lookup entries. This is the base for `lookupAsync(hostname, options)` and `lookup(hostname, options, callback)`.
*/
query(hostname: string): Promise>;
/**
* An asynchronous function which makes a new DNS lookup query and updates the database. This is used by `query(hostname, family)` if no entry in the database is present. Returns an array of objects with `address`, `family`, `ttl` and `expires` properties.
*/
queryAndCache(hostname: string): Promise>;
/**
* Attaches itself to an Agent instance.
*/
install(agent: Agent): void;
/**
* Removes itself from an Agent instance.
*/
uninstall(agent: Agent): void;
/**
* Updates interface info. For example, you need to run this when you plug or unplug your WiFi driver.
*
* **Note:** Running `updateInterfaceInfo()` will trigger `clear()` only on network interface removal.
*/
updateInterfaceInfo(): void;
/**
* Clears the cache for the given hostname. If the hostname argument is not present, the entire cache will be emptied.
*/
clear(hostname?: string): void;
}
cacheable-lookup-6.0.4/index.test-d.ts 0000664 0000000 0000000 00000003720 14133234510 0017573 0 ustar 00root root 0000000 0000000 import {Resolver, lookup} from 'dns';
import {Agent} from 'https';
import {expectType} from 'tsd';
import Keyv = require('keyv');
import QuickLRU = require('quick-lru');
import CacheableLookup, {EntryObject} from '.';
(async () => {
const cacheable = new CacheableLookup();
const agent = new Agent();
new CacheableLookup({
cache: new Keyv(),
fallbackDuration: 0,
errorTtl: 0,
maxTtl: 0,
resolver: new Resolver()
});
new CacheableLookup({
cache: new QuickLRU({maxSize: 100}),
fallbackDuration: 0,
errorTtl: 0,
maxTtl: 0,
resolver: new Resolver(),
lookup
});
new CacheableLookup({
lookup: false
});
expectType(cacheable.servers);
expectType(await cacheable.lookupAsync('localhost', 4));
expectType(await cacheable.lookupAsync('localhost', {all: false}));
expectType>(await cacheable.lookupAsync('localhost', {all: true}));
cacheable.lookup('localhost', 6, (error, address, family) => {
expectType(error);
expectType(address);
expectType<4 | 6>(family);
});
cacheable.lookup('localhost', {all: false}, (error, address, family) => {
expectType(error);
expectType(address);
expectType<4 | 6>(family);
});
cacheable.lookup('localhost', {all: true}, (error, results) => {
expectType(error);
expectType>(results);
});
// @types/node has invalid typings :(
// expectType(lookup);
expectType>(await cacheable.query('localhost'));
expectType>(await cacheable.queryAndCache('localhost'));
expectType(cacheable.updateInterfaceInfo());
expectType(cacheable.install(agent));
expectType(cacheable.uninstall(agent));
expectType(cacheable.clear('localhost'));
expectType(cacheable.clear());
cacheable.servers = ['127.0.0.1'];
})();
cacheable-lookup-6.0.4/package.json 0000664 0000000 0000000 00000001770 14133234510 0017206 0 ustar 00root root 0000000 0000000 {
"name": "cacheable-lookup",
"version": "6.0.4",
"description": "A cacheable dns.lookup(…) that respects TTL",
"engines": {
"node": ">=10.6.0"
},
"files": [
"source",
"index.d.ts"
],
"main": "source/index.js",
"types": "index.d.ts",
"scripts": {
"test": "xo && nyc --reporter=lcovonly --reporter=text ava && tsd"
},
"repository": {
"type": "git",
"url": "git+https://github.com/szmarczak/cacheable-lookup.git"
},
"keywords": [
"dns",
"lookup",
"cacheable",
"ttl"
],
"author": "Szymon Marczak",
"license": "MIT",
"bugs": {
"url": "https://github.com/szmarczak/cacheable-lookup/issues"
},
"homepage": "https://github.com/szmarczak/cacheable-lookup#readme",
"devDependencies": {
"@types/keyv": "^3.1.1",
"ava": "^3.8.2",
"benchmark": "^2.1.4",
"coveralls": "^3.0.9",
"keyv": "^4.0.0",
"nyc": "^15.0.0",
"proxyquire": "^2.1.3",
"tsd": "^0.11.0",
"quick-lru": "^5.1.0",
"xo": "^0.25.3"
}
}
cacheable-lookup-6.0.4/source/ 0000775 0000000 0000000 00000000000 14133234510 0016213 5 ustar 00root root 0000000 0000000 cacheable-lookup-6.0.4/source/index.js 0000664 0000000 0000000 00000022774 14133234510 0017674 0 ustar 00root root 0000000 0000000 'use strict';
const {
V4MAPPED,
ADDRCONFIG,
ALL,
promises: {
Resolver: AsyncResolver
},
lookup: dnsLookup
} = require('dns');
const {promisify} = require('util');
const os = require('os');
const kCacheableLookupCreateConnection = Symbol('cacheableLookupCreateConnection');
const kCacheableLookupInstance = Symbol('cacheableLookupInstance');
const kExpires = Symbol('expires');
const supportsALL = typeof ALL === 'number';
const verifyAgent = agent => {
if (!(agent && typeof agent.createConnection === 'function')) {
throw new Error('Expected an Agent instance as the first argument');
}
};
const map4to6 = entries => {
for (const entry of entries) {
if (entry.family === 6) {
continue;
}
entry.address = `::ffff:${entry.address}`;
entry.family = 6;
}
};
const getIfaceInfo = () => {
let has4 = false;
let has6 = false;
for (const device of Object.values(os.networkInterfaces())) {
for (const iface of device) {
if (iface.internal) {
continue;
}
if (iface.family === 'IPv6') {
has6 = true;
} else {
has4 = true;
}
if (has4 && has6) {
return {has4, has6};
}
}
}
return {has4, has6};
};
const isIterable = map => {
return Symbol.iterator in map;
};
const ignoreNoResultErrors = dnsPromise => {
return dnsPromise.catch(error => {
if (
error.code === 'ENODATA' ||
error.code === 'ENOTFOUND' ||
error.code === 'ENOENT' // Windows: name exists, but not this record type
) {
return [];
}
throw error;
});
};
const ttl = {ttl: true};
const all = {all: true};
const all4 = {all: true, family: 4};
const all6 = {all: true, family: 6};
class CacheableLookup {
constructor({
cache = new Map(),
maxTtl = Infinity,
fallbackDuration = 3600,
errorTtl = 0.15,
resolver = new AsyncResolver(),
lookup = dnsLookup
} = {}) {
this.maxTtl = maxTtl;
this.errorTtl = errorTtl;
this._cache = cache;
this._resolver = resolver;
this._dnsLookup = lookup && promisify(lookup);
if (this._resolver instanceof AsyncResolver) {
this._resolve4 = this._resolver.resolve4.bind(this._resolver);
this._resolve6 = this._resolver.resolve6.bind(this._resolver);
} else {
this._resolve4 = promisify(this._resolver.resolve4.bind(this._resolver));
this._resolve6 = promisify(this._resolver.resolve6.bind(this._resolver));
}
this._iface = getIfaceInfo();
this._pending = {};
this._nextRemovalTime = false;
this._hostnamesToFallback = new Set();
this.fallbackDuration = fallbackDuration;
if (fallbackDuration > 0) {
const interval = setInterval(() => {
this._hostnamesToFallback.clear();
}, fallbackDuration * 1000);
/* istanbul ignore next: There is no `interval.unref()` when running inside an Electron renderer */
if (interval.unref) {
interval.unref();
}
this._fallbackInterval = interval;
}
this.lookup = this.lookup.bind(this);
this.lookupAsync = this.lookupAsync.bind(this);
}
set servers(servers) {
this.clear();
this._resolver.setServers(servers);
}
get servers() {
return this._resolver.getServers();
}
lookup(hostname, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
} else if (typeof options === 'number') {
options = {
family: options
};
}
if (!callback) {
throw new Error('Callback must be a function.');
}
// eslint-disable-next-line promise/prefer-await-to-then
this.lookupAsync(hostname, options).then(result => {
if (options.all) {
callback(null, result);
} else {
callback(null, result.address, result.family, result.expires, result.ttl);
}
}, callback);
}
async lookupAsync(hostname, options = {}) {
if (typeof options === 'number') {
options = {
family: options
};
}
let cached = await this.query(hostname);
if (options.family === 6) {
const filtered = cached.filter(entry => entry.family === 6);
if (options.hints & V4MAPPED) {
if ((supportsALL && options.hints & ALL) || filtered.length === 0) {
map4to6(cached);
} else {
cached = filtered;
}
} else {
cached = filtered;
}
} else if (options.family === 4) {
cached = cached.filter(entry => entry.family === 4);
}
if (options.hints & ADDRCONFIG) {
const {_iface} = this;
cached = cached.filter(entry => entry.family === 6 ? _iface.has6 : _iface.has4);
}
if (cached.length === 0) {
const error = new Error(`cacheableLookup ENOTFOUND ${hostname}`);
error.code = 'ENOTFOUND';
error.hostname = hostname;
throw error;
}
if (options.all) {
return cached;
}
return cached[0];
}
async query(hostname) {
let cached = await this._cache.get(hostname);
if (!cached) {
const pending = this._pending[hostname];
if (pending) {
cached = await pending;
} else {
const newPromise = this.queryAndCache(hostname);
this._pending[hostname] = newPromise;
try {
cached = await newPromise;
} finally {
delete this._pending[hostname];
}
}
}
cached = cached.map(entry => {
return {...entry};
});
return cached;
}
async _resolve(hostname) {
// ANY is unsafe as it doesn't trigger new queries in the underlying server.
const [A, AAAA] = await Promise.all([
ignoreNoResultErrors(this._resolve4(hostname, ttl)),
ignoreNoResultErrors(this._resolve6(hostname, ttl))
]);
let aTtl = 0;
let aaaaTtl = 0;
let cacheTtl = 0;
const now = Date.now();
for (const entry of A) {
entry.family = 4;
entry.expires = now + (entry.ttl * 1000);
aTtl = Math.max(aTtl, entry.ttl);
}
for (const entry of AAAA) {
entry.family = 6;
entry.expires = now + (entry.ttl * 1000);
aaaaTtl = Math.max(aaaaTtl, entry.ttl);
}
if (A.length > 0) {
if (AAAA.length > 0) {
cacheTtl = Math.min(aTtl, aaaaTtl);
} else {
cacheTtl = aTtl;
}
} else {
cacheTtl = aaaaTtl;
}
return {
entries: [
...A,
...AAAA
],
cacheTtl
};
}
async _lookup(hostname) {
try {
const [A, AAAA] = await Promise.all([
// Passing {all: true} doesn't return all IPv4 and IPv6 entries.
// See https://github.com/szmarczak/cacheable-lookup/issues/42
ignoreNoResultErrors(this._dnsLookup(hostname, all4)),
ignoreNoResultErrors(this._dnsLookup(hostname, all6))
]);
return {
entries: [
...A,
...AAAA
],
cacheTtl: 0
};
} catch {
return {
entries: [],
cacheTtl: 0
};
}
}
async _set(hostname, data, cacheTtl) {
if (this.maxTtl > 0 && cacheTtl > 0) {
cacheTtl = Math.min(cacheTtl, this.maxTtl) * 1000;
data[kExpires] = Date.now() + cacheTtl;
try {
await this._cache.set(hostname, data, cacheTtl);
} catch (error) {
this.lookupAsync = async () => {
const cacheError = new Error('Cache Error. Please recreate the CacheableLookup instance.');
cacheError.cause = error;
throw cacheError;
};
}
if (isIterable(this._cache)) {
this._tick(cacheTtl);
}
}
}
async queryAndCache(hostname) {
if (this._hostnamesToFallback.has(hostname)) {
return this._dnsLookup(hostname, all);
}
let query = await this._resolve(hostname);
if (query.entries.length === 0 && this._dnsLookup) {
query = await this._lookup(hostname);
if (query.entries.length !== 0 && this.fallbackDuration > 0) {
// Use `dns.lookup(...)` for that particular hostname
this._hostnamesToFallback.add(hostname);
}
}
const cacheTtl = query.entries.length === 0 ? this.errorTtl : query.cacheTtl;
await this._set(hostname, query.entries, cacheTtl);
return query.entries;
}
_tick(ms) {
const nextRemovalTime = this._nextRemovalTime;
if (!nextRemovalTime || ms < nextRemovalTime) {
clearTimeout(this._removalTimeout);
this._nextRemovalTime = ms;
this._removalTimeout = setTimeout(() => {
this._nextRemovalTime = false;
let nextExpiry = Infinity;
const now = Date.now();
for (const [hostname, entries] of this._cache) {
const expires = entries[kExpires];
if (now >= expires) {
this._cache.delete(hostname);
} else if (expires < nextExpiry) {
nextExpiry = expires;
}
}
if (nextExpiry !== Infinity) {
this._tick(nextExpiry - now);
}
}, ms);
/* istanbul ignore next: There is no `timeout.unref()` when running inside an Electron renderer */
if (this._removalTimeout.unref) {
this._removalTimeout.unref();
}
}
}
install(agent) {
verifyAgent(agent);
if (kCacheableLookupCreateConnection in agent) {
throw new Error('CacheableLookup has been already installed');
}
agent[kCacheableLookupCreateConnection] = agent.createConnection;
agent[kCacheableLookupInstance] = this;
agent.createConnection = (options, callback) => {
if (!('lookup' in options)) {
options.lookup = this.lookup;
}
return agent[kCacheableLookupCreateConnection](options, callback);
};
}
uninstall(agent) {
verifyAgent(agent);
if (agent[kCacheableLookupCreateConnection]) {
if (agent[kCacheableLookupInstance] !== this) {
throw new Error('The agent is not owned by this CacheableLookup instance');
}
agent.createConnection = agent[kCacheableLookupCreateConnection];
delete agent[kCacheableLookupCreateConnection];
delete agent[kCacheableLookupInstance];
}
}
updateInterfaceInfo() {
const {_iface} = this;
this._iface = getIfaceInfo();
if ((_iface.has4 && !this._iface.has4) || (_iface.has6 && !this._iface.has6)) {
this._cache.clear();
}
}
clear(hostname) {
if (hostname) {
this._cache.delete(hostname);
return;
}
this._cache.clear();
}
}
module.exports = CacheableLookup;
module.exports.default = CacheableLookup;
cacheable-lookup-6.0.4/tests/ 0000775 0000000 0000000 00000000000 14133234510 0016055 5 ustar 00root root 0000000 0000000 cacheable-lookup-6.0.4/tests/test.js 0000664 0000000 0000000 00000061764 14133234510 0017410 0 ustar 00root root 0000000 0000000 const {V4MAPPED, ADDRCONFIG, ALL} = require('dns');
const {Resolver: AsyncResolver} = require('dns').promises;
const {promisify} = require('util');
const http = require('http');
const test = require('ava');
const Keyv = require('keyv');
const proxyquire = require('proxyquire');
const QuickLRU = require('quick-lru');
const makeRequest = options => new Promise((resolve, reject) => {
http.get(options, resolve).once('error', reject);
});
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const mockedInterfaces = options => {
const createInterfaces = (options = {}) => {
const interfaces = {
lo: [
{
internal: true
}
],
eth0: []
};
if (options.has4) {
interfaces.eth0.push({
address: '192.168.0.111',
netmask: '255.255.255.0',
family: 'IPv4',
mac: '00:00:00:00:00:00',
internal: false,
cidr: '192.168.0.111/24'
});
}
if (options.has6) {
interfaces.eth0.push({
address: 'fe80::c962:2946:a4e2:9f05',
netmask: 'ffff:ffff:ffff:ffff::',
family: 'IPv6',
mac: '00:00:00:00:00:00',
scopeid: 8,
internal: false,
cidr: 'fe80::c962:2946:a4e2:9f05/64'
});
}
return interfaces;
};
let interfaces = createInterfaces(options);
const _updateInterfaces = (options = {}) => {
interfaces = createInterfaces(options);
};
const result = proxyquire('../source', {
os: {
networkInterfaces: () => interfaces
}
});
result._updateInterfaces = _updateInterfaces;
return result;
};
const createResolver = () => {
let counter = {
4: 0,
6: 0,
lookup: 0
};
const resolver = {
servers: ['127.0.0.1'],
getServers() {
return [...resolver.servers];
},
setServers(servers) {
resolver.servers = [...servers];
},
resolve: (hostname, options, callback) => {
let data;
for (const server of resolver.servers) {
if (resolver.data[server][hostname]) {
data = resolver.data[server][hostname];
break;
}
}
if (hostname === 'econnrefused') {
const error = new Error(`ECONNREFUSED ${hostname}`);
error.code = 'ECONNREFUSED';
callback(error);
return;
}
if (!data) {
const error = new Error(`ENOTFOUND ${hostname}`);
error.code = 'ENOTFOUND';
callback(error);
return;
}
if (data.length === 0) {
const error = new Error(`ENODATA ${hostname}`);
error.code = 'ENODATA';
callback(error);
return;
}
if (options.family === 4 || options.family === 6) {
data = data.filter(entry => entry.family === options.family);
}
callback(null, JSON.parse(JSON.stringify(data)));
},
resolve4: (hostname, options, callback) => {
counter[4]++;
return resolver.resolve(hostname, {...options, family: 4}, callback);
},
resolve6: (hostname, options, callback) => {
counter[6]++;
return resolver.resolve(hostname, {...options, family: 6}, callback);
},
lookup: (hostname, options, callback) => {
// No need to implement hints yet
counter.lookup++;
if (!resolver.lookupData[hostname]) {
const error = new Error(`ENOTFOUND ${hostname}`);
error.code = 'ENOTFOUND';
error.hostname = hostname;
callback(error);
return;
}
let entries = resolver.lookupData[hostname];
if (options.family === 4 || options.family === 6) {
entries = entries.filter(entry => entry.family === options.family);
}
if (options.all) {
callback(null, entries);
return;
}
callback(null, entries[0]);
},
data: {
'127.0.0.1': {
localhost: [
{address: '127.0.0.1', family: 4, ttl: 60},
{address: '::ffff:127.0.0.2', family: 6, ttl: 60}
],
example: [
{address: '127.0.0.127', family: 4, ttl: 60}
],
temporary: [
{address: '127.0.0.1', family: 4, ttl: 1}
],
twoSeconds: [
{address: '127.0.0.1', family: 4, ttl: 2}
],
ttl: [
{address: '127.0.0.1', family: 4, ttl: 1}
],
maxTtl: [
{address: '127.0.0.1', family: 4, ttl: 60}
],
static4: [
{address: '127.0.0.1', family: 4, ttl: 1}
],
zeroTtl: [
{address: '127.0.0.127', family: 4, ttl: 0}
],
multiple: [
{address: '127.0.0.127', family: 4, ttl: 0},
{address: '127.0.0.128', family: 4, ttl: 0}
],
outdated: [
{address: '127.0.0.1', family: 4, ttl: 1}
]
},
'192.168.0.100': {
unique: [
{address: '127.0.0.1', family: 4, ttl: 60}
]
}
},
lookupData: {
osHostname: [
{address: '127.0.0.1', family: 4},
{address: '127.0.0.2', family: 4}
],
outdated: [
{address: '127.0.0.127', family: 4}
]
},
get counter() {
return counter;
},
resetCounter() {
counter = {
4: 0,
6: 0,
lookup: 0
};
}
};
return resolver;
};
const resolver = createResolver();
const CacheableLookup = proxyquire('../source', {
dns: {
lookup: resolver.lookup
}
});
const verify = (t, entry, value) => {
if (Array.isArray(value)) {
// eslint-disable-next-line guard-for-in
for (const key in value) {
t.true(typeof entry[key].expires === 'number' && entry[key].expires >= Date.now() - 1000);
t.true(typeof entry[key].ttl === 'number' && entry[key].ttl >= 0);
if (!('ttl' in value[key]) && 'ttl' in entry[key]) {
value[key].ttl = entry[key].ttl;
}
if (!('expires' in value[key]) && 'expires' in entry[key]) {
value[key].expires = entry[key].expires;
}
}
} else {
t.true(typeof entry.expires === 'number' && entry.expires >= Date.now() - 1000);
t.true(typeof entry.ttl === 'number' && entry.ttl >= 0);
if (!('ttl' in value)) {
value.ttl = entry.ttl;
}
if (!('expires' in value)) {
value.expires = entry.expires;
}
}
t.deepEqual(entry, value);
};
test('options.family', async t => {
const cacheable = new CacheableLookup({resolver});
// IPv4
let entry = await cacheable.lookupAsync('localhost', {family: 4});
verify(t, entry, {
address: '127.0.0.1',
family: 4
});
// IPv6
entry = await cacheable.lookupAsync('localhost', {family: 6});
verify(t, entry, {
address: '::ffff:127.0.0.2',
family: 6
});
});
test('options.all', async t => {
const cacheable = new CacheableLookup({resolver});
const entries = await cacheable.lookupAsync('localhost', {all: true});
verify(t, entries, [
{address: '127.0.0.1', family: 4},
{address: '::ffff:127.0.0.2', family: 6}
]);
});
test('options.all mixed with options.family', async t => {
const cacheable = new CacheableLookup({resolver});
// IPv4
let entries = await cacheable.lookupAsync('localhost', {all: true, family: 4});
verify(t, entries, [
{address: '127.0.0.1', family: 4}
]);
// IPv6
entries = await cacheable.lookupAsync('localhost', {all: true, family: 6});
verify(t, entries, [
{address: '::ffff:127.0.0.2', family: 6}
]);
});
test('V4MAPPED hint', async t => {
const cacheable = new CacheableLookup({resolver});
// Make sure default behavior is right
await t.throwsAsync(cacheable.lookupAsync('static4', {family: 6}), {code: 'ENOTFOUND'});
// V4MAPPED
{
const entries = await cacheable.lookupAsync('static4', {family: 6, hints: V4MAPPED});
verify(t, entries, {address: '::ffff:127.0.0.1', family: 6});
}
{
const entries = await cacheable.lookupAsync('localhost', {family: 6, hints: V4MAPPED});
verify(t, entries, {address: '::ffff:127.0.0.2', family: 6});
}
});
if (process.versions.node.split('.')[0] >= 14) {
test('ALL hint', async t => {
const cacheable = new CacheableLookup({resolver});
// ALL
const entries = await cacheable.lookupAsync('localhost', {family: 6, hints: V4MAPPED | ALL, all: true});
verify(t, entries, [
{address: '::ffff:127.0.0.1', family: 6, ttl: 60},
{address: '::ffff:127.0.0.2', family: 6, ttl: 60}
]);
});
}
test('ADDRCONFIG hint', async t => {
//=> has6 = false, family = 6
{
const CacheableLookup = mockedInterfaces({has4: true, has6: false});
const cacheable = new CacheableLookup({resolver});
await t.throwsAsync(cacheable.lookupAsync('localhost', {family: 6, hints: ADDRCONFIG}), {code: 'ENOTFOUND'});
}
//=> has6 = true, family = 6
{
const CacheableLookup = mockedInterfaces({has4: true, has6: true});
const cacheable = new CacheableLookup({resolver});
verify(t, await cacheable.lookupAsync('localhost', {family: 6, hints: ADDRCONFIG}), {
address: '::ffff:127.0.0.2',
family: 6
});
}
//=> has4 = false, family = 4
{
const CacheableLookup = mockedInterfaces({has4: false, has6: true});
const cacheable = new CacheableLookup({resolver});
await t.throwsAsync(cacheable.lookupAsync('localhost', {family: 4, hints: ADDRCONFIG}), {code: 'ENOTFOUND'});
}
//=> has4 = true, family = 4
{
const CacheableLookup = mockedInterfaces({has4: true, has6: true});
const cacheable = new CacheableLookup({resolver});
verify(t, await cacheable.lookupAsync('localhost', {family: 4, hints: ADDRCONFIG}), {
address: '127.0.0.1',
family: 4
});
}
// Update interface info
{
const CacheableLookup = mockedInterfaces({has4: false, has6: true});
const cacheable = new CacheableLookup({resolver});
await t.throwsAsync(cacheable.lookupAsync('localhost', {family: 4, hints: ADDRCONFIG}), {code: 'ENOTFOUND'});
//=> has4 = true, family = 4
CacheableLookup._updateInterfaces({has4: true, has6: true}); // Override os.networkInterfaces()
cacheable.updateInterfaceInfo();
verify(t, await cacheable.lookupAsync('localhost', {family: 4, hints: ADDRCONFIG}), {
address: '127.0.0.1',
family: 4
});
}
});
test.serial('caching works', async t => {
const cacheable = new CacheableLookup({resolver});
// Make sure default behavior is right
let entries = await cacheable.lookupAsync('temporary', {all: true, family: 4});
verify(t, entries, [
{address: '127.0.0.1', family: 4}
]);
// Update DNS data
const resovlerEntry = resolver.data['127.0.0.1'].temporary[0];
const {address: resolverAddress} = resovlerEntry;
resovlerEntry.address = '127.0.0.2';
// Lookup again
entries = await cacheable.lookupAsync('temporary', {all: true, family: 4});
verify(t, entries, [
{address: '127.0.0.1', family: 4}
]);
// Restore back
resovlerEntry.address = resolverAddress;
});
test('respects ttl', async t => {
const cacheable = new CacheableLookup({resolver});
// Make sure default behavior is right
let entries = await cacheable.lookupAsync('ttl', {all: true, family: 4});
verify(t, entries, [
{address: '127.0.0.1', family: 4}
]);
// Update DNS data
const resolverEntry = resolver.data['127.0.0.1'].ttl[0];
const {address: resolverAddress} = resolverEntry;
resolverEntry.address = '127.0.0.2';
// Wait until it expires
await sleep((resolverEntry.ttl * 1000) + 1);
// Lookup again
entries = await cacheable.lookupAsync('ttl', {all: true, family: 4});
verify(t, entries, [
{address: '127.0.0.2', family: 4}
]);
// Restore back
resolverEntry.address = resolverAddress;
});
test('throw when there are entries available but not for the requested family', async t => {
const cacheable = new CacheableLookup({resolver});
await t.throwsAsync(cacheable.lookupAsync('static4', {family: 6}), {code: 'ENOTFOUND'});
});
test('custom servers', async t => {
const cacheable = new CacheableLookup({resolver: createResolver()});
// .servers (get)
t.deepEqual(cacheable.servers, ['127.0.0.1']);
await t.throwsAsync(cacheable.lookupAsync('unique'), {code: 'ENOTFOUND'});
// .servers (set)
cacheable.servers = ['127.0.0.1', '192.168.0.100'];
verify(t, await cacheable.lookupAsync('unique'), {
address: '127.0.0.1',
family: 4
});
// Verify
t.deepEqual(cacheable.servers, ['127.0.0.1', '192.168.0.100']);
});
test('callback style', async t => {
const cacheable = new CacheableLookup({resolver});
// Custom promise for this particular test
const lookup = (...args) => new Promise((resolve, reject) => {
cacheable.lookup(...args, (error, ...data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
// Without options
let result = await lookup('localhost');
t.is(result.length, 4);
t.is(result[0], '127.0.0.1');
t.is(result[1], 4);
t.true(typeof result[2] === 'number' && result[2] >= Date.now() - 1000);
t.true(typeof result[3] === 'number' && result[3] >= 0);
// With options
result = await lookup('localhost', {family: 6, all: true});
t.is(result.length, 1);
verify(t, result[0], [{address: '::ffff:127.0.0.2', family: 6}]);
});
test('works', async t => {
const cacheable = new CacheableLookup({resolver});
verify(t, await cacheable.lookupAsync('localhost'), {
address: '127.0.0.1',
family: 4
});
});
test('options.maxTtl', async t => {
//=> maxTtl = 1
{
const cacheable = new CacheableLookup({resolver, maxTtl: 1});
// Make sure default behavior is right
verify(t, await cacheable.lookupAsync('maxTtl'), {
address: '127.0.0.1',
family: 4
});
// Update DNS data
const resolverEntry = resolver.data['127.0.0.1'].maxTtl[0];
resolverEntry.address = '127.0.0.2';
// Wait until it expires
await sleep((cacheable.maxTtl * 1000) + 1);
// Lookup again
verify(t, await cacheable.lookupAsync('maxTtl'), {
address: '127.0.0.2',
family: 4
});
// Reset
resolverEntry.address = '127.0.0.1';
}
//=> maxTtl = 0
{
const cacheable = new CacheableLookup({resolver, maxTtl: 0});
// Make sure default behavior is right
verify(t, await cacheable.lookupAsync('maxTtl'), {
address: '127.0.0.1',
family: 4
});
// Update DNS data
const resolverEntry = resolver.data['127.0.0.1'].maxTtl[0];
resolverEntry.address = '127.0.0.2';
// Wait until it expires
await sleep((cacheable.maxTtl * 1000) + 1);
// Lookup again
verify(t, await cacheable.lookupAsync('maxTtl'), {
address: '127.0.0.2',
family: 4
});
// Reset
resolverEntry.address = '127.0.0.1';
}
});
test('entry with 0 ttl', async t => {
const cacheable = new CacheableLookup({resolver});
// Make sure default behavior is right
verify(t, await cacheable.lookupAsync('zeroTtl'), {
address: '127.0.0.127',
family: 4
});
// Update DNS data
resolver.data['127.0.0.1'].zeroTtl[0].address = '127.0.0.1';
// Lookup again
verify(t, await cacheable.lookupAsync('zeroTtl'), {
address: '127.0.0.1',
family: 4
});
});
test('http example', async t => {
const cacheable = new CacheableLookup({resolver});
const options = {
hostname: 'example',
port: 9999,
lookup: cacheable.lookup
};
await t.throwsAsync(makeRequest(options), {
message: 'connect ECONNREFUSED 127.0.0.127:9999'
});
});
test('.lookup() and .lookupAsync() are automatically bounded', async t => {
const cacheable = new CacheableLookup({resolver});
await t.notThrowsAsync(cacheable.lookupAsync('localhost'));
await t.notThrowsAsync(promisify(cacheable.lookup)('localhost'));
t.throws(() => cacheable.lookup('localhost'), {
message: 'Callback must be a function.'
});
});
test('works (Internet connection)', async t => {
const cacheable = new CacheableLookup();
const {address, family} = await cacheable.lookupAsync('1dot1dot1dot1.cloudflare-dns.com');
t.true(address === '1.1.1.1' || address === '1.0.0.1');
t.is(family, 4);
});
test.serial('install & uninstall', async t => {
const cacheable = new CacheableLookup({resolver});
cacheable.install(http.globalAgent);
const options = {
hostname: 'example',
port: 9999
};
await t.throwsAsync(makeRequest(options), {
message: 'connect ECONNREFUSED 127.0.0.127:9999'
});
cacheable.uninstall(http.globalAgent);
await t.throwsAsync(makeRequest(options), {
message: /^getaddrinfo (?:ENOTFOUND|EAI_AGAIN) example/
});
http.globalAgent.destroy();
});
test('`.install()` throws if no Agent provided', t => {
const cacheable = new CacheableLookup();
t.throws(() => cacheable.install(), {
message: 'Expected an Agent instance as the first argument'
});
t.throws(() => cacheable.install(1), {
message: 'Expected an Agent instance as the first argument'
});
});
test('`.uninstall()` throws if no Agent provided', t => {
const cacheable = new CacheableLookup();
t.throws(() => cacheable.uninstall(), {
message: 'Expected an Agent instance as the first argument'
});
t.throws(() => cacheable.uninstall(1), {
message: 'Expected an Agent instance as the first argument'
});
});
test.serial('`.uninstall()` does not alter unmodified Agents', t => {
const cacheable = new CacheableLookup();
const {createConnection} = http.globalAgent;
cacheable.uninstall(http.globalAgent);
t.is(createConnection, http.globalAgent.createConnection);
});
test.serial('throws if double-installing CacheableLookup', t => {
const cacheable = new CacheableLookup();
cacheable.install(http.globalAgent);
t.throws(() => cacheable.install(http.globalAgent), {
message: 'CacheableLookup has been already installed'
});
cacheable.uninstall(http.globalAgent);
});
test.serial('install - providing custom lookup function anyway', async t => {
const a = new CacheableLookup();
const b = new CacheableLookup({resolver});
a.install(http.globalAgent);
const options = {
hostname: 'example',
port: 9999,
lookup: b.lookup
};
await t.throwsAsync(makeRequest(options), {
message: 'connect ECONNREFUSED 127.0.0.127:9999'
});
a.uninstall(http.globalAgent);
});
test.serial('throws when calling `.uninstall()` on the wrong instance', t => {
const a = new CacheableLookup();
const b = new CacheableLookup({resolver});
a.install(http.globalAgent);
t.throws(() => b.uninstall(http.globalAgent), {
message: 'The agent is not owned by this CacheableLookup instance'
});
a.uninstall(http.globalAgent);
});
test('async resolver (Internet connection)', async t => {
const cacheable = new CacheableLookup({resolver: new AsyncResolver()});
t.is(typeof cacheable._resolve4, 'function');
t.is(typeof cacheable._resolve6, 'function');
const {address} = await cacheable.lookupAsync('1dot1dot1dot1.cloudflare-dns.com');
t.true(address === '1.1.1.1' || address === '1.0.0.1');
});
test('clear() works', async t => {
const cacheable = new CacheableLookup({resolver});
await cacheable.lookupAsync('localhost');
t.is(cacheable._cache.size, 1);
cacheable.clear();
t.is(cacheable._cache.size, 0);
});
test('ttl works', async t => {
const cacheable = new CacheableLookup({resolver});
await Promise.all([cacheable.lookupAsync('temporary'), cacheable.lookupAsync('ttl')]);
t.is(cacheable._cache.size, 2);
await sleep(2001);
t.is(cacheable._cache.size, 0);
});
test('custom cache support', async t => {
const cache = new Keyv();
const cacheable = new CacheableLookup({
resolver,
cache
});
await cacheable.lookupAsync('temporary');
const [entry] = await cache.get('temporary');
t.is(entry.address, '127.0.0.1');
t.is(entry.family, 4);
t.is(entry.ttl, 1);
await sleep(entry.ttl * 1001);
const newEntry = await cache.get('temporary');
t.is(newEntry, undefined);
});
test.serial('fallback works', async t => {
const cacheable = new CacheableLookup({resolver, fallbackDuration: 0.1});
resolver.resetCounter();
const entries = await cacheable.lookupAsync('osHostname', {all: true});
t.is(entries.length, 2);
t.is(entries[0].address, '127.0.0.1');
t.is(entries[0].family, 4);
t.is(entries[1].address, '127.0.0.2');
t.is(entries[1].family, 4);
t.is(cacheable._cache.size, 0);
await cacheable.lookupAsync('osHostname', {all: true});
t.deepEqual(resolver.counter, {
6: 1,
4: 1,
lookup: 3
});
await sleep(100);
t.is(cacheable._hostnamesToFallback.size, 0);
});
test.serial('fallback works if ip change', async t => {
const cacheable = new CacheableLookup({resolver, fallbackDuration: 3600});
resolver.resetCounter();
resolver.lookupData.osHostnameChange = [
{address: '127.0.0.1', family: 4},
{address: '127.0.0.2', family: 4}
];
// First call: do not enter in `if (this._hostnamesToFallback.has(hostname)) {`
const entries = await cacheable.query('osHostnameChange', {all: true});
t.is(entries.length, 2);
t.is(entries[0].address, '127.0.0.1');
t.is(entries[0].family, 4);
t.is(entries[1].address, '127.0.0.2');
t.is(entries[1].family, 4);
t.is(cacheable._cache.size, 0);
// Second call: enter in `if (this._hostnamesToFallback.has(hostname)) {`
// And use _dnsLookup
// This call is used to ensure that this._pending is cleaned up when the promise is resolved
await cacheable.query('osHostnameChange', {all: true});
// Third call: enter in `if (this._hostnamesToFallback.has(hostname)) {`
// And use _dnsLookup
// Address should be different
resolver.lookupData.osHostnameChange = [
{address: '127.0.0.3', family: 4},
{address: '127.0.0.4', family: 4}
];
const entries2 = await cacheable.query('osHostnameChange', {all: true});
t.is(entries2.length, 2);
t.is(entries2[0].address, '127.0.0.3');
t.is(entries2[0].family, 4);
t.is(entries2[1].address, '127.0.0.4');
t.is(entries2[1].family, 4);
t.is(cacheable._cache.size, 0);
delete resolver.lookupData.osHostnameChange;
});
test('real DNS queries first', async t => {
const resolver = createResolver({delay: 0});
const cacheable = new CacheableLookup({
resolver,
fallbackDuration: 3600,
lookup: resolver.lookup
});
{
const entries = await cacheable.lookupAsync('outdated', {all: true});
verify(t, entries, [
{address: '127.0.0.1', family: 4}
]);
}
await new Promise(resolve => setTimeout(resolve, 100));
{
const entries = await cacheable.lookupAsync('outdated', {all: true});
verify(t, entries, [
{address: '127.0.0.1', family: 4}
]);
}
});
test('fallback can be turned off', async t => {
const cacheable = new CacheableLookup({resolver, lookup: false});
await t.throwsAsync(cacheable.lookupAsync('osHostname', {all: true}), {
message: 'cacheableLookup ENOTFOUND osHostname'
});
});
test('errors are cached', async t => {
const cacheable = new CacheableLookup({resolver, errorTtl: 0.1});
await t.throwsAsync(cacheable.lookupAsync('doesNotExist'), {
code: 'ENOTFOUND'
});
t.is(cacheable._cache.size, 1);
await sleep((cacheable.errorTtl * 1000) + 1);
t.is(cacheable._cache.size, 0);
});
test('passing family as options', async t => {
const cacheable = new CacheableLookup({resolver});
const promisified = promisify(cacheable.lookup);
const entry = await cacheable.lookupAsync('localhost', 6);
t.is(entry.address, '::ffff:127.0.0.2');
t.is(entry.family, 6);
const address = await promisified('localhost', 6);
t.is(address, '::ffff:127.0.0.2');
});
test('clear(hostname) works', async t => {
const cacheable = new CacheableLookup({resolver});
await cacheable.lookupAsync('localhost');
await cacheable.lookupAsync('temporary');
cacheable.clear('localhost');
t.is(cacheable._cache.size, 1);
});
test('prevents overloading DNS', async t => {
const resolver = createResolver();
const {lookupAsync} = new CacheableLookup({
resolver,
lookup: resolver.lookup
});
await Promise.all([lookupAsync('localhost'), lookupAsync('localhost')]);
t.deepEqual(resolver.counter, {
4: 1,
6: 1,
lookup: 0
});
});
test('returns IPv6 if no other entries available', async t => {
const CacheableLookup = mockedInterfaces({has4: false, has6: true});
const cacheable = new CacheableLookup({resolver});
verify(t, await cacheable.lookupAsync('localhost', {hints: ADDRCONFIG}), {
address: '::ffff:127.0.0.2',
family: 6
});
});
test('throws when no internet connection', async t => {
const cacheable = new CacheableLookup({resolver});
await t.throwsAsync(cacheable.lookupAsync('econnrefused'), {
code: 'ECONNREFUSED'
});
});
test('full-featured custom cache', async t => {
const cache = new QuickLRU({maxSize: 1});
const cacheable = new CacheableLookup({
resolver,
cache
});
{
const entry = await cacheable.lookupAsync('localhost');
verify(t, entry, {
address: '127.0.0.1',
family: 4
});
}
t.is(cache.size, 1);
{
const entry = await cacheable.lookupAsync('localhost');
verify(t, entry, {
address: '127.0.0.1',
family: 4
});
}
t.is(cache.size, 1);
{
const entry = await cacheable.lookupAsync('temporary');
verify(t, entry, {
address: '127.0.0.1',
family: 4
});
}
t.is(cache.size, 1);
});
test('throws when the cache instance is broken', async t => {
const cacheable = new CacheableLookup({
resolver,
cache: {
get: () => {},
set: () => {
throw new Error('Something broke.');
}
}
});
await t.notThrowsAsync(cacheable.lookupAsync('localhost'));
const error = await t.throwsAsync(cacheable.lookupAsync('localhost'), {
message: 'Cache Error. Please recreate the CacheableLookup instance.'
});
t.is(error.cause.message, 'Something broke.');
});
test('slow dns.lookup', async t => {
const cacheable = new CacheableLookup({
resolver,
lookup: (hostname, options, callback) => {
t.is(hostname, 'osHostname');
t.is(options.all, true);
t.true(options.family === 4 || options.family === 6);
setTimeout(() => {
if (options.family === 4) {
callback(null, [
{address: '127.0.0.1', family: 4}
]);
}
if (options.family === 6) {
callback(null, [
{address: '::1', family: 6}
]);
}
}, 10);
}
});
const entry = await cacheable.lookupAsync('osHostname');
t.deepEqual(entry, {
address: '127.0.0.1',
family: 4
});
});
test.cb.failing('throws original lookup error if not recognized', t => {
const cl = new CacheableLookup({
lookup: (hostname, options, callback) => {
callback(new Error('Fake DNS error'));
}
});
cl.lookup('example.test', error => {
t.is(error.message, 'Fake DNS error');
t.end();
});
});