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.
Files changed (212) hide show
  1. mcp_proxy_adapter/__main__.py +27 -7
  2. mcp_proxy_adapter/api/app.py +209 -79
  3. mcp_proxy_adapter/api/handlers.py +16 -5
  4. mcp_proxy_adapter/api/middleware/__init__.py +14 -9
  5. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
  6. mcp_proxy_adapter/api/middleware/factory.py +36 -12
  7. mcp_proxy_adapter/api/middleware/protocol_middleware.py +84 -18
  8. mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
  9. mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -0
  10. mcp_proxy_adapter/commands/__init__.py +7 -1
  11. mcp_proxy_adapter/commands/base.py +7 -4
  12. mcp_proxy_adapter/commands/builtin_commands.py +8 -2
  13. mcp_proxy_adapter/commands/command_registry.py +8 -0
  14. mcp_proxy_adapter/commands/echo_command.py +81 -0
  15. mcp_proxy_adapter/commands/health_command.py +1 -1
  16. mcp_proxy_adapter/commands/help_command.py +21 -14
  17. mcp_proxy_adapter/commands/proxy_registration_command.py +326 -185
  18. mcp_proxy_adapter/commands/role_test_command.py +141 -0
  19. mcp_proxy_adapter/commands/security_command.py +488 -0
  20. mcp_proxy_adapter/commands/ssl_setup_command.py +234 -351
  21. mcp_proxy_adapter/commands/token_management_command.py +1 -1
  22. mcp_proxy_adapter/config.py +323 -40
  23. mcp_proxy_adapter/core/app_factory.py +410 -0
  24. mcp_proxy_adapter/core/app_runner.py +272 -0
  25. mcp_proxy_adapter/core/certificate_utils.py +291 -73
  26. mcp_proxy_adapter/core/client.py +574 -0
  27. mcp_proxy_adapter/core/client_manager.py +284 -0
  28. mcp_proxy_adapter/core/client_security.py +384 -0
  29. mcp_proxy_adapter/core/logging.py +8 -3
  30. mcp_proxy_adapter/core/mtls_asgi.py +156 -0
  31. mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
  32. mcp_proxy_adapter/core/protocol_manager.py +169 -10
  33. mcp_proxy_adapter/core/proxy_client.py +602 -0
  34. mcp_proxy_adapter/core/proxy_registration.py +299 -47
  35. mcp_proxy_adapter/core/security_adapter.py +12 -15
  36. mcp_proxy_adapter/core/security_integration.py +286 -0
  37. mcp_proxy_adapter/core/server_adapter.py +282 -0
  38. mcp_proxy_adapter/core/server_engine.py +270 -0
  39. mcp_proxy_adapter/core/ssl_utils.py +13 -12
  40. mcp_proxy_adapter/core/transport_manager.py +5 -5
  41. mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
  42. mcp_proxy_adapter/examples/__init__.py +13 -4
  43. mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
  44. mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
  45. mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
  46. mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
  47. mcp_proxy_adapter/examples/commands/__init__.py +5 -0
  48. mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
  49. mcp_proxy_adapter/examples/debug_request_state.py +112 -0
  50. mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
  51. mcp_proxy_adapter/examples/demo_client.py +275 -0
  52. mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
  53. mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
  54. mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
  55. mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
  56. mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
  57. mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
  58. mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
  59. mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  60. mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
  61. mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
  62. mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  63. mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
  64. mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
  65. mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
  66. mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
  67. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
  68. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
  69. mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
  70. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
  71. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
  72. mcp_proxy_adapter/examples/full_application/main.py +173 -0
  73. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
  74. mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
  75. mcp_proxy_adapter/examples/generate_certificates.py +177 -0
  76. mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
  77. mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
  78. mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
  79. mcp_proxy_adapter/examples/run_example.py +59 -0
  80. mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
  81. mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
  82. mcp_proxy_adapter/examples/run_security_tests.py +544 -0
  83. mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
  84. mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
  85. mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
  86. mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
  87. mcp_proxy_adapter/examples/security_test_client.py +782 -0
  88. mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
  89. mcp_proxy_adapter/examples/test_config.py +148 -0
  90. mcp_proxy_adapter/examples/test_config_generator.py +86 -0
  91. mcp_proxy_adapter/examples/test_examples.py +281 -0
  92. mcp_proxy_adapter/examples/universal_client.py +620 -0
  93. mcp_proxy_adapter/main.py +66 -148
  94. mcp_proxy_adapter/utils/config_generator.py +1008 -0
  95. mcp_proxy_adapter/version.py +5 -2
  96. mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
  97. mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
  98. mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
  99. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/licenses/LICENSE +2 -2
  100. mcp_proxy_adapter/api/middleware/auth.py +0 -146
  101. mcp_proxy_adapter/api/middleware/auth_adapter.py +0 -235
  102. mcp_proxy_adapter/api/middleware/mtls_adapter.py +0 -305
  103. mcp_proxy_adapter/api/middleware/mtls_middleware.py +0 -296
  104. mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
  105. mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +0 -241
  106. mcp_proxy_adapter/api/middleware/roles_adapter.py +0 -365
  107. mcp_proxy_adapter/api/middleware/roles_middleware.py +0 -381
  108. mcp_proxy_adapter/api/middleware/security.py +0 -376
  109. mcp_proxy_adapter/api/middleware/token_auth_middleware.py +0 -261
  110. mcp_proxy_adapter/examples/README.md +0 -124
  111. mcp_proxy_adapter/examples/basic_server/README.md +0 -60
  112. mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
  113. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
  114. mcp_proxy_adapter/examples/basic_server/config.json +0 -70
  115. mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +0 -54
  116. mcp_proxy_adapter/examples/basic_server/config_http.json +0 -70
  117. mcp_proxy_adapter/examples/basic_server/config_http_only.json +0 -52
  118. mcp_proxy_adapter/examples/basic_server/config_https.json +0 -58
  119. mcp_proxy_adapter/examples/basic_server/config_mtls.json +0 -58
  120. mcp_proxy_adapter/examples/basic_server/config_ssl.json +0 -46
  121. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
  122. mcp_proxy_adapter/examples/basic_server/server.py +0 -114
  123. mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
  124. mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
  125. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -566
  126. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
  127. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
  128. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
  129. mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +0 -105
  130. mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +0 -129
  131. mcp_proxy_adapter/examples/custom_commands/config.json +0 -118
  132. mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +0 -46
  133. mcp_proxy_adapter/examples/custom_commands/config_https_only.json +0 -46
  134. mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +0 -33
  135. mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +0 -46
  136. mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +0 -33
  137. mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +0 -33
  138. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
  139. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
  140. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
  141. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
  142. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
  143. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
  144. mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
  145. mcp_proxy_adapter/examples/custom_commands/full_help_response.json +0 -1
  146. mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +0 -629
  147. mcp_proxy_adapter/examples/custom_commands/get_openapi.py +0 -103
  148. mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
  149. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
  150. mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +0 -129
  151. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
  152. mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +0 -278
  153. mcp_proxy_adapter/examples/custom_commands/server.py +0 -252
  154. mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +0 -75
  155. mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +0 -299
  156. mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +0 -278
  157. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
  158. mcp_proxy_adapter/examples/custom_commands/test_openapi.py +0 -27
  159. mcp_proxy_adapter/examples/custom_commands/test_registry.py +0 -23
  160. mcp_proxy_adapter/examples/custom_commands/test_simple.py +0 -19
  161. mcp_proxy_adapter/examples/custom_project_example/README.md +0 -103
  162. mcp_proxy_adapter/examples/custom_project_example/README_EN.md +0 -103
  163. mcp_proxy_adapter/examples/deployment/README.md +0 -49
  164. mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
  165. mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
  166. mcp_proxy_adapter/examples/deployment/config.json +0 -29
  167. mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
  168. mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
  169. mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
  170. mcp_proxy_adapter/examples/deployment/run.sh +0 -43
  171. mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
  172. mcp_proxy_adapter/examples/simple_custom_commands/README.md +0 -149
  173. mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +0 -149
  174. mcp_proxy_adapter/schemas/base_schema.json +0 -114
  175. mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
  176. mcp_proxy_adapter/schemas/roles_schema.json +0 -162
  177. mcp_proxy_adapter/tests/__init__.py +0 -0
  178. mcp_proxy_adapter/tests/api/__init__.py +0 -3
  179. mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
  180. mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
  181. mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
  182. mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
  183. mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
  184. mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
  185. mcp_proxy_adapter/tests/commands/__init__.py +0 -3
  186. mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
  187. mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
  188. mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
  189. mcp_proxy_adapter/tests/conftest.py +0 -131
  190. mcp_proxy_adapter/tests/functional/__init__.py +0 -3
  191. mcp_proxy_adapter/tests/functional/test_api.py +0 -253
  192. mcp_proxy_adapter/tests/integration/__init__.py +0 -3
  193. mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
  194. mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
  195. mcp_proxy_adapter/tests/performance/__init__.py +0 -3
  196. mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
  197. mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
  198. mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
  199. mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
  200. mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
  201. mcp_proxy_adapter/tests/test_base_command.py +0 -123
  202. mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
  203. mcp_proxy_adapter/tests/test_command_registry.py +0 -281
  204. mcp_proxy_adapter/tests/test_config.py +0 -127
  205. mcp_proxy_adapter/tests/test_utils.py +0 -65
  206. mcp_proxy_adapter/tests/unit/__init__.py +0 -3
  207. mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
  208. mcp_proxy_adapter/tests/unit/test_config.py +0 -270
  209. mcp_proxy_adapter-6.0.0.dist-info/METADATA +0 -201
  210. mcp_proxy_adapter-6.0.0.dist-info/RECORD +0 -179
  211. {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
  212. {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
- """Initialize the protocol manager."""
26
- self.protocols_config = config.get("protocols", {})
27
- self.enabled = self.protocols_config.get("enabled", True)
28
- self.allowed_protocols = self.protocols_config.get("allowed_protocols", ["http"])
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
- return self.protocols_config.get(protocol_lower, {}).copy()
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
- ssl_config = config.get("ssl", {})
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
- ssl_config = config.get("ssl", {})
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 = ProtocolManager()
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