rate_limiter.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. # -*- coding: utf-8 -*-
  2. """
  3. Rate Limiting für Netzwerk-Verbindungen.
  4. Bietet Token-Bucket-basiertes Rate-Limiting pro IP und global.
  5. """
  6. import asyncio
  7. import time
  8. from dataclasses import dataclass, field
  9. from enum import IntEnum, auto
  10. from typing import Any
  11. class RateLimitResult(IntEnum):
  12. """Ergebnis einer Rate-Limit-Prüfung."""
  13. ALLOWED = auto() # Anfrage erlaubt
  14. LIMITED = auto() # Rate-Limit erreicht
  15. BLOCKED = auto() # Dauerhaft blockiert (zu viele Überschreitungen)
  16. @dataclass
  17. class RateLimitConfig:
  18. """
  19. Konfiguration für Rate-Limiting.
  20. Attributes:
  21. requests_per_second: Maximale Anfragen pro Sekunde
  22. burst_size: Maximale Burst-Größe (Token-Bucket-Kapazität)
  23. block_threshold: Anzahl Überschreitungen bis zur Blockierung
  24. block_duration: Blockierungsdauer in Sekunden
  25. cleanup_interval: Intervall für Speicherbereinigung in Sekunden
  26. """
  27. requests_per_second: float = 10.0
  28. burst_size: int = 20
  29. block_threshold: int = 100
  30. block_duration: float = 300.0 # 5 Minuten
  31. cleanup_interval: float = 60.0 # 1 Minute
  32. @dataclass
  33. class TokenBucket:
  34. """
  35. Token-Bucket für Rate-Limiting.
  36. Der Bucket füllt sich kontinuierlich mit Tokens.
  37. Jede Anfrage verbraucht ein Token.
  38. """
  39. capacity: int
  40. tokens: float
  41. refill_rate: float # Tokens pro Sekunde
  42. last_update: float = field(default_factory=time.monotonic)
  43. violations: int = 0
  44. blocked_until: float | None = None
  45. def refill(self) -> None:
  46. """Füllt den Bucket basierend auf verstrichener Zeit."""
  47. now = time.monotonic()
  48. elapsed = now - self.last_update
  49. self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
  50. self.last_update = now
  51. def consume(self, tokens: int = 1) -> bool:
  52. """
  53. Versucht, Tokens zu verbrauchen.
  54. Args:
  55. tokens: Anzahl zu verbrauchender Tokens
  56. Returns:
  57. True wenn erfolgreich, False wenn nicht genug Tokens
  58. """
  59. self.refill()
  60. if self.tokens >= tokens:
  61. self.tokens -= tokens
  62. return True
  63. self.violations += 1
  64. return False
  65. def is_blocked(self) -> bool:
  66. """Prüft, ob der Bucket blockiert ist."""
  67. if self.blocked_until is None:
  68. return False
  69. if time.monotonic() > self.blocked_until:
  70. self.blocked_until = None
  71. return False
  72. return True
  73. def block(self, duration: float) -> None:
  74. """Blockiert den Bucket für eine bestimmte Zeit."""
  75. self.blocked_until = time.monotonic() + duration
  76. class IPRateLimiter:
  77. """
  78. Rate-Limiter pro IP-Adresse.
  79. Verfolgt Anfragen pro IP und limitiert übermäßige Anfragen.
  80. Example:
  81. limiter = IPRateLimiter(RateLimitConfig(
  82. requests_per_second=10,
  83. burst_size=20
  84. ))
  85. result = limiter.check("192.168.1.100")
  86. if result == RateLimitResult.ALLOWED:
  87. # Anfrage verarbeiten
  88. pass
  89. elif result == RateLimitResult.LIMITED:
  90. # 429 Too Many Requests senden
  91. pass
  92. elif result == RateLimitResult.BLOCKED:
  93. # Verbindung abweisen
  94. pass
  95. """
  96. def __init__(self, config: RateLimitConfig | None = None) -> None:
  97. """
  98. Initialisiert den IP-Rate-Limiter.
  99. Args:
  100. config: Rate-Limit-Konfiguration
  101. """
  102. self._config = config or RateLimitConfig()
  103. self._buckets: dict[str, TokenBucket] = {}
  104. self._lock = asyncio.Lock()
  105. self._cleanup_task: asyncio.Task | None = None
  106. def _get_or_create_bucket(self, ip: str) -> TokenBucket:
  107. """Gibt den Bucket für eine IP zurück oder erstellt einen neuen."""
  108. if ip not in self._buckets:
  109. self._buckets[ip] = TokenBucket(
  110. capacity=self._config.burst_size,
  111. tokens=float(self._config.burst_size),
  112. refill_rate=self._config.requests_per_second
  113. )
  114. return self._buckets[ip]
  115. def check(self, ip: str, tokens: int = 1) -> RateLimitResult:
  116. """
  117. Prüft und verbraucht Rate-Limit für eine IP.
  118. Args:
  119. ip: Die IP-Adresse
  120. tokens: Anzahl zu verbrauchender Tokens
  121. Returns:
  122. RateLimitResult
  123. """
  124. bucket = self._get_or_create_bucket(ip)
  125. # Prüfe Blockierung
  126. if bucket.is_blocked():
  127. return RateLimitResult.BLOCKED
  128. # Prüfe Rate-Limit
  129. if not bucket.consume(tokens):
  130. # Prüfe ob Blockierung nötig
  131. if bucket.violations >= self._config.block_threshold:
  132. bucket.block(self._config.block_duration)
  133. return RateLimitResult.BLOCKED
  134. return RateLimitResult.LIMITED
  135. return RateLimitResult.ALLOWED
  136. async def check_async(self, ip: str, tokens: int = 1) -> RateLimitResult:
  137. """Thread-sichere Prüfung mit Lock."""
  138. async with self._lock:
  139. return self.check(ip, tokens)
  140. def reset(self, ip: str) -> None:
  141. """Setzt das Rate-Limit für eine IP zurück."""
  142. if ip in self._buckets:
  143. del self._buckets[ip]
  144. def unblock(self, ip: str) -> bool:
  145. """
  146. Hebt die Blockierung einer IP auf.
  147. Setzt auch die Tokens auf volle Kapazität zurück,
  148. damit die IP wieder Anfragen stellen kann.
  149. Args:
  150. ip: Die IP-Adresse
  151. Returns:
  152. True wenn IP blockiert war und entsperrt wurde
  153. """
  154. if ip in self._buckets:
  155. bucket = self._buckets[ip]
  156. if bucket.is_blocked():
  157. bucket.blocked_until = None
  158. bucket.violations = 0
  159. # Tokens auf volle Kapazität zurücksetzen
  160. bucket.tokens = float(bucket.capacity)
  161. return True
  162. return False
  163. def get_status(self, ip: str) -> dict[str, Any]:
  164. """
  165. Gibt den Status für eine IP zurück.
  166. Args:
  167. ip: Die IP-Adresse
  168. Returns:
  169. Status-Dictionary
  170. """
  171. if ip not in self._buckets:
  172. return {
  173. "exists": False,
  174. "tokens": self._config.burst_size,
  175. "blocked": False,
  176. "violations": 0
  177. }
  178. bucket = self._buckets[ip]
  179. bucket.refill()
  180. return {
  181. "exists": True,
  182. "tokens": bucket.tokens,
  183. "capacity": bucket.capacity,
  184. "blocked": bucket.is_blocked(),
  185. "blocked_until": bucket.blocked_until,
  186. "violations": bucket.violations
  187. }
  188. def get_blocked_ips(self) -> list[str]:
  189. """Gibt alle blockierten IPs zurück."""
  190. return [
  191. ip for ip, bucket in self._buckets.items()
  192. if bucket.is_blocked()
  193. ]
  194. def cleanup(self) -> int:
  195. """
  196. Entfernt inaktive Buckets.
  197. Returns:
  198. Anzahl entfernter Buckets
  199. """
  200. now = time.monotonic()
  201. stale_threshold = 300.0 # 5 Minuten Inaktivität
  202. to_remove = []
  203. for ip, bucket in self._buckets.items():
  204. # Entferne nur wenn nicht blockiert und lange inaktiv
  205. if not bucket.is_blocked():
  206. if now - bucket.last_update > stale_threshold:
  207. to_remove.append(ip)
  208. for ip in to_remove:
  209. del self._buckets[ip]
  210. return len(to_remove)
  211. async def start_cleanup_task(self) -> None:
  212. """Startet den automatischen Cleanup-Task."""
  213. if self._cleanup_task is not None:
  214. return
  215. async def cleanup_loop():
  216. while True:
  217. await asyncio.sleep(self._config.cleanup_interval)
  218. self.cleanup()
  219. self._cleanup_task = asyncio.create_task(cleanup_loop())
  220. async def stop_cleanup_task(self) -> None:
  221. """Stoppt den Cleanup-Task."""
  222. if self._cleanup_task is not None:
  223. self._cleanup_task.cancel()
  224. try:
  225. await self._cleanup_task
  226. except asyncio.CancelledError:
  227. pass
  228. self._cleanup_task = None
  229. def get_statistics(self) -> dict[str, Any]:
  230. """Gibt Statistiken zurück."""
  231. blocked = sum(1 for b in self._buckets.values() if b.is_blocked())
  232. total_violations = sum(b.violations for b in self._buckets.values())
  233. return {
  234. "tracked_ips": len(self._buckets),
  235. "blocked_ips": blocked,
  236. "total_violations": total_violations,
  237. "config": {
  238. "requests_per_second": self._config.requests_per_second,
  239. "burst_size": self._config.burst_size,
  240. "block_threshold": self._config.block_threshold,
  241. "block_duration": self._config.block_duration
  242. }
  243. }
  244. class GlobalRateLimiter:
  245. """
  246. Globales Rate-Limit für alle Verbindungen.
  247. Schützt den Server vor Überlastung unabhängig von der Quelle.
  248. Example:
  249. limiter = GlobalRateLimiter(
  250. max_requests_per_second=1000,
  251. burst_size=2000
  252. )
  253. if limiter.check() == RateLimitResult.ALLOWED:
  254. # Anfrage verarbeiten
  255. pass
  256. """
  257. def __init__(
  258. self,
  259. max_requests_per_second: float = 1000.0,
  260. burst_size: int = 2000
  261. ) -> None:
  262. """
  263. Initialisiert den globalen Rate-Limiter.
  264. Args:
  265. max_requests_per_second: Maximale Anfragen pro Sekunde
  266. burst_size: Maximale Burst-Größe
  267. """
  268. self._bucket = TokenBucket(
  269. capacity=burst_size,
  270. tokens=float(burst_size),
  271. refill_rate=max_requests_per_second
  272. )
  273. self._lock = asyncio.Lock()
  274. self._total_requests: int = 0
  275. self._limited_requests: int = 0
  276. def check(self, tokens: int = 1) -> RateLimitResult:
  277. """
  278. Prüft das globale Rate-Limit.
  279. Args:
  280. tokens: Anzahl zu verbrauchender Tokens
  281. Returns:
  282. RateLimitResult
  283. """
  284. self._total_requests += 1
  285. if self._bucket.consume(tokens):
  286. return RateLimitResult.ALLOWED
  287. self._limited_requests += 1
  288. return RateLimitResult.LIMITED
  289. async def check_async(self, tokens: int = 1) -> RateLimitResult:
  290. """Thread-sichere Prüfung mit Lock."""
  291. async with self._lock:
  292. return self.check(tokens)
  293. def get_status(self) -> dict[str, Any]:
  294. """Gibt den aktuellen Status zurück."""
  295. self._bucket.refill()
  296. return {
  297. "tokens_available": self._bucket.tokens,
  298. "capacity": self._bucket.capacity,
  299. "refill_rate": self._bucket.refill_rate,
  300. "total_requests": self._total_requests,
  301. "limited_requests": self._limited_requests,
  302. "limit_ratio": self._limited_requests / max(1, self._total_requests)
  303. }
  304. class ConnectionRateLimiter:
  305. """
  306. Kombinierter Rate-Limiter mit IP- und Global-Limits.
  307. Kombiniert IP-spezifische und globale Rate-Limits.
  308. Example:
  309. limiter = ConnectionRateLimiter(
  310. ip_config=RateLimitConfig(requests_per_second=10),
  311. global_rps=1000
  312. )
  313. result = await limiter.check("192.168.1.100")
  314. if result != RateLimitResult.ALLOWED:
  315. # Anfrage ablehnen
  316. return
  317. """
  318. def __init__(
  319. self,
  320. ip_config: RateLimitConfig | None = None,
  321. global_rps: float = 1000.0,
  322. global_burst: int = 2000
  323. ) -> None:
  324. """
  325. Initialisiert den kombinierten Rate-Limiter.
  326. Args:
  327. ip_config: Konfiguration für IP-basiertes Limiting
  328. global_rps: Globale Anfragen pro Sekunde
  329. global_burst: Globale Burst-Größe
  330. """
  331. self._ip_limiter = IPRateLimiter(ip_config)
  332. self._global_limiter = GlobalRateLimiter(global_rps, global_burst)
  333. @property
  334. def ip_limiter(self) -> IPRateLimiter:
  335. """IP-basierter Rate-Limiter."""
  336. return self._ip_limiter
  337. @property
  338. def global_limiter(self) -> GlobalRateLimiter:
  339. """Globaler Rate-Limiter."""
  340. return self._global_limiter
  341. def check(self, ip: str, tokens: int = 1) -> RateLimitResult:
  342. """
  343. Prüft beide Rate-Limits.
  344. Args:
  345. ip: Die IP-Adresse
  346. tokens: Anzahl Tokens
  347. Returns:
  348. Das restriktivere Ergebnis
  349. """
  350. # Globales Limit zuerst
  351. global_result = self._global_limiter.check(tokens)
  352. if global_result != RateLimitResult.ALLOWED:
  353. return global_result
  354. # Dann IP-Limit
  355. return self._ip_limiter.check(ip, tokens)
  356. async def check_async(self, ip: str, tokens: int = 1) -> RateLimitResult:
  357. """Thread-sichere Prüfung."""
  358. global_result = await self._global_limiter.check_async(tokens)
  359. if global_result != RateLimitResult.ALLOWED:
  360. return global_result
  361. return await self._ip_limiter.check_async(ip, tokens)
  362. async def start(self) -> None:
  363. """Startet Hintergrund-Tasks."""
  364. await self._ip_limiter.start_cleanup_task()
  365. async def stop(self) -> None:
  366. """Stoppt Hintergrund-Tasks."""
  367. await self._ip_limiter.stop_cleanup_task()
  368. def get_statistics(self) -> dict[str, Any]:
  369. """Gibt kombinierte Statistiken zurück."""
  370. return {
  371. "ip": self._ip_limiter.get_statistics(),
  372. "global": self._global_limiter.get_status()
  373. }