37 lines
1.1 KiB
Python
37 lines
1.1 KiB
Python
import threading
|
|
import time
|
|
from collections import defaultdict
|
|
|
|
from fastapi import Request
|
|
|
|
from app.core.errors import AppError
|
|
|
|
_buckets: dict[str, list[float]] = defaultdict(list)
|
|
_lock = threading.Lock()
|
|
|
|
|
|
def _client_ip(request: Request) -> str:
|
|
forwarded = request.headers.get("x-forwarded-for")
|
|
if forwarded:
|
|
return forwarded.split(",")[0].strip()
|
|
if request.client and request.client.host:
|
|
return request.client.host
|
|
return "unknown"
|
|
|
|
|
|
def enforce_rate_limit(request: Request, *, scope: str, limit: int, window_seconds: int) -> None:
|
|
"""In-memory sliding window limiter (per process). Use edge/WAF limits in production at scale."""
|
|
import os
|
|
|
|
if os.environ.get("DESI_CLANS_DISABLE_RATE_LIMIT", "").lower() in {"1", "true", "yes"}:
|
|
return
|
|
if limit <= 0:
|
|
return
|
|
key = f"{scope}:{_client_ip(request)}"
|
|
now = time.time()
|
|
with _lock:
|
|
window = _buckets[key]
|
|
window[:] = [stamp for stamp in window if now - stamp < window_seconds]
|
|
if len(window) >= limit:
|
|
raise AppError("Too many requests. Try again shortly.", status_code=429)
|
|
window.append(now)
|