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,602 @@
1
+ """
2
+ Proxy Client Module
3
+
4
+ This module provides a client for registering with MCP proxy servers
5
+ using mcp_security_framework for secure authentication and connections.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ import asyncio
12
+ import json
13
+ import time
14
+ import ssl
15
+ from typing import Dict, Any, Optional, Tuple, List
16
+ from urllib.parse import urljoin, urlparse
17
+ from pathlib import Path
18
+
19
+ import aiohttp
20
+ from aiohttp import ClientTimeout, TCPConnector
21
+
22
+ # Import framework components
23
+ try:
24
+ from mcp_security_framework.core.client_security import ClientSecurityManager
25
+ from mcp_security_framework.schemas.config import ClientSecurityConfig
26
+ from mcp_security_framework.schemas.models import AuthResult, ValidationResult
27
+ from mcp_security_framework.utils.crypto_utils import generate_api_key, create_jwt_token
28
+ from mcp_security_framework.utils.cert_utils import validate_certificate_format
29
+ SECURITY_FRAMEWORK_AVAILABLE = True
30
+ except ImportError:
31
+ SECURITY_FRAMEWORK_AVAILABLE = False
32
+ ClientSecurityManager = None
33
+ ClientSecurityConfig = None
34
+ AuthResult = None
35
+ ValidationResult = None
36
+
37
+ from mcp_proxy_adapter.core.logging import logger
38
+
39
+
40
+ class ProxyClientError(Exception):
41
+ """Exception raised when proxy client operations fail."""
42
+ pass
43
+
44
+
45
+ class ProxyClient:
46
+ """
47
+ Client for registering with MCP proxy servers.
48
+
49
+ Provides secure registration, heartbeat, and discovery functionality
50
+ using mcp_security_framework for authentication and SSL/TLS.
51
+ """
52
+
53
+ def __init__(self, config: Dict[str, Any]):
54
+ """
55
+ Initialize proxy client.
56
+
57
+ Args:
58
+ config: Client configuration
59
+ """
60
+ self.config = config
61
+ self.registration_config = config.get("registration", {})
62
+
63
+ # Basic settings
64
+ self.proxy_url = self.registration_config.get("server_url")
65
+ self.server_id = self.registration_config.get("proxy_info", {}).get("name", "mcp_proxy_adapter")
66
+ self.server_name = self.registration_config.get("proxy_info", {}).get("name", "MCP Proxy Adapter")
67
+ self.description = self.registration_config.get("proxy_info", {}).get("description", "")
68
+
69
+ # Authentication settings
70
+ self.auth_method = self.registration_config.get("auth_method", "none")
71
+ self.auth_config = self._get_auth_config()
72
+
73
+ # Heartbeat settings
74
+ heartbeat_config = self.registration_config.get("heartbeat", {})
75
+ self.heartbeat_interval = heartbeat_config.get("interval", 300)
76
+ self.heartbeat_timeout = heartbeat_config.get("timeout", 30)
77
+ self.retry_attempts = heartbeat_config.get("retry_attempts", 3)
78
+ self.retry_delay = heartbeat_config.get("retry_delay", 60)
79
+
80
+ # Auto discovery settings
81
+ discovery_config = self.registration_config.get("auto_discovery", {})
82
+ self.discovery_enabled = discovery_config.get("enabled", False)
83
+ self.discovery_urls = discovery_config.get("discovery_urls", [])
84
+ self.discovery_interval = discovery_config.get("discovery_interval", 3600)
85
+
86
+ # Initialize security manager
87
+ self.security_manager = self._create_security_manager()
88
+
89
+ # State
90
+ self.registered = False
91
+ self.server_key: Optional[str] = None
92
+ self.server_url: Optional[str] = None
93
+ self.heartbeat_task: Optional[asyncio.Task] = None
94
+ self.discovery_task: Optional[asyncio.Task] = None
95
+
96
+ logger.info("Proxy client initialized with security framework integration")
97
+
98
+ def _get_auth_config(self) -> Dict[str, Any]:
99
+ """Get authentication configuration based on auth method."""
100
+ if self.auth_method == "certificate":
101
+ return self.registration_config.get("certificate", {})
102
+ elif self.auth_method == "token":
103
+ return self.registration_config.get("token", {})
104
+ elif self.auth_method == "api_key":
105
+ return self.registration_config.get("api_key", {})
106
+ else:
107
+ return {}
108
+
109
+ def _create_security_manager(self) -> Optional[ClientSecurityManager]:
110
+ """Create client security manager."""
111
+ if not SECURITY_FRAMEWORK_AVAILABLE:
112
+ logger.warning("mcp_security_framework not available, using basic client")
113
+ return None
114
+
115
+ try:
116
+ # Create client security configuration
117
+ client_security_config = self.registration_config.get("client_security", {})
118
+
119
+ if not client_security_config.get("enabled", False):
120
+ logger.info("Client security disabled in configuration")
121
+ return None
122
+
123
+ # Create security config
124
+ security_config = {
125
+ "security": {
126
+ "ssl": {
127
+ "enabled": client_security_config.get("ssl_enabled", False),
128
+ "client_cert_file": client_security_config.get("certificate_auth", {}).get("cert_file"),
129
+ "client_key_file": client_security_config.get("certificate_auth", {}).get("key_file"),
130
+ "ca_cert_file": client_security_config.get("certificate_auth", {}).get("ca_cert_file"),
131
+ "verify_mode": "CERT_REQUIRED",
132
+ "min_tls_version": "TLSv1.2",
133
+ "check_hostname": True,
134
+ "check_expiry": True
135
+ },
136
+ "auth": {
137
+ "enabled": True,
138
+ "methods": client_security_config.get("auth_methods", ["api_key"]),
139
+ "api_keys": {
140
+ client_security_config.get("api_key_auth", {}).get("key", "default"): {
141
+ "roles": ["proxy_client"],
142
+ "permissions": ["register", "heartbeat", "discover"]
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ return ClientSecurityManager(security_config)
150
+
151
+ except Exception as e:
152
+ logger.error(f"Failed to create security manager: {e}")
153
+ return None
154
+
155
+ def set_server_url(self, server_url: str) -> None:
156
+ """
157
+ Set the server URL for registration.
158
+
159
+ Args:
160
+ server_url: The URL where this server is accessible.
161
+ """
162
+ self.server_url = server_url
163
+ logger.info(f"Proxy client server URL set to: {server_url}")
164
+
165
+ def _get_auth_headers(self) -> Dict[str, str]:
166
+ """
167
+ Get authentication headers for requests.
168
+
169
+ Returns:
170
+ Dictionary of authentication headers
171
+ """
172
+ headers = {"Content-Type": "application/json"}
173
+
174
+ if not self.security_manager:
175
+ return headers
176
+
177
+ try:
178
+ if self.auth_method == "certificate":
179
+ return self.security_manager.get_client_auth_headers("certificate")
180
+ elif self.auth_method == "token":
181
+ token = self.auth_config.get("token")
182
+ return self.security_manager.get_client_auth_headers("jwt", token=token)
183
+ elif self.auth_method == "api_key":
184
+ api_key = self.auth_config.get("key")
185
+ return self.security_manager.get_client_auth_headers("api_key", api_key=api_key)
186
+ else:
187
+ return headers
188
+ except Exception as e:
189
+ logger.error(f"Failed to get auth headers: {e}")
190
+ return headers
191
+
192
+ def _create_ssl_context(self) -> Optional[ssl.SSLContext]:
193
+ """
194
+ Create SSL context for secure connections.
195
+
196
+ Returns:
197
+ SSL context or None if SSL not needed
198
+ """
199
+ if not self.security_manager:
200
+ return None
201
+
202
+ try:
203
+ return self.security_manager.create_client_ssl_context()
204
+ except Exception as e:
205
+ logger.error(f"Failed to create SSL context: {e}")
206
+ return None
207
+
208
+ async def register(self) -> bool:
209
+ """
210
+ Register with the proxy server.
211
+
212
+ Returns:
213
+ True if registration was successful, False otherwise.
214
+ """
215
+ if not self.proxy_url:
216
+ logger.error("Proxy URL not configured")
217
+ return False
218
+
219
+ if not self.server_url:
220
+ logger.error("Server URL not set")
221
+ return False
222
+
223
+ # Prepare registration data
224
+ proxy_info = self.registration_config.get("proxy_info", {})
225
+ registration_data = {
226
+ "server_id": self.server_id,
227
+ "server_url": self.server_url,
228
+ "server_name": self.server_name,
229
+ "description": self.description,
230
+ "version": proxy_info.get("version", "1.0.0"),
231
+ "capabilities": proxy_info.get("capabilities", ["jsonrpc", "rest"]),
232
+ "endpoints": proxy_info.get("endpoints", {
233
+ "jsonrpc": "/api/jsonrpc",
234
+ "rest": "/cmd",
235
+ "health": "/health"
236
+ }),
237
+ "auth_method": self.auth_method,
238
+ "security_enabled": self.security_manager is not None
239
+ }
240
+
241
+ logger.info(f"Attempting to register with proxy at {self.proxy_url}")
242
+ logger.debug(f"Registration data: {registration_data}")
243
+
244
+ for attempt in range(self.retry_attempts):
245
+ try:
246
+ success, result = await self._make_request("/register", registration_data)
247
+
248
+ if success:
249
+ self.registered = True
250
+ self.server_key = result.get("server_key")
251
+ logger.info(f"✅ Successfully registered with proxy. Server key: {self.server_key}")
252
+
253
+ # Start heartbeat and discovery
254
+ await self._start_background_tasks()
255
+
256
+ return True
257
+ else:
258
+ error_msg = result.get("error", {}).get("message", "Unknown error")
259
+ logger.warning(f"❌ Registration attempt {attempt + 1} failed: {error_msg}")
260
+
261
+ if attempt < self.retry_attempts - 1:
262
+ logger.info(f"Retrying in {self.retry_delay} seconds...")
263
+ await asyncio.sleep(self.retry_delay)
264
+
265
+ except Exception as e:
266
+ logger.error(f"❌ Registration attempt {attempt + 1} failed with exception: {e}")
267
+
268
+ if attempt < self.retry_attempts - 1:
269
+ logger.info(f"Retrying in {self.retry_delay} seconds...")
270
+ await asyncio.sleep(self.retry_delay)
271
+
272
+ logger.error(f"❌ Failed to register with proxy after {self.retry_attempts} attempts")
273
+ return False
274
+
275
+ async def unregister(self) -> bool:
276
+ """
277
+ Unregister from the proxy server.
278
+
279
+ Returns:
280
+ True if unregistration was successful, False otherwise.
281
+ """
282
+ if not self.registered or not self.server_key:
283
+ logger.info("Not registered with proxy, skipping unregistration")
284
+ return True
285
+
286
+ # Stop background tasks
287
+ await self._stop_background_tasks()
288
+
289
+ # Extract copy_number from server_key
290
+ try:
291
+ copy_number = int(self.server_key.split("_")[-1])
292
+ except (ValueError, IndexError):
293
+ copy_number = 1
294
+
295
+ unregistration_data = {
296
+ "server_id": self.server_id,
297
+ "copy_number": copy_number
298
+ }
299
+
300
+ logger.info(f"Attempting to unregister from proxy at {self.proxy_url}")
301
+
302
+ try:
303
+ success, result = await self._make_request("/unregister", unregistration_data)
304
+
305
+ if success:
306
+ unregistered = result.get("unregistered", False)
307
+ if unregistered:
308
+ logger.info("✅ Successfully unregistered from proxy")
309
+ else:
310
+ logger.warning("⚠️ Server was not found in proxy registry")
311
+
312
+ self.registered = False
313
+ self.server_key = None
314
+ return True
315
+ else:
316
+ error_msg = result.get("error", {}).get("message", "Unknown error")
317
+ logger.error(f"❌ Failed to unregister from proxy: {error_msg}")
318
+ return False
319
+
320
+ except Exception as e:
321
+ logger.error(f"❌ Unregistration failed with exception: {e}")
322
+ return False
323
+
324
+ async def send_heartbeat(self) -> bool:
325
+ """
326
+ Send heartbeat to proxy server.
327
+
328
+ Returns:
329
+ True if heartbeat was successful, False otherwise.
330
+ """
331
+ if not self.server_key:
332
+ return False
333
+
334
+ heartbeat_data = {
335
+ "server_id": self.server_id,
336
+ "server_key": self.server_key,
337
+ "timestamp": int(time.time()),
338
+ "status": "healthy"
339
+ }
340
+
341
+ try:
342
+ success, result = await self._make_request("/heartbeat", heartbeat_data)
343
+
344
+ if success:
345
+ logger.debug("Heartbeat sent successfully")
346
+ return True
347
+ else:
348
+ logger.warning(f"Heartbeat failed: {result.get('error', {}).get('message', 'Unknown error')}")
349
+ return False
350
+
351
+ except Exception as e:
352
+ logger.error(f"Heartbeat error: {e}")
353
+ return False
354
+
355
+ async def discover_proxies(self) -> List[Dict[str, Any]]:
356
+ """
357
+ Discover available proxy servers.
358
+
359
+ Returns:
360
+ List of discovered proxy servers.
361
+ """
362
+ if not self.discovery_enabled:
363
+ return []
364
+
365
+ discovered_proxies = []
366
+
367
+ for discovery_url in self.discovery_urls:
368
+ try:
369
+ success, result = await self._make_request("/discover", {}, base_url=discovery_url)
370
+
371
+ if success:
372
+ proxies = result.get("proxies", [])
373
+ discovered_proxies.extend(proxies)
374
+ logger.info(f"Discovered {len(proxies)} proxies from {discovery_url}")
375
+ else:
376
+ logger.warning(f"Discovery failed for {discovery_url}")
377
+
378
+ except Exception as e:
379
+ logger.error(f"Discovery error for {discovery_url}: {e}")
380
+
381
+ return discovered_proxies
382
+
383
+ async def _make_request(self, endpoint: str, data: Dict[str, Any], base_url: Optional[str] = None) -> Tuple[bool, Dict[str, Any]]:
384
+ """
385
+ Make HTTP request to proxy server.
386
+
387
+ Args:
388
+ endpoint: API endpoint
389
+ data: Request data
390
+ base_url: Base URL (optional, uses self.proxy_url if not provided)
391
+
392
+ Returns:
393
+ Tuple of (success, result)
394
+ """
395
+ url = urljoin(base_url or self.proxy_url, endpoint)
396
+
397
+ # Get authentication headers
398
+ headers = self._get_auth_headers()
399
+
400
+ # Create SSL context if needed
401
+ ssl_context = self._create_ssl_context()
402
+
403
+ # Create connector with SSL context
404
+ connector = None
405
+ if ssl_context:
406
+ connector = TCPConnector(ssl=ssl_context)
407
+
408
+ try:
409
+ timeout = ClientTimeout(total=self.heartbeat_timeout)
410
+
411
+ async with aiohttp.ClientSession(connector=connector) as session:
412
+ async with session.post(
413
+ url,
414
+ json=data,
415
+ headers=headers,
416
+ timeout=timeout
417
+ ) as response:
418
+ result = await response.json()
419
+
420
+ # Validate response if security manager available
421
+ if self.security_manager:
422
+ self.security_manager.validate_server_response(dict(response.headers))
423
+
424
+ return response.status == 200, result
425
+ except Exception as e:
426
+ logger.error(f"Request failed: {e}")
427
+ return False, {"error": {"message": str(e)}}
428
+ finally:
429
+ if connector:
430
+ await connector.close()
431
+
432
+ async def _start_background_tasks(self) -> None:
433
+ """Start heartbeat and discovery background tasks."""
434
+ # Start heartbeat
435
+ if self.registration_config.get("heartbeat", {}).get("enabled", True):
436
+ self.heartbeat_task = asyncio.create_task(self._heartbeat_loop())
437
+ logger.info("Heartbeat task started")
438
+
439
+ # Start discovery
440
+ if self.discovery_enabled:
441
+ self.discovery_task = asyncio.create_task(self._discovery_loop())
442
+ logger.info("Discovery task started")
443
+
444
+ async def _stop_background_tasks(self) -> None:
445
+ """Stop background tasks."""
446
+ # Stop heartbeat
447
+ if self.heartbeat_task and not self.heartbeat_task.done():
448
+ self.heartbeat_task.cancel()
449
+ try:
450
+ await self.heartbeat_task
451
+ except asyncio.CancelledError:
452
+ pass
453
+ logger.info("Heartbeat task stopped")
454
+
455
+ # Stop discovery
456
+ if self.discovery_task and not self.discovery_task.done():
457
+ self.discovery_task.cancel()
458
+ try:
459
+ await self.discovery_task
460
+ except asyncio.CancelledError:
461
+ pass
462
+ logger.info("Discovery task stopped")
463
+
464
+ async def _heartbeat_loop(self) -> None:
465
+ """Heartbeat loop to keep registration alive."""
466
+ while self.registered:
467
+ try:
468
+ await asyncio.sleep(self.heartbeat_interval)
469
+
470
+ if not self.registered:
471
+ break
472
+
473
+ # Send heartbeat
474
+ success = await self.send_heartbeat()
475
+ if not success:
476
+ logger.warning("Heartbeat failed, attempting to re-register")
477
+ await self.register()
478
+
479
+ except asyncio.CancelledError:
480
+ break
481
+ except Exception as e:
482
+ logger.error(f"Heartbeat error: {e}")
483
+
484
+ async def _discovery_loop(self) -> None:
485
+ """Discovery loop to find new proxy servers."""
486
+ while self.registered:
487
+ try:
488
+ await asyncio.sleep(self.discovery_interval)
489
+
490
+ if not self.registered:
491
+ break
492
+
493
+ # Discover proxies
494
+ proxies = await self.discover_proxies()
495
+ if proxies:
496
+ logger.info(f"Discovered {len(proxies)} proxy servers")
497
+
498
+ # Register with new proxies if configured
499
+ if self.registration_config.get("auto_discovery", {}).get("register_on_discovery", False):
500
+ for proxy in proxies:
501
+ proxy_url = proxy.get("url")
502
+ if proxy_url and proxy_url != self.proxy_url:
503
+ logger.info(f"Attempting to register with discovered proxy: {proxy_url}")
504
+ # Store original URL and try to register with new proxy
505
+ original_url = self.proxy_url
506
+ self.proxy_url = proxy_url
507
+ await self.register()
508
+ self.proxy_url = original_url
509
+
510
+ except asyncio.CancelledError:
511
+ break
512
+ except Exception as e:
513
+ logger.error(f"Discovery error: {e}")
514
+
515
+ def get_status(self) -> Dict[str, Any]:
516
+ """
517
+ Get current client status.
518
+
519
+ Returns:
520
+ Dictionary with client status information.
521
+ """
522
+ status = {
523
+ "enabled": self.registration_config.get("enabled", False),
524
+ "registered": self.registered,
525
+ "server_key": self.server_key,
526
+ "server_url": self.server_url,
527
+ "proxy_url": self.proxy_url,
528
+ "server_id": self.server_id,
529
+ "auth_method": self.auth_method,
530
+ "heartbeat_active": self.heartbeat_task is not None and not self.heartbeat_task.done(),
531
+ "discovery_active": self.discovery_task is not None and not self.discovery_task.done()
532
+ }
533
+
534
+ # Add security information
535
+ if self.security_manager:
536
+ status["security_enabled"] = True
537
+ status["ssl_enabled"] = self.security_manager.is_ssl_enabled()
538
+ status["auth_methods"] = self.security_manager.get_supported_auth_methods()
539
+ else:
540
+ status["security_enabled"] = False
541
+
542
+ return status
543
+
544
+
545
+ # Global proxy client instance
546
+ proxy_client: Optional[ProxyClient] = None
547
+
548
+
549
+ def initialize_proxy_client(config: Dict[str, Any]) -> None:
550
+ """
551
+ Initialize global proxy client.
552
+
553
+ Args:
554
+ config: Application configuration
555
+ """
556
+ global proxy_client
557
+ proxy_client = ProxyClient(config)
558
+
559
+
560
+ async def register_with_proxy(server_url: str) -> bool:
561
+ """
562
+ Register with proxy server.
563
+
564
+ Args:
565
+ server_url: The URL where this server is accessible.
566
+
567
+ Returns:
568
+ True if registration was successful, False otherwise.
569
+ """
570
+ if not proxy_client:
571
+ logger.error("Proxy client not initialized")
572
+ return False
573
+
574
+ proxy_client.set_server_url(server_url)
575
+ return await proxy_client.register()
576
+
577
+
578
+ async def unregister_from_proxy() -> bool:
579
+ """
580
+ Unregister from proxy server.
581
+
582
+ Returns:
583
+ True if unregistration was successful, False otherwise.
584
+ """
585
+ if not proxy_client:
586
+ logger.error("Proxy client not initialized")
587
+ return False
588
+
589
+ return await proxy_client.unregister()
590
+
591
+
592
+ def get_proxy_client_status() -> Dict[str, Any]:
593
+ """
594
+ Get proxy client status.
595
+
596
+ Returns:
597
+ Dictionary with client status information.
598
+ """
599
+ if not proxy_client:
600
+ return {"error": "Proxy client not initialized"}
601
+
602
+ return proxy_client.get_status()