'use strict'; const {Resolver, V4MAPPED, ADDRCONFIG} = require('dns'); const {promisify} = require('util'); const os = require('os'); const Keyv = require('keyv'); const kCacheableLookupData = Symbol('cacheableLookupData'); const kCacheableLookupInstance = Symbol('cacheableLookupInstance'); 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) { 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) { break; } } } return {has4, has6}; }; class CacheableLookup { constructor({cacheAdapter, maxTtl = Infinity, resolver} = {}) { this.cache = new Keyv({ uri: typeof cacheAdapter === 'string' && cacheAdapter, store: typeof cacheAdapter !== 'string' && cacheAdapter, namespace: 'cached-lookup' }); this.maxTtl = maxTtl; this._resolver = resolver || new Resolver(); this._resolve4 = promisify(this._resolver.resolve4.bind(this._resolver)); this._resolve6 = promisify(this._resolver.resolve6.bind(this._resolver)); this._iface = getIfaceInfo(); this.lookup = this.lookup.bind(this); this.lookupAsync = this.lookupAsync.bind(this); } set servers(servers) { this._resolver.setServers(servers); } get servers() { return this._resolver.getServers(); } lookup(hostname, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } // eslint-disable-next-line promise/prefer-await-to-then this.lookupAsync(hostname, {...options, throwNotFound: true}).then(result => { if (options.all) { callback(null, result); } else { callback(null, result.address, result.family, result.expires, result.ttl); } }).catch(callback); } async lookupAsync(hostname, options = {}) { let cached; if (!options.family && options.all) { const [cached4, cached6] = await Promise.all([this.lookupAsync(hostname, {all: true, family: 4}), this.lookupAsync(hostname, {all: true, family: 6})]); cached = [...cached4, ...cached6]; } else { cached = await this.query(hostname, options.family || 4); if (cached.length === 0 && options.family === 6 && options.hints & V4MAPPED) { cached = await this.query(hostname, 4); map4to6(cached); } } if (options.hints & ADDRCONFIG) { const {_iface} = this; cached = cached.filter(entry => entry.family === 6 ? _iface.has6 : _iface.has4); } if (cached.length === 0 && options.throwNotFound) { const error = new Error(`ENOTFOUND ${hostname}`); error.code = 'ENOTFOUND'; error.hostname = hostname; throw error; } const now = Date.now(); cached = cached.filter(entry => entry.ttl === 0 || now < entry.expires); if (options.all) { return cached; } if (cached.length === 1) { return cached[0]; } if (cached.length === 0) { return undefined; } return this._getEntry(cached); } async query(hostname, family) { let cached = await this.cache.get(`${hostname}:${family}`); if (!cached) { cached = await this.queryAndCache(hostname, family); } return cached; } async queryAndCache(hostname, family) { const resolve = family === 4 ? this._resolve4 : this._resolve6; const entries = await resolve(hostname, {ttl: true}); if (entries === undefined) { return []; } const now = Date.now(); let cacheTtl = 0; for (const entry of entries) { cacheTtl = Math.max(cacheTtl, entry.ttl); entry.family = family; entry.expires = now + (entry.ttl * 1000); } cacheTtl = Math.min(this.maxTtl, cacheTtl) * 1000; if (this.maxTtl !== 0 && cacheTtl !== 0) { await this.cache.set(`${hostname}:${family}`, entries, cacheTtl); } return entries; } _getEntry(entries) { return entries[Math.floor(Math.random() * entries.length)]; } install(agent) { verifyAgent(agent); if (kCacheableLookupData in agent) { throw new Error('CacheableLookup has been already installed'); } agent[kCacheableLookupData] = agent.createConnection; agent[kCacheableLookupInstance] = this; agent.createConnection = (options, callback) => { if (!('lookup' in options)) { options.lookup = this.lookup; } return agent[kCacheableLookupData](options, callback); }; } uninstall(agent) { verifyAgent(agent); if (agent[kCacheableLookupData]) { if (agent[kCacheableLookupInstance] !== this) { throw new Error('The agent is not owned by this CacheableLookup instance'); } agent.createConnection = agent[kCacheableLookupData]; delete agent[kCacheableLookupData]; delete agent[kCacheableLookupInstance]; } } updateInterfaceInfo() { this._iface = getIfaceInfo(); } } module.exports = CacheableLookup; module.exports.default = CacheableLookup;