mcp-proxy-adapter 4.1.1__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.
Files changed (200) hide show
  1. mcp_proxy_adapter/__main__.py +32 -0
  2. mcp_proxy_adapter/api/app.py +290 -33
  3. mcp_proxy_adapter/api/handlers.py +32 -6
  4. mcp_proxy_adapter/api/middleware/__init__.py +38 -32
  5. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  6. mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
  7. mcp_proxy_adapter/api/middleware/factory.py +243 -0
  8. mcp_proxy_adapter/api/middleware/logging.py +32 -6
  9. mcp_proxy_adapter/api/middleware/protocol_middleware.py +201 -0
  10. mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
  11. mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
  12. mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -0
  13. mcp_proxy_adapter/commands/__init__.py +19 -4
  14. mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
  15. mcp_proxy_adapter/commands/base.py +66 -32
  16. mcp_proxy_adapter/commands/builtin_commands.py +95 -0
  17. mcp_proxy_adapter/commands/catalog_manager.py +838 -0
  18. mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
  19. mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
  20. mcp_proxy_adapter/commands/command_registry.py +711 -354
  21. mcp_proxy_adapter/commands/dependency_manager.py +245 -0
  22. mcp_proxy_adapter/commands/echo_command.py +81 -0
  23. mcp_proxy_adapter/commands/health_command.py +8 -1
  24. mcp_proxy_adapter/commands/help_command.py +21 -14
  25. mcp_proxy_adapter/commands/hooks.py +200 -167
  26. mcp_proxy_adapter/commands/key_management_command.py +506 -0
  27. mcp_proxy_adapter/commands/load_command.py +176 -0
  28. mcp_proxy_adapter/commands/plugins_command.py +235 -0
  29. mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
  30. mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
  31. mcp_proxy_adapter/commands/reload_command.py +48 -50
  32. mcp_proxy_adapter/commands/result.py +1 -0
  33. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  34. mcp_proxy_adapter/commands/roles_management_command.py +697 -0
  35. mcp_proxy_adapter/commands/security_command.py +488 -0
  36. mcp_proxy_adapter/commands/ssl_setup_command.py +366 -0
  37. mcp_proxy_adapter/commands/token_management_command.py +529 -0
  38. mcp_proxy_adapter/commands/transport_management_command.py +144 -0
  39. mcp_proxy_adapter/commands/unload_command.py +158 -0
  40. mcp_proxy_adapter/config.py +394 -14
  41. mcp_proxy_adapter/core/app_factory.py +410 -0
  42. mcp_proxy_adapter/core/app_runner.py +272 -0
  43. mcp_proxy_adapter/core/auth_validator.py +606 -0
  44. mcp_proxy_adapter/core/certificate_utils.py +1045 -0
  45. mcp_proxy_adapter/core/client.py +574 -0
  46. mcp_proxy_adapter/core/client_manager.py +284 -0
  47. mcp_proxy_adapter/core/client_security.py +384 -0
  48. mcp_proxy_adapter/core/config_converter.py +405 -0
  49. mcp_proxy_adapter/core/config_validator.py +218 -0
  50. mcp_proxy_adapter/core/logging.py +19 -3
  51. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  52. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  53. mcp_proxy_adapter/core/protocol_manager.py +385 -0
  54. mcp_proxy_adapter/core/proxy_client.py +602 -0
  55. mcp_proxy_adapter/core/proxy_registration.py +522 -0
  56. mcp_proxy_adapter/core/role_utils.py +426 -0
  57. mcp_proxy_adapter/core/security_adapter.py +370 -0
  58. mcp_proxy_adapter/core/security_factory.py +239 -0
  59. mcp_proxy_adapter/core/security_integration.py +286 -0
  60. mcp_proxy_adapter/core/server_adapter.py +282 -0
  61. mcp_proxy_adapter/core/server_engine.py +270 -0
  62. mcp_proxy_adapter/core/settings.py +1 -0
  63. mcp_proxy_adapter/core/ssl_utils.py +234 -0
  64. mcp_proxy_adapter/core/transport_manager.py +292 -0
  65. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  66. mcp_proxy_adapter/custom_openapi.py +22 -11
  67. mcp_proxy_adapter/examples/__init__.py +13 -4
  68. mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
  69. mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
  70. mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
  71. mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
  72. mcp_proxy_adapter/examples/commands/__init__.py +5 -0
  73. mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
  74. mcp_proxy_adapter/examples/debug_request_state.py +112 -0
  75. mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
  76. mcp_proxy_adapter/examples/demo_client.py +275 -0
  77. mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
  78. mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
  79. mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
  80. mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
  81. mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
  82. mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
  83. mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
  84. mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  85. mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
  86. mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
  87. mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  88. mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
  89. mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
  90. mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
  91. mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
  92. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
  93. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  94. mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
  95. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
  96. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  97. mcp_proxy_adapter/examples/full_application/main.py +173 -0
  98. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
  99. mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
  100. mcp_proxy_adapter/examples/generate_certificates.py +177 -0
  101. mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
  102. mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
  103. mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
  104. mcp_proxy_adapter/examples/run_example.py +59 -0
  105. mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
  106. mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
  107. mcp_proxy_adapter/examples/run_security_tests.py +544 -0
  108. mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
  109. mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
  110. mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
  111. mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
  112. mcp_proxy_adapter/examples/security_test_client.py +782 -0
  113. mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
  114. mcp_proxy_adapter/examples/test_config.py +148 -0
  115. mcp_proxy_adapter/examples/test_config_generator.py +86 -0
  116. mcp_proxy_adapter/examples/test_examples.py +281 -0
  117. mcp_proxy_adapter/examples/universal_client.py +620 -0
  118. mcp_proxy_adapter/main.py +93 -0
  119. mcp_proxy_adapter/utils/config_generator.py +1008 -0
  120. mcp_proxy_adapter/version.py +5 -2
  121. mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
  122. mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
  123. mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
  124. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/licenses/LICENSE +2 -2
  125. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  126. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  127. mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
  128. mcp_proxy_adapter/examples/README.md +0 -124
  129. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  130. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  131. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  132. mcp_proxy_adapter/examples/basic_server/config.json +0 -35
  133. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  134. mcp_proxy_adapter/examples/basic_server/server.py +0 -103
  135. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  136. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  137. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -250
  138. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  139. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  140. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  141. mcp_proxy_adapter/examples/custom_commands/config.json +0 -35
  142. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  143. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  144. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  145. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  146. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  147. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  148. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  149. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  150. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  151. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  152. mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
  153. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  154. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  155. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  156. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  157. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  158. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  159. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  160. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  161. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  162. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  163. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  164. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  165. mcp_proxy_adapter/tests/__init__.py +0 -0
  166. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  167. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  168. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  169. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  170. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  171. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  172. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  173. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  174. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  175. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  176. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  177. mcp_proxy_adapter/tests/conftest.py +0 -131
  178. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  179. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  180. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  181. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  182. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  183. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  184. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  185. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  186. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  187. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  188. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  189. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  190. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  191. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  192. mcp_proxy_adapter/tests/test_config.py +0 -127
  193. mcp_proxy_adapter/tests/test_utils.py +0 -65
  194. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  195. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  196. mcp_proxy_adapter/tests/unit/test_config.py +0 -217
  197. mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
  198. mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
  199. {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
  200. {mcp_proxy_adapter-4.1.1.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)