mcp-proxy-adapter 4.1.1__py3-none-any.whl → 6.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_proxy_adapter/__main__.py +12 -0
- mcp_proxy_adapter/api/app.py +254 -33
- mcp_proxy_adapter/api/handlers.py +32 -6
- mcp_proxy_adapter/api/middleware/__init__.py +36 -30
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
- mcp_proxy_adapter/api/middleware/factory.py +243 -0
- mcp_proxy_adapter/api/middleware/logging.py +32 -6
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +135 -0
- mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
- mcp_proxy_adapter/api/middleware/unified_security.py +152 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +83 -0
- mcp_proxy_adapter/commands/__init__.py +19 -4
- mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
- mcp_proxy_adapter/commands/base.py +66 -32
- mcp_proxy_adapter/commands/builtin_commands.py +95 -0
- mcp_proxy_adapter/commands/catalog_manager.py +838 -0
- mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
- mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
- mcp_proxy_adapter/commands/command_registry.py +711 -354
- mcp_proxy_adapter/commands/dependency_manager.py +245 -0
- mcp_proxy_adapter/commands/echo_command.py +81 -0
- mcp_proxy_adapter/commands/health_command.py +7 -0
- mcp_proxy_adapter/commands/help_command.py +21 -14
- mcp_proxy_adapter/commands/hooks.py +200 -167
- mcp_proxy_adapter/commands/key_management_command.py +506 -0
- mcp_proxy_adapter/commands/load_command.py +176 -0
- mcp_proxy_adapter/commands/plugins_command.py +235 -0
- mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
- mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
- mcp_proxy_adapter/commands/reload_command.py +48 -50
- mcp_proxy_adapter/commands/result.py +1 -0
- mcp_proxy_adapter/commands/role_test_command.py +141 -0
- mcp_proxy_adapter/commands/roles_management_command.py +697 -0
- mcp_proxy_adapter/commands/security_command.py +488 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +483 -0
- mcp_proxy_adapter/commands/token_management_command.py +529 -0
- mcp_proxy_adapter/commands/transport_management_command.py +144 -0
- mcp_proxy_adapter/commands/unload_command.py +158 -0
- mcp_proxy_adapter/config.py +159 -2
- mcp_proxy_adapter/core/app_factory.py +326 -0
- mcp_proxy_adapter/core/auth_validator.py +606 -0
- mcp_proxy_adapter/core/certificate_utils.py +827 -0
- mcp_proxy_adapter/core/client_security.py +384 -0
- mcp_proxy_adapter/core/config_converter.py +405 -0
- mcp_proxy_adapter/core/config_validator.py +218 -0
- mcp_proxy_adapter/core/logging.py +19 -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 +235 -0
- mcp_proxy_adapter/core/proxy_client.py +602 -0
- mcp_proxy_adapter/core/proxy_registration.py +522 -0
- mcp_proxy_adapter/core/role_utils.py +426 -0
- mcp_proxy_adapter/core/security_adapter.py +370 -0
- mcp_proxy_adapter/core/security_factory.py +239 -0
- mcp_proxy_adapter/core/security_integration.py +277 -0
- mcp_proxy_adapter/core/server_adapter.py +345 -0
- mcp_proxy_adapter/core/server_engine.py +364 -0
- mcp_proxy_adapter/core/settings.py +1 -0
- mcp_proxy_adapter/core/ssl_utils.py +233 -0
- mcp_proxy_adapter/core/transport_manager.py +292 -0
- mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
- mcp_proxy_adapter/custom_openapi.py +22 -11
- mcp_proxy_adapter/examples/README.md +230 -97
- mcp_proxy_adapter/examples/README_EN.md +258 -0
- mcp_proxy_adapter/examples/SECURITY_TESTING.md +455 -0
- mcp_proxy_adapter/examples/__pycache__/security_configurations.cpython-312.pyc +0 -0
- mcp_proxy_adapter/examples/__pycache__/security_test_client.cpython-312.pyc +0 -0
- mcp_proxy_adapter/examples/basic_framework/configs/http_auth.json +37 -0
- mcp_proxy_adapter/examples/basic_framework/configs/http_simple.json +23 -0
- mcp_proxy_adapter/examples/basic_framework/configs/https_auth.json +39 -0
- mcp_proxy_adapter/examples/basic_framework/configs/https_simple.json +25 -0
- mcp_proxy_adapter/examples/basic_framework/configs/mtls_no_roles.json +39 -0
- mcp_proxy_adapter/examples/basic_framework/configs/mtls_with_roles.json +45 -0
- mcp_proxy_adapter/examples/basic_framework/main.py +63 -0
- mcp_proxy_adapter/examples/basic_framework/roles.json +21 -0
- mcp_proxy_adapter/examples/cert_config.json +9 -0
- mcp_proxy_adapter/examples/certs/admin.crt +32 -0
- mcp_proxy_adapter/examples/certs/admin.key +52 -0
- mcp_proxy_adapter/examples/certs/admin_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/admin_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/ca_cert.pem +23 -0
- mcp_proxy_adapter/examples/certs/ca_cert.srl +1 -0
- mcp_proxy_adapter/examples/certs/ca_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/cert_config.json +9 -0
- mcp_proxy_adapter/examples/certs/client.crt +32 -0
- mcp_proxy_adapter/examples/certs/client.key +52 -0
- mcp_proxy_adapter/examples/certs/client_admin.crt +32 -0
- mcp_proxy_adapter/examples/certs/client_admin.key +52 -0
- mcp_proxy_adapter/examples/certs/client_user.crt +32 -0
- mcp_proxy_adapter/examples/certs/client_user.key +52 -0
- mcp_proxy_adapter/examples/certs/guest_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/guest_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/mcp_proxy_adapter_ca_ca.crt +23 -0
- mcp_proxy_adapter/examples/certs/proxy_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/proxy_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/readonly.crt +32 -0
- mcp_proxy_adapter/examples/certs/readonly.key +52 -0
- mcp_proxy_adapter/examples/certs/readonly_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/readonly_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/server.crt +32 -0
- mcp_proxy_adapter/examples/certs/server.key +52 -0
- mcp_proxy_adapter/examples/certs/server_cert.pem +32 -0
- mcp_proxy_adapter/examples/certs/server_key.pem +52 -0
- mcp_proxy_adapter/examples/certs/test_ca_ca.crt +20 -0
- mcp_proxy_adapter/examples/certs/user.crt +32 -0
- mcp_proxy_adapter/examples/certs/user.key +52 -0
- mcp_proxy_adapter/examples/certs/user_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/user_key.pem +28 -0
- mcp_proxy_adapter/examples/client_configs/api_key_client.json +13 -0
- mcp_proxy_adapter/examples/client_configs/basic_auth_client.json +13 -0
- mcp_proxy_adapter/examples/client_configs/certificate_client.json +22 -0
- mcp_proxy_adapter/examples/client_configs/jwt_client.json +15 -0
- mcp_proxy_adapter/examples/client_configs/no_auth_client.json +9 -0
- mcp_proxy_adapter/examples/commands/__init__.py +1 -0
- mcp_proxy_adapter/examples/create_certificates_simple.py +307 -0
- mcp_proxy_adapter/examples/debug_request_state.py +144 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +205 -0
- mcp_proxy_adapter/examples/demo_client.py +341 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +99 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +106 -0
- mcp_proxy_adapter/examples/full_application/configs/http_auth.json +37 -0
- mcp_proxy_adapter/examples/full_application/configs/http_simple.json +23 -0
- mcp_proxy_adapter/examples/full_application/configs/https_auth.json +39 -0
- mcp_proxy_adapter/examples/full_application/configs/https_simple.json +25 -0
- mcp_proxy_adapter/examples/full_application/configs/mtls_no_roles.json +39 -0
- mcp_proxy_adapter/examples/full_application/configs/mtls_with_roles.json +45 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +97 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +95 -0
- mcp_proxy_adapter/examples/full_application/main.py +138 -0
- mcp_proxy_adapter/examples/full_application/roles.json +21 -0
- mcp_proxy_adapter/examples/generate_all_certificates.py +429 -0
- mcp_proxy_adapter/examples/generate_certificates.py +121 -0
- mcp_proxy_adapter/examples/keys/ca_key.pem +28 -0
- mcp_proxy_adapter/examples/keys/mcp_proxy_adapter_ca_ca.key +28 -0
- mcp_proxy_adapter/examples/keys/test_ca_ca.key +28 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log +220 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.1 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.2 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.3 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.4 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.5 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log +220 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.1 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.2 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.3 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.4 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.5 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log +2 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.1 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.2 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.3 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.4 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.5 +1 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +401 -0
- mcp_proxy_adapter/examples/roles.json +38 -0
- mcp_proxy_adapter/examples/run_example.py +81 -0
- mcp_proxy_adapter/examples/run_security_tests.py +326 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +300 -0
- mcp_proxy_adapter/examples/security_test_client.py +743 -0
- mcp_proxy_adapter/examples/server_configs/config_basic_http.json +204 -0
- mcp_proxy_adapter/examples/server_configs/config_http_token.json +238 -0
- mcp_proxy_adapter/examples/server_configs/config_https.json +215 -0
- mcp_proxy_adapter/examples/server_configs/config_https_token.json +231 -0
- mcp_proxy_adapter/examples/server_configs/config_mtls.json +215 -0
- mcp_proxy_adapter/examples/server_configs/config_proxy_registration.json +250 -0
- mcp_proxy_adapter/examples/server_configs/config_simple.json +46 -0
- mcp_proxy_adapter/examples/server_configs/roles.json +38 -0
- mcp_proxy_adapter/examples/test_examples.py +344 -0
- mcp_proxy_adapter/examples/universal_client.py +628 -0
- mcp_proxy_adapter/main.py +186 -0
- mcp_proxy_adapter/utils/config_generator.py +639 -0
- mcp_proxy_adapter/version.py +2 -1
- mcp_proxy_adapter-6.1.0.dist-info/METADATA +205 -0
- mcp_proxy_adapter-6.1.0.dist-info/RECORD +193 -0
- mcp_proxy_adapter-6.1.0.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/licenses/LICENSE +2 -2
- mcp_proxy_adapter/api/middleware/auth.py +0 -146
- mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
- mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
- mcp_proxy_adapter/examples/__init__.py +0 -7
- 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 -35
- mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
- mcp_proxy_adapter/examples/basic_server/server.py +0 -103
- 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 -250
- 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/config.json +0 -35
- 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/hooks.py +0 -230
- mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
- mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
- mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
- 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/schemas/base_schema.json +0 -114
- mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
- 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 -217
- mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
- mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,156 @@
|
|
1
|
+
"""
|
2
|
+
Custom ASGI application for mTLS support.
|
3
|
+
|
4
|
+
This module provides a custom ASGI application that properly handles
|
5
|
+
client certificates in mTLS connections.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import ssl
|
9
|
+
import logging
|
10
|
+
from typing import Dict, Any, Optional
|
11
|
+
from starlette.applications import Starlette
|
12
|
+
from starlette.requests import Request
|
13
|
+
from starlette.responses import Response
|
14
|
+
from starlette.types import ASGIApp, Receive, Send, Scope
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
class MTLSASGIApp:
|
20
|
+
"""
|
21
|
+
Custom ASGI application that properly handles mTLS client certificates.
|
22
|
+
|
23
|
+
This wrapper ensures that client certificates are properly extracted
|
24
|
+
and made available to the FastAPI application.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(self, app: ASGIApp, ssl_config: Dict[str, Any]):
|
28
|
+
"""
|
29
|
+
Initialize MTLS ASGI application.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
app: The underlying ASGI application (FastAPI)
|
33
|
+
ssl_config: SSL configuration for mTLS
|
34
|
+
"""
|
35
|
+
self.app = app
|
36
|
+
self.ssl_config = ssl_config
|
37
|
+
self.verify_client = ssl_config.get("verify_client", False)
|
38
|
+
self.client_cert_required = ssl_config.get("client_cert_required", False)
|
39
|
+
|
40
|
+
logger.info(f"MTLS ASGI app initialized: verify_client={self.verify_client}, "
|
41
|
+
f"client_cert_required={self.client_cert_required}")
|
42
|
+
|
43
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
44
|
+
"""
|
45
|
+
Handle ASGI request with mTLS support.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
scope: ASGI scope
|
49
|
+
receive: ASGI receive callable
|
50
|
+
send: ASGI send callable
|
51
|
+
"""
|
52
|
+
try:
|
53
|
+
# Extract client certificate from SSL context
|
54
|
+
if scope["type"] == "http" and "ssl" in scope:
|
55
|
+
client_cert = self._extract_client_certificate(scope)
|
56
|
+
if client_cert:
|
57
|
+
# Store certificate in scope for middleware access
|
58
|
+
scope["client_certificate"] = client_cert
|
59
|
+
logger.debug(f"Client certificate extracted: {client_cert.get('subject', {})}")
|
60
|
+
elif self.client_cert_required:
|
61
|
+
logger.warning("Client certificate required but not provided")
|
62
|
+
# Return 401 Unauthorized
|
63
|
+
await self._send_unauthorized_response(send)
|
64
|
+
return
|
65
|
+
|
66
|
+
# Call the underlying application
|
67
|
+
await self.app(scope, receive, send)
|
68
|
+
|
69
|
+
except Exception as e:
|
70
|
+
logger.error(f"Error in MTLS ASGI app: {e}")
|
71
|
+
await self._send_error_response(send, str(e))
|
72
|
+
|
73
|
+
def _extract_client_certificate(self, scope: Scope) -> Optional[Dict[str, Any]]:
|
74
|
+
"""
|
75
|
+
Extract client certificate from SSL context.
|
76
|
+
|
77
|
+
Args:
|
78
|
+
scope: ASGI scope
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
Client certificate data or None
|
82
|
+
"""
|
83
|
+
try:
|
84
|
+
ssl_context = scope.get("ssl")
|
85
|
+
if not ssl_context:
|
86
|
+
return None
|
87
|
+
|
88
|
+
# Get peer certificate
|
89
|
+
cert = ssl_context.getpeercert()
|
90
|
+
if cert:
|
91
|
+
return cert
|
92
|
+
|
93
|
+
return None
|
94
|
+
|
95
|
+
except Exception as e:
|
96
|
+
logger.error(f"Failed to extract client certificate: {e}")
|
97
|
+
return None
|
98
|
+
|
99
|
+
async def _send_unauthorized_response(self, send: Send) -> None:
|
100
|
+
"""
|
101
|
+
Send 401 Unauthorized response.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
send: ASGI send callable
|
105
|
+
"""
|
106
|
+
response = {
|
107
|
+
"type": "http.response.start",
|
108
|
+
"status": 401,
|
109
|
+
"headers": [
|
110
|
+
(b"content-type", b"application/json"),
|
111
|
+
(b"content-length", b"163"),
|
112
|
+
],
|
113
|
+
}
|
114
|
+
await send(response)
|
115
|
+
|
116
|
+
body = b'{"jsonrpc": "2.0", "error": {"code": -32001, "message": "Unauthorized: Client certificate required"}, "id": null}'
|
117
|
+
await send({"type": "http.response.body", "body": body})
|
118
|
+
|
119
|
+
async def _send_error_response(self, send: Send, error_message: str) -> None:
|
120
|
+
"""
|
121
|
+
Send error response.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
send: ASGI send callable
|
125
|
+
error_message: Error message
|
126
|
+
"""
|
127
|
+
response = {
|
128
|
+
"type": "http.response.start",
|
129
|
+
"status": 500,
|
130
|
+
"headers": [
|
131
|
+
(b"content-type", b"application/json"),
|
132
|
+
],
|
133
|
+
}
|
134
|
+
await send(response)
|
135
|
+
|
136
|
+
body = f'{{"jsonrpc": "2.0", "error": {{"code": -32603, "message": "Internal error: {error_message}"}}, "id": null}}'.encode()
|
137
|
+
await send({"type": "http.response.body", "body": body})
|
138
|
+
|
139
|
+
|
140
|
+
def create_mtls_asgi_app(app: ASGIApp, ssl_config: Dict[str, Any]) -> ASGIApp:
|
141
|
+
"""
|
142
|
+
Create MTLS-enabled ASGI application.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
app: The underlying ASGI application (FastAPI)
|
146
|
+
ssl_config: SSL configuration for mTLS
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
MTLS-enabled ASGI application
|
150
|
+
"""
|
151
|
+
if ssl_config.get("mode") == "mtls" or ssl_config.get("verify_client", False):
|
152
|
+
logger.info("Creating MTLS-enabled ASGI application")
|
153
|
+
return MTLSASGIApp(app, ssl_config)
|
154
|
+
else:
|
155
|
+
logger.info("Creating standard ASGI application (no mTLS)")
|
156
|
+
return app
|
@@ -0,0 +1,187 @@
|
|
1
|
+
"""
|
2
|
+
MTLS ASGI Application Wrapper
|
3
|
+
|
4
|
+
This module provides an ASGI application wrapper that extracts client certificates
|
5
|
+
from the SSL context and makes them available to FastAPI middleware.
|
6
|
+
|
7
|
+
Author: Vasiliy Zdanovskiy
|
8
|
+
email: vasilyvz@gmail.com
|
9
|
+
Version: 1.0.0
|
10
|
+
"""
|
11
|
+
|
12
|
+
import logging
|
13
|
+
import ssl
|
14
|
+
from typing import Dict, Any, Optional
|
15
|
+
from cryptography import x509
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class MTLSASGIApp:
|
21
|
+
"""
|
22
|
+
ASGI application wrapper for mTLS support.
|
23
|
+
|
24
|
+
Extracts client certificates from SSL context and stores them in ASGI scope
|
25
|
+
for access by FastAPI middleware.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(self, app, ssl_config: Dict[str, Any]):
|
29
|
+
"""
|
30
|
+
Initialize MTLS ASGI app.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
app: The underlying ASGI application
|
34
|
+
ssl_config: SSL configuration dictionary
|
35
|
+
"""
|
36
|
+
self.app = app
|
37
|
+
self.ssl_config = ssl_config
|
38
|
+
self.client_cert_required = ssl_config.get("client_cert_required", True)
|
39
|
+
|
40
|
+
logger.info(f"MTLS ASGI app initialized: client_cert_required={self.client_cert_required}")
|
41
|
+
|
42
|
+
async def __call__(self, scope: Dict[str, Any], receive, send):
|
43
|
+
"""
|
44
|
+
Handle ASGI request with mTLS support.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
scope: ASGI scope dictionary
|
48
|
+
receive: ASGI receive callable
|
49
|
+
send: ASGI send callable
|
50
|
+
"""
|
51
|
+
try:
|
52
|
+
# Extract client certificate from SSL context
|
53
|
+
if scope["type"] == "http" and "ssl" in scope:
|
54
|
+
client_cert = self._extract_client_certificate(scope)
|
55
|
+
if client_cert:
|
56
|
+
# Store certificate in scope for middleware access
|
57
|
+
scope["client_certificate"] = client_cert
|
58
|
+
logger.debug(f"Client certificate extracted: {client_cert.get('subject', {})}")
|
59
|
+
elif self.client_cert_required:
|
60
|
+
logger.warning("Client certificate required but not provided")
|
61
|
+
# Return 401 Unauthorized
|
62
|
+
await self._send_unauthorized_response(send)
|
63
|
+
return
|
64
|
+
|
65
|
+
# Call the underlying application
|
66
|
+
await self.app(scope, receive, send)
|
67
|
+
|
68
|
+
except Exception as e:
|
69
|
+
logger.error(f"Error in MTLS ASGI app: {e}")
|
70
|
+
await self._send_error_response(send, str(e))
|
71
|
+
|
72
|
+
def _extract_client_certificate(self, scope: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
73
|
+
"""
|
74
|
+
Extract client certificate from SSL context.
|
75
|
+
|
76
|
+
Args:
|
77
|
+
scope: ASGI scope dictionary
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
Certificate dictionary or None if not found
|
81
|
+
"""
|
82
|
+
try:
|
83
|
+
ssl_context = scope.get("ssl")
|
84
|
+
if not ssl_context:
|
85
|
+
logger.debug("No SSL context found in scope")
|
86
|
+
return None
|
87
|
+
|
88
|
+
# Try to get peer certificate
|
89
|
+
if hasattr(ssl_context, 'getpeercert'):
|
90
|
+
cert_data = ssl_context.getpeercert(binary_form=True)
|
91
|
+
if cert_data:
|
92
|
+
# Parse certificate
|
93
|
+
cert = x509.load_der_x509_certificate(cert_data)
|
94
|
+
return self._cert_to_dict(cert)
|
95
|
+
else:
|
96
|
+
logger.debug("No certificate data in SSL context")
|
97
|
+
return None
|
98
|
+
else:
|
99
|
+
logger.debug("SSL context has no getpeercert method")
|
100
|
+
return None
|
101
|
+
|
102
|
+
except Exception as e:
|
103
|
+
logger.error(f"Failed to extract client certificate: {e}")
|
104
|
+
return None
|
105
|
+
|
106
|
+
def _cert_to_dict(self, cert: x509.Certificate) -> Dict[str, Any]:
|
107
|
+
"""
|
108
|
+
Convert x509 certificate to dictionary.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
cert: x509 certificate object
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
Certificate dictionary
|
115
|
+
"""
|
116
|
+
try:
|
117
|
+
# Extract subject
|
118
|
+
subject = {}
|
119
|
+
for name in cert.subject:
|
120
|
+
subject[name.oid._name] = name.value
|
121
|
+
|
122
|
+
# Extract issuer
|
123
|
+
issuer = {}
|
124
|
+
for name in cert.issuer:
|
125
|
+
issuer[name.oid._name] = name.value
|
126
|
+
|
127
|
+
return {
|
128
|
+
"subject": subject,
|
129
|
+
"issuer": issuer,
|
130
|
+
"serial_number": str(cert.serial_number),
|
131
|
+
"not_valid_before": cert.not_valid_before.isoformat(),
|
132
|
+
"not_valid_after": cert.not_valid_after.isoformat(),
|
133
|
+
"version": cert.version.value,
|
134
|
+
"signature_algorithm_oid": cert.signature_algorithm_oid._name,
|
135
|
+
"public_key": {
|
136
|
+
"key_size": cert.public_key().key_size if hasattr(cert.public_key(), 'key_size') else None,
|
137
|
+
"public_numbers": str(cert.public_key().public_numbers()) if hasattr(cert.public_key(), 'public_numbers') else None
|
138
|
+
}
|
139
|
+
}
|
140
|
+
except Exception as e:
|
141
|
+
logger.error(f"Failed to convert certificate to dict: {e}")
|
142
|
+
return {"error": str(e)}
|
143
|
+
|
144
|
+
async def _send_unauthorized_response(self, send):
|
145
|
+
"""Send 401 Unauthorized response."""
|
146
|
+
await send({
|
147
|
+
"type": "http.response.start",
|
148
|
+
"status": 401,
|
149
|
+
"headers": [
|
150
|
+
(b"content-type", b"application/json"),
|
151
|
+
(b"content-length", b"0")
|
152
|
+
]
|
153
|
+
})
|
154
|
+
await send({
|
155
|
+
"type": "http.response.body",
|
156
|
+
"body": b""
|
157
|
+
})
|
158
|
+
|
159
|
+
async def _send_error_response(self, send, error_message: str):
|
160
|
+
"""Send error response."""
|
161
|
+
body = f'{{"error": "{error_message}"}}'.encode('utf-8')
|
162
|
+
await send({
|
163
|
+
"type": "http.response.start",
|
164
|
+
"status": 500,
|
165
|
+
"headers": [
|
166
|
+
(b"content-type", b"application/json"),
|
167
|
+
(b"content-length", str(len(body)).encode())
|
168
|
+
]
|
169
|
+
})
|
170
|
+
await send({
|
171
|
+
"type": "http.response.body",
|
172
|
+
"body": body
|
173
|
+
})
|
174
|
+
|
175
|
+
|
176
|
+
def create_mtls_asgi_app(app, ssl_config: Dict[str, Any]):
|
177
|
+
"""
|
178
|
+
Create MTLS ASGI application wrapper.
|
179
|
+
|
180
|
+
Args:
|
181
|
+
app: The underlying ASGI application
|
182
|
+
ssl_config: SSL configuration dictionary
|
183
|
+
|
184
|
+
Returns:
|
185
|
+
MTLS ASGI app wrapper
|
186
|
+
"""
|
187
|
+
return MTLSASGIApp(app, ssl_config)
|
@@ -0,0 +1,235 @@
|
|
1
|
+
"""
|
2
|
+
Protocol management module for MCP Proxy Adapter.
|
3
|
+
|
4
|
+
This module provides functionality for managing and validating protocol configurations,
|
5
|
+
including HTTP, HTTPS, and MTLS protocols with their respective ports.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import ssl
|
9
|
+
from typing import Dict, List, Optional, Tuple, Union
|
10
|
+
from urllib.parse import urlparse
|
11
|
+
|
12
|
+
from mcp_proxy_adapter.config import config
|
13
|
+
from mcp_proxy_adapter.core.logging import logger
|
14
|
+
|
15
|
+
|
16
|
+
class ProtocolManager:
|
17
|
+
"""
|
18
|
+
Manages protocol configurations and validates protocol access.
|
19
|
+
|
20
|
+
This class handles the validation of allowed protocols and their associated ports,
|
21
|
+
ensuring that only configured protocols are accessible.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(self):
|
25
|
+
"""Initialize the protocol manager."""
|
26
|
+
self._load_config()
|
27
|
+
|
28
|
+
def _load_config(self):
|
29
|
+
"""Load protocol configuration from config."""
|
30
|
+
self.protocols_config = config.get("protocols", {})
|
31
|
+
self.enabled = self.protocols_config.get("enabled", True)
|
32
|
+
self.allowed_protocols = self.protocols_config.get("allowed_protocols", ["http"])
|
33
|
+
logger.debug(f"Protocol manager loaded config: enabled={self.enabled}, allowed_protocols={self.allowed_protocols}")
|
34
|
+
|
35
|
+
def reload_config(self):
|
36
|
+
"""Reload protocol configuration."""
|
37
|
+
self._load_config()
|
38
|
+
|
39
|
+
def is_protocol_allowed(self, protocol: str) -> bool:
|
40
|
+
"""
|
41
|
+
Check if a protocol is allowed based on configuration.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
protocol: Protocol name (http, https, mtls)
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
True if protocol is allowed, False otherwise
|
48
|
+
"""
|
49
|
+
if not self.enabled:
|
50
|
+
logger.debug("Protocol management is disabled, allowing all protocols")
|
51
|
+
return True
|
52
|
+
|
53
|
+
protocol_lower = protocol.lower()
|
54
|
+
is_allowed = protocol_lower in self.allowed_protocols
|
55
|
+
|
56
|
+
logger.debug(f"Protocol '{protocol}' allowed: {is_allowed}")
|
57
|
+
return is_allowed
|
58
|
+
|
59
|
+
def get_protocol_port(self, protocol: str) -> Optional[int]:
|
60
|
+
"""
|
61
|
+
Get the configured port for a specific protocol.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
protocol: Protocol name (http, https, mtls)
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
Port number if configured, None otherwise
|
68
|
+
"""
|
69
|
+
protocol_lower = protocol.lower()
|
70
|
+
protocol_config = self.protocols_config.get(protocol_lower, {})
|
71
|
+
|
72
|
+
if not protocol_config.get("enabled", False):
|
73
|
+
logger.debug(f"Protocol '{protocol}' is not enabled")
|
74
|
+
return None
|
75
|
+
|
76
|
+
port = protocol_config.get("port")
|
77
|
+
logger.debug(f"Protocol '{protocol}' port: {port}")
|
78
|
+
return port
|
79
|
+
|
80
|
+
def get_allowed_protocols(self) -> List[str]:
|
81
|
+
"""
|
82
|
+
Get list of all allowed protocols.
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
List of allowed protocol names
|
86
|
+
"""
|
87
|
+
return self.allowed_protocols.copy()
|
88
|
+
|
89
|
+
def get_protocol_config(self, protocol: str) -> Dict:
|
90
|
+
"""
|
91
|
+
Get full configuration for a specific protocol.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
protocol: Protocol name (http, https, mtls)
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
Protocol configuration dictionary
|
98
|
+
"""
|
99
|
+
protocol_lower = protocol.lower()
|
100
|
+
return self.protocols_config.get(protocol_lower, {}).copy()
|
101
|
+
|
102
|
+
def validate_url_protocol(self, url: str) -> Tuple[bool, Optional[str]]:
|
103
|
+
"""
|
104
|
+
Validate if the URL protocol is allowed.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
url: URL to validate
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
Tuple of (is_allowed, error_message)
|
111
|
+
"""
|
112
|
+
try:
|
113
|
+
parsed = urlparse(url)
|
114
|
+
protocol = parsed.scheme.lower()
|
115
|
+
|
116
|
+
if not protocol:
|
117
|
+
return False, "No protocol specified in URL"
|
118
|
+
|
119
|
+
if not self.is_protocol_allowed(protocol):
|
120
|
+
return False, f"Protocol '{protocol}' is not allowed. Allowed protocols: {self.allowed_protocols}"
|
121
|
+
|
122
|
+
return True, None
|
123
|
+
|
124
|
+
except Exception as e:
|
125
|
+
return False, f"Invalid URL format: {str(e)}"
|
126
|
+
|
127
|
+
def get_ssl_context_for_protocol(self, protocol: str) -> Optional[ssl.SSLContext]:
|
128
|
+
"""
|
129
|
+
Get SSL context for HTTPS or MTLS protocol.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
protocol: Protocol name (https, mtls)
|
133
|
+
|
134
|
+
Returns:
|
135
|
+
SSL context if protocol requires SSL, None otherwise
|
136
|
+
"""
|
137
|
+
if protocol.lower() not in ["https", "mtls"]:
|
138
|
+
return None
|
139
|
+
|
140
|
+
ssl_config = config.get("ssl", {})
|
141
|
+
|
142
|
+
if not ssl_config.get("enabled", False):
|
143
|
+
logger.warning(f"SSL required for protocol '{protocol}' but SSL is disabled")
|
144
|
+
return None
|
145
|
+
|
146
|
+
cert_file = ssl_config.get("cert_file")
|
147
|
+
key_file = ssl_config.get("key_file")
|
148
|
+
|
149
|
+
if not cert_file or not key_file:
|
150
|
+
logger.warning(f"SSL required for protocol '{protocol}' but certificate files not configured")
|
151
|
+
return None
|
152
|
+
|
153
|
+
try:
|
154
|
+
from mcp_proxy_adapter.core.ssl_utils import SSLUtils
|
155
|
+
|
156
|
+
ssl_context = SSLUtils.create_ssl_context(
|
157
|
+
cert_file=cert_file,
|
158
|
+
key_file=key_file,
|
159
|
+
ca_cert=ssl_config.get("ca_cert"),
|
160
|
+
verify_client=protocol.lower() == "mtls" or ssl_config.get("verify_client", False),
|
161
|
+
cipher_suites=ssl_config.get("cipher_suites", []),
|
162
|
+
min_tls_version=ssl_config.get("min_tls_version", "1.2"),
|
163
|
+
max_tls_version=ssl_config.get("max_tls_version", "1.3")
|
164
|
+
)
|
165
|
+
|
166
|
+
logger.info(f"SSL context created for protocol '{protocol}'")
|
167
|
+
return ssl_context
|
168
|
+
|
169
|
+
except Exception as e:
|
170
|
+
logger.error(f"Failed to create SSL context for protocol '{protocol}': {e}")
|
171
|
+
return None
|
172
|
+
|
173
|
+
def get_protocol_info(self) -> Dict[str, Dict]:
|
174
|
+
"""
|
175
|
+
Get information about all configured protocols.
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
Dictionary with protocol information
|
179
|
+
"""
|
180
|
+
info = {}
|
181
|
+
|
182
|
+
for protocol in ["http", "https", "mtls"]:
|
183
|
+
protocol_config = self.get_protocol_config(protocol)
|
184
|
+
info[protocol] = {
|
185
|
+
"enabled": protocol_config.get("enabled", False),
|
186
|
+
"allowed": self.is_protocol_allowed(protocol),
|
187
|
+
"port": protocol_config.get("port"),
|
188
|
+
"requires_ssl": protocol in ["https", "mtls"],
|
189
|
+
"ssl_context_available": self.get_ssl_context_for_protocol(protocol) is not None
|
190
|
+
}
|
191
|
+
|
192
|
+
return info
|
193
|
+
|
194
|
+
def validate_protocol_configuration(self) -> List[str]:
|
195
|
+
"""
|
196
|
+
Validate the current protocol configuration.
|
197
|
+
|
198
|
+
Returns:
|
199
|
+
List of validation errors (empty if configuration is valid)
|
200
|
+
"""
|
201
|
+
errors = []
|
202
|
+
|
203
|
+
if not self.enabled:
|
204
|
+
return errors
|
205
|
+
|
206
|
+
# Check if allowed protocols are configured
|
207
|
+
for protocol in self.allowed_protocols:
|
208
|
+
if protocol not in ["http", "https", "mtls"]:
|
209
|
+
errors.append(f"Unknown protocol '{protocol}' in allowed_protocols")
|
210
|
+
continue
|
211
|
+
|
212
|
+
protocol_config = self.get_protocol_config(protocol)
|
213
|
+
|
214
|
+
if not protocol_config.get("enabled", False):
|
215
|
+
errors.append(f"Protocol '{protocol}' is in allowed_protocols but not enabled")
|
216
|
+
continue
|
217
|
+
|
218
|
+
port = protocol_config.get("port")
|
219
|
+
if not port:
|
220
|
+
errors.append(f"Protocol '{protocol}' is enabled but no port configured")
|
221
|
+
continue
|
222
|
+
|
223
|
+
# Check SSL requirements
|
224
|
+
if protocol in ["https", "mtls"]:
|
225
|
+
ssl_config = config.get("ssl", {})
|
226
|
+
if not ssl_config.get("enabled", False):
|
227
|
+
errors.append(f"Protocol '{protocol}' requires SSL but SSL is disabled")
|
228
|
+
elif not ssl_config.get("cert_file") or not ssl_config.get("key_file"):
|
229
|
+
errors.append(f"Protocol '{protocol}' requires SSL but certificate files not configured")
|
230
|
+
|
231
|
+
return errors
|
232
|
+
|
233
|
+
|
234
|
+
# Global protocol manager instance
|
235
|
+
protocol_manager = ProtocolManager()
|