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