import { ddosDetected } from "@/lib/services/alert"; import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; /** * App-level abuse / DDoS guard — the web-tier-feasible half of AtomCMS's DDoS * protection. It can't touch iptables (that's a host-only optimisation), but it * DOES the actual mitigation a CMS needs: count requests per IP and, when one * floods past the threshold, add it to website_ip_blacklist (which the access * guard then enforces) and fire the existing ddosDetected() alert. * * OFF by default; staff enable + tune it via website_settings: * abuse_guard_enabled ("1"), abuse_guard_threshold (req, default 200), * abuse_guard_window_seconds (default 10). */ type Bucket = { count: number; resetAt: number }; const buckets = new Map(); const recentlyBlocked = new Set(); let blacklist = new Set(); let blacklistLoadedAt = 0; const BLACKLIST_TTL = 30_000; function isPrivate(ip: string): boolean { return ( !ip || ip === "0.0.0.0" || ip === "::1" || ip.startsWith("127.") || ip.startsWith("10.") || ip.startsWith("192.168.") ); } /** Cached blacklist lookup (refreshed every 30s — no DB hit per request). */ export async function isIpBlacklisted(ip: string): Promise { if (isPrivate(ip)) return false; const now = Date.now(); if (now - blacklistLoadedAt >= BLACKLIST_TTL) { try { const rows = await prisma.websiteIpBlacklist.findMany({ select: { ipAddress: true } }); blacklist = new Set(rows.map((r) => r.ipAddress)); blacklistLoadedAt = now; } catch { /* keep stale set on DB error */ } } return blacklist.has(ip); } /** Count a request; auto-blacklist + alert the IP if it floods (when enabled). */ export async function recordRequest(ip: string): Promise { if (isPrivate(ip)) return; if (!(await siteSettings.getBool("abuse_guard_enabled", false))) return; const limit = Number(await siteSettings.get("abuse_guard_threshold", "200")) || 200; const windowMs = (Number(await siteSettings.get("abuse_guard_window_seconds", "10")) || 10) * 1000; const now = Date.now(); if (buckets.size > 10_000) { for (const [k, v] of buckets) if (now >= v.resetAt) buckets.delete(k); } const b = buckets.get(ip); if (!b || now >= b.resetAt) { buckets.set(ip, { count: 1, resetAt: now + windowMs }); return; } b.count += 1; if (b.count >= limit && !recentlyBlocked.has(ip)) { recentlyBlocked.add(ip); setTimeout(() => recentlyBlocked.delete(ip), 60_000); try { await prisma.websiteIpBlacklist.create({ data: { ipAddress: ip, createdAt: new Date(), updatedAt: new Date() }, }); blacklistLoadedAt = 0; // force a refresh so the block takes effect at once await ddosDetected(ip, b.count); } catch { /* ignore — alert/blacklist best-effort */ } } }