Socket
Socket
Sign inDemoInstall

cacheable-lookup

Package Overview
Dependencies
0
Maintainers
2
Versions
34
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 4.3.0 to 5.0.0

5

package.json
{
"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"
}
}

90

README.md

@@ -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 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc