/** * Token bucket rate limiting implementation for client IP and proxy management * Provides comprehensive rate limiting with two-level caching for optimal performance */ import { RATE_LIMIT_CONFIG, calculateDynamicRateLimits } from "./config"; import { getProxyEndpoints } from "./proxyManager"; // Import types from worker-configuration for consistency interface Env { CACHE_KV: KVNamespace; RATE_LIMIT_KV: KVNamespace; ANALYTICS: AnalyticsEngineDataset; PROXY_URLS?: string; PROXY_WEIGHTS?: string; } interface RateLimitEntry { tokens: number; lastRefill: number; } /** * Rate limiting result interface */ export interface RateLimitResult { allowed: boolean; reason?: string; } /** * Get dynamic rate limits based on available proxy endpoints * @param env Environment bindings containing proxy configuration * @returns Object containing calculated rate limits */ function getDynamicRateLimits(env: Env) { const proxyEndpoints = getProxyEndpoints(env); return calculateDynamicRateLimits(proxyEndpoints.length); } /** * Proxy backend rate limiting configuration constants */ const PROXY_TOKENS_PER_SECOND = RATE_LIMIT_CONFIG.PROXY_TOKENS_PER_SECOND; const PROXY_MAX_TOKENS = RATE_LIMIT_CONFIG.PROXY_MAX_TOKENS; const PROXY_REFILL_RATE = PROXY_TOKENS_PER_SECOND; /** * In-memory cache for performance optimization * Uses Map for fast lookups and TTL-based cleanup */ const rateLimitCache = new Map< string, { tokens: number; lastRefill: number; lastUpdate: number } >(); const CACHE_TTL = 5000; // 5 seconds TTL for cache entries /** * Extract client IP address from request headers * Supports Cloudflare and standard forwarded headers * @param request The incoming request object * @returns The client IP address or "unknown" if not found */ export function getClientIP(request: Request): string { return ( request.headers.get("CF-Connecting-IP") || request.headers.get("X-Forwarded-For")?.split(",")[0]?.trim() || "unknown" ); } /** * Check rate limit for a specific client IP using token bucket algorithm * Implements two-level caching: in-memory cache and Cloudflare KV storage * @param clientIP The client IP address to check * @param env Environment bindings containing KV namespaces * @returns Promise - True if request is allowed, false if rate limited */ export async function checkRateLimit( clientIP: string, env: Env ): Promise { const key = `rate_limit:${clientIP}`; const now = Date.now(); try { const rateLimits = getDynamicRateLimits(env); const maxTokens = rateLimits.TOKENS_PER_MINUTE; // Check in-memory cache const cached = rateLimitCache.get(key); let tokens = maxTokens; let lastRefill = now; if (cached && now - cached.lastUpdate < CACHE_TTL) { // Use cached data const timePassed = (now - cached.lastRefill) / 1000; tokens = Math.min( maxTokens, cached.tokens + timePassed * rateLimits.REFILL_RATE ); lastRefill = cached.lastRefill; } else { // Get data from KV try { const existing = (await env.RATE_LIMIT_KV.get( key, "json" )) as RateLimitEntry | null; if (existing) { const timePassed = (now - existing.lastRefill) / 1000; tokens = Math.min( maxTokens, existing.tokens + timePassed * rateLimits.REFILL_RATE ); lastRefill = existing.lastRefill; } } catch (kvError) { // KV read failed, continue with in-memory cache only // Error is silently handled to maintain service availability } } if (tokens < 1) { // Update cache rateLimitCache.set(key, { tokens, lastRefill: now, lastUpdate: now, }); // Async KV update env.RATE_LIMIT_KV.put(key, JSON.stringify({ tokens, lastRefill: now }), { expirationTtl: 3600, }).catch(() => { // KV update failed, continue silently }); return false; } // Consume one token tokens -= 1; // Update cache rateLimitCache.set(key, { tokens, lastRefill: now, lastUpdate: now, }); // Async KV update env.RATE_LIMIT_KV.put(key, JSON.stringify({ tokens, lastRefill: now }), { expirationTtl: 3600, }).catch(() => { // KV update failed, continue silently }); return true; } catch (error) { // Rate limit check failed, allow request to maintain service availability return true; } } /** * Delay execution for specified number of seconds * Used for implementing backoff strategies * @param seconds - Number of seconds to delay * @returns Promise that resolves after the specified delay */ export function delayRequest(seconds: number): Promise { return new Promise((resolve) => setTimeout(resolve, seconds * 1000)); } /** * Clean up expired cache entries to prevent memory leaks * Performs on-demand cleanup when cache grows beyond threshold * @private */ function cleanupCacheIfNeeded(): void { // Only clean up when cache has more than 100 items to avoid frequent operations if (rateLimitCache.size > 100) { const now = Date.now(); for (const [key, entry] of rateLimitCache.entries()) { if (now - entry.lastUpdate > CACHE_TTL * 2) { rateLimitCache.delete(key); } } } } /** * Combined rate limit check for both client and proxy backend limits * Provides comprehensive rate limiting across multiple dimensions * @param clientIP - The client IP address to check * @param endpoint - The endpoint URL to check (null for direct requests) * @param env - Environment bindings containing KV namespaces * @returns Promise - Object indicating if request is allowed and reason if not */ export async function checkCombinedRateLimit( clientIP: string, endpoint: string, env: Env ): Promise { // Clean up cache on demand cleanupCacheIfNeeded(); // First check client rate limit const clientAllowed = await checkRateLimit(clientIP, env); if (!clientAllowed) { return { allowed: false, reason: "Client rate limit exceeded" }; } // If endpoint exists, check proxy rate limit if (endpoint && endpoint !== "https://www2.deepl.com/jsonrpc") { const proxyAllowed = await checkProxyRateLimit(endpoint, env); if (!proxyAllowed) { return { allowed: false, reason: "Proxy rate limit exceeded" }; } } return { allowed: true }; } /** * Efficient proxy backend rate limiting using token bucket algorithm * Implements two-level caching to reduce KV latency and improve performance * @param proxyUrl - The proxy URL to rate limit * @param env - Environment bindings containing KV namespaces * @returns Promise - True if request is allowed, false if rate limited */ export async function checkProxyRateLimit( proxyUrl: string, env: Env ): Promise { const key = `proxy_rate_limit:${proxyUrl}`; const now = Date.now(); try { // First check in-memory cache for optimal performance const cached = rateLimitCache.get(key); let tokens = PROXY_MAX_TOKENS; let lastRefill = now; if (cached && now - cached.lastUpdate < CACHE_TTL) { // Use cached data if still valid const timePassed = (now - cached.lastRefill) / 1000; tokens = Math.min( PROXY_MAX_TOKENS, cached.tokens + timePassed * PROXY_REFILL_RATE ); lastRefill = cached.lastRefill; } else { // Fallback to KV storage, but don't block request on failure try { const existing = (await env.RATE_LIMIT_KV.get( key, "json" )) as RateLimitEntry | null; if (existing) { const timePassed = (now - existing.lastRefill) / 1000; tokens = Math.min( PROXY_MAX_TOKENS, existing.tokens + timePassed * PROXY_REFILL_RATE ); lastRefill = existing.lastRefill; } } catch (kvError) { // KV read failed, continue using default or cached values // Error is silently handled to maintain service availability } } // Check if request should be rate limited if (tokens < 1) { // Update cache with current state rateLimitCache.set(key, { tokens, lastRefill: now, lastUpdate: now, }); // Async KV update without blocking response env.RATE_LIMIT_KV.put(key, JSON.stringify({ tokens, lastRefill: now }), { expirationTtl: 3600, }).catch(() => { // KV update failed, continue silently }); return false; } // Consume one token tokens -= 1; // Update cache with new token count rateLimitCache.set(key, { tokens, lastRefill: now, lastUpdate: now, }); // Async KV update without blocking response env.RATE_LIMIT_KV.put(key, JSON.stringify({ tokens, lastRefill: now }), { expirationTtl: 3600, }).catch(() => { // KV update failed, continue silently }); return true; } catch (error) { // Proxy rate limit check failed, allow request to maintain service availability return true; } }