mcp-proxy-adapter 6.0.0__py3-none-any.whl → 6.0.1__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_proxy_adapter/__main__.py +27 -7
- mcp_proxy_adapter/api/app.py +209 -79
- mcp_proxy_adapter/api/handlers.py +16 -5
- mcp_proxy_adapter/api/middleware/__init__.py +14 -9
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
- mcp_proxy_adapter/api/middleware/factory.py +36 -12
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +84 -18
- mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -0
- mcp_proxy_adapter/commands/__init__.py +7 -1
- mcp_proxy_adapter/commands/base.py +7 -4
- mcp_proxy_adapter/commands/builtin_commands.py +8 -2
- mcp_proxy_adapter/commands/command_registry.py +8 -0
- mcp_proxy_adapter/commands/echo_command.py +81 -0
- mcp_proxy_adapter/commands/health_command.py +1 -1
- mcp_proxy_adapter/commands/help_command.py +21 -14
- mcp_proxy_adapter/commands/proxy_registration_command.py +326 -185
- mcp_proxy_adapter/commands/role_test_command.py +141 -0
- mcp_proxy_adapter/commands/security_command.py +488 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +234 -351
- mcp_proxy_adapter/commands/token_management_command.py +1 -1
- mcp_proxy_adapter/config.py +323 -40
- mcp_proxy_adapter/core/app_factory.py +410 -0
- mcp_proxy_adapter/core/app_runner.py +272 -0
- mcp_proxy_adapter/core/certificate_utils.py +291 -73
- mcp_proxy_adapter/core/client.py +574 -0
- mcp_proxy_adapter/core/client_manager.py +284 -0
- mcp_proxy_adapter/core/client_security.py +384 -0
- mcp_proxy_adapter/core/logging.py +8 -3
- mcp_proxy_adapter/core/mtls_asgi.py +156 -0
- mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
- mcp_proxy_adapter/core/protocol_manager.py +169 -10
- mcp_proxy_adapter/core/proxy_client.py +602 -0
- mcp_proxy_adapter/core/proxy_registration.py +299 -47
- mcp_proxy_adapter/core/security_adapter.py +12 -15
- mcp_proxy_adapter/core/security_integration.py +286 -0
- mcp_proxy_adapter/core/server_adapter.py +282 -0
- mcp_proxy_adapter/core/server_engine.py +270 -0
- mcp_proxy_adapter/core/ssl_utils.py +13 -12
- mcp_proxy_adapter/core/transport_manager.py +5 -5
- mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
- mcp_proxy_adapter/examples/__init__.py +13 -4
- mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/commands/__init__.py +5 -0
- mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
- mcp_proxy_adapter/examples/debug_request_state.py +112 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
- mcp_proxy_adapter/examples/demo_client.py +275 -0
- mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
- mcp_proxy_adapter/examples/generate_certificates.py +177 -0
- mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
- mcp_proxy_adapter/examples/run_example.py +59 -0
- mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
- mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
- mcp_proxy_adapter/examples/run_security_tests.py +544 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
- mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
- mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
- mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/security_test_client.py +782 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
- mcp_proxy_adapter/examples/test_config.py +148 -0
- mcp_proxy_adapter/examples/test_config_generator.py +86 -0
- mcp_proxy_adapter/examples/test_examples.py +281 -0
- mcp_proxy_adapter/examples/universal_client.py +620 -0
- mcp_proxy_adapter/main.py +66 -148
- mcp_proxy_adapter/utils/config_generator.py +1008 -0
- mcp_proxy_adapter/version.py +5 -2
- mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
- mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
- mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/licenses/LICENSE +2 -2
- mcp_proxy_adapter/api/middleware/auth.py +0 -146
- mcp_proxy_adapter/api/middleware/auth_adapter.py +0 -235
- mcp_proxy_adapter/api/middleware/mtls_adapter.py +0 -305
- mcp_proxy_adapter/api/middleware/mtls_middleware.py +0 -296
- mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
- mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +0 -241
- mcp_proxy_adapter/api/middleware/roles_adapter.py +0 -365
- mcp_proxy_adapter/api/middleware/roles_middleware.py +0 -381
- mcp_proxy_adapter/api/middleware/security.py +0 -376
- mcp_proxy_adapter/api/middleware/token_auth_middleware.py +0 -261
- mcp_proxy_adapter/examples/README.md +0 -124
- mcp_proxy_adapter/examples/basic_server/README.md +0 -60
- mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
- mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
- mcp_proxy_adapter/examples/basic_server/config.json +0 -70
- mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +0 -54
- mcp_proxy_adapter/examples/basic_server/config_http.json +0 -70
- mcp_proxy_adapter/examples/basic_server/config_http_only.json +0 -52
- mcp_proxy_adapter/examples/basic_server/config_https.json +0 -58
- mcp_proxy_adapter/examples/basic_server/config_mtls.json +0 -58
- mcp_proxy_adapter/examples/basic_server/config_ssl.json +0 -46
- mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
- mcp_proxy_adapter/examples/basic_server/server.py +0 -114
- mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
- mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -566
- mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
- mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +0 -105
- mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +0 -129
- mcp_proxy_adapter/examples/custom_commands/config.json +0 -118
- mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +0 -46
- mcp_proxy_adapter/examples/custom_commands/config_https_only.json +0 -46
- mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +0 -33
- mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +0 -46
- mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +0 -33
- mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +0 -33
- mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
- mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
- mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
- mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
- mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
- mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
- mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
- mcp_proxy_adapter/examples/custom_commands/full_help_response.json +0 -1
- mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +0 -629
- mcp_proxy_adapter/examples/custom_commands/get_openapi.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
- mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
- mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +0 -129
- mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +0 -278
- mcp_proxy_adapter/examples/custom_commands/server.py +0 -252
- mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +0 -75
- mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +0 -299
- mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +0 -278
- mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
- mcp_proxy_adapter/examples/custom_commands/test_openapi.py +0 -27
- mcp_proxy_adapter/examples/custom_commands/test_registry.py +0 -23
- mcp_proxy_adapter/examples/custom_commands/test_simple.py +0 -19
- mcp_proxy_adapter/examples/custom_project_example/README.md +0 -103
- mcp_proxy_adapter/examples/custom_project_example/README_EN.md +0 -103
- mcp_proxy_adapter/examples/deployment/README.md +0 -49
- mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
- mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
- mcp_proxy_adapter/examples/deployment/config.json +0 -29
- mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
- mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
- mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
- mcp_proxy_adapter/examples/deployment/run.sh +0 -43
- mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
- mcp_proxy_adapter/examples/simple_custom_commands/README.md +0 -149
- mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +0 -149
- mcp_proxy_adapter/schemas/base_schema.json +0 -114
- mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
- mcp_proxy_adapter/schemas/roles_schema.json +0 -162
- mcp_proxy_adapter/tests/__init__.py +0 -0
- mcp_proxy_adapter/tests/api/__init__.py +0 -3
- mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
- mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
- mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
- mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
- mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
- mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
- mcp_proxy_adapter/tests/commands/__init__.py +0 -3
- mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
- mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
- mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
- mcp_proxy_adapter/tests/conftest.py +0 -131
- mcp_proxy_adapter/tests/functional/__init__.py +0 -3
- mcp_proxy_adapter/tests/functional/test_api.py +0 -253
- mcp_proxy_adapter/tests/integration/__init__.py +0 -3
- mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
- mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
- mcp_proxy_adapter/tests/performance/__init__.py +0 -3
- mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
- mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
- mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
- mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
- mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
- mcp_proxy_adapter/tests/test_base_command.py +0 -123
- mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
- mcp_proxy_adapter/tests/test_command_registry.py +0 -281
- mcp_proxy_adapter/tests/test_config.py +0 -127
- mcp_proxy_adapter/tests/test_utils.py +0 -65
- mcp_proxy_adapter/tests/unit/__init__.py +0 -3
- mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
- mcp_proxy_adapter/tests/unit/test_config.py +0 -270
- mcp_proxy_adapter-6.0.0.dist-info/METADATA +0 -201
- mcp_proxy_adapter-6.0.0.dist-info/RECORD +0 -179
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/top_level.txt +0 -0
@@ -1,152 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Middleware for rate limiting.
|
3
|
-
"""
|
4
|
-
|
5
|
-
import time
|
6
|
-
from typing import Dict, List, Callable, Awaitable
|
7
|
-
from collections import defaultdict
|
8
|
-
|
9
|
-
from fastapi import Request, Response
|
10
|
-
from starlette.responses import JSONResponse
|
11
|
-
|
12
|
-
from mcp_proxy_adapter.core.logging import logger
|
13
|
-
from .base import BaseMiddleware
|
14
|
-
|
15
|
-
class RateLimitMiddleware(BaseMiddleware):
|
16
|
-
"""
|
17
|
-
Middleware for limiting request rate.
|
18
|
-
"""
|
19
|
-
|
20
|
-
def __init__(self, app, rate_limit: int = 100, time_window: int = 60,
|
21
|
-
by_ip: bool = True, by_user: bool = True,
|
22
|
-
public_paths: List[str] = None):
|
23
|
-
"""
|
24
|
-
Initializes middleware for rate limiting.
|
25
|
-
|
26
|
-
Args:
|
27
|
-
app: FastAPI application
|
28
|
-
rate_limit: Maximum number of requests in the specified time period
|
29
|
-
time_window: Time period in seconds
|
30
|
-
by_ip: Limit requests by IP address
|
31
|
-
by_user: Limit requests by user
|
32
|
-
public_paths: List of paths for which rate limiting is not applied
|
33
|
-
"""
|
34
|
-
super().__init__(app)
|
35
|
-
self.rate_limit = rate_limit
|
36
|
-
self.time_window = time_window
|
37
|
-
self.by_ip = by_ip
|
38
|
-
self.by_user = by_user
|
39
|
-
self.public_paths = public_paths or [
|
40
|
-
"/docs",
|
41
|
-
"/redoc",
|
42
|
-
"/openapi.json",
|
43
|
-
"/health"
|
44
|
-
]
|
45
|
-
|
46
|
-
# Storage for requests by IP
|
47
|
-
self.ip_requests = defaultdict(list)
|
48
|
-
|
49
|
-
# Storage for requests by user
|
50
|
-
self.user_requests = defaultdict(list)
|
51
|
-
|
52
|
-
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
53
|
-
"""
|
54
|
-
Processes request and checks rate limit.
|
55
|
-
|
56
|
-
Args:
|
57
|
-
request: Request.
|
58
|
-
call_next: Next handler.
|
59
|
-
|
60
|
-
Returns:
|
61
|
-
Response.
|
62
|
-
"""
|
63
|
-
# Check if path is public
|
64
|
-
path = request.url.path
|
65
|
-
if self._is_public_path(path):
|
66
|
-
# If path is public, skip rate limiting
|
67
|
-
return await call_next(request)
|
68
|
-
|
69
|
-
# Current time
|
70
|
-
current_time = time.time()
|
71
|
-
|
72
|
-
# Get client IP address
|
73
|
-
client_ip = request.client.host if request.client else "unknown"
|
74
|
-
|
75
|
-
# Get user from request state (if any)
|
76
|
-
username = getattr(request.state, "username", None)
|
77
|
-
|
78
|
-
# Check limit by IP
|
79
|
-
if self.by_ip and client_ip != "unknown":
|
80
|
-
# Clean old requests
|
81
|
-
self._clean_old_requests(self.ip_requests[client_ip], current_time)
|
82
|
-
|
83
|
-
# Check number of requests
|
84
|
-
if len(self.ip_requests[client_ip]) >= self.rate_limit:
|
85
|
-
logger.warning(f"Rate limit exceeded for IP: {client_ip} | Path: {path}")
|
86
|
-
return self._create_error_response("Rate limit exceeded", 429)
|
87
|
-
|
88
|
-
# Add current request
|
89
|
-
self.ip_requests[client_ip].append(current_time)
|
90
|
-
|
91
|
-
# Check limit by user
|
92
|
-
if self.by_user and username:
|
93
|
-
# Clean old requests
|
94
|
-
self._clean_old_requests(self.user_requests[username], current_time)
|
95
|
-
|
96
|
-
# Check number of requests
|
97
|
-
if len(self.user_requests[username]) >= self.rate_limit:
|
98
|
-
logger.warning(f"Rate limit exceeded for user: {username} | Path: {path}")
|
99
|
-
return self._create_error_response("Rate limit exceeded", 429)
|
100
|
-
|
101
|
-
# Add current request
|
102
|
-
self.user_requests[username].append(current_time)
|
103
|
-
|
104
|
-
# Call the next middleware or main handler
|
105
|
-
return await call_next(request)
|
106
|
-
|
107
|
-
def _clean_old_requests(self, requests: List[float], current_time: float) -> None:
|
108
|
-
"""
|
109
|
-
Cleans old requests that are outside the time window.
|
110
|
-
|
111
|
-
Args:
|
112
|
-
requests: List of request timestamps.
|
113
|
-
current_time: Current time.
|
114
|
-
"""
|
115
|
-
min_time = current_time - self.time_window
|
116
|
-
while requests and requests[0] < min_time:
|
117
|
-
requests.pop(0)
|
118
|
-
|
119
|
-
def _is_public_path(self, path: str) -> bool:
|
120
|
-
"""
|
121
|
-
Checks if the path is public.
|
122
|
-
|
123
|
-
Args:
|
124
|
-
path: Path to check.
|
125
|
-
|
126
|
-
Returns:
|
127
|
-
True if path is public, False otherwise.
|
128
|
-
"""
|
129
|
-
return any(path.startswith(public_path) for public_path in self.public_paths)
|
130
|
-
|
131
|
-
def _create_error_response(self, message: str, status_code: int) -> Response:
|
132
|
-
"""
|
133
|
-
Creates error response in JSON-RPC format.
|
134
|
-
|
135
|
-
Args:
|
136
|
-
message: Error message.
|
137
|
-
status_code: HTTP status code.
|
138
|
-
|
139
|
-
Returns:
|
140
|
-
JSON response with error.
|
141
|
-
"""
|
142
|
-
return JSONResponse(
|
143
|
-
status_code=status_code,
|
144
|
-
content={
|
145
|
-
"jsonrpc": "2.0",
|
146
|
-
"error": {
|
147
|
-
"code": -32000,
|
148
|
-
"message": message
|
149
|
-
},
|
150
|
-
"id": None
|
151
|
-
}
|
152
|
-
)
|
@@ -1,241 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Rate Limit Middleware Adapter for backward compatibility.
|
3
|
-
|
4
|
-
This module provides an adapter that maintains the same interface as RateLimitMiddleware
|
5
|
-
while using the new SecurityMiddleware internally.
|
6
|
-
"""
|
7
|
-
|
8
|
-
import time
|
9
|
-
from typing import Dict, List, Callable, Awaitable, Any
|
10
|
-
from collections import defaultdict
|
11
|
-
|
12
|
-
from fastapi import Request, Response
|
13
|
-
from starlette.responses import JSONResponse
|
14
|
-
|
15
|
-
from mcp_proxy_adapter.core.logging import logger
|
16
|
-
from .base import BaseMiddleware
|
17
|
-
from .security import SecurityMiddleware
|
18
|
-
|
19
|
-
|
20
|
-
class RateLimitMiddlewareAdapter(BaseMiddleware):
|
21
|
-
"""
|
22
|
-
Adapter for RateLimitMiddleware that uses SecurityMiddleware internally.
|
23
|
-
|
24
|
-
Maintains the same interface as the original RateLimitMiddleware for backward compatibility.
|
25
|
-
"""
|
26
|
-
|
27
|
-
def __init__(self, app, rate_limit: int = 100, time_window: int = 60,
|
28
|
-
by_ip: bool = True, by_user: bool = True,
|
29
|
-
public_paths: List[str] = None):
|
30
|
-
"""
|
31
|
-
Initialize rate limit middleware adapter.
|
32
|
-
|
33
|
-
Args:
|
34
|
-
app: FastAPI application
|
35
|
-
rate_limit: Maximum number of requests in the specified time period
|
36
|
-
time_window: Time period in seconds
|
37
|
-
by_ip: Limit requests by IP address
|
38
|
-
by_user: Limit requests by user
|
39
|
-
public_paths: List of paths for which rate limiting is not applied
|
40
|
-
"""
|
41
|
-
super().__init__(app)
|
42
|
-
|
43
|
-
# Store original parameters for backward compatibility
|
44
|
-
self.rate_limit = rate_limit
|
45
|
-
self.time_window = time_window
|
46
|
-
self.by_ip = by_ip
|
47
|
-
self.by_user = by_user
|
48
|
-
self.public_paths = public_paths or [
|
49
|
-
"/docs",
|
50
|
-
"/redoc",
|
51
|
-
"/openapi.json",
|
52
|
-
"/health"
|
53
|
-
]
|
54
|
-
|
55
|
-
# Legacy storage for backward compatibility
|
56
|
-
self.ip_requests = defaultdict(list)
|
57
|
-
self.user_requests = defaultdict(list)
|
58
|
-
|
59
|
-
# Create internal security middleware
|
60
|
-
self.security_middleware = self._create_security_middleware()
|
61
|
-
|
62
|
-
logger.info(f"RateLimitMiddlewareAdapter initialized: rate_limit={rate_limit}, "
|
63
|
-
f"time_window={time_window}, by_ip={by_ip}, by_user={by_user}")
|
64
|
-
|
65
|
-
def _create_security_middleware(self) -> SecurityMiddleware:
|
66
|
-
"""
|
67
|
-
Create internal SecurityMiddleware with RateLimitMiddleware configuration.
|
68
|
-
|
69
|
-
Returns:
|
70
|
-
SecurityMiddleware instance
|
71
|
-
"""
|
72
|
-
# Convert RateLimitMiddleware config to SecurityMiddleware config
|
73
|
-
security_config = {
|
74
|
-
"security": {
|
75
|
-
"enabled": True,
|
76
|
-
"auth": {
|
77
|
-
"enabled": False
|
78
|
-
},
|
79
|
-
"ssl": {
|
80
|
-
"enabled": False
|
81
|
-
},
|
82
|
-
"permissions": {
|
83
|
-
"enabled": False
|
84
|
-
},
|
85
|
-
"rate_limit": {
|
86
|
-
"enabled": True,
|
87
|
-
"requests_per_minute": self.rate_limit,
|
88
|
-
"requests_per_hour": self.rate_limit * 60,
|
89
|
-
"burst_limit": self.rate_limit // 10,
|
90
|
-
"by_ip": self.by_ip,
|
91
|
-
"by_user": self.by_user
|
92
|
-
},
|
93
|
-
"public_paths": self.public_paths
|
94
|
-
}
|
95
|
-
}
|
96
|
-
|
97
|
-
return SecurityMiddleware(self.app, security_config)
|
98
|
-
|
99
|
-
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
100
|
-
"""
|
101
|
-
Process request using internal SecurityMiddleware with legacy fallback.
|
102
|
-
|
103
|
-
Args:
|
104
|
-
request: Request object
|
105
|
-
call_next: Next handler
|
106
|
-
|
107
|
-
Returns:
|
108
|
-
Response object
|
109
|
-
"""
|
110
|
-
# Check if path is public
|
111
|
-
path = request.url.path
|
112
|
-
if self._is_public_path(path):
|
113
|
-
return await call_next(request)
|
114
|
-
|
115
|
-
# Try to use SecurityMiddleware first
|
116
|
-
try:
|
117
|
-
await self.security_middleware.before_request(request)
|
118
|
-
return await call_next(request)
|
119
|
-
|
120
|
-
except Exception as e:
|
121
|
-
# Fallback to legacy rate limiting if SecurityMiddleware fails
|
122
|
-
logger.warning(f"SecurityMiddleware rate limiting failed, using legacy fallback: {e}")
|
123
|
-
return await self._legacy_rate_limit_check(request, call_next)
|
124
|
-
|
125
|
-
async def _legacy_rate_limit_check(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
126
|
-
"""
|
127
|
-
Legacy rate limiting implementation as fallback.
|
128
|
-
|
129
|
-
Args:
|
130
|
-
request: Request object
|
131
|
-
call_next: Next handler
|
132
|
-
|
133
|
-
Returns:
|
134
|
-
Response object
|
135
|
-
"""
|
136
|
-
current_time = time.time()
|
137
|
-
client_ip = request.client.host if request.client else "unknown"
|
138
|
-
username = getattr(request.state, "username", None)
|
139
|
-
|
140
|
-
# Check limit by IP
|
141
|
-
if self.by_ip and client_ip != "unknown":
|
142
|
-
self._clean_old_requests(self.ip_requests[client_ip], current_time)
|
143
|
-
|
144
|
-
if len(self.ip_requests[client_ip]) >= self.rate_limit:
|
145
|
-
logger.warning(f"Rate limit exceeded for IP: {client_ip}")
|
146
|
-
return self._create_error_response("Rate limit exceeded", 429)
|
147
|
-
|
148
|
-
self.ip_requests[client_ip].append(current_time)
|
149
|
-
|
150
|
-
# Check limit by user
|
151
|
-
if self.by_user and username:
|
152
|
-
self._clean_old_requests(self.user_requests[username], current_time)
|
153
|
-
|
154
|
-
if len(self.user_requests[username]) >= self.rate_limit:
|
155
|
-
logger.warning(f"Rate limit exceeded for user: {username}")
|
156
|
-
return self._create_error_response("Rate limit exceeded", 429)
|
157
|
-
|
158
|
-
self.user_requests[username].append(current_time)
|
159
|
-
|
160
|
-
return await call_next(request)
|
161
|
-
|
162
|
-
def _clean_old_requests(self, requests_list: List[float], current_time: float) -> None:
|
163
|
-
"""
|
164
|
-
Remove old requests from the list.
|
165
|
-
|
166
|
-
Args:
|
167
|
-
requests_list: List of request timestamps
|
168
|
-
current_time: Current time
|
169
|
-
"""
|
170
|
-
cutoff_time = current_time - self.time_window
|
171
|
-
requests_list[:] = [req_time for req_time in requests_list if req_time > cutoff_time]
|
172
|
-
|
173
|
-
def _is_public_path(self, path: str) -> bool:
|
174
|
-
"""
|
175
|
-
Check if the path is public (doesn't require rate limiting).
|
176
|
-
|
177
|
-
Args:
|
178
|
-
path: Request path
|
179
|
-
|
180
|
-
Returns:
|
181
|
-
True if path is public, False otherwise
|
182
|
-
"""
|
183
|
-
return any(path.startswith(public_path) for public_path in self.public_paths)
|
184
|
-
|
185
|
-
def _create_error_response(self, message: str, status_code: int) -> JSONResponse:
|
186
|
-
"""
|
187
|
-
Create error response in RateLimitMiddleware format.
|
188
|
-
|
189
|
-
Args:
|
190
|
-
message: Error message
|
191
|
-
status_code: HTTP status code
|
192
|
-
|
193
|
-
Returns:
|
194
|
-
JSONResponse with error
|
195
|
-
"""
|
196
|
-
return JSONResponse(
|
197
|
-
status_code=status_code,
|
198
|
-
content={
|
199
|
-
"jsonrpc": "2.0",
|
200
|
-
"error": {
|
201
|
-
"code": -32008 if status_code == 429 else -32603,
|
202
|
-
"message": message,
|
203
|
-
"data": {
|
204
|
-
"rate_limit": self.rate_limit,
|
205
|
-
"time_window": self.time_window,
|
206
|
-
"status_code": status_code
|
207
|
-
}
|
208
|
-
},
|
209
|
-
"id": None
|
210
|
-
}
|
211
|
-
)
|
212
|
-
|
213
|
-
def get_rate_limit_info(self, request: Request) -> Dict[str, Any]:
|
214
|
-
"""
|
215
|
-
Get rate limit information for the request (backward compatibility).
|
216
|
-
|
217
|
-
Args:
|
218
|
-
request: Request object
|
219
|
-
|
220
|
-
Returns:
|
221
|
-
Dictionary with rate limit information
|
222
|
-
"""
|
223
|
-
client_ip = request.client.host if request.client else "unknown"
|
224
|
-
username = getattr(request.state, "username", None)
|
225
|
-
|
226
|
-
info = {
|
227
|
-
"rate_limit": self.rate_limit,
|
228
|
-
"time_window": self.time_window,
|
229
|
-
"by_ip": self.by_ip,
|
230
|
-
"by_user": self.by_user
|
231
|
-
}
|
232
|
-
|
233
|
-
if self.by_ip and client_ip != "unknown":
|
234
|
-
info["ip_requests"] = len(self.ip_requests[client_ip])
|
235
|
-
info["ip_remaining"] = max(0, self.rate_limit - len(self.ip_requests[client_ip]))
|
236
|
-
|
237
|
-
if self.by_user and username:
|
238
|
-
info["user_requests"] = len(self.user_requests[username])
|
239
|
-
info["user_remaining"] = max(0, self.rate_limit - len(self.user_requests[username]))
|
240
|
-
|
241
|
-
return info
|