217 lines
5.0 KiB
JavaScript
217 lines
5.0 KiB
JavaScript
'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;
|