From 92005a9cbe4f4eaa518a66d5b24948356bbd492b Mon Sep 17 00:00:00 2001 From: unfr Date: Sun, 10 May 2026 01:14:33 +0200 Subject: [PATCH] Cache LRU sur entry endpoints (movie/tv/imdb/providers) + allowlist loopback du rate limit --- .env.example | 4 +++- config.js | 5 +++++ routes/api.js | 21 +++++++++++++++++++-- server.js | 9 ++++++--- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 45aa9ca..c191d99 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/config.js b/config.js index 179a64d..2de6f7a 100644 --- a/config.js +++ b/config.js @@ -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'); diff --git a/routes/api.js b/routes/api.js index 258dd42..de75b97 100644 --- a/routes/api.js +++ b/routes/api.js @@ -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; } } diff --git a/server.js b/server.js index 0aecab7..4a503a7 100644 --- a/server.js +++ b/server.js @@ -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);