mcp-proxy-adapter 6.9.43__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/__init__.py +47 -0
- mcp_proxy_adapter/__main__.py +13 -0
- mcp_proxy_adapter/api/__init__.py +0 -0
- mcp_proxy_adapter/api/app.py +66 -0
- mcp_proxy_adapter/api/core/__init__.py +18 -0
- mcp_proxy_adapter/api/core/app_factory.py +355 -0
- mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
- mcp_proxy_adapter/api/core/registration_context.py +356 -0
- mcp_proxy_adapter/api/core/registration_manager.py +266 -0
- mcp_proxy_adapter/api/core/registration_tasks.py +84 -0
- mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
- mcp_proxy_adapter/api/handlers.py +181 -0
- mcp_proxy_adapter/api/middleware/__init__.py +21 -0
- mcp_proxy_adapter/api/middleware/base.py +54 -0
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +73 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +76 -0
- mcp_proxy_adapter/api/middleware/factory.py +147 -0
- mcp_proxy_adapter/api/middleware/logging.py +31 -0
- mcp_proxy_adapter/api/middleware/performance.py +51 -0
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +140 -0
- mcp_proxy_adapter/api/middleware/transport_middleware.py +87 -0
- mcp_proxy_adapter/api/middleware/unified_security.py +223 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +132 -0
- mcp_proxy_adapter/api/openapi/__init__.py +21 -0
- mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
- mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
- mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
- mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
- mcp_proxy_adapter/api/schemas.py +270 -0
- mcp_proxy_adapter/api/tool_integration.py +131 -0
- mcp_proxy_adapter/api/tools.py +163 -0
- mcp_proxy_adapter/cli/__init__.py +12 -0
- mcp_proxy_adapter/cli/commands/__init__.py +15 -0
- mcp_proxy_adapter/cli/commands/client.py +100 -0
- mcp_proxy_adapter/cli/commands/config_generate.py +35 -0
- mcp_proxy_adapter/cli/commands/config_validate.py +74 -0
- mcp_proxy_adapter/cli/commands/generate.py +259 -0
- mcp_proxy_adapter/cli/commands/server.py +174 -0
- mcp_proxy_adapter/cli/commands/sets.py +128 -0
- mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
- mcp_proxy_adapter/cli/examples/__init__.py +8 -0
- mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
- mcp_proxy_adapter/cli/examples/https_token.py +96 -0
- mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
- mcp_proxy_adapter/cli/main.py +63 -0
- mcp_proxy_adapter/cli/parser.py +338 -0
- mcp_proxy_adapter/cli/validators.py +231 -0
- mcp_proxy_adapter/client/jsonrpc_client/__init__.py +9 -0
- mcp_proxy_adapter/client/jsonrpc_client/client.py +42 -0
- mcp_proxy_adapter/client/jsonrpc_client/command_api.py +45 -0
- mcp_proxy_adapter/client/jsonrpc_client/proxy_api.py +224 -0
- mcp_proxy_adapter/client/jsonrpc_client/queue_api.py +60 -0
- mcp_proxy_adapter/client/jsonrpc_client/transport.py +108 -0
- mcp_proxy_adapter/client/proxy.py +123 -0
- mcp_proxy_adapter/commands/__init__.py +66 -0
- mcp_proxy_adapter/commands/auth_validation_command.py +69 -0
- mcp_proxy_adapter/commands/base.py +389 -0
- mcp_proxy_adapter/commands/builtin_commands.py +30 -0
- mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
- mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
- mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
- mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
- mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
- mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
- mcp_proxy_adapter/commands/catalog_manager.py +97 -0
- mcp_proxy_adapter/commands/cert_monitor_command.py +552 -0
- mcp_proxy_adapter/commands/certificate_management_command.py +562 -0
- mcp_proxy_adapter/commands/command_registry.py +298 -0
- mcp_proxy_adapter/commands/config_command.py +102 -0
- mcp_proxy_adapter/commands/dependency_container.py +40 -0
- mcp_proxy_adapter/commands/dependency_manager.py +143 -0
- mcp_proxy_adapter/commands/echo_command.py +48 -0
- mcp_proxy_adapter/commands/health_command.py +142 -0
- mcp_proxy_adapter/commands/help_command.py +175 -0
- mcp_proxy_adapter/commands/hooks.py +172 -0
- mcp_proxy_adapter/commands/key_management_command.py +484 -0
- mcp_proxy_adapter/commands/load_command.py +123 -0
- mcp_proxy_adapter/commands/plugins_command.py +246 -0
- mcp_proxy_adapter/commands/protocol_management_command.py +216 -0
- mcp_proxy_adapter/commands/proxy_registration_command.py +319 -0
- mcp_proxy_adapter/commands/queue_commands.py +750 -0
- mcp_proxy_adapter/commands/registration_status_command.py +76 -0
- mcp_proxy_adapter/commands/registry/__init__.py +18 -0
- mcp_proxy_adapter/commands/registry/command_info.py +103 -0
- mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
- mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
- mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
- mcp_proxy_adapter/commands/reload_command.py +136 -0
- mcp_proxy_adapter/commands/result.py +157 -0
- mcp_proxy_adapter/commands/role_test_command.py +99 -0
- mcp_proxy_adapter/commands/roles_management_command.py +502 -0
- mcp_proxy_adapter/commands/security_command.py +472 -0
- mcp_proxy_adapter/commands/settings_command.py +113 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +306 -0
- mcp_proxy_adapter/commands/token_management_command.py +500 -0
- mcp_proxy_adapter/commands/transport_management_command.py +129 -0
- mcp_proxy_adapter/commands/unload_command.py +92 -0
- mcp_proxy_adapter/config.py +32 -0
- mcp_proxy_adapter/core/__init__.py +8 -0
- mcp_proxy_adapter/core/app_factory.py +560 -0
- mcp_proxy_adapter/core/app_runner.py +318 -0
- mcp_proxy_adapter/core/auth_validator.py +508 -0
- mcp_proxy_adapter/core/certificate/__init__.py +20 -0
- mcp_proxy_adapter/core/certificate/certificate_creator.py +372 -0
- mcp_proxy_adapter/core/certificate/certificate_extractor.py +185 -0
- mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
- mcp_proxy_adapter/core/certificate/certificate_validator.py +388 -0
- mcp_proxy_adapter/core/certificate/ssl_context_manager.py +65 -0
- mcp_proxy_adapter/core/certificate_utils.py +249 -0
- mcp_proxy_adapter/core/client.py +608 -0
- mcp_proxy_adapter/core/client_manager.py +271 -0
- mcp_proxy_adapter/core/client_security.py +411 -0
- mcp_proxy_adapter/core/config/__init__.py +18 -0
- mcp_proxy_adapter/core/config/config.py +237 -0
- mcp_proxy_adapter/core/config/config_factory.py +22 -0
- mcp_proxy_adapter/core/config/config_loader.py +66 -0
- mcp_proxy_adapter/core/config/feature_manager.py +31 -0
- mcp_proxy_adapter/core/config/simple_config.py +116 -0
- mcp_proxy_adapter/core/config/simple_config_generator.py +100 -0
- mcp_proxy_adapter/core/config/simple_config_validator.py +380 -0
- mcp_proxy_adapter/core/config_converter.py +252 -0
- mcp_proxy_adapter/core/config_validator.py +211 -0
- mcp_proxy_adapter/core/crl_utils.py +362 -0
- mcp_proxy_adapter/core/errors.py +276 -0
- mcp_proxy_adapter/core/job_manager.py +54 -0
- mcp_proxy_adapter/core/logging.py +250 -0
- mcp_proxy_adapter/core/mtls_asgi.py +140 -0
- mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
- mcp_proxy_adapter/core/mtls_proxy.py +229 -0
- mcp_proxy_adapter/core/mtls_server.py +154 -0
- mcp_proxy_adapter/core/protocol_manager.py +232 -0
- mcp_proxy_adapter/core/proxy/__init__.py +19 -0
- mcp_proxy_adapter/core/proxy/auth_manager.py +26 -0
- mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +160 -0
- mcp_proxy_adapter/core/proxy/registration_client.py +186 -0
- mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
- mcp_proxy_adapter/core/proxy_client.py +184 -0
- mcp_proxy_adapter/core/proxy_registration.py +80 -0
- mcp_proxy_adapter/core/role_utils.py +103 -0
- mcp_proxy_adapter/core/security_adapter.py +343 -0
- mcp_proxy_adapter/core/security_factory.py +96 -0
- mcp_proxy_adapter/core/security_integration.py +342 -0
- mcp_proxy_adapter/core/server_adapter.py +251 -0
- mcp_proxy_adapter/core/server_engine.py +217 -0
- mcp_proxy_adapter/core/settings.py +260 -0
- mcp_proxy_adapter/core/signal_handler.py +107 -0
- mcp_proxy_adapter/core/ssl_utils.py +161 -0
- mcp_proxy_adapter/core/transport_manager.py +153 -0
- mcp_proxy_adapter/core/unified_config_adapter.py +471 -0
- mcp_proxy_adapter/core/utils.py +101 -0
- mcp_proxy_adapter/core/validation/__init__.py +21 -0
- mcp_proxy_adapter/core/validation/config_validator.py +219 -0
- mcp_proxy_adapter/core/validation/file_validator.py +131 -0
- mcp_proxy_adapter/core/validation/protocol_validator.py +190 -0
- mcp_proxy_adapter/core/validation/security_validator.py +140 -0
- mcp_proxy_adapter/core/validation/validation_result.py +27 -0
- mcp_proxy_adapter/custom_openapi.py +58 -0
- mcp_proxy_adapter/examples/__init__.py +16 -0
- 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 +52 -0
- mcp_proxy_adapter/examples/bugfix_certificate_config.py +261 -0
- mcp_proxy_adapter/examples/cert_manager_bugfix.py +203 -0
- mcp_proxy_adapter/examples/check_config.py +413 -0
- mcp_proxy_adapter/examples/client_usage_example.py +164 -0
- mcp_proxy_adapter/examples/commands/__init__.py +5 -0
- mcp_proxy_adapter/examples/config_builder.py +234 -0
- mcp_proxy_adapter/examples/config_cli.py +282 -0
- mcp_proxy_adapter/examples/create_test_configs.py +174 -0
- mcp_proxy_adapter/examples/debug_request_state.py +130 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +191 -0
- mcp_proxy_adapter/examples/demo_client.py +287 -0
- mcp_proxy_adapter/examples/full_application/__init__.py +13 -0
- mcp_proxy_adapter/examples/full_application/commands/__init__.py +8 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +45 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +52 -0
- mcp_proxy_adapter/examples/full_application/commands/echo_command.py +32 -0
- mcp_proxy_adapter/examples/full_application/commands/help_command.py +54 -0
- mcp_proxy_adapter/examples/full_application/commands/list_command.py +57 -0
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +5 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +29 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +27 -0
- mcp_proxy_adapter/examples/full_application/main.py +264 -0
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +81 -0
- mcp_proxy_adapter/examples/full_application/run_mtls.py +252 -0
- mcp_proxy_adapter/examples/full_application/run_simple.py +152 -0
- mcp_proxy_adapter/examples/full_application/test_minimal_server.py +45 -0
- mcp_proxy_adapter/examples/full_application/test_server.py +163 -0
- mcp_proxy_adapter/examples/full_application/test_simple_server.py +62 -0
- mcp_proxy_adapter/examples/generate_config.py +502 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +335 -0
- mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
- mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
- mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
- mcp_proxy_adapter/examples/queue_server_example.py +85 -0
- mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
- mcp_proxy_adapter/examples/required_certificates.py +208 -0
- mcp_proxy_adapter/examples/run_example.py +77 -0
- mcp_proxy_adapter/examples/run_full_test_suite.py +619 -0
- mcp_proxy_adapter/examples/run_proxy_server.py +153 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +435 -0
- mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
- mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
- mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
- mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
- mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
- mcp_proxy_adapter/examples/security_test_client.py +72 -0
- mcp_proxy_adapter/examples/setup/__init__.py +24 -0
- mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
- mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
- mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
- mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
- mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
- mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +235 -0
- mcp_proxy_adapter/examples/simple_protocol_test.py +125 -0
- mcp_proxy_adapter/examples/test_chk_hostname_automated.py +211 -0
- mcp_proxy_adapter/examples/test_config.py +205 -0
- mcp_proxy_adapter/examples/test_config_builder.py +110 -0
- mcp_proxy_adapter/examples/test_examples.py +308 -0
- mcp_proxy_adapter/examples/test_framework_complete.py +267 -0
- mcp_proxy_adapter/examples/test_mcp_server.py +187 -0
- mcp_proxy_adapter/examples/test_protocol_examples.py +337 -0
- mcp_proxy_adapter/examples/universal_client.py +674 -0
- mcp_proxy_adapter/examples/update_config_certificates.py +135 -0
- mcp_proxy_adapter/examples/validate_generator_compatibility.py +385 -0
- mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +61 -0
- mcp_proxy_adapter/integrations/__init__.py +25 -0
- mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
- mcp_proxy_adapter/main.py +313 -0
- mcp_proxy_adapter/openapi.py +375 -0
- mcp_proxy_adapter/schemas/base_schema.json +114 -0
- mcp_proxy_adapter/schemas/openapi_schema.json +314 -0
- mcp_proxy_adapter/schemas/roles.json +37 -0
- mcp_proxy_adapter/schemas/roles_schema.json +162 -0
- mcp_proxy_adapter/version.py +5 -0
- mcp_proxy_adapter-6.9.43.dist-info/METADATA +739 -0
- mcp_proxy_adapter-6.9.43.dist-info/RECORD +242 -0
- mcp_proxy_adapter-6.9.43.dist-info/WHEEL +5 -0
- mcp_proxy_adapter-6.9.43.dist-info/entry_points.txt +12 -0
- mcp_proxy_adapter-6.9.43.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,140 @@
|
|
|
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.types import ASGIApp, Receive, Send, Scope
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MTLSASGIApp:
|
|
17
|
+
"""
|
|
18
|
+
Custom ASGI application that properly handles mTLS client certificates.
|
|
19
|
+
|
|
20
|
+
This wrapper ensures that client certificates are properly extracted
|
|
21
|
+
and made available to the FastAPI application.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, app: ASGIApp, ssl_config: Dict[str, Any]):
|
|
25
|
+
"""
|
|
26
|
+
Initialize MTLS ASGI application.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
app: The underlying ASGI application (FastAPI)
|
|
30
|
+
ssl_config: SSL configuration for mTLS
|
|
31
|
+
"""
|
|
32
|
+
self.app = app
|
|
33
|
+
self.ssl_config = ssl_config
|
|
34
|
+
self.verify_client = ssl_config.get("verify_client", False)
|
|
35
|
+
self.client_cert_required = ssl_config.get("client_cert_required", False)
|
|
36
|
+
|
|
37
|
+
get_global_logger().info(
|
|
38
|
+
f"MTLS ASGI app initialized: verify_client={self.verify_client}, "
|
|
39
|
+
f"client_cert_required={self.client_cert_required}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Handle ASGI request with mTLS support.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
scope: ASGI scope
|
|
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
|
+
get_global_logger().debug(
|
|
59
|
+
f"Client certificate extracted: {client_cert.get('subject', {})}"
|
|
60
|
+
)
|
|
61
|
+
elif self.client_cert_required:
|
|
62
|
+
get_global_logger().warning("Client certificate required but not provided")
|
|
63
|
+
# Return 401 Unauthorized
|
|
64
|
+
await self._send_unauthorized_response(send)
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# Call the underlying application
|
|
68
|
+
await self.app(scope, receive, send)
|
|
69
|
+
|
|
70
|
+
except Exception as e:
|
|
71
|
+
get_global_logger().error(f"Error in MTLS ASGI app: {e}")
|
|
72
|
+
await self._send_error_response(send, str(e))
|
|
73
|
+
|
|
74
|
+
def _extract_client_certificate(self, scope: Scope) -> Optional[Dict[str, Any]]:
|
|
75
|
+
"""
|
|
76
|
+
Extract client certificate from SSL context.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
scope: ASGI scope
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Client certificate data or None
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
ssl_context = scope.get("ssl")
|
|
86
|
+
if not ssl_context:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
# Get peer certificate
|
|
90
|
+
cert = ssl_context.getpeercert()
|
|
91
|
+
if cert:
|
|
92
|
+
return cert
|
|
93
|
+
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
get_global_logger().error(f"Failed to extract client certificate: {e}")
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
async def _send_unauthorized_response(self, send: Send) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Send 401 Unauthorized response.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
send: ASGI send callable
|
|
106
|
+
"""
|
|
107
|
+
response = {
|
|
108
|
+
"type": "http.response.start",
|
|
109
|
+
"status": 401,
|
|
110
|
+
"headers": [
|
|
111
|
+
(b"content-type", b"application/json"),
|
|
112
|
+
(b"content-length", b"163"),
|
|
113
|
+
],
|
|
114
|
+
}
|
|
115
|
+
await send(response)
|
|
116
|
+
|
|
117
|
+
body = b'{"jsonrpc": "2.0", "error": {"code": -32001, "message": "Unauthorized: Client certificate required"}, "id": null}'
|
|
118
|
+
await send({"type": "http.response.body", "body": body})
|
|
119
|
+
|
|
120
|
+
async def _send_error_response(self, send: Send, error_message: str) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Send error response.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
send: ASGI send callable
|
|
126
|
+
error_message: Error message
|
|
127
|
+
"""
|
|
128
|
+
response = {
|
|
129
|
+
"type": "http.response.start",
|
|
130
|
+
"status": 500,
|
|
131
|
+
"headers": [
|
|
132
|
+
(b"content-type", b"application/json"),
|
|
133
|
+
],
|
|
134
|
+
}
|
|
135
|
+
await send(response)
|
|
136
|
+
|
|
137
|
+
body = f'{{"jsonrpc": "2.0", "error": {{"code": -32603, "message": "Internal error: {error_message}"}}, "id": null}}'.encode()
|
|
138
|
+
await send({"type": "http.response.body", "body": body})
|
|
139
|
+
|
|
140
|
+
|
|
@@ -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
|
+
get_global_logger().info(
|
|
41
|
+
f"MTLS ASGI app initialized: client_cert_required={self.client_cert_required}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
async def __call__(self, scope: Dict[str, Any], receive, send):
|
|
45
|
+
"""
|
|
46
|
+
Handle ASGI request with mTLS support.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
scope: ASGI scope dictionary
|
|
50
|
+
receive: ASGI receive callable
|
|
51
|
+
send: ASGI send callable
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
# Extract client certificate from SSL context
|
|
55
|
+
if scope["type"] == "http" and "ssl" in scope:
|
|
56
|
+
client_cert = self._extract_client_certificate(scope)
|
|
57
|
+
if client_cert:
|
|
58
|
+
# Store certificate in scope for middleware access
|
|
59
|
+
scope["client_certificate"] = client_cert
|
|
60
|
+
get_global_logger().debug(
|
|
61
|
+
f"Client certificate extracted: {client_cert.get('subject', {})}"
|
|
62
|
+
)
|
|
63
|
+
elif self.client_cert_required:
|
|
64
|
+
get_global_logger().warning("Client certificate required but not provided")
|
|
65
|
+
# Return 401 Unauthorized
|
|
66
|
+
await self._send_unauthorized_response(send)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# Call the underlying application
|
|
70
|
+
await self.app(scope, receive, send)
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
get_global_logger().error(f"Error in MTLS ASGI app: {e}")
|
|
74
|
+
await self._send_error_response(send, str(e))
|
|
75
|
+
|
|
76
|
+
def _extract_client_certificate(
|
|
77
|
+
self, scope: Dict[str, Any]
|
|
78
|
+
) -> Optional[Dict[str, Any]]:
|
|
79
|
+
"""
|
|
80
|
+
Extract client certificate from SSL context.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
scope: ASGI scope dictionary
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Certificate dictionary or None if not found
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
ssl_context = scope.get("ssl")
|
|
90
|
+
if not ssl_context:
|
|
91
|
+
get_global_logger().debug("No SSL context found in scope")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
# Try to get peer certificate
|
|
95
|
+
if hasattr(ssl_context, "getpeercert"):
|
|
96
|
+
cert_data = ssl_context.getpeercert(binary_form=True)
|
|
97
|
+
if cert_data:
|
|
98
|
+
# Parse certificate
|
|
99
|
+
cert = x509.load_der_x509_certificate(cert_data)
|
|
100
|
+
return self._cert_to_dict(cert)
|
|
101
|
+
else:
|
|
102
|
+
get_global_logger().debug("No certificate data in SSL context")
|
|
103
|
+
return None
|
|
104
|
+
else:
|
|
105
|
+
get_global_logger().debug("SSL context has no getpeercert method")
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
get_global_logger().error(f"Failed to extract client certificate: {e}")
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def _cert_to_dict(self, cert: x509.Certificate) -> Dict[str, Any]:
|
|
113
|
+
"""
|
|
114
|
+
Convert x509 certificate to dictionary.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
cert: x509 certificate object
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Certificate dictionary
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
# Extract subject
|
|
124
|
+
subject = {}
|
|
125
|
+
for name in cert.subject:
|
|
126
|
+
subject[name.oid._name] = name.value
|
|
127
|
+
|
|
128
|
+
# Extract issuer
|
|
129
|
+
issuer = {}
|
|
130
|
+
for name in cert.issuer:
|
|
131
|
+
issuer[name.oid._name] = name.value
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"subject": subject,
|
|
135
|
+
"issuer": issuer,
|
|
136
|
+
"serial_number": str(cert.serial_number),
|
|
137
|
+
"not_valid_before": cert.not_valid_before.isoformat(),
|
|
138
|
+
"not_valid_after": cert.not_valid_after.isoformat(),
|
|
139
|
+
"version": cert.version.value,
|
|
140
|
+
"signature_algorithm_oid": cert.signature_algorithm_oid._name,
|
|
141
|
+
"public_key": {
|
|
142
|
+
"key_size": (
|
|
143
|
+
cert.public_key().key_size
|
|
144
|
+
if hasattr(cert.public_key(), "key_size")
|
|
145
|
+
else None
|
|
146
|
+
),
|
|
147
|
+
"public_numbers": (
|
|
148
|
+
str(cert.public_key().public_numbers())
|
|
149
|
+
if hasattr(cert.public_key(), "public_numbers")
|
|
150
|
+
else None
|
|
151
|
+
),
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
except Exception as e:
|
|
155
|
+
get_global_logger().error(f"Failed to convert certificate to dict: {e}")
|
|
156
|
+
return {"error": str(e)}
|
|
157
|
+
|
|
158
|
+
async def _send_unauthorized_response(self, send):
|
|
159
|
+
"""Send 401 Unauthorized response."""
|
|
160
|
+
await send(
|
|
161
|
+
{
|
|
162
|
+
"type": "http.response.start",
|
|
163
|
+
"status": 401,
|
|
164
|
+
"headers": [
|
|
165
|
+
(b"content-type", b"application/json"),
|
|
166
|
+
(b"content-length", b"0"),
|
|
167
|
+
],
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
await send({"type": "http.response.body", "body": b""})
|
|
171
|
+
|
|
172
|
+
async def _send_error_response(self, send, error_message: str):
|
|
173
|
+
"""Send error response."""
|
|
174
|
+
body = f'{{"error": "{error_message}"}}'.encode("utf-8")
|
|
175
|
+
await send(
|
|
176
|
+
{
|
|
177
|
+
"type": "http.response.start",
|
|
178
|
+
"status": 500,
|
|
179
|
+
"headers": [
|
|
180
|
+
(b"content-type", b"application/json"),
|
|
181
|
+
(b"content-length", str(len(body)).encode()),
|
|
182
|
+
],
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
await send({"type": "http.response.body", "body": body})
|
|
186
|
+
|
|
187
|
+
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mTLS Proxy for MCP Proxy Adapter
|
|
3
|
+
|
|
4
|
+
This module provides mTLS proxy functionality that accepts mTLS connections
|
|
5
|
+
and proxies them to the internal hypercorn server.
|
|
6
|
+
|
|
7
|
+
Author: Vasiliy Zdanovskiy
|
|
8
|
+
email: vasilyvz@gmail.com
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import ssl
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Optional, Dict, Any
|
|
15
|
+
|
|
16
|
+
from .logging import get_global_logger
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MTLSProxy:
|
|
22
|
+
"""
|
|
23
|
+
mTLS Proxy that accepts mTLS connections and proxies them to internal server.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
external_host: str,
|
|
29
|
+
external_port: int,
|
|
30
|
+
internal_host: str = "127.0.0.1",
|
|
31
|
+
internal_port: int = 9000,
|
|
32
|
+
cert_file: Optional[str] = None,
|
|
33
|
+
key_file: Optional[str] = None,
|
|
34
|
+
ca_cert: Optional[str] = None,
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Initialize mTLS Proxy.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
external_host: External host to bind to
|
|
41
|
+
external_port: External port to bind to
|
|
42
|
+
internal_host: Internal server host
|
|
43
|
+
internal_port: Internal server port
|
|
44
|
+
cert_file: Server certificate file
|
|
45
|
+
key_file: Server private key file
|
|
46
|
+
ca_cert: CA certificate file for client verification
|
|
47
|
+
"""
|
|
48
|
+
self.external_host = external_host
|
|
49
|
+
self.external_port = external_port
|
|
50
|
+
self.internal_host = internal_host
|
|
51
|
+
self.internal_port = internal_port
|
|
52
|
+
self.cert_file = cert_file
|
|
53
|
+
self.key_file = key_file
|
|
54
|
+
self.ca_cert = ca_cert
|
|
55
|
+
self.server = None
|
|
56
|
+
|
|
57
|
+
async def start(self):
|
|
58
|
+
"""Start the mTLS proxy server."""
|
|
59
|
+
try:
|
|
60
|
+
# Create SSL context
|
|
61
|
+
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
|
62
|
+
ssl_context.load_cert_chain(self.cert_file, self.key_file)
|
|
63
|
+
|
|
64
|
+
if self.ca_cert:
|
|
65
|
+
ssl_context.load_verify_locations(self.ca_cert)
|
|
66
|
+
ssl_context.verify_mode = ssl.CERT_REQUIRED
|
|
67
|
+
else:
|
|
68
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
69
|
+
|
|
70
|
+
# Start server
|
|
71
|
+
self.server = await asyncio.start_server(
|
|
72
|
+
self._handle_client,
|
|
73
|
+
self.external_host,
|
|
74
|
+
self.external_port,
|
|
75
|
+
ssl=ssl_context,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
get_global_logger().info(
|
|
79
|
+
f"🔐 mTLS Proxy started on {self.external_host}:{self.external_port}"
|
|
80
|
+
)
|
|
81
|
+
get_global_logger().info(
|
|
82
|
+
f"🌐 Proxying to {self.internal_host}:{self.internal_port}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
get_global_logger().error(f"❌ Failed to start mTLS proxy: {e}")
|
|
87
|
+
raise
|
|
88
|
+
|
|
89
|
+
async def _proxy_data(self, reader, writer, direction):
|
|
90
|
+
"""Proxy data between reader and writer."""
|
|
91
|
+
try:
|
|
92
|
+
while True:
|
|
93
|
+
data = await reader.read(4096)
|
|
94
|
+
if not data:
|
|
95
|
+
break
|
|
96
|
+
writer.write(data)
|
|
97
|
+
await writer.drain()
|
|
98
|
+
except Exception as e:
|
|
99
|
+
get_global_logger().debug(f"Proxy connection closed ({direction}): {e}")
|
|
100
|
+
finally:
|
|
101
|
+
try:
|
|
102
|
+
writer.close()
|
|
103
|
+
await writer.wait_closed()
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
async def _handle_client(self, reader, writer):
|
|
108
|
+
"""
|
|
109
|
+
Handle incoming client connection.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
reader: Client reader stream
|
|
113
|
+
writer: Client writer stream
|
|
114
|
+
"""
|
|
115
|
+
internal_reader = None
|
|
116
|
+
internal_writer = None
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
# Connect to internal server
|
|
120
|
+
internal_reader, internal_writer = await asyncio.open_connection(
|
|
121
|
+
self.internal_host, self.internal_port
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Create bidirectional proxy tasks
|
|
125
|
+
client_to_server = asyncio.create_task(
|
|
126
|
+
self._proxy_data(reader, internal_writer, "client->server")
|
|
127
|
+
)
|
|
128
|
+
server_to_client = asyncio.create_task(
|
|
129
|
+
self._proxy_data(internal_reader, writer, "server->client")
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Wait for either direction to complete
|
|
133
|
+
done, pending = await asyncio.wait(
|
|
134
|
+
[client_to_server, server_to_client],
|
|
135
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Cancel pending tasks
|
|
139
|
+
for task in pending:
|
|
140
|
+
task.cancel()
|
|
141
|
+
|
|
142
|
+
# Wait for cancellation
|
|
143
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
get_global_logger().debug(f"Client connection error: {e}")
|
|
147
|
+
finally:
|
|
148
|
+
# Clean up connections
|
|
149
|
+
if internal_writer:
|
|
150
|
+
try:
|
|
151
|
+
internal_writer.close()
|
|
152
|
+
await internal_writer.wait_closed()
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
if writer:
|
|
156
|
+
try:
|
|
157
|
+
writer.close()
|
|
158
|
+
await writer.wait_closed()
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
async def start_mtls_proxy(
|
|
164
|
+
config: Dict[str, Any], internal_port: Optional[int] = None
|
|
165
|
+
) -> Optional[MTLSProxy]:
|
|
166
|
+
"""
|
|
167
|
+
Start mTLS proxy from configuration.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
config: Configuration dictionary
|
|
171
|
+
internal_port: Internal server port (hypercorn port). If not provided,
|
|
172
|
+
will be calculated as external_port + 1000
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
MTLSProxy instance if started successfully, None otherwise
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
server_config = config.get("server", {})
|
|
179
|
+
transport_config = config.get("transport", {})
|
|
180
|
+
ssl_config = config.get("ssl", {})
|
|
181
|
+
|
|
182
|
+
# Get external host and port
|
|
183
|
+
external_host = server_config.get("host", "0.0.0.0")
|
|
184
|
+
external_port = server_config.get("port", 8001)
|
|
185
|
+
|
|
186
|
+
# Get internal port (use provided or calculate)
|
|
187
|
+
if internal_port is None:
|
|
188
|
+
internal_port = external_port + 1000
|
|
189
|
+
|
|
190
|
+
# Get certificate paths - try multiple locations
|
|
191
|
+
cert_file = (
|
|
192
|
+
ssl_config.get("cert_file")
|
|
193
|
+
or transport_config.get("ssl", {}).get("cert_file")
|
|
194
|
+
or transport_config.get("cert_file")
|
|
195
|
+
)
|
|
196
|
+
key_file = (
|
|
197
|
+
ssl_config.get("key_file")
|
|
198
|
+
or transport_config.get("ssl", {}).get("key_file")
|
|
199
|
+
or transport_config.get("key_file")
|
|
200
|
+
)
|
|
201
|
+
ca_cert = (
|
|
202
|
+
ssl_config.get("ca_cert")
|
|
203
|
+
or transport_config.get("ssl", {}).get("ca_cert")
|
|
204
|
+
or transport_config.get("ca_cert")
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if not cert_file or not key_file:
|
|
208
|
+
get_global_logger().warning(
|
|
209
|
+
"mTLS certificates not found, skipping mTLS proxy"
|
|
210
|
+
)
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
# Create and start proxy
|
|
214
|
+
proxy = MTLSProxy(
|
|
215
|
+
external_host=external_host,
|
|
216
|
+
external_port=external_port,
|
|
217
|
+
internal_host="127.0.0.1",
|
|
218
|
+
internal_port=internal_port,
|
|
219
|
+
cert_file=cert_file,
|
|
220
|
+
key_file=key_file,
|
|
221
|
+
ca_cert=ca_cert,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
await proxy.start()
|
|
225
|
+
return proxy
|
|
226
|
+
|
|
227
|
+
except Exception as e:
|
|
228
|
+
get_global_logger().error(f"Failed to start mTLS proxy: {e}")
|
|
229
|
+
return None
|