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
@@ -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)
|
@@ -21,11 +21,106 @@ class ProtocolManager:
|
|
21
21
|
ensuring that only configured protocols are accessible.
|
22
22
|
"""
|
23
23
|
|
24
|
-
def __init__(self):
|
25
|
-
"""
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
def __init__(self, app_config: Optional[Dict] = None):
|
25
|
+
"""
|
26
|
+
Initialize the protocol manager.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
app_config: Application configuration dictionary (optional)
|
30
|
+
"""
|
31
|
+
self.app_config = app_config
|
32
|
+
self._load_config()
|
33
|
+
|
34
|
+
def _load_config(self):
|
35
|
+
"""Load protocol configuration from config."""
|
36
|
+
# Use provided config or fallback to global config; normalize types
|
37
|
+
current_config = self.app_config if self.app_config is not None else config.get_all()
|
38
|
+
logger.debug(f"ProtocolManager._load_config - current_config type: {type(current_config)}")
|
39
|
+
|
40
|
+
if not hasattr(current_config, 'get'):
|
41
|
+
# Not a dict-like config, fallback to global
|
42
|
+
logger.debug(f"ProtocolManager._load_config - current_config is not dict-like, falling back to global config")
|
43
|
+
current_config = config.get_all()
|
44
|
+
|
45
|
+
logger.debug(f"ProtocolManager._load_config - final current_config type: {type(current_config)}")
|
46
|
+
if hasattr(current_config, 'get'):
|
47
|
+
logger.debug(f"ProtocolManager._load_config - current_config keys: {list(current_config.keys()) if hasattr(current_config, 'keys') else 'no keys'}")
|
48
|
+
|
49
|
+
# Get protocols configuration
|
50
|
+
logger.debug(f"ProtocolManager._load_config - before getting protocols")
|
51
|
+
try:
|
52
|
+
self.protocols_config = current_config.get("protocols", {})
|
53
|
+
logger.debug(f"ProtocolManager._load_config - protocols_config type: {type(self.protocols_config)}")
|
54
|
+
if hasattr(self.protocols_config, 'get'):
|
55
|
+
logger.debug(f"ProtocolManager._load_config - protocols_config is dict-like")
|
56
|
+
else:
|
57
|
+
logger.debug(f"ProtocolManager._load_config - protocols_config is NOT dict-like: {repr(self.protocols_config)}")
|
58
|
+
except Exception as e:
|
59
|
+
logger.debug(f"ProtocolManager._load_config - ERROR getting protocols: {e}")
|
60
|
+
self.protocols_config = {}
|
61
|
+
|
62
|
+
self.enabled = self.protocols_config.get("enabled", True) if hasattr(self.protocols_config, 'get') else True
|
63
|
+
|
64
|
+
# Get SSL configuration to determine allowed protocols
|
65
|
+
ssl_enabled = self._is_ssl_enabled(current_config)
|
66
|
+
|
67
|
+
# Set allowed protocols based on SSL configuration
|
68
|
+
if ssl_enabled:
|
69
|
+
# If SSL is enabled, allow both HTTP and HTTPS
|
70
|
+
self.allowed_protocols = self.protocols_config.get("allowed_protocols", ["http", "https"])
|
71
|
+
# Ensure HTTPS is in allowed protocols if SSL is enabled
|
72
|
+
if "https" not in self.allowed_protocols:
|
73
|
+
self.allowed_protocols.append("https")
|
74
|
+
else:
|
75
|
+
# If SSL is disabled, only allow HTTP
|
76
|
+
self.allowed_protocols = self.protocols_config.get("allowed_protocols", ["http"])
|
77
|
+
# Remove HTTPS from allowed protocols if SSL is disabled
|
78
|
+
if "https" in self.allowed_protocols:
|
79
|
+
self.allowed_protocols.remove("https")
|
80
|
+
|
81
|
+
logger.debug(f"Protocol manager loaded config: enabled={self.enabled}, allowed_protocols={self.allowed_protocols}, ssl_enabled={ssl_enabled}")
|
82
|
+
|
83
|
+
def _is_ssl_enabled(self, current_config: Dict) -> bool:
|
84
|
+
"""
|
85
|
+
Check if SSL is enabled in configuration.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
current_config: Current configuration dictionary
|
89
|
+
|
90
|
+
Returns:
|
91
|
+
True if SSL is enabled, False otherwise
|
92
|
+
"""
|
93
|
+
# Try security framework SSL config first
|
94
|
+
security_config = current_config.get("security", {})
|
95
|
+
ssl_config = security_config.get("ssl", {})
|
96
|
+
|
97
|
+
if ssl_config.get("enabled", False):
|
98
|
+
logger.debug("SSL enabled via security.ssl configuration")
|
99
|
+
return True
|
100
|
+
|
101
|
+
# Fallback to legacy SSL config
|
102
|
+
legacy_ssl_config = current_config.get("ssl", {})
|
103
|
+
if legacy_ssl_config.get("enabled", False):
|
104
|
+
logger.debug("SSL enabled via legacy ssl configuration")
|
105
|
+
return True
|
106
|
+
|
107
|
+
logger.debug("SSL is disabled in configuration")
|
108
|
+
return False
|
109
|
+
|
110
|
+
def update_config(self, new_config: Dict):
|
111
|
+
"""
|
112
|
+
Update configuration and reload protocol settings.
|
113
|
+
|
114
|
+
Args:
|
115
|
+
new_config: New configuration dictionary
|
116
|
+
"""
|
117
|
+
self.app_config = new_config
|
118
|
+
self._load_config()
|
119
|
+
logger.info(f"Protocol manager configuration updated: allowed_protocols={self.allowed_protocols}")
|
120
|
+
|
121
|
+
def reload_config(self):
|
122
|
+
"""Reload protocol configuration from global config."""
|
123
|
+
self._load_config()
|
29
124
|
|
30
125
|
def is_protocol_allowed(self, protocol: str) -> bool:
|
31
126
|
"""
|
@@ -88,7 +183,14 @@ class ProtocolManager:
|
|
88
183
|
Protocol configuration dictionary
|
89
184
|
"""
|
90
185
|
protocol_lower = protocol.lower()
|
91
|
-
|
186
|
+
cfg = self.protocols_config.get(protocol_lower, {})
|
187
|
+
# Ensure dict type
|
188
|
+
if isinstance(cfg, dict):
|
189
|
+
try:
|
190
|
+
return cfg.copy()
|
191
|
+
except Exception:
|
192
|
+
return {}
|
193
|
+
return {}
|
92
194
|
|
93
195
|
def validate_url_protocol(self, url: str) -> Tuple[bool, Optional[str]]:
|
94
196
|
"""
|
@@ -128,7 +230,11 @@ class ProtocolManager:
|
|
128
230
|
if protocol.lower() not in ["https", "mtls"]:
|
129
231
|
return None
|
130
232
|
|
131
|
-
|
233
|
+
# Use provided config or fallback to global config
|
234
|
+
current_config = self.app_config if self.app_config is not None else config.get_all()
|
235
|
+
|
236
|
+
# Get SSL configuration
|
237
|
+
ssl_config = self._get_ssl_config(current_config)
|
132
238
|
|
133
239
|
if not ssl_config.get("enabled", False):
|
134
240
|
logger.warning(f"SSL required for protocol '{protocol}' but SSL is disabled")
|
@@ -161,6 +267,33 @@ class ProtocolManager:
|
|
161
267
|
logger.error(f"Failed to create SSL context for protocol '{protocol}': {e}")
|
162
268
|
return None
|
163
269
|
|
270
|
+
def _get_ssl_config(self, current_config: Dict) -> Dict:
|
271
|
+
"""
|
272
|
+
Get SSL configuration from config.
|
273
|
+
|
274
|
+
Args:
|
275
|
+
current_config: Current configuration dictionary
|
276
|
+
|
277
|
+
Returns:
|
278
|
+
SSL configuration dictionary
|
279
|
+
"""
|
280
|
+
# Try security framework SSL config first
|
281
|
+
security_config = current_config.get("security", {})
|
282
|
+
ssl_config = security_config.get("ssl", {})
|
283
|
+
|
284
|
+
if ssl_config.get("enabled", False):
|
285
|
+
logger.debug("Using security.ssl configuration")
|
286
|
+
return ssl_config
|
287
|
+
|
288
|
+
# Fallback to legacy SSL config
|
289
|
+
legacy_ssl_config = current_config.get("ssl", {})
|
290
|
+
if legacy_ssl_config.get("enabled", False):
|
291
|
+
logger.debug("Using legacy ssl configuration")
|
292
|
+
return legacy_ssl_config
|
293
|
+
|
294
|
+
# Return empty config if SSL is disabled
|
295
|
+
return {"enabled": False}
|
296
|
+
|
164
297
|
def get_protocol_info(self) -> Dict[str, Dict]:
|
165
298
|
"""
|
166
299
|
Get information about all configured protocols.
|
@@ -213,7 +346,10 @@ class ProtocolManager:
|
|
213
346
|
|
214
347
|
# Check SSL requirements
|
215
348
|
if protocol in ["https", "mtls"]:
|
216
|
-
|
349
|
+
# Use provided config or fallback to global config
|
350
|
+
current_config = self.app_config if self.app_config is not None else config.get_all()
|
351
|
+
ssl_config = self._get_ssl_config(current_config)
|
352
|
+
|
217
353
|
if not ssl_config.get("enabled", False):
|
218
354
|
errors.append(f"Protocol '{protocol}' requires SSL but SSL is disabled")
|
219
355
|
elif not ssl_config.get("cert_file") or not ssl_config.get("key_file"):
|
@@ -222,5 +358,28 @@ class ProtocolManager:
|
|
222
358
|
return errors
|
223
359
|
|
224
360
|
|
225
|
-
# Global protocol manager instance
|
226
|
-
protocol_manager =
|
361
|
+
# Global protocol manager instance - will be updated with config when needed
|
362
|
+
protocol_manager = None
|
363
|
+
|
364
|
+
def get_protocol_manager(app_config: Optional[Dict] = None) -> ProtocolManager:
|
365
|
+
"""
|
366
|
+
Get protocol manager instance with current configuration.
|
367
|
+
|
368
|
+
Args:
|
369
|
+
app_config: Application configuration dictionary (optional)
|
370
|
+
|
371
|
+
Returns:
|
372
|
+
ProtocolManager instance
|
373
|
+
"""
|
374
|
+
global protocol_manager
|
375
|
+
|
376
|
+
# If no app_config provided, use global config
|
377
|
+
if app_config is None:
|
378
|
+
app_config = config.get_all()
|
379
|
+
|
380
|
+
# Create new instance if none exists or config changed
|
381
|
+
if protocol_manager is None or protocol_manager.app_config != app_config:
|
382
|
+
protocol_manager = ProtocolManager(app_config)
|
383
|
+
logger.info("Protocol manager created with new configuration")
|
384
|
+
|
385
|
+
return protocol_manager
|