cacheable-lookup
Advanced tools
Comparing version 4.3.0 to 5.0.0
{ | ||
"name": "cacheable-lookup", | ||
"version": "4.3.0", | ||
"version": "5.0.0", | ||
"description": "A cacheable dns.lookup(…) that respects the TTL", | ||
@@ -35,3 +35,3 @@ "engines": { | ||
"@types/keyv": "^3.1.1", | ||
"ava": "^3.7.1", | ||
"ava": "^3.8.2", | ||
"benchmark": "^2.1.4", | ||
@@ -43,4 +43,5 @@ "coveralls": "^3.0.9", | ||
"tsd": "^0.11.0", | ||
"quick-lru": "^5.1.0", | ||
"xo": "^0.25.3" | ||
} | ||
} |
@@ -19,2 +19,3 @@ # cacheable-lookup | ||
const CacheableLookup = require('cacheable-lookup'); | ||
const cacheable = new CacheableLookup(); | ||
@@ -32,4 +33,4 @@ | ||
const CacheableLookup = require('cacheable-lookup'); | ||
const cacheable = new CacheableLookup(); | ||
cacheable.install(http.globalAgent); | ||
@@ -48,4 +49,11 @@ | ||
#### cache | ||
#### options | ||
Type: `object`<br> | ||
Default: `{}` | ||
Options used to cache the DNS lookups. | ||
##### cache | ||
Type: `Map` | [`Keyv`](https://github.com/lukechilds/keyv/)<br> | ||
@@ -58,9 +66,18 @@ Default: `new Map()` | ||
#### options | ||
**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: | ||
Type: `object`<br> | ||
Default: `{}` | ||
```js | ||
const http = require('http'); | ||
const CacheableLookup = require('cacheable-lookup'); | ||
const QuickLRU = require('quick-lru'); | ||
Options used to cache the DNS lookups. | ||
const cacheable = new CacheableLookup({ | ||
cache: new QuickLRU({maxSize: 1000}) | ||
}); | ||
http.get('http://example.com', {lookup: cacheable.lookup}, response => { | ||
// Handle the response here | ||
}); | ||
``` | ||
##### options.maxTtl | ||
@@ -77,13 +94,11 @@ | ||
##### options.fallbackTtl | ||
##### options.fallbackDuration | ||
Type: `number`<br> | ||
Default: `1` | ||
Default: `3600` (1 hour) | ||
The lifetime of the entries received from the OS (TTL in seconds). | ||
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**: This option is independent, `options.maxTtl` does not affect this. | ||
If you don't query internal hostnames (such as `localhost`, `database.local` etc.), it is strongly recommended to set this value to `0`. | ||
**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.errorTtl | ||
@@ -94,3 +109,3 @@ | ||
The time how long it needs to remember queries that threw `ENOTFOUND` (TTL in seconds). | ||
The time how long it needs to remember queries that threw `ENOTFOUND` or `ENODATA` (TTL in seconds). | ||
@@ -108,16 +123,11 @@ **Note**: This option is independent, `options.maxTtl` does not affect this. | ||
##### options.customHostsPath | ||
##### options.lookup | ||
Type: `string`<br> | ||
Default: `undefined` (OS-specific) | ||
Type: `Function`<br> | ||
Default: [`dns.lookup`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) | ||
The full path to the `hosts` file. Set this to `false` to prevent loading entries from the `hosts` file. | ||
The fallback function to use when the DNS server responds with `ENOTFOUND` or `ENODATA`. | ||
##### options.watchingHostsFile | ||
**Note**: This has no effect if the `fallbackDuration` option is less than `1`. | ||
Type: `boolean`<br> | ||
Default: `false` | ||
If set to `true`, it will watch the `hosts` file and update the cache. | ||
### Entry object | ||
@@ -143,3 +153,3 @@ | ||
**Note**: This is not present when using the native `dns.lookup(...)`! | ||
**Note**: This is not present when falling back to `dns.lookup(...)`! | ||
@@ -150,3 +160,3 @@ The timestamp (`Date.now() + ttl * 1000`) when the entry expires. | ||
**Note**: This is not present when using the native `dns.lookup(...)`! | ||
**Note**: This is not present when falling back to `dns.lookup(...)`! | ||
@@ -166,3 +176,3 @@ The time in seconds for its lifetime. | ||
The DNS servers used to make queries. Can be overridden - doing so will trigger `cacheableLookup.updateInterfaceInfo()`. | ||
The DNS servers used to make queries. Can be overridden - doing so will clear the cache. | ||
@@ -178,4 +188,2 @@ #### [lookup(hostname, options, callback)](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) | ||
**Note**: If entry(ies) were not found, it will return `undefined` by default. | ||
##### hostname | ||
@@ -207,6 +215,2 @@ | ||
#### tick() | ||
Deprecated - it is a noop. Outdated entries are removed automatically. | ||
#### updateInterfaceInfo() | ||
@@ -216,7 +220,7 @@ | ||
**Note:** Running `updateInterfaceInfo()` will also trigger `clear()`! | ||
**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 cleared. | ||
Clears the cache for the given hostname. If the hostname argument is not present, the entire cache will be emptied. | ||
@@ -231,8 +235,8 @@ ## High performance | ||
``` | ||
CacheableLookup#lookupAsync x 2,421,707 ops/sec ±1.11% (86 runs sampled) | ||
CacheableLookup#lookupAsync.all x 2,338,741 ops/sec ±1.74% (84 runs sampled) | ||
CacheableLookup#lookupAsync.all.ADDRCONFIG x 2,238,534 ops/sec ±0.94% (89 runs sampled) | ||
CacheableLookup#lookup x 2,298,645 ops/sec ±1.26% (87 runs sampled) | ||
CacheableLookup#lookup.all x 2,260,194 ops/sec ±1.49% (87 runs sampled) | ||
CacheableLookup#lookup.all.ADDRCONFIG x 2,133,142 ops/sec ±1.52% (86 runs sampled) | ||
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) | ||
@@ -244,8 +248,2 @@ dns#lookup.all x 7,249 ops/sec ±0.40% (86 runs sampled) | ||
The package is based on [`dns.resolve4(…)`](https://nodejs.org/api/dns.html#dns_dns_resolve4_hostname_options_callback) and [`dns.resolve6(…)`](https://nodejs.org/api/dns.html#dns_dns_resolve6_hostname_options_callback). | ||
[Why not `dns.lookup(…)`?](https://github.com/nodejs/node/issues/25560#issuecomment-455596215) | ||
> It is not possible to use `dns.lookup(…)` because underlying calls like [getaddrinfo](http://man7.org/linux/man-pages/man3/getaddrinfo.3.html) have no concept of servers or TTL (caching is done on OS level instead). | ||
## Related | ||
@@ -252,0 +250,0 @@ |
@@ -5,14 +5,17 @@ 'use strict'; | ||
ADDRCONFIG, | ||
ALL, | ||
promises: { | ||
Resolver: AsyncResolver | ||
}, | ||
lookup | ||
lookup: dnsLookup | ||
} = require('dns'); | ||
const {promisify} = require('util'); | ||
const os = require('os'); | ||
const {getResolver: getHostsResolver} = require('./hosts-resolver'); | ||
const kCacheableLookupCreateConnection = Symbol('cacheableLookupCreateConnection'); | ||
const kCacheableLookupInstance = Symbol('cacheableLookupInstance'); | ||
const kExpires = Symbol('expires'); | ||
const supportsALL = typeof ALL === 'number'; | ||
const verifyAgent = agent => { | ||
@@ -26,2 +29,6 @@ if (!(agent && typeof agent.createConnection === 'function')) { | ||
for (const entry of entries) { | ||
if (entry.family === 6) { | ||
continue; | ||
} | ||
entry.address = `::ffff:${entry.address}`; | ||
@@ -57,16 +64,19 @@ entry.family = 6; | ||
const isIterable = map => { | ||
return Symbol.iterator in map; | ||
}; | ||
const ttl = {ttl: true}; | ||
const all = {all: true}; | ||
class CacheableLookup { | ||
constructor({ | ||
customHostsPath, | ||
watchingHostsFile = false, | ||
cache = new Map(), | ||
maxTtl = Infinity, | ||
fallbackDuration = 3600, | ||
errorTtl = 0.15, | ||
resolver = new AsyncResolver(), | ||
fallbackTtl = 1, | ||
errorTtl = 0.15 | ||
lookup = dnsLookup | ||
} = {}) { | ||
this.maxTtl = maxTtl; | ||
this.fallbackTtl = fallbackTtl; | ||
this.errorTtl = errorTtl; | ||
@@ -76,5 +86,4 @@ | ||
this._resolver = resolver; | ||
this._dnsLookup = promisify(lookup); | ||
this._lookup = promisify(lookup); | ||
if (this._resolver instanceof AsyncResolver) { | ||
@@ -89,8 +98,22 @@ this._resolve4 = this._resolver.resolve4.bind(this._resolver); | ||
this._iface = getIfaceInfo(); | ||
this._hostsResolver = getHostsResolver({customHostsPath, watching: watchingHostsFile}); | ||
this._pending = {}; | ||
this._nextRemovalTime = false; | ||
this._hostnamesToFallback = new Set(); | ||
if (fallbackDuration < 1) { | ||
this._fallback = false; | ||
} else { | ||
this._fallback = true; | ||
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.lookup = this.lookup.bind(this); | ||
@@ -101,3 +124,3 @@ this.lookupAsync = this.lookupAsync.bind(this); | ||
set servers(servers) { | ||
this.updateInterfaceInfo(); | ||
this.clear(); | ||
@@ -147,4 +170,8 @@ this._resolver.setServers(servers); | ||
if (filtered.length === 0 && options.hints & V4MAPPED) { | ||
map4to6(cached); | ||
if (options.hints & V4MAPPED) { | ||
if ((supportsALL && options.hints & ALL) || filtered.length === 0) { | ||
map4to6(cached); | ||
} else { | ||
cached = filtered; | ||
} | ||
} else { | ||
@@ -163,3 +190,3 @@ cached = filtered; | ||
if (cached.length === 0) { | ||
const error = new Error(`ENOTFOUND ${hostname}`); | ||
const error = new Error(`cacheableLookup ENOTFOUND ${hostname}`); | ||
error.code = 'ENOTFOUND'; | ||
@@ -175,11 +202,7 @@ error.hostname = hostname; | ||
if (cached.length === 1) { | ||
return cached[0]; | ||
} | ||
return this._getEntry(cached, hostname); | ||
return cached[0]; | ||
} | ||
async query(hostname) { | ||
let cached = await this._hostsResolver.get(hostname) || await this._cache.get(hostname); | ||
let cached = await this._cache.get(hostname); | ||
@@ -206,85 +229,152 @@ if (!cached) { | ||
async queryAndCache(hostname) { | ||
// We could make an ANY query, but DNS servers may reject that. | ||
const [As, AAAAs] = await Promise.all([this._resolve4(hostname, ttl).catch(() => []), this._resolve6(hostname, ttl).catch(() => [])]); | ||
async _resolve(hostname) { | ||
const wrap = async promise => { | ||
try { | ||
return await promise; | ||
} catch (error) { | ||
if (error.code === 'ENODATA' || error.code === 'ENOTFOUND') { | ||
return []; | ||
} | ||
throw error; | ||
} | ||
}; | ||
// ANY is unsafe as it doesn't trigger new queries in the underlying server. | ||
const [A, AAAA] = await Promise.all([ | ||
this._resolve4(hostname, ttl), | ||
this._resolve6(hostname, ttl) | ||
].map(promise => wrap(promise))); | ||
let aTtl = 0; | ||
let aaaaTtl = 0; | ||
let cacheTtl = 0; | ||
if (As) { | ||
for (const entry of As) { | ||
entry.family = 4; | ||
entry.expires = Date.now() + (entry.ttl * 1000); | ||
const now = Date.now(); | ||
// Is the TTL the same for all entries? | ||
cacheTtl = Math.max(cacheTtl, entry.ttl); | ||
} | ||
for (const entry of A) { | ||
entry.family = 4; | ||
entry.expires = now + (entry.ttl * 1000); | ||
aTtl = Math.max(aTtl, entry.ttl); | ||
} | ||
if (AAAAs) { | ||
for (const entry of AAAAs) { | ||
entry.family = 6; | ||
entry.expires = Date.now() + (entry.ttl * 1000); | ||
for (const entry of AAAA) { | ||
entry.family = 6; | ||
entry.expires = now + (entry.ttl * 1000); | ||
// Is the TTL the same for all entries? | ||
cacheTtl = Math.max(cacheTtl, entry.ttl); | ||
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; | ||
} | ||
let entries = [...(As || []), ...(AAAAs || [])]; | ||
return { | ||
entries: [ | ||
...A, | ||
...AAAA | ||
], | ||
cacheTtl, | ||
isLookup: false | ||
}; | ||
} | ||
if (entries.length === 0) { | ||
try { | ||
entries = await this._lookup(hostname, { | ||
all: true | ||
}); | ||
async _lookup(hostname) { | ||
const empty = { | ||
entries: [], | ||
cacheTtl: 0, | ||
isLookup: true | ||
}; | ||
for (const entry of entries) { | ||
entry.ttl = this.fallbackTtl; | ||
entry.expires = Date.now() + (entry.ttl * 1000); | ||
} | ||
if (!this._fallback) { | ||
return empty; | ||
} | ||
cacheTtl = this.fallbackTtl * 1000; | ||
} catch (error) { | ||
delete this._pending[hostname]; | ||
try { | ||
const entries = await this._dnsLookup(hostname, { | ||
all: true | ||
}); | ||
if (error.code === 'ENOTFOUND') { | ||
cacheTtl = this.errorTtl * 1000; | ||
return { | ||
entries, | ||
cacheTtl: 0, | ||
isLookup: true | ||
}; | ||
} catch (_) { | ||
return empty; | ||
} | ||
} | ||
entries.expires = Date.now() + cacheTtl; | ||
await this._cache.set(hostname, entries, cacheTtl); | ||
async _set(hostname, data, cacheTtl) { | ||
if (this.maxTtl > 0 && cacheTtl > 0) { | ||
cacheTtl = Math.min(cacheTtl, this.maxTtl) * 1000; | ||
data[kExpires] = Date.now() + cacheTtl; | ||
this._tick(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 error; | ||
throw cacheError; | ||
}; | ||
} | ||
} else { | ||
cacheTtl = Math.min(this.maxTtl, cacheTtl) * 1000; | ||
if (isIterable(this._cache)) { | ||
this._tick(cacheTtl); | ||
} | ||
} | ||
} | ||
if (this.maxTtl > 0 && cacheTtl > 0) { | ||
entries.expires = Date.now() + cacheTtl; | ||
await this._cache.set(hostname, entries, cacheTtl); | ||
async queryAndCache(hostname) { | ||
if (this._hostnamesToFallback.has(hostname)) { | ||
return this._dnsLookup(hostname, all); | ||
} | ||
this._tick(cacheTtl); | ||
const resolverPromise = this._resolve(hostname); | ||
const lookupPromise = this._lookup(hostname); | ||
let query = await Promise.race([ | ||
resolverPromise, | ||
lookupPromise | ||
]); | ||
if (query.isLookup && query.entries.length === 0) { | ||
query = await resolverPromise; | ||
} | ||
delete this._pending[hostname]; | ||
(async () => { | ||
if (query.isLookup) { | ||
try { | ||
const realDnsQuery = await resolverPromise; | ||
return entries; | ||
} | ||
// If no DNS entries found | ||
if (realDnsQuery.entries.length === 0) { | ||
// Use `dns.lookup(...)` for that particular hostname | ||
this._hostnamesToFallback.add(hostname); | ||
} else { | ||
await this._set(hostname, realDnsQuery.entries, realDnsQuery.cacheTtl); | ||
} | ||
} catch (_) {} | ||
} else { | ||
const cacheTtl = query.entries.length === 0 ? this.errorTtl : query.cacheTtl; | ||
// eslint-disable-next-line no-unused-vars | ||
_getEntry(entries, hostname) { | ||
return entries[0]; | ||
await this._set(hostname, query.entries, cacheTtl); | ||
} | ||
delete this._pending[hostname]; | ||
})(); | ||
return query.entries; | ||
} | ||
/* istanbul ignore next: deprecated */ | ||
tick() {} | ||
_tick(ms) { | ||
if (!(this._cache instanceof Map) || ms === undefined) { | ||
return; | ||
} | ||
const nextRemovalTime = this._nextRemovalTime; | ||
@@ -304,3 +394,5 @@ | ||
for (const [hostname, {expires}] of this._cache) { | ||
for (const [hostname, entries] of this._cache) { | ||
const expires = entries[kExpires]; | ||
if (now >= expires) { | ||
@@ -360,4 +452,9 @@ this._cache.delete(hostname); | ||
updateInterfaceInfo() { | ||
const {_iface} = this; | ||
this._iface = getIfaceInfo(); | ||
this._cache.clear(); | ||
if ((_iface.has4 && !this._iface.has4) || (_iface.has6 && !this._iface.has6)) { | ||
this._cache.clear(); | ||
} | ||
} | ||
@@ -364,0 +461,0 @@ |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
0
1
24264
10
5
500
241