Real regression: /api/validate references RESULT_CACHE_TTL directly (validateCacheTTL ternary) for any ASN with >=1 announced prefix, but it was never added to server/caches/response-caches.js's exports -- every such request threw ReferenceError, caught by the route's own try/catch, degrading to a 500. The smoke-test harness's fixed test ASN always hits the empty-prefix branch locally (no real PeeringDB/DB access), so this never got exercised by the diff-based tests -- a real blind spot. Caught by running `eslint --rule no-undef` across server.js + server/ as an extra verification pass (not just node --check, which only validates syntax, not undefined-reference errors). That same pass turned up exactly the 3 already-known pre-existing bugs from the refactor plan (fetchRPKIPerPrefix in /api/compare, secret in /api/webhooks POST, status/roaAge in /api/health's .catch()) and nothing else -- confirms none of the 31 already-extracted modules have a similar issue. Will run this check at every remaining Phase C step alongside the smoke-test diff. Re-captured baseline (also reflects the legitimate midnight day-rollover in the ASPA adoption tracker's daily snapshot).
114 lines
3.9 KiB
JavaScript
114 lines
3.9 KiB
JavaScript
// Task 6: In-memory cache with TTL + Rate Limiting
|
|
const responseCache = new Map();
|
|
|
|
function cacheGet(key) {
|
|
const entry = responseCache.get(key);
|
|
if (!entry) return null;
|
|
if (Date.now() > entry.expires) {
|
|
responseCache.delete(key);
|
|
return null;
|
|
}
|
|
return entry.data;
|
|
}
|
|
|
|
function cacheSet(key, data, ttlMs) {
|
|
responseCache.set(key, { data, expires: Date.now() + ttlMs });
|
|
// Evict old entries periodically (keep cache under 500 entries)
|
|
if (responseCache.size > 500) {
|
|
const now = Date.now();
|
|
for (const [k, v] of responseCache) {
|
|
if (now > v.expires) responseCache.delete(k);
|
|
}
|
|
}
|
|
}
|
|
|
|
const CACHE_TTL_LOOKUP = 5 * 60 * 1000; // 5 minutes
|
|
const CACHE_TTL_LOOKUP_DEGRADED = 90 * 1000; // matches the existing 90s guard for suspicious /api/validate zero-counts
|
|
const CACHE_TTL_DEFAULT = 5 * 60 * 1000; // 5 minutes
|
|
|
|
// RDAP Cache — prevents 429 flooding on LACNIC/AFRINIC/APNIC/ARIN
|
|
const rdapCache = new Map(); // key: asn string, value: { data, ts }
|
|
const RDAP_CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
|
|
|
function rdapCacheGet(asn) {
|
|
const e = rdapCache.get(String(asn));
|
|
if (e && (Date.now() - e.ts) < RDAP_CACHE_TTL) return e.data;
|
|
return undefined; // undefined = not cached, null = cached miss
|
|
}
|
|
function rdapCacheSet(asn, data) {
|
|
if (rdapCache.size > 5000) {
|
|
rdapCache.delete(rdapCache.keys().next().value);
|
|
}
|
|
rdapCache.set(String(asn), { data, ts: Date.now() });
|
|
}
|
|
|
|
// WHOIS Cache — 24h TTL, prevents repeated RDAP hammering
|
|
const whoisCache = new Map(); // key: asn string, value: { data, ts, ttl }
|
|
const WHOIS_CACHE_TTL = 24 * 60 * 60 * 1000;
|
|
// A "not found" result after all 5 RIR sources failed is far more often a shared
|
|
// network blip (DNS, outbound connectivity, simultaneous rate-limit) than a real
|
|
// ASN existing nowhere across RIPE+APNIC+ARIN+LACNIC+AFRINIC -- fetchJSON can't
|
|
// tell timeout/network-error apart from a genuine 404, so cache that specific
|
|
// conclusion much more briefly than a confirmed, parsed WHOIS record.
|
|
const WHOIS_NOT_FOUND_CACHE_TTL = 10 * 60 * 1000;
|
|
function whoisCacheGet(asn) {
|
|
const e = whoisCache.get(String(asn));
|
|
if (e && (Date.now() - e.ts) < (e.ttl || WHOIS_CACHE_TTL)) return e.data;
|
|
return undefined;
|
|
}
|
|
function whoisCacheSet(asn, data, ttl) {
|
|
if (whoisCache.size > 5000) whoisCache.delete(whoisCache.keys().next().value);
|
|
whoisCache.set(String(asn), { data, ts: Date.now(), ttl: ttl || WHOIS_CACHE_TTL });
|
|
}
|
|
|
|
// Quick-IX Cache — 1h TTL, for Peering Recommendations
|
|
const quickIxCache = new Map(); // key: asn string, value: { data, ts }
|
|
const QUICK_IX_CACHE_TTL = 60 * 60 * 1000;
|
|
function quickIxCacheGet(asn) {
|
|
const e = quickIxCache.get(String(asn));
|
|
if (e && (Date.now() - e.ts) < QUICK_IX_CACHE_TTL) return e.data;
|
|
return undefined;
|
|
}
|
|
function quickIxCacheSet(asn, data) {
|
|
if (quickIxCache.size > 2000) quickIxCache.delete(quickIxCache.keys().next().value);
|
|
quickIxCache.set(String(asn), { data, ts: Date.now() });
|
|
}
|
|
|
|
// bgproutes + ASPA result caches — 15min TTL, prevent re-hitting slow APIs
|
|
const bgproutesResultCache = new Map();
|
|
const aspaResultCache = new Map();
|
|
const validateResultCache = new Map();
|
|
const RESULT_CACHE_TTL = 15 * 60 * 1000; // 15 minutes
|
|
function resultCacheGet(map, key) {
|
|
const e = map.get(String(key));
|
|
const ttl = e && e.ttl ? e.ttl : RESULT_CACHE_TTL;
|
|
if (e && (Date.now() - e.ts) < ttl) return e.data;
|
|
return undefined;
|
|
}
|
|
function resultCacheSet(map, key, data, ttlMs) {
|
|
if (map.size > 2000) map.delete(map.keys().next().value);
|
|
map.set(String(key), { data, ts: Date.now(), ttl: ttlMs || RESULT_CACHE_TTL });
|
|
}
|
|
|
|
module.exports = {
|
|
responseCache,
|
|
cacheGet,
|
|
cacheSet,
|
|
RESULT_CACHE_TTL,
|
|
CACHE_TTL_LOOKUP,
|
|
CACHE_TTL_LOOKUP_DEGRADED,
|
|
CACHE_TTL_DEFAULT,
|
|
rdapCacheGet,
|
|
rdapCacheSet,
|
|
whoisCacheGet,
|
|
whoisCacheSet,
|
|
WHOIS_NOT_FOUND_CACHE_TTL,
|
|
quickIxCacheGet,
|
|
quickIxCacheSet,
|
|
bgproutesResultCache,
|
|
aspaResultCache,
|
|
validateResultCache,
|
|
resultCacheGet,
|
|
resultCacheSet,
|
|
};
|