UNPKG

fetch-dns

Version:

A drop-in replacement of Node's 'dns' module using 'fetch' and DNS-over-HTTPS

196 lines 6.52 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const lodash_1 = tslib_1.__importDefault(require("lodash")); const isFQDN_1 = tslib_1.__importDefault(require("validator/lib/isFQDN")); const events_1 = require("events"); const CLEAN_PERIOD_MILLIS = 600 * 1000; const PERIOD_BETWEEN_CLEANS = 1000; const MAX_LISTENERS_FOR_COUNTER = 4096; const MILLIS_PER_SECOND = 1000; function itIsEmpty(it) { if (!it || lodash_1.default.isNil(it)) return false; return it.isEmpty(); } function itIsNotEmpty(it) { if (!it || lodash_1.default.isNil(it)) return false; return !it.isEmpty(); } const counter = new (class Counter extends events_1.EventEmitter { constructor() { super(...arguments); this.pit = Number.MIN_SAFE_INTEGER; this.horizon = Number.MIN_SAFE_INTEGER; this.lastCleaned = Date.now(); } shouldFire() { return (this.lastUsedValue() % PERIOD_BETWEEN_CLEANS === 0 || this.lastCleaned + CLEAN_PERIOD_MILLIS < Date.now()); } lastUsedValue() { return this.pit; } runClean() { lodash_1.default.defer(this.emit.bind(this), "clean", Math.max(this.lastUsedValue() - PERIOD_BETWEEN_CLEANS, this.horizon)); } nextValue() { if (this.lastUsedValue() === Number.MAX_SAFE_INTEGER) { this.horizon = Number.MAX_SAFE_INTEGER; this.runClean(); this.horizon = this.pit = Number.MIN_SAFE_INTEGER; } else if (this.shouldFire()) { this.runClean(); this.horizon = this.lastUsedValue(); this.lastCleaned = Date.now(); } return this.pit++; } })(); counter.setMaxListeners(MAX_LISTENERS_FOR_COUNTER); function reverseHostname(hostname) { if (!isFQDN_1.default(hostname)) { throw new Error(`Hostname to cache is not a fully qualified domain name (FQDN): ${JSON.stringify(hostname)} (${typeof hostname})`); } return lodash_1.default.reject(lodash_1.default.reverse(hostname.split(".")), lodash_1.default.isEmpty); } class RecordCache { constructor(records) { this._setAt = Date.now(); this._setPit = counter.nextValue(); this._records = lodash_1.default.cloneDeep(records); this._listenForClean(); } _listenForClean() { if (lodash_1.default.isEmpty(this._records)) return; counter.once("clean", this.clean.bind(this)); } get records() { const now = Date.now(); return lodash_1.default.cloneDeep(lodash_1.default.filter(this._records, (record) => { if (lodash_1.default.isEmpty(record) || lodash_1.default.isEmpty(record.record)) return false; const ttlSeconds = record.ttl; return ttlSeconds * MILLIS_PER_SECOND + this._setAt >= now; })); } clean(horizon = Number.MIN_SAFE_INTEGER) { const { _setPit, _records } = this; if (lodash_1.default.isEmpty(_records)) return; if (_setPit <= horizon) { _records.length = 0; } else { this._listenForClean(); } } isEmpty() { this.clean(); return lodash_1.default.isEmpty(this._records); } } class ResultCache { constructor() { this._mapping = {}; this._subdomains = {}; this._listenForClean(); } _listenForClean() { counter.once("clean", this.clean.bind(this)); } isEmpty() { this.clean(); return (lodash_1.default.every(this._mapping, itIsEmpty) && lodash_1.default.every(this._subdomains, itIsEmpty)); } clean() { lodash_1.default.forOwn(this._subdomains, (results, sub) => { if (itIsEmpty(results)) { lodash_1.default.unset(this._subdomains, sub); } }); lodash_1.default.forOwn(this._mapping, (records, rrtype) => { if (itIsEmpty(records)) { lodash_1.default.unset(this._mapping, rrtype); } }); this._listenForClean(); } getRecordCache(rrtype) { const cache = this._mapping[rrtype]; if (itIsNotEmpty(cache)) { return cache; } else { lodash_1.default.unset(this._mapping, rrtype); return false; } } getSubdomain(sub) { const oldSub = this._subdomains[sub]; if (!oldSub || lodash_1.default.isEmpty(oldSub)) { return (this._subdomains[sub] = new ResultCache()); } else { return oldSub; } } setRecordCache(rrtype, records) { if (lodash_1.default.isEmpty(lodash_1.default.compact(records))) { lodash_1.default.unset(this._mapping, rrtype); } else { this._mapping[rrtype] = new RecordCache(records); } } setResult(domainParts, rrtype, records) { if (lodash_1.default.isEmpty(domainParts)) { this.setRecordCache(rrtype, records); } else { const [head, ...rest] = domainParts; this.getSubdomain(head).setResult(rest, rrtype, records); } } getResult(domainParts, rrtype) { if (lodash_1.default.isEmpty(domainParts)) { const result = this.getRecordCache(rrtype); if (result !== false && itIsNotEmpty(result)) { return lodash_1.default.map(result.records, "record"); } else { return false; } } else { const [head, ...rest] = domainParts; return this.getSubdomain(head).getResult(rest, rrtype); } } } exports.default = new (class DnsCache { constructor() { this.root = new ResultCache(); } put(hostname, rrtype, records) { records = lodash_1.default.compact(records); if (lodash_1.default.isEmpty(hostname) || lodash_1.default.isEmpty(records)) return; this.root.setResult(reverseHostname(hostname), rrtype, records); } check(hostname, rrtype) { if (lodash_1.default.isEmpty(hostname)) return false; const result = this.root.getResult(reverseHostname(hostname), rrtype); if (result && !lodash_1.default.isEmpty(result)) { return result; } else { return false; } } })(); //# sourceMappingURL=Cache.js.map