Cache LRU sur entry endpoints (movie/tv/imdb/providers) + allowlist loopback du rate limit
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user