Cache LRU sur entry endpoints (movie/tv/imdb/providers) + allowlist loopback du rate limit

This commit is contained in:
unfr
2026-05-10 01:14:33 +02:00
parent 8247544d1b
commit 92005a9cbe
4 changed files with 33 additions and 6 deletions

View File

@@ -11,8 +11,10 @@ SESSION_SECRET=change_me_to_a_random_32_chars_str
PORT=3000
HOST=0.0.0.0
PAGE_TITLE=Index protégé
# Plafond global de requetes par seconde par IP
# Plafond global de requetes par seconde par IP (loopback toujours exempte)
RATE_LIMIT_PER_SEC=50
# IPs supplementaires exemptees du rate limit (internes / scripts trustes)
#RATE_LIMIT_ALLOWLIST=85.17.118.137,10.0.0.5
# --- URLs externes (laisse les defauts sauf si tu changes de domaine) ---
#TMDB_API_BASE=https://api.themoviedb.org/3

View File

@@ -35,6 +35,11 @@ export const SESSION_SECRET = required('SESSION_SECRET');
export const PORT = int('PORT', 3000);
export const HOST = str('HOST', '0.0.0.0');
export const RATE_LIMIT_PER_SEC = int('RATE_LIMIT_PER_SEC', 50);
// Comma-separated IPs exempted from rate limit. Loopback always exempted.
export const RATE_LIMIT_ALLOWLIST = (str('RATE_LIMIT_ALLOWLIST', '') || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
// URLs externes
export const TMDB_API_BASE = str('TMDB_API_BASE', 'https://api.themoviedb.org/3');

View File

@@ -20,19 +20,36 @@ import { filterAndLower } from '../lib/titleFilter.js';
const searchCache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 60 });
// Entry cache covers movie/tv/imdb/providers responses. Keeps hot files in RAM
// so repeat lookups skip disk + JSON.parse. TTL 30 min (cron may rewrite
// underlying detail file via /changes — 30 min keeps staleness bounded).
const entryCache = new LRUCache({ max: 5000, ttl: 1000 * 60 * 30 });
async function getDetail(type, id) {
const key = `d:${type}:${id}`;
const hit = entryCache.get(key);
if (hit !== undefined) return hit;
try {
const buf = await readFile(entryPath(type, id), 'utf8');
return JSON.parse(buf);
const obj = JSON.parse(buf);
entryCache.set(key, obj);
return obj;
} catch {
entryCache.set(key, null);
return null;
}
}
async function getProviders(type, id) {
const key = `p:${type}:${id}`;
const hit = entryCache.get(key);
if (hit !== undefined) return hit;
try {
return JSON.parse(await readFile(justwatchPath(type, id), 'utf8'));
const obj = JSON.parse(await readFile(justwatchPath(type, id), 'utf8'));
entryCache.set(key, obj);
return obj;
} catch {
entryCache.set(key, null);
return null;
}
}

View File

@@ -4,7 +4,7 @@ import rateLimit from '@fastify/rate-limit';
import secureSession from '@fastify/secure-session';
import fastifyStatic from '@fastify/static';
import Fastify from 'fastify';
import { HOST, PORT, RATE_LIMIT_PER_SEC, ROOT, SESSION_SECRET } from './config.js';
import { HOST, PORT, RATE_LIMIT_ALLOWLIST, RATE_LIMIT_PER_SEC, ROOT, SESSION_SECRET } from './config.js';
import { startWatchers } from './lib/dataReload.js';
import { preloadMappings } from './lib/imdbMapping.js';
import { getRatings } from './lib/imdbRatings.js';
@@ -18,12 +18,15 @@ import searchRoutes from './routes/search.js';
const fastify = Fastify({ logger: true, trustProxy: true });
// Loopback always exempted (internal scripts on the same host). Extra IPs via
// RATE_LIMIT_ALLOWLIST env var. Public IPs still rate-limited at 50/s.
const RL_ALLOW = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1', ...RATE_LIMIT_ALLOWLIST]);
await fastify.register(rateLimit, {
max: RATE_LIMIT_PER_SEC,
timeWindow: '1 second',
// Skip rate limiting for /health and /metrics so monitoring is never throttled
skipOnError: true,
allowList: (req) => req.url === '/health' || req.url === '/metrics',
allowList: (req) => req.url === '/health' || req.url === '/metrics' || RL_ALLOW.has(req.ip),
});
await fastify.register(formbody);