mcp-security-framework 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_security_framework/__init__.py +96 -0
- mcp_security_framework/cli/__init__.py +18 -0
- mcp_security_framework/cli/cert_cli.py +511 -0
- mcp_security_framework/cli/security_cli.py +791 -0
- mcp_security_framework/constants.py +209 -0
- mcp_security_framework/core/__init__.py +61 -0
- mcp_security_framework/core/auth_manager.py +1011 -0
- mcp_security_framework/core/cert_manager.py +1663 -0
- mcp_security_framework/core/permission_manager.py +735 -0
- mcp_security_framework/core/rate_limiter.py +602 -0
- mcp_security_framework/core/security_manager.py +943 -0
- mcp_security_framework/core/ssl_manager.py +735 -0
- mcp_security_framework/examples/__init__.py +75 -0
- mcp_security_framework/examples/django_example.py +615 -0
- mcp_security_framework/examples/fastapi_example.py +472 -0
- mcp_security_framework/examples/flask_example.py +506 -0
- mcp_security_framework/examples/gateway_example.py +803 -0
- mcp_security_framework/examples/microservice_example.py +690 -0
- mcp_security_framework/examples/standalone_example.py +576 -0
- mcp_security_framework/middleware/__init__.py +250 -0
- mcp_security_framework/middleware/auth_middleware.py +292 -0
- mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
- mcp_security_framework/middleware/fastapi_middleware.py +757 -0
- mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
- mcp_security_framework/middleware/flask_middleware.py +591 -0
- mcp_security_framework/middleware/mtls_middleware.py +439 -0
- mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
- mcp_security_framework/middleware/security_middleware.py +507 -0
- mcp_security_framework/schemas/__init__.py +109 -0
- mcp_security_framework/schemas/config.py +694 -0
- mcp_security_framework/schemas/models.py +709 -0
- mcp_security_framework/schemas/responses.py +686 -0
- mcp_security_framework/tests/__init__.py +0 -0
- mcp_security_framework/utils/__init__.py +121 -0
- mcp_security_framework/utils/cert_utils.py +525 -0
- mcp_security_framework/utils/crypto_utils.py +475 -0
- mcp_security_framework/utils/validation_utils.py +571 -0
- mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
- mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
- mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
- mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
- mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_cli/__init__.py +0 -0
- tests/test_cli/test_cert_cli.py +379 -0
- tests/test_cli/test_security_cli.py +657 -0
- tests/test_core/__init__.py +0 -0
- tests/test_core/test_auth_manager.py +582 -0
- tests/test_core/test_cert_manager.py +795 -0
- tests/test_core/test_permission_manager.py +395 -0
- tests/test_core/test_rate_limiter.py +626 -0
- tests/test_core/test_security_manager.py +841 -0
- tests/test_core/test_ssl_manager.py +532 -0
- tests/test_examples/__init__.py +8 -0
- tests/test_examples/test_fastapi_example.py +264 -0
- tests/test_examples/test_flask_example.py +238 -0
- tests/test_examples/test_standalone_example.py +292 -0
- tests/test_integration/__init__.py +0 -0
- tests/test_integration/test_auth_flow.py +502 -0
- tests/test_integration/test_certificate_flow.py +527 -0
- tests/test_integration/test_fastapi_integration.py +341 -0
- tests/test_integration/test_flask_integration.py +398 -0
- tests/test_integration/test_standalone_integration.py +493 -0
- tests/test_middleware/__init__.py +0 -0
- tests/test_middleware/test_fastapi_middleware.py +523 -0
- tests/test_middleware/test_flask_middleware.py +582 -0
- tests/test_middleware/test_security_middleware.py +493 -0
- tests/test_schemas/__init__.py +0 -0
- tests/test_schemas/test_config.py +811 -0
- tests/test_schemas/test_models.py +879 -0
- tests/test_schemas/test_responses.py +1054 -0
- tests/test_schemas/test_serialization.py +493 -0
- tests/test_utils/__init__.py +0 -0
- tests/test_utils/test_cert_utils.py +510 -0
- tests/test_utils/test_crypto_utils.py +603 -0
- tests/test_utils/test_validation_utils.py +477 -0
@@ -0,0 +1,602 @@
|
|
1
|
+
"""
|
2
|
+
Rate Limiter Module
|
3
|
+
|
4
|
+
This module provides comprehensive rate limiting functionality for the
|
5
|
+
MCP Security Framework. It implements flexible rate limiting with support
|
6
|
+
for multiple identifiers, configurable windows, and various storage backends.
|
7
|
+
|
8
|
+
Key Features:
|
9
|
+
- Configurable rate limiting windows
|
10
|
+
- Support for multiple identifiers (IP, user, global)
|
11
|
+
- Burst limit support
|
12
|
+
- In-memory storage backend
|
13
|
+
- Cleanup of expired entries
|
14
|
+
- Rate limit status tracking
|
15
|
+
|
16
|
+
Classes:
|
17
|
+
RateLimiter: Main rate limiting class
|
18
|
+
RateLimitEntry: Internal rate limit entry
|
19
|
+
RateLimitStorage: Abstract storage interface
|
20
|
+
|
21
|
+
Author: MCP Security Team
|
22
|
+
Version: 1.0.0
|
23
|
+
License: MIT
|
24
|
+
"""
|
25
|
+
|
26
|
+
import logging
|
27
|
+
import threading
|
28
|
+
import time
|
29
|
+
from collections import defaultdict
|
30
|
+
from datetime import datetime, timedelta, timezone
|
31
|
+
from typing import Dict, Optional, Set
|
32
|
+
|
33
|
+
from ..schemas.config import RateLimitConfig
|
34
|
+
from ..schemas.models import RateLimitStatus
|
35
|
+
|
36
|
+
|
37
|
+
class RateLimitEntry:
|
38
|
+
"""
|
39
|
+
Rate Limit Entry Class
|
40
|
+
|
41
|
+
This class represents a single rate limit entry for tracking
|
42
|
+
requests within a specific time window.
|
43
|
+
|
44
|
+
Attributes:
|
45
|
+
identifier: Rate limiting identifier
|
46
|
+
count: Current request count
|
47
|
+
window_start: Start time of current window
|
48
|
+
window_size: Size of window in seconds
|
49
|
+
limit: Maximum allowed requests
|
50
|
+
"""
|
51
|
+
|
52
|
+
def __init__(self, identifier: str, limit: int, window_size: int):
|
53
|
+
"""
|
54
|
+
Initialize rate limit entry.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
identifier: Rate limiting identifier
|
58
|
+
limit: Maximum allowed requests
|
59
|
+
window_size: Window size in seconds
|
60
|
+
"""
|
61
|
+
self.identifier = identifier
|
62
|
+
self.count = 0
|
63
|
+
self.window_start = datetime.now(timezone.utc)
|
64
|
+
self.window_size = window_size
|
65
|
+
self.limit = limit
|
66
|
+
|
67
|
+
def is_expired(self) -> bool:
|
68
|
+
"""
|
69
|
+
Check if the rate limit entry is expired.
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
bool: True if entry is expired, False otherwise
|
73
|
+
"""
|
74
|
+
now = datetime.now(timezone.utc)
|
75
|
+
return (now - self.window_start).total_seconds() >= self.window_size
|
76
|
+
|
77
|
+
def reset_window(self) -> None:
|
78
|
+
"""Reset the rate limit window."""
|
79
|
+
self.count = 0
|
80
|
+
self.window_start = datetime.now(timezone.utc)
|
81
|
+
|
82
|
+
def increment(self) -> int:
|
83
|
+
"""
|
84
|
+
Increment request count.
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
int: New request count
|
88
|
+
"""
|
89
|
+
self.count += 1
|
90
|
+
return self.count
|
91
|
+
|
92
|
+
def get_status(self) -> RateLimitStatus:
|
93
|
+
"""
|
94
|
+
Get current rate limit status.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
RateLimitStatus: Current rate limit status
|
98
|
+
"""
|
99
|
+
now = datetime.now(timezone.utc)
|
100
|
+
window_end = self.window_start.replace(tzinfo=timezone.utc) + timedelta(
|
101
|
+
seconds=self.window_size
|
102
|
+
)
|
103
|
+
|
104
|
+
is_exceeded = self.count > self.limit
|
105
|
+
remaining_requests = max(0, self.limit - self.count)
|
106
|
+
|
107
|
+
return RateLimitStatus(
|
108
|
+
identifier=self.identifier,
|
109
|
+
current_count=self.count,
|
110
|
+
limit=self.limit,
|
111
|
+
window_start=self.window_start,
|
112
|
+
window_end=window_end,
|
113
|
+
is_exceeded=is_exceeded,
|
114
|
+
remaining_requests=remaining_requests,
|
115
|
+
reset_time=window_end,
|
116
|
+
window_size_seconds=self.window_size,
|
117
|
+
)
|
118
|
+
|
119
|
+
|
120
|
+
class RateLimiter:
|
121
|
+
"""
|
122
|
+
Rate Limiter Class
|
123
|
+
|
124
|
+
This class provides comprehensive rate limiting functionality with
|
125
|
+
support for multiple identifiers, configurable windows, and various
|
126
|
+
storage backends.
|
127
|
+
|
128
|
+
The RateLimiter implements:
|
129
|
+
- Configurable rate limiting windows
|
130
|
+
- Support for multiple identifiers (IP, user, global)
|
131
|
+
- Burst limit support
|
132
|
+
- In-memory storage backend
|
133
|
+
- Cleanup of expired entries
|
134
|
+
- Rate limit status tracking
|
135
|
+
|
136
|
+
Attributes:
|
137
|
+
config: Rate limiting configuration
|
138
|
+
logger: Logger instance for rate limiting operations
|
139
|
+
_entries: Dictionary of rate limit entries
|
140
|
+
_lock: Thread lock for thread safety
|
141
|
+
_cleanup_thread: Background cleanup thread
|
142
|
+
_stop_cleanup: Flag to stop cleanup thread
|
143
|
+
"""
|
144
|
+
|
145
|
+
def __init__(self, config: RateLimitConfig):
|
146
|
+
"""
|
147
|
+
Initialize Rate Limiter.
|
148
|
+
|
149
|
+
Args:
|
150
|
+
config: Rate limiting configuration containing limits,
|
151
|
+
window sizes, and storage settings. Must be a valid
|
152
|
+
RateLimitConfig instance with proper rate limiting
|
153
|
+
parameters.
|
154
|
+
|
155
|
+
Raises:
|
156
|
+
ValueError: If configuration is invalid
|
157
|
+
|
158
|
+
Example:
|
159
|
+
>>> config = RateLimitConfig(enabled=True, default_requests_per_minute=60)
|
160
|
+
>>> rate_limiter = RateLimiter(config)
|
161
|
+
"""
|
162
|
+
self.config = config
|
163
|
+
self.logger = logging.getLogger(__name__)
|
164
|
+
|
165
|
+
# Initialize storage
|
166
|
+
self._entries: Dict[str, RateLimitEntry] = {}
|
167
|
+
self._lock = threading.RLock()
|
168
|
+
|
169
|
+
# Initialize cleanup thread
|
170
|
+
self._stop_cleanup = False
|
171
|
+
self._cleanup_thread = None
|
172
|
+
|
173
|
+
if self.config.enabled:
|
174
|
+
self._start_cleanup_thread()
|
175
|
+
|
176
|
+
self.logger.info(
|
177
|
+
"Rate limiter initialized",
|
178
|
+
extra={
|
179
|
+
"enabled": config.enabled,
|
180
|
+
"default_requests_per_minute": config.default_requests_per_minute,
|
181
|
+
"window_size_seconds": config.window_size_seconds,
|
182
|
+
"storage_backend": config.storage_backend,
|
183
|
+
},
|
184
|
+
)
|
185
|
+
|
186
|
+
def check_rate_limit(self, identifier: str, limit: Optional[int] = None) -> bool:
|
187
|
+
"""
|
188
|
+
Check if rate limit is exceeded for the given identifier.
|
189
|
+
|
190
|
+
This method checks if the rate limit is exceeded for the specified
|
191
|
+
identifier. It automatically handles window resets and burst limits.
|
192
|
+
|
193
|
+
Args:
|
194
|
+
identifier: Rate limiting identifier (IP, user ID, etc.)
|
195
|
+
Must be a non-empty string identifying the request source.
|
196
|
+
limit: Custom limit for this check. If None, uses default
|
197
|
+
limit from configuration. Must be a positive integer.
|
198
|
+
|
199
|
+
Returns:
|
200
|
+
bool: True if rate limit is not exceeded, False if exceeded.
|
201
|
+
Returns True when the request can proceed, False when
|
202
|
+
rate limit is exceeded.
|
203
|
+
|
204
|
+
Raises:
|
205
|
+
ValueError: If identifier is empty or limit is invalid
|
206
|
+
|
207
|
+
Example:
|
208
|
+
>>> rate_limiter = RateLimiter(config)
|
209
|
+
>>> if rate_limiter.check_rate_limit("192.168.1.1"):
|
210
|
+
... # Process request
|
211
|
+
... pass
|
212
|
+
>>> else:
|
213
|
+
... # Rate limit exceeded
|
214
|
+
... pass
|
215
|
+
"""
|
216
|
+
if not identifier:
|
217
|
+
raise ValueError("Identifier cannot be empty")
|
218
|
+
|
219
|
+
if limit is not None and limit <= 0:
|
220
|
+
raise ValueError("Limit must be a positive integer")
|
221
|
+
|
222
|
+
if not self.config.enabled:
|
223
|
+
return True
|
224
|
+
|
225
|
+
# Use default limit if not specified
|
226
|
+
if limit is None:
|
227
|
+
limit = self.config.default_requests_per_minute * self.config.burst_limit
|
228
|
+
|
229
|
+
with self._lock:
|
230
|
+
entry = self._get_or_create_entry(identifier, limit)
|
231
|
+
|
232
|
+
# Check if window needs to be reset
|
233
|
+
if entry.is_expired():
|
234
|
+
entry.reset_window()
|
235
|
+
self.logger.debug(
|
236
|
+
"Rate limit window reset",
|
237
|
+
extra={"identifier": identifier, "limit": limit},
|
238
|
+
)
|
239
|
+
|
240
|
+
# Check if rate limit is exceeded (including the current request)
|
241
|
+
is_exceeded = entry.count >= limit
|
242
|
+
|
243
|
+
if is_exceeded:
|
244
|
+
self.logger.warning(
|
245
|
+
"Rate limit exceeded",
|
246
|
+
extra={
|
247
|
+
"identifier": identifier,
|
248
|
+
"current_count": entry.count,
|
249
|
+
"limit": limit,
|
250
|
+
},
|
251
|
+
)
|
252
|
+
else:
|
253
|
+
self.logger.debug(
|
254
|
+
"Rate limit check passed",
|
255
|
+
extra={
|
256
|
+
"identifier": identifier,
|
257
|
+
"current_count": entry.count,
|
258
|
+
"limit": limit,
|
259
|
+
},
|
260
|
+
)
|
261
|
+
|
262
|
+
return not is_exceeded
|
263
|
+
|
264
|
+
def increment_request_count(
|
265
|
+
self, identifier: str, limit: Optional[int] = None
|
266
|
+
) -> int:
|
267
|
+
"""
|
268
|
+
Increment request count for the given identifier.
|
269
|
+
|
270
|
+
This method increments the request count for the specified identifier
|
271
|
+
and returns the new count. It automatically handles window resets.
|
272
|
+
|
273
|
+
Args:
|
274
|
+
identifier: Rate limiting identifier (IP, user ID, etc.)
|
275
|
+
Must be a non-empty string identifying the request source.
|
276
|
+
limit: Custom limit for this increment. If None, uses default
|
277
|
+
limit from configuration. Must be a positive integer.
|
278
|
+
|
279
|
+
Returns:
|
280
|
+
int: New request count after increment.
|
281
|
+
Returns the updated count for the current window.
|
282
|
+
|
283
|
+
Raises:
|
284
|
+
ValueError: If identifier is empty or limit is invalid
|
285
|
+
|
286
|
+
Example:
|
287
|
+
>>> rate_limiter = RateLimiter(config)
|
288
|
+
>>> new_count = rate_limiter.increment_request_count("192.168.1.1")
|
289
|
+
>>> print(f"New request count: {new_count}")
|
290
|
+
"""
|
291
|
+
if not identifier:
|
292
|
+
raise ValueError("Identifier cannot be empty")
|
293
|
+
|
294
|
+
if limit is not None and limit <= 0:
|
295
|
+
raise ValueError("Limit must be a positive integer")
|
296
|
+
|
297
|
+
if not self.config.enabled:
|
298
|
+
return 0
|
299
|
+
|
300
|
+
# Use default limit if not specified
|
301
|
+
if limit is None:
|
302
|
+
limit = self.config.default_requests_per_minute * self.config.burst_limit
|
303
|
+
|
304
|
+
with self._lock:
|
305
|
+
entry = self._get_or_create_entry(identifier, limit)
|
306
|
+
|
307
|
+
# Check if window needs to be reset
|
308
|
+
if entry.is_expired():
|
309
|
+
entry.reset_window()
|
310
|
+
self.logger.debug(
|
311
|
+
"Rate limit window reset before increment",
|
312
|
+
extra={"identifier": identifier, "limit": limit},
|
313
|
+
)
|
314
|
+
|
315
|
+
# Increment count
|
316
|
+
new_count = entry.increment()
|
317
|
+
|
318
|
+
self.logger.debug(
|
319
|
+
"Request count incremented",
|
320
|
+
extra={
|
321
|
+
"identifier": identifier,
|
322
|
+
"new_count": new_count,
|
323
|
+
"limit": limit,
|
324
|
+
},
|
325
|
+
)
|
326
|
+
|
327
|
+
return new_count
|
328
|
+
|
329
|
+
def reset_rate_limit(self, identifier: str) -> None:
|
330
|
+
"""
|
331
|
+
Reset rate limit for the given identifier.
|
332
|
+
|
333
|
+
This method completely resets the rate limit for the specified
|
334
|
+
identifier, clearing all request counts and starting a new window.
|
335
|
+
|
336
|
+
Args:
|
337
|
+
identifier: Rate limiting identifier (IP, user ID, etc.)
|
338
|
+
Must be a non-empty string identifying the request source.
|
339
|
+
|
340
|
+
Raises:
|
341
|
+
ValueError: If identifier is empty
|
342
|
+
|
343
|
+
Example:
|
344
|
+
>>> rate_limiter = RateLimiter(config)
|
345
|
+
>>> rate_limiter.reset_rate_limit("192.168.1.1")
|
346
|
+
"""
|
347
|
+
if not identifier:
|
348
|
+
raise ValueError("Identifier cannot be empty")
|
349
|
+
|
350
|
+
with self._lock:
|
351
|
+
if identifier in self._entries:
|
352
|
+
del self._entries[identifier]
|
353
|
+
self.logger.info("Rate limit reset", extra={"identifier": identifier})
|
354
|
+
|
355
|
+
def get_rate_limit_status(
|
356
|
+
self, identifier: str, limit: Optional[int] = None
|
357
|
+
) -> RateLimitStatus:
|
358
|
+
"""
|
359
|
+
Get current rate limit status for the given identifier.
|
360
|
+
|
361
|
+
This method returns detailed information about the current rate
|
362
|
+
limit status for the specified identifier.
|
363
|
+
|
364
|
+
Args:
|
365
|
+
identifier: Rate limiting identifier (IP, user ID, etc.)
|
366
|
+
Must be a non-empty string identifying the request source.
|
367
|
+
limit: Custom limit for status check. If None, uses default
|
368
|
+
limit from configuration. Must be a positive integer.
|
369
|
+
|
370
|
+
Returns:
|
371
|
+
RateLimitStatus: Current rate limit status containing count,
|
372
|
+
limit, window information, and reset time.
|
373
|
+
|
374
|
+
Raises:
|
375
|
+
ValueError: If identifier is empty or limit is invalid
|
376
|
+
|
377
|
+
Example:
|
378
|
+
>>> rate_limiter = RateLimiter(config)
|
379
|
+
>>> status = rate_limiter.get_rate_limit_status("192.168.1.1")
|
380
|
+
>>> print(f"Current count: {status.current_count}")
|
381
|
+
>>> print(f"Remaining requests: {status.remaining_requests}")
|
382
|
+
"""
|
383
|
+
if not identifier:
|
384
|
+
raise ValueError("Identifier cannot be empty")
|
385
|
+
|
386
|
+
if limit is not None and limit <= 0:
|
387
|
+
raise ValueError("Limit must be a positive integer")
|
388
|
+
|
389
|
+
if not self.config.enabled:
|
390
|
+
# Return default status when rate limiting is disabled
|
391
|
+
now = datetime.now(timezone.utc)
|
392
|
+
return RateLimitStatus(
|
393
|
+
identifier=identifier,
|
394
|
+
current_count=0,
|
395
|
+
limit=limit or self.config.default_requests_per_minute,
|
396
|
+
window_start=now,
|
397
|
+
window_end=now,
|
398
|
+
is_exceeded=False,
|
399
|
+
remaining_requests=limit or self.config.default_requests_per_minute,
|
400
|
+
reset_time=now,
|
401
|
+
window_size_seconds=self.config.window_size_seconds,
|
402
|
+
)
|
403
|
+
|
404
|
+
# Use default limit if not specified
|
405
|
+
if limit is None:
|
406
|
+
limit = self.config.default_requests_per_minute * self.config.burst_limit
|
407
|
+
|
408
|
+
with self._lock:
|
409
|
+
entry = self._get_or_create_entry(identifier, limit)
|
410
|
+
|
411
|
+
# Check if window needs to be reset
|
412
|
+
if entry.is_expired():
|
413
|
+
entry.reset_window()
|
414
|
+
self.logger.debug(
|
415
|
+
"Rate limit window reset during status check",
|
416
|
+
extra={"identifier": identifier, "limit": limit},
|
417
|
+
)
|
418
|
+
|
419
|
+
return entry.get_status()
|
420
|
+
|
421
|
+
def is_exempt(
|
422
|
+
self,
|
423
|
+
identifier: str,
|
424
|
+
path: Optional[str] = None,
|
425
|
+
roles: Optional[Set[str]] = None,
|
426
|
+
) -> bool:
|
427
|
+
"""
|
428
|
+
Check if the identifier is exempt from rate limiting.
|
429
|
+
|
430
|
+
This method checks if the identifier, path, or roles are exempt
|
431
|
+
from rate limiting based on configuration.
|
432
|
+
|
433
|
+
Args:
|
434
|
+
identifier: Rate limiting identifier (IP, user ID, etc.)
|
435
|
+
Must be a non-empty string identifying the request source.
|
436
|
+
path: Request path to check for exemption
|
437
|
+
Optional path to check against exempt_paths configuration.
|
438
|
+
roles: User roles to check for exemption
|
439
|
+
Optional set of roles to check against exempt_roles configuration.
|
440
|
+
|
441
|
+
Returns:
|
442
|
+
bool: True if exempt from rate limiting, False otherwise.
|
443
|
+
Returns True when the request should bypass rate limiting.
|
444
|
+
|
445
|
+
Example:
|
446
|
+
>>> rate_limiter = RateLimiter(config)
|
447
|
+
>>> is_exempt = rate_limiter.is_exempt("192.168.1.1", "/health")
|
448
|
+
>>> if is_exempt:
|
449
|
+
... # Skip rate limiting
|
450
|
+
... pass
|
451
|
+
"""
|
452
|
+
if not identifier:
|
453
|
+
return False
|
454
|
+
|
455
|
+
# Check exempt paths
|
456
|
+
if path and path in self.config.exempt_paths:
|
457
|
+
self.logger.debug(
|
458
|
+
"Rate limit exempt due to path",
|
459
|
+
extra={"identifier": identifier, "path": path},
|
460
|
+
)
|
461
|
+
return True
|
462
|
+
|
463
|
+
# Check exempt roles
|
464
|
+
if roles and any(role in self.config.exempt_roles for role in roles):
|
465
|
+
self.logger.debug(
|
466
|
+
"Rate limit exempt due to role",
|
467
|
+
extra={"identifier": identifier, "roles": list(roles)},
|
468
|
+
)
|
469
|
+
return True
|
470
|
+
|
471
|
+
return False
|
472
|
+
|
473
|
+
def cleanup_expired_entries(self) -> int:
|
474
|
+
"""
|
475
|
+
Clean up expired rate limit entries.
|
476
|
+
|
477
|
+
This method removes all expired rate limit entries to prevent
|
478
|
+
memory leaks and maintain performance.
|
479
|
+
|
480
|
+
Returns:
|
481
|
+
int: Number of entries cleaned up.
|
482
|
+
Returns the count of removed expired entries.
|
483
|
+
|
484
|
+
Example:
|
485
|
+
>>> rate_limiter = RateLimiter(config)
|
486
|
+
>>> cleaned_count = rate_limiter.cleanup_expired_entries()
|
487
|
+
>>> print(f"Cleaned up {cleaned_count} expired entries")
|
488
|
+
"""
|
489
|
+
if not self.config.enabled:
|
490
|
+
return 0
|
491
|
+
|
492
|
+
with self._lock:
|
493
|
+
expired_identifiers = [
|
494
|
+
identifier
|
495
|
+
for identifier, entry in self._entries.items()
|
496
|
+
if entry.is_expired()
|
497
|
+
]
|
498
|
+
|
499
|
+
for identifier in expired_identifiers:
|
500
|
+
del self._entries[identifier]
|
501
|
+
|
502
|
+
if expired_identifiers:
|
503
|
+
self.logger.info(
|
504
|
+
"Cleaned up expired rate limit entries",
|
505
|
+
extra={
|
506
|
+
"cleaned_count": len(expired_identifiers),
|
507
|
+
"remaining_entries": len(self._entries),
|
508
|
+
},
|
509
|
+
)
|
510
|
+
|
511
|
+
return len(expired_identifiers)
|
512
|
+
|
513
|
+
def get_statistics(self) -> Dict[str, any]:
|
514
|
+
"""
|
515
|
+
Get rate limiter statistics.
|
516
|
+
|
517
|
+
This method returns comprehensive statistics about the rate limiter
|
518
|
+
including entry counts, memory usage, and performance metrics.
|
519
|
+
|
520
|
+
Returns:
|
521
|
+
Dict[str, any]: Rate limiter statistics containing entry counts,
|
522
|
+
memory usage, and performance information.
|
523
|
+
|
524
|
+
Example:
|
525
|
+
>>> rate_limiter = RateLimiter(config)
|
526
|
+
>>> stats = rate_limiter.get_statistics()
|
527
|
+
>>> print(f"Active entries: {stats['active_entries']}")
|
528
|
+
"""
|
529
|
+
with self._lock:
|
530
|
+
active_entries = len(self._entries)
|
531
|
+
expired_entries = sum(
|
532
|
+
1 for entry in self._entries.values() if entry.is_expired()
|
533
|
+
)
|
534
|
+
|
535
|
+
return {
|
536
|
+
"enabled": self.config.enabled,
|
537
|
+
"active_entries": active_entries,
|
538
|
+
"expired_entries": expired_entries,
|
539
|
+
"total_entries": active_entries + expired_entries,
|
540
|
+
"default_requests_per_minute": self.config.default_requests_per_minute,
|
541
|
+
"window_size_seconds": self.config.window_size_seconds,
|
542
|
+
"storage_backend": self.config.storage_backend,
|
543
|
+
"cleanup_interval": self.config.cleanup_interval,
|
544
|
+
}
|
545
|
+
|
546
|
+
def _get_or_create_entry(self, identifier: str, limit: int) -> RateLimitEntry:
|
547
|
+
"""
|
548
|
+
Get or create a rate limit entry for the identifier.
|
549
|
+
|
550
|
+
Args:
|
551
|
+
identifier: Rate limiting identifier
|
552
|
+
limit: Request limit
|
553
|
+
|
554
|
+
Returns:
|
555
|
+
RateLimitEntry: Rate limit entry for the identifier
|
556
|
+
"""
|
557
|
+
if identifier not in self._entries:
|
558
|
+
self._entries[identifier] = RateLimitEntry(
|
559
|
+
identifier, limit, self.config.window_size_seconds
|
560
|
+
)
|
561
|
+
|
562
|
+
return self._entries[identifier]
|
563
|
+
|
564
|
+
def _start_cleanup_thread(self) -> None:
|
565
|
+
"""Start background cleanup thread."""
|
566
|
+
if self._cleanup_thread is not None:
|
567
|
+
return
|
568
|
+
|
569
|
+
self._stop_cleanup = False
|
570
|
+
self._cleanup_thread = threading.Thread(
|
571
|
+
target=self._cleanup_worker, daemon=True, name="RateLimiterCleanup"
|
572
|
+
)
|
573
|
+
self._cleanup_thread.start()
|
574
|
+
|
575
|
+
self.logger.info(
|
576
|
+
"Started rate limiter cleanup thread",
|
577
|
+
extra={"cleanup_interval": self.config.cleanup_interval},
|
578
|
+
)
|
579
|
+
|
580
|
+
def _cleanup_worker(self) -> None:
|
581
|
+
"""Background cleanup worker thread."""
|
582
|
+
while not self._stop_cleanup:
|
583
|
+
try:
|
584
|
+
time.sleep(self.config.cleanup_interval)
|
585
|
+
if not self._stop_cleanup:
|
586
|
+
self.cleanup_expired_entries()
|
587
|
+
except Exception as e:
|
588
|
+
self.logger.error(
|
589
|
+
"Error in cleanup worker", extra={"error": str(e)}, exc_info=True
|
590
|
+
)
|
591
|
+
|
592
|
+
def stop_cleanup(self) -> None:
|
593
|
+
"""Stop background cleanup thread."""
|
594
|
+
self._stop_cleanup = True
|
595
|
+
if self._cleanup_thread and self._cleanup_thread.is_alive():
|
596
|
+
self._cleanup_thread.join(timeout=5)
|
597
|
+
|
598
|
+
self.logger.info("Stopped rate limiter cleanup thread")
|
599
|
+
|
600
|
+
def __del__(self):
|
601
|
+
"""Cleanup on destruction."""
|
602
|
+
self.stop_cleanup()
|