Reduces DB queries ~80% by caching clean IPs longer. Blocked IPs keep short TTL for quick unblock propagation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
import { createClient } from "@libsql/client/web";
|
|
import * as BunnySDK from "@bunny.net/edgescript-sdk";
|
|
import process from "node:process";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Database client
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const db = createClient({
|
|
url: process.env.BUNNY_DATABASE_URL!,
|
|
authToken: process.env.BUNNY_DATABASE_AUTH_TOKEN!,
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// In-memory cache (per edge node, 60s TTL)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface CacheEntry {
|
|
blocked: boolean;
|
|
expiresAt: number;
|
|
}
|
|
|
|
const cache = new Map<string, CacheEntry>();
|
|
const CACHE_TTL_BLOCKED_MS = 60_000; // 차단 IP: 1분 (빠른 해제 반영)
|
|
const CACHE_TTL_CLEAN_MS = 300_000; // 정상 IP: 5분 (DB 부하 감소)
|
|
const CACHE_MAX_SIZE = 50_000;
|
|
|
|
function cacheGet(ip: string): boolean | null {
|
|
const entry = cache.get(ip);
|
|
if (!entry) return null;
|
|
if (entry.expiresAt <= Date.now()) {
|
|
cache.delete(ip);
|
|
return null;
|
|
}
|
|
return entry.blocked;
|
|
}
|
|
|
|
function cacheSet(ip: string, blocked: boolean): void {
|
|
if (cache.size >= CACHE_MAX_SIZE) {
|
|
// Evict expired entries first
|
|
const now = Date.now();
|
|
for (const [key, val] of cache) {
|
|
if (val.expiresAt <= now) cache.delete(key);
|
|
}
|
|
// If still too large, clear entirely
|
|
if (cache.size >= CACHE_MAX_SIZE) cache.clear();
|
|
}
|
|
const ttl = blocked ? CACHE_TTL_BLOCKED_MS : CACHE_TTL_CLEAN_MS;
|
|
cache.set(ip, { blocked, expiresAt: Date.now() + ttl });
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Blocklist lookup
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function isBlocked(ip: string): Promise<boolean> {
|
|
const cached = cacheGet(ip);
|
|
if (cached !== null) return cached;
|
|
|
|
try {
|
|
const result = await db.execute({
|
|
sql: "SELECT 1 FROM blocklist WHERE ip = ? AND (expires_at IS NULL OR expires_at > datetime('now'))",
|
|
args: [ip],
|
|
});
|
|
const blocked = result.rows.length > 0;
|
|
cacheSet(ip, blocked);
|
|
return blocked;
|
|
} catch (err) {
|
|
// Fail-open: on DB error, allow the request through
|
|
console.error("Blocklist lookup failed, allowing request:", err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Middleware
|
|
// ---------------------------------------------------------------------------
|
|
|
|
BunnySDK.net.http
|
|
.servePullZone()
|
|
.onOriginRequest(async (ctx) => {
|
|
const ip = ctx.request.headers.get("X-Real-Ip");
|
|
|
|
if (!ip) {
|
|
return ctx.request;
|
|
}
|
|
|
|
if (await isBlocked(ip)) {
|
|
return new Response("Forbidden", { status: 403 });
|
|
}
|
|
|
|
return ctx.request;
|
|
});
|