Files
huishou/node_modules/cacheable-lookup/index.js
T
2026-07-27 13:37:48 +08:00

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;