Files
deeplx/src/lib/rateLimit.ts
T
xixu-me 8afa94767e Initial repository setup and core files added
Add configuration, documentation, code of conduct, contributing guide, license, and README files. Set up .gitignore, coverage config, GitHub issue templates, pull request template, and initial source code, scripts, and tests for DeepLX optimized for Cloudflare Workers.
2025-07-27 02:09:25 +08:00

326 lines
9.3 KiB
TypeScript

/**
* 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<boolean> - True if request is allowed, false if rate limited
*/
async function checkRateLimit(clientIP: string, env: Env): Promise<boolean> {
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 async function delayRequest(seconds: number): Promise<void> {
return new Promise((resolve) => {
// Use a simple delay implementation for Workers environment
const start = Date.now();
const checkTime = () => {
if (Date.now() - start >= seconds * 1000) {
resolve();
} else {
// Use minimal delay to prevent blocking
Promise.resolve().then(checkTime);
}
};
checkTime();
});
}
/**
* 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<RateLimitResult> - Object indicating if request is allowed and reason if not
*/
export async function checkCombinedRateLimit(
clientIP: string,
endpoint: string,
env: Env
): Promise<RateLimitResult> {
// 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<boolean> - True if request is allowed, false if rate limited
*/
export async function checkProxyRateLimit(
proxyUrl: string,
env: Env
): Promise<boolean> {
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;
}
}