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
|
PORT=3000
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
PAGE_TITLE=Index protégé
|
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
|
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) ---
|
# --- URLs externes (laisse les defauts sauf si tu changes de domaine) ---
|
||||||
#TMDB_API_BASE=https://api.themoviedb.org/3
|
#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 PORT = int('PORT', 3000);
|
||||||
export const HOST = str('HOST', '0.0.0.0');
|
export const HOST = str('HOST', '0.0.0.0');
|
||||||
export const RATE_LIMIT_PER_SEC = int('RATE_LIMIT_PER_SEC', 50);
|
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
|
// URLs externes
|
||||||
export const TMDB_API_BASE = str('TMDB_API_BASE', 'https://api.themoviedb.org/3');
|
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 });
|
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) {
|
async function getDetail(type, id) {
|
||||||
|
const key = `d:${type}:${id}`;
|
||||||
|
const hit = entryCache.get(key);
|
||||||
|
if (hit !== undefined) return hit;
|
||||||
try {
|
try {
|
||||||
const buf = await readFile(entryPath(type, id), 'utf8');
|
const buf = await readFile(entryPath(type, id), 'utf8');
|
||||||
return JSON.parse(buf);
|
const obj = JSON.parse(buf);
|
||||||
|
entryCache.set(key, obj);
|
||||||
|
return obj;
|
||||||
} catch {
|
} catch {
|
||||||
|
entryCache.set(key, null);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getProviders(type, id) {
|
async function getProviders(type, id) {
|
||||||
|
const key = `p:${type}:${id}`;
|
||||||
|
const hit = entryCache.get(key);
|
||||||
|
if (hit !== undefined) return hit;
|
||||||
try {
|
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 {
|
} catch {
|
||||||
|
entryCache.set(key, null);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import rateLimit from '@fastify/rate-limit';
|
|||||||
import secureSession from '@fastify/secure-session';
|
import secureSession from '@fastify/secure-session';
|
||||||
import fastifyStatic from '@fastify/static';
|
import fastifyStatic from '@fastify/static';
|
||||||
import Fastify from 'fastify';
|
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 { startWatchers } from './lib/dataReload.js';
|
||||||
import { preloadMappings } from './lib/imdbMapping.js';
|
import { preloadMappings } from './lib/imdbMapping.js';
|
||||||
import { getRatings } from './lib/imdbRatings.js';
|
import { getRatings } from './lib/imdbRatings.js';
|
||||||
@@ -18,12 +18,15 @@ import searchRoutes from './routes/search.js';
|
|||||||
|
|
||||||
const fastify = Fastify({ logger: true, trustProxy: true });
|
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, {
|
await fastify.register(rateLimit, {
|
||||||
max: RATE_LIMIT_PER_SEC,
|
max: RATE_LIMIT_PER_SEC,
|
||||||
timeWindow: '1 second',
|
timeWindow: '1 second',
|
||||||
// Skip rate limiting for /health and /metrics so monitoring is never throttled
|
|
||||||
skipOnError: true,
|
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);
|
await fastify.register(formbody);
|
||||||
|
|||||||
Reference in New Issue
Block a user