Repository navigation
Expand file tree
/
Copy pathperformance.py
More file actions
130 lines (104 loc) · 4.18 KB
/
Copy pathperformance.py
File metadata and controls
130 lines (104 loc) · 4.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
"""
Performance optimizations for high-traffic scenarios.
Includes rate limiting and caching.
"""
import time
from collections.abc import Callable
from typing import Any
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
from starlette.status import HTTP_429_TOO_MANY_REQUESTS
from config import config, logger
class RateLimiter(BaseHTTPMiddleware):
"""
Token bucket rate limiter.
Prevents API abuse and protects against DDoS.
"""
def __init__(self, app):
super().__init__(app)
self.requests: dict = {} # {ip: [(timestamp, path)]}
@staticmethod
def _limits() -> dict[str, int]:
"""Per-IP rate limits (requests per window), read from config."""
return {
"/api/vote/": config.rate_limit.limit_vote_get,
"/api/vote": config.rate_limit.limit_vote_post,
"/api/proxies": config.rate_limit.limit_proxies,
"/api/add-proxy": config.rate_limit.limit_add_proxy,
"/": config.rate_limit.limit_homepage,
}
async def dispatch(self, request: Request, call_next: Callable) -> Response:
"""Check rate limit before processing."""
if not config.rate_limit.enabled:
return await call_next(request)
client_ip = request.client.host if request.client else "unknown"
path = request.url.path
# Find the most specific matching limit (longest prefix wins, so the
# result does not depend on the order of the limits).
limit = None
for prefix, rate in self._limits().items():
if path.startswith(prefix) and (
limit is None or len(prefix) > len(limit[0])
):
limit = (prefix, rate)
limit = limit[1] if limit else None
if limit is None:
return await call_next(request)
current_time = time.time()
window_start = current_time - config.rate_limit.window_seconds
# Initialize or clean up
if client_ip not in self.requests:
if len(self.requests) >= config.rate_limit.max_clients:
oldest_client = min(
self.requests,
key=lambda client: (
self.requests[client][-1][0] if self.requests[client] else 0
),
)
del self.requests[oldest_client]
self.requests[client_ip] = []
# Remove old requests
self.requests[client_ip] = [
(ts, p) for ts, p in self.requests[client_ip] if ts > window_start
]
# Check limit for this path
path_requests = len([p for _, p in self.requests[client_ip] if p == path])
if path_requests >= limit:
logger.warning(
"Rate limit exceeded for {client} on {path}",
client=client_ip,
path=path,
)
return Response(
content=config.messages.rate_limit_exceeded,
status_code=HTTP_429_TOO_MANY_REQUESTS,
)
# Record request
self.requests[client_ip].append((current_time, path))
return await call_next(request)
class CacheStore:
"""Simple in-memory cache with TTL."""
def __init__(self):
self.cache: dict = {} # {key: (value, expires_at)}
def get(self, key: str) -> Any | None:
"""Get value from cache if not expired."""
if key in self.cache:
value, expires_at = self.cache[key]
if time.time() < expires_at:
return value
del self.cache[key]
return None
def set(self, key: str, value: Any, ttl_seconds: int | None = None) -> None:
"""Store value with TTL."""
if ttl_seconds is None:
ttl_seconds = config.cache.ttl_seconds
self.cache[key] = (value, time.time() + ttl_seconds)
def invalidate(self, pattern: str = "") -> None:
"""Invalidate cache entries matching pattern."""
if not pattern:
self.cache.clear()
else:
self.cache = {k: v for k, v in self.cache.items() if pattern not in k}
# Global cache instance
cache_store = CacheStore()