mcp-proxy-adapter 6.9.28__py3-none-any.whl → 6.9.30__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.
Potentially problematic release.
This version of mcp-proxy-adapter might be problematic. Click here for more details.
- mcp_proxy_adapter/__init__.py +10 -0
- mcp_proxy_adapter/__main__.py +8 -21
- mcp_proxy_adapter/api/app.py +10 -913
- mcp_proxy_adapter/api/core/__init__.py +18 -0
- mcp_proxy_adapter/api/core/app_factory.py +243 -0
- mcp_proxy_adapter/api/core/lifespan_manager.py +55 -0
- mcp_proxy_adapter/api/core/registration_manager.py +166 -0
- mcp_proxy_adapter/api/core/ssl_context_factory.py +88 -0
- mcp_proxy_adapter/api/handlers.py +78 -199
- mcp_proxy_adapter/api/middleware/__init__.py +1 -44
- mcp_proxy_adapter/api/middleware/base.py +0 -42
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +0 -85
- mcp_proxy_adapter/api/middleware/error_handling.py +1 -127
- mcp_proxy_adapter/api/middleware/factory.py +0 -94
- mcp_proxy_adapter/api/middleware/logging.py +0 -112
- mcp_proxy_adapter/api/middleware/performance.py +0 -35
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +2 -98
- mcp_proxy_adapter/api/middleware/transport_middleware.py +0 -37
- mcp_proxy_adapter/api/middleware/unified_security.py +10 -10
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +0 -118
- mcp_proxy_adapter/api/openapi/__init__.py +21 -0
- mcp_proxy_adapter/api/openapi/command_integration.py +105 -0
- mcp_proxy_adapter/api/openapi/openapi_generator.py +40 -0
- mcp_proxy_adapter/api/openapi/openapi_registry.py +62 -0
- mcp_proxy_adapter/api/openapi/schema_loader.py +116 -0
- mcp_proxy_adapter/api/schemas.py +0 -61
- mcp_proxy_adapter/api/tool_integration.py +0 -117
- mcp_proxy_adapter/api/tools.py +0 -46
- mcp_proxy_adapter/cli/__init__.py +12 -0
- mcp_proxy_adapter/cli/commands/__init__.py +15 -0
- mcp_proxy_adapter/cli/commands/client.py +100 -0
- mcp_proxy_adapter/cli/commands/config_generate.py +21 -0
- mcp_proxy_adapter/cli/commands/config_validate.py +36 -0
- mcp_proxy_adapter/cli/commands/generate.py +259 -0
- mcp_proxy_adapter/cli/commands/server.py +174 -0
- mcp_proxy_adapter/cli/commands/sets.py +128 -0
- mcp_proxy_adapter/cli/commands/testconfig.py +177 -0
- mcp_proxy_adapter/cli/examples/__init__.py +8 -0
- mcp_proxy_adapter/cli/examples/http_basic.py +82 -0
- mcp_proxy_adapter/cli/examples/https_token.py +96 -0
- mcp_proxy_adapter/cli/examples/mtls_roles.py +103 -0
- mcp_proxy_adapter/cli/main.py +63 -0
- mcp_proxy_adapter/cli/parser.py +324 -0
- mcp_proxy_adapter/cli/validators.py +231 -0
- mcp_proxy_adapter/client/jsonrpc_client.py +406 -0
- mcp_proxy_adapter/client/proxy.py +45 -0
- mcp_proxy_adapter/commands/__init__.py +44 -28
- mcp_proxy_adapter/commands/auth_validation_command.py +7 -344
- mcp_proxy_adapter/commands/base.py +19 -43
- mcp_proxy_adapter/commands/builtin_commands.py +0 -75
- mcp_proxy_adapter/commands/catalog/__init__.py +20 -0
- mcp_proxy_adapter/commands/catalog/catalog_loader.py +34 -0
- mcp_proxy_adapter/commands/catalog/catalog_manager.py +122 -0
- mcp_proxy_adapter/commands/catalog/catalog_syncer.py +149 -0
- mcp_proxy_adapter/commands/catalog/command_catalog.py +43 -0
- mcp_proxy_adapter/commands/catalog/dependency_manager.py +37 -0
- mcp_proxy_adapter/commands/catalog_manager.py +58 -928
- mcp_proxy_adapter/commands/cert_monitor_command.py +0 -88
- mcp_proxy_adapter/commands/certificate_management_command.py +0 -45
- mcp_proxy_adapter/commands/command_registry.py +172 -904
- mcp_proxy_adapter/commands/config_command.py +0 -28
- mcp_proxy_adapter/commands/dependency_container.py +1 -70
- mcp_proxy_adapter/commands/dependency_manager.py +0 -128
- mcp_proxy_adapter/commands/echo_command.py +0 -34
- mcp_proxy_adapter/commands/health_command.py +0 -3
- mcp_proxy_adapter/commands/help_command.py +0 -159
- mcp_proxy_adapter/commands/hooks.py +0 -137
- mcp_proxy_adapter/commands/key_management_command.py +0 -25
- mcp_proxy_adapter/commands/load_command.py +7 -78
- mcp_proxy_adapter/commands/plugins_command.py +0 -16
- mcp_proxy_adapter/commands/protocol_management_command.py +0 -28
- mcp_proxy_adapter/commands/proxy_registration_command.py +0 -88
- mcp_proxy_adapter/commands/queue_commands.py +750 -0
- mcp_proxy_adapter/commands/registration_status_command.py +0 -43
- mcp_proxy_adapter/commands/registry/__init__.py +18 -0
- mcp_proxy_adapter/commands/registry/command_info.py +103 -0
- mcp_proxy_adapter/commands/registry/command_loader.py +207 -0
- mcp_proxy_adapter/commands/registry/command_manager.py +119 -0
- mcp_proxy_adapter/commands/registry/command_registry.py +217 -0
- mcp_proxy_adapter/commands/reload_command.py +0 -80
- mcp_proxy_adapter/commands/result.py +25 -77
- mcp_proxy_adapter/commands/role_test_command.py +0 -44
- mcp_proxy_adapter/commands/roles_management_command.py +0 -199
- mcp_proxy_adapter/commands/security_command.py +0 -30
- mcp_proxy_adapter/commands/settings_command.py +0 -68
- mcp_proxy_adapter/commands/ssl_setup_command.py +0 -42
- mcp_proxy_adapter/commands/token_management_command.py +0 -1
- mcp_proxy_adapter/commands/transport_management_command.py +0 -20
- mcp_proxy_adapter/commands/unload_command.py +0 -71
- mcp_proxy_adapter/config.py +15 -626
- mcp_proxy_adapter/core/__init__.py +5 -39
- mcp_proxy_adapter/core/app_factory.py +14 -36
- mcp_proxy_adapter/core/app_runner.py +0 -27
- mcp_proxy_adapter/core/auth_validator.py +1 -93
- mcp_proxy_adapter/core/certificate/__init__.py +20 -0
- mcp_proxy_adapter/core/certificate/certificate_creator.py +371 -0
- mcp_proxy_adapter/core/certificate/certificate_extractor.py +183 -0
- mcp_proxy_adapter/core/certificate/certificate_utils.py +249 -0
- mcp_proxy_adapter/core/certificate/certificate_validator.py +110 -0
- mcp_proxy_adapter/core/certificate/ssl_context_manager.py +70 -0
- mcp_proxy_adapter/core/certificate_utils.py +64 -903
- mcp_proxy_adapter/core/client.py +10 -9
- mcp_proxy_adapter/core/client_manager.py +0 -19
- mcp_proxy_adapter/core/client_security.py +0 -2
- mcp_proxy_adapter/core/config/__init__.py +18 -0
- mcp_proxy_adapter/core/config/config.py +195 -0
- mcp_proxy_adapter/core/config/config_factory.py +22 -0
- mcp_proxy_adapter/core/config/config_loader.py +66 -0
- mcp_proxy_adapter/core/config/feature_manager.py +31 -0
- mcp_proxy_adapter/core/config/simple_config.py +112 -0
- mcp_proxy_adapter/core/config/simple_config_generator.py +50 -0
- mcp_proxy_adapter/core/config/simple_config_validator.py +96 -0
- mcp_proxy_adapter/core/config_converter.py +0 -186
- mcp_proxy_adapter/core/config_validator.py +96 -1238
- mcp_proxy_adapter/core/errors.py +7 -42
- mcp_proxy_adapter/core/job_manager.py +54 -0
- mcp_proxy_adapter/core/logging.py +2 -22
- mcp_proxy_adapter/core/mtls_asgi.py +0 -20
- mcp_proxy_adapter/core/mtls_asgi_app.py +0 -12
- mcp_proxy_adapter/core/mtls_proxy.py +0 -80
- mcp_proxy_adapter/core/mtls_server.py +3 -173
- mcp_proxy_adapter/core/protocol_manager.py +1 -191
- mcp_proxy_adapter/core/proxy/__init__.py +22 -0
- mcp_proxy_adapter/core/proxy/auth_manager.py +27 -0
- mcp_proxy_adapter/core/proxy/proxy_registration_manager.py +137 -0
- mcp_proxy_adapter/core/proxy/registration_client.py +60 -0
- mcp_proxy_adapter/core/proxy/ssl_manager.py +101 -0
- mcp_proxy_adapter/core/proxy_client.py +0 -1
- mcp_proxy_adapter/core/proxy_registration.py +36 -913
- mcp_proxy_adapter/core/role_utils.py +0 -308
- mcp_proxy_adapter/core/security_adapter.py +1 -36
- mcp_proxy_adapter/core/security_factory.py +1 -150
- mcp_proxy_adapter/core/security_integration.py +0 -33
- mcp_proxy_adapter/core/server_adapter.py +1 -40
- mcp_proxy_adapter/core/server_engine.py +2 -173
- mcp_proxy_adapter/core/settings.py +0 -127
- mcp_proxy_adapter/core/signal_handler.py +0 -65
- mcp_proxy_adapter/core/ssl_utils.py +19 -137
- mcp_proxy_adapter/core/transport_manager.py +0 -151
- mcp_proxy_adapter/core/unified_config_adapter.py +1 -193
- mcp_proxy_adapter/core/utils.py +1 -182
- mcp_proxy_adapter/core/validation/__init__.py +21 -0
- mcp_proxy_adapter/core/validation/config_validator.py +211 -0
- mcp_proxy_adapter/core/validation/file_validator.py +73 -0
- mcp_proxy_adapter/core/validation/protocol_validator.py +191 -0
- mcp_proxy_adapter/core/validation/security_validator.py +58 -0
- mcp_proxy_adapter/core/validation/validation_result.py +27 -0
- mcp_proxy_adapter/custom_openapi.py +33 -652
- mcp_proxy_adapter/examples/bugfix_certificate_config.py +0 -23
- mcp_proxy_adapter/examples/check_config.py +0 -2
- mcp_proxy_adapter/examples/client_usage_example.py +164 -0
- mcp_proxy_adapter/examples/config_builder.py +13 -2
- mcp_proxy_adapter/examples/config_cli.py +0 -1
- mcp_proxy_adapter/examples/create_test_configs.py +0 -46
- mcp_proxy_adapter/examples/debug_request_state.py +0 -1
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +0 -47
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +0 -45
- mcp_proxy_adapter/examples/full_application/commands/echo_command.py +0 -12
- mcp_proxy_adapter/examples/full_application/commands/help_command.py +0 -12
- mcp_proxy_adapter/examples/full_application/commands/list_command.py +0 -7
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +0 -2
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +0 -59
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +0 -54
- mcp_proxy_adapter/examples/full_application/main.py +186 -150
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +0 -107
- mcp_proxy_adapter/examples/full_application/test_minimal_server.py +0 -24
- mcp_proxy_adapter/examples/full_application/test_server.py +0 -58
- mcp_proxy_adapter/examples/generate_config.py +65 -11
- mcp_proxy_adapter/examples/queue_demo_simple.py +632 -0
- mcp_proxy_adapter/examples/queue_integration_example.py +578 -0
- mcp_proxy_adapter/examples/queue_server_demo.py +82 -0
- mcp_proxy_adapter/examples/queue_server_example.py +85 -0
- mcp_proxy_adapter/examples/queue_server_simple.py +173 -0
- mcp_proxy_adapter/examples/required_certificates.py +0 -2
- mcp_proxy_adapter/examples/run_full_test_suite.py +0 -29
- mcp_proxy_adapter/examples/run_proxy_server.py +31 -71
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +0 -27
- mcp_proxy_adapter/examples/security_test/__init__.py +18 -0
- mcp_proxy_adapter/examples/security_test/auth_manager.py +14 -0
- mcp_proxy_adapter/examples/security_test/ssl_context_manager.py +28 -0
- mcp_proxy_adapter/examples/security_test/test_client.py +159 -0
- mcp_proxy_adapter/examples/security_test/test_result.py +22 -0
- mcp_proxy_adapter/examples/security_test_client.py +24 -1075
- mcp_proxy_adapter/examples/setup/__init__.py +24 -0
- mcp_proxy_adapter/examples/setup/certificate_manager.py +215 -0
- mcp_proxy_adapter/examples/setup/config_generator.py +12 -0
- mcp_proxy_adapter/examples/setup/config_validator.py +118 -0
- mcp_proxy_adapter/examples/setup/environment_setup.py +62 -0
- mcp_proxy_adapter/examples/setup/test_files_generator.py +10 -0
- mcp_proxy_adapter/examples/setup/test_runner.py +89 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +133 -1425
- mcp_proxy_adapter/examples/test_config.py +0 -3
- mcp_proxy_adapter/examples/test_config_builder.py +25 -405
- mcp_proxy_adapter/examples/test_examples.py +0 -1
- mcp_proxy_adapter/examples/test_framework_complete.py +0 -2
- mcp_proxy_adapter/examples/test_mcp_server.py +0 -1
- mcp_proxy_adapter/examples/test_protocol_examples.py +0 -1
- mcp_proxy_adapter/examples/universal_client.py +0 -6
- mcp_proxy_adapter/examples/update_config_certificates.py +0 -1
- mcp_proxy_adapter/examples/validate_generator_compatibility.py +0 -1
- mcp_proxy_adapter/examples/validate_generator_compatibility_simple.py +0 -187
- mcp_proxy_adapter/integrations/__init__.py +25 -0
- mcp_proxy_adapter/integrations/queuemgr_integration.py +462 -0
- mcp_proxy_adapter/main.py +70 -62
- mcp_proxy_adapter/openapi.py +0 -22
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.30.dist-info}/METADATA +2 -1
- mcp_proxy_adapter-6.9.30.dist-info/RECORD +235 -0
- {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.30.dist-info}/entry_points.txt +1 -1
- mcp_proxy_adapter-6.9.28.dist-info/RECORD +0 -149
- {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.30.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.9.28.dist-info → mcp_proxy_adapter-6.9.30.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# flake8: noqa: E501
|
|
2
1
|
"""
|
|
3
2
|
Module for proxy registration functionality with security framework integration.
|
|
4
3
|
|
|
@@ -10,940 +9,57 @@ Author: Vasiliy Zdanovskiy
|
|
|
10
9
|
email: vasilyvz@gmail.com
|
|
11
10
|
"""
|
|
12
11
|
|
|
13
|
-
import
|
|
14
|
-
import time
|
|
15
|
-
import ssl
|
|
16
|
-
import traceback
|
|
17
|
-
from typing import Dict, Any, Optional, Tuple
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
from urllib.parse import urljoin
|
|
12
|
+
from typing import Dict, Any
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
ProxyRegistrationManager,
|
|
15
|
+
ProxyRegistrationError,
|
|
16
|
+
initialize_proxy_registration,
|
|
17
|
+
get_proxy_registration_status,
|
|
18
|
+
)
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class ProxyRegistrationError(Exception):
|
|
30
|
-
"""Exception raised when proxy registration fails."""
|
|
31
|
-
|
|
32
|
-
pass
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class ProxyRegistrationManager:
|
|
36
|
-
"""
|
|
37
|
-
Manager for proxy registration functionality with security framework integration.
|
|
38
|
-
|
|
39
|
-
Handles automatic registration and unregistration of the server
|
|
40
|
-
with the MCP proxy server using secure authentication methods.
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
def __init__(self, config: Dict[str, Any]):
|
|
44
|
-
"""
|
|
45
|
-
Initialize the proxy registration manager.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
config: Application configuration
|
|
49
|
-
"""
|
|
50
|
-
self.config = config
|
|
51
|
-
# Try both registration and proxy_registration for backward compatibility
|
|
52
|
-
self.registration_config = config.get(
|
|
53
|
-
"registration", config.get("proxy_registration", {})
|
|
54
|
-
)
|
|
55
|
-
# Auto-fill minimal TLS settings from global SSL if missing to support minimal config
|
|
56
|
-
try:
|
|
57
|
-
reg_ssl = self.registration_config.get("ssl", {})
|
|
58
|
-
if not isinstance(reg_ssl, dict):
|
|
59
|
-
reg_ssl = {}
|
|
60
|
-
global_ssl = config.get("security", {}).get("ssl", {}) or config.get("ssl", {})
|
|
61
|
-
if isinstance(global_ssl, dict):
|
|
62
|
-
if not reg_ssl.get("ca_cert") and global_ssl.get("ca_cert"):
|
|
63
|
-
reg_ssl["ca_cert"] = global_ssl.get("ca_cert")
|
|
64
|
-
# Keep verify_mode if already set; default to CERT_REQUIRED if global verify_client true
|
|
65
|
-
if not reg_ssl.get("verify_mode") and isinstance(global_ssl.get("verify_client"), bool):
|
|
66
|
-
reg_ssl["verify_mode"] = "CERT_REQUIRED" if global_ssl.get("verify_client") else "CERT_NONE"
|
|
67
|
-
if reg_ssl:
|
|
68
|
-
self.registration_config["ssl"] = reg_ssl
|
|
69
|
-
except Exception:
|
|
70
|
-
pass
|
|
71
|
-
|
|
72
|
-
# Check if registration is enabled
|
|
73
|
-
self.enabled = self.registration_config.get("enabled", False)
|
|
74
|
-
|
|
75
|
-
# Basic registration settings - only validate if enabled
|
|
76
|
-
if self.enabled:
|
|
77
|
-
self.proxy_url = self.registration_config.get("proxy_url")
|
|
78
|
-
if not self.proxy_url:
|
|
79
|
-
raise ValueError(
|
|
80
|
-
"proxy_url is required in registration configuration. "
|
|
81
|
-
"Please specify a valid proxy URL in your configuration."
|
|
82
|
-
)
|
|
83
|
-
else:
|
|
84
|
-
self.proxy_url = None
|
|
85
|
-
|
|
86
|
-
if self.enabled:
|
|
87
|
-
self.server_id = self.registration_config.get("server_id")
|
|
88
|
-
if not self.server_id:
|
|
89
|
-
# Try to get from proxy_info.name as fallback
|
|
90
|
-
self.server_id = self.registration_config.get("proxy_info", {}).get("name")
|
|
91
|
-
if not self.server_id:
|
|
92
|
-
raise ValueError(
|
|
93
|
-
"server_id is required in registration configuration. "
|
|
94
|
-
"Please specify a server_id or proxy_info.name in your configuration."
|
|
95
|
-
)
|
|
96
|
-
# UUID is mandatory for registration payload
|
|
97
|
-
# Try to get UUID from proxy_registration section first, then from root config
|
|
98
|
-
self.uuid = self.registration_config.get("uuid") or self.config.get("uuid")
|
|
99
|
-
if not self.uuid:
|
|
100
|
-
raise ValueError(
|
|
101
|
-
"uuid is required in registration configuration. "
|
|
102
|
-
"Please specify a UUID under 'proxy_registration.uuid' or at root level in your configuration."
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
# Validate UUID4 format
|
|
106
|
-
import re
|
|
107
|
-
uuid4_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
|
|
108
|
-
if not re.match(uuid4_pattern, self.uuid, re.IGNORECASE):
|
|
109
|
-
error_msg = f"Invalid UUID4 format: '{self.uuid}'. Expected format: xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx"
|
|
110
|
-
get_global_logger().error(f"❌ UUID validation failed: {error_msg}")
|
|
111
|
-
raise ValueError(error_msg)
|
|
112
|
-
|
|
113
|
-
get_global_logger().info(f"✅ UUID validation passed: {self.uuid}")
|
|
114
|
-
self.server_name = self.registration_config.get("server_name")
|
|
115
|
-
if not self.server_name:
|
|
116
|
-
# Try to get from proxy_info.name as fallback
|
|
117
|
-
self.server_name = self.registration_config.get("proxy_info", {}).get("name")
|
|
118
|
-
if not self.server_name:
|
|
119
|
-
raise ValueError(
|
|
120
|
-
"server_name is required in registration configuration. "
|
|
121
|
-
"Please specify a server_name or proxy_info.name in your configuration."
|
|
122
|
-
)
|
|
123
|
-
self.description = self.registration_config.get("description")
|
|
124
|
-
if not self.description:
|
|
125
|
-
# Try to get from proxy_info.description as fallback
|
|
126
|
-
self.description = self.registration_config.get("proxy_info", {}).get("description")
|
|
127
|
-
if not self.description:
|
|
128
|
-
raise ValueError(
|
|
129
|
-
"description is required in registration configuration. "
|
|
130
|
-
"Please specify a description or proxy_info.description in your configuration."
|
|
131
|
-
)
|
|
132
|
-
else:
|
|
133
|
-
self.server_id = None
|
|
134
|
-
self.uuid = None
|
|
135
|
-
self.server_name = None
|
|
136
|
-
self.description = None
|
|
137
|
-
|
|
138
|
-
if self.enabled:
|
|
139
|
-
self.version = self.registration_config.get("version")
|
|
140
|
-
if not self.version:
|
|
141
|
-
# Try to get from proxy_info.version as fallback
|
|
142
|
-
self.version = self.registration_config.get("proxy_info", {}).get("version")
|
|
143
|
-
if not self.version:
|
|
144
|
-
raise ValueError(
|
|
145
|
-
"version is required in registration configuration. "
|
|
146
|
-
"Please specify a version or proxy_info.version in your configuration."
|
|
147
|
-
)
|
|
148
|
-
else:
|
|
149
|
-
self.version = None
|
|
150
|
-
|
|
151
|
-
# Heartbeat settings - only validate if enabled
|
|
152
|
-
if self.enabled:
|
|
153
|
-
heartbeat_config = self.registration_config.get("heartbeat", {})
|
|
154
|
-
heartbeat_enabled = heartbeat_config.get("enabled", True)
|
|
155
|
-
|
|
156
|
-
if heartbeat_enabled:
|
|
157
|
-
self.timeout = heartbeat_config.get("timeout")
|
|
158
|
-
if self.timeout is None:
|
|
159
|
-
raise ValueError(
|
|
160
|
-
"heartbeat.timeout is required in registration configuration. "
|
|
161
|
-
"Please specify a timeout value."
|
|
162
|
-
)
|
|
163
|
-
self.retry_attempts = heartbeat_config.get("retry_attempts")
|
|
164
|
-
if self.retry_attempts is None:
|
|
165
|
-
raise ValueError(
|
|
166
|
-
"heartbeat.retry_attempts is required in registration configuration. "
|
|
167
|
-
"Please specify a retry_attempts value."
|
|
168
|
-
)
|
|
169
|
-
self.retry_delay = heartbeat_config.get("retry_delay")
|
|
170
|
-
if self.retry_delay is None:
|
|
171
|
-
raise ValueError(
|
|
172
|
-
"heartbeat.retry_delay is required in registration configuration. "
|
|
173
|
-
"Please specify a retry_delay value."
|
|
174
|
-
)
|
|
175
|
-
self.heartbeat_interval = heartbeat_config.get("interval")
|
|
176
|
-
if self.heartbeat_interval is None:
|
|
177
|
-
raise ValueError(
|
|
178
|
-
"heartbeat.interval is required in registration configuration. "
|
|
179
|
-
"Please specify an interval value."
|
|
180
|
-
)
|
|
181
|
-
else:
|
|
182
|
-
# Heartbeat disabled - use defaults
|
|
183
|
-
self.timeout = heartbeat_config.get("timeout", 30)
|
|
184
|
-
self.retry_attempts = heartbeat_config.get("retry_attempts", 3)
|
|
185
|
-
self.retry_delay = heartbeat_config.get("retry_delay", 5)
|
|
186
|
-
self.heartbeat_interval = heartbeat_config.get("interval", 30)
|
|
187
|
-
else:
|
|
188
|
-
self.timeout = None
|
|
189
|
-
self.retry_attempts = None
|
|
190
|
-
self.retry_delay = None
|
|
191
|
-
self.heartbeat_interval = None
|
|
192
|
-
|
|
193
|
-
# Auto registration settings
|
|
194
|
-
if self.enabled:
|
|
195
|
-
self.auto_register = self.registration_config.get("enabled")
|
|
196
|
-
if self.auto_register is None:
|
|
197
|
-
raise ValueError(
|
|
198
|
-
"enabled is required in registration configuration. "
|
|
199
|
-
"Please specify whether registration is enabled (true/false)."
|
|
200
|
-
)
|
|
201
|
-
else:
|
|
202
|
-
self.auto_register = False
|
|
203
|
-
|
|
204
|
-
self.auto_unregister = True # Always unregister on shutdown
|
|
205
|
-
|
|
206
|
-
# Initialize client security manager
|
|
207
|
-
self.client_security = create_client_security_manager(config)
|
|
208
|
-
|
|
209
|
-
# Registration state
|
|
210
|
-
self.registered = False
|
|
211
|
-
self.server_key: Optional[str] = None
|
|
212
|
-
self.server_url: Optional[str] = None
|
|
213
|
-
self.heartbeat_task: Optional[asyncio.Task] = None
|
|
214
|
-
|
|
215
|
-
get_global_logger().info(
|
|
216
|
-
"Proxy registration manager initialized with security framework integration"
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
def is_enabled(self) -> bool:
|
|
220
|
-
"""
|
|
221
|
-
Check if proxy registration is enabled.
|
|
222
|
-
|
|
223
|
-
Returns:
|
|
224
|
-
True if registration is enabled, False otherwise.
|
|
225
|
-
"""
|
|
226
|
-
return self.enabled
|
|
227
|
-
|
|
228
|
-
def set_server_url(self, server_url: str) -> None:
|
|
229
|
-
"""
|
|
230
|
-
Set the server URL for registration.
|
|
231
|
-
|
|
232
|
-
Args:
|
|
233
|
-
server_url: The URL where this server is accessible.
|
|
234
|
-
"""
|
|
235
|
-
self.server_url = server_url
|
|
236
|
-
get_global_logger().info(f"Proxy registration server URL set to: {server_url}")
|
|
237
|
-
|
|
238
|
-
def _get_auth_headers(self) -> Dict[str, str]:
|
|
239
|
-
"""
|
|
240
|
-
Get authentication headers for registration requests.
|
|
241
|
-
|
|
242
|
-
Returns:
|
|
243
|
-
Dictionary of authentication headers
|
|
244
|
-
"""
|
|
245
|
-
if not self.client_security:
|
|
246
|
-
return {"Content-Type": "application/json"}
|
|
247
|
-
|
|
248
|
-
auth_method = self.registration_config.get("auth_method", "certificate")
|
|
249
|
-
|
|
250
|
-
if auth_method == "certificate":
|
|
251
|
-
return self.client_security.get_client_auth_headers("certificate")
|
|
252
|
-
elif auth_method == "token":
|
|
253
|
-
token_config = self.registration_config.get("token", {})
|
|
254
|
-
token = token_config.get("token")
|
|
255
|
-
return self.client_security.get_client_auth_headers("jwt", token=token)
|
|
256
|
-
elif auth_method == "api_key":
|
|
257
|
-
api_key_config = self.registration_config.get("api_key", {})
|
|
258
|
-
api_key = api_key_config.get("key")
|
|
259
|
-
return self.client_security.get_client_auth_headers(
|
|
260
|
-
"api_key", api_key=api_key
|
|
261
|
-
)
|
|
262
|
-
else:
|
|
263
|
-
return {"Content-Type": "application/json"}
|
|
264
|
-
|
|
265
|
-
def _create_ssl_context(self) -> Optional[ssl.SSLContext]:
|
|
266
|
-
"""
|
|
267
|
-
Create SSL context for secure connections using registration SSL configuration.
|
|
268
|
-
|
|
269
|
-
Returns:
|
|
270
|
-
SSL context or None if SSL not needed
|
|
271
|
-
"""
|
|
272
|
-
get_global_logger().debug("_create_ssl_context called")
|
|
273
|
-
|
|
274
|
-
# Decide SSL strictly by proxy URL scheme: use SSL only for https proxy URLs
|
|
275
|
-
try:
|
|
276
|
-
from urllib.parse import urlparse as _urlparse
|
|
277
|
-
scheme = _urlparse(self.proxy_url).scheme if self.proxy_url else "http"
|
|
278
|
-
if scheme.lower() != "https":
|
|
279
|
-
get_global_logger().debug("Proxy URL is HTTP, skipping SSL context creation for registration")
|
|
280
|
-
return None
|
|
281
|
-
except Exception:
|
|
282
|
-
get_global_logger().debug("Failed to parse proxy_url, assuming HTTP and skipping SSL context")
|
|
283
|
-
return None
|
|
284
|
-
|
|
285
|
-
if not self.client_security:
|
|
286
|
-
get_global_logger().debug("SSL context creation failed: client_security is None")
|
|
287
|
-
return None
|
|
288
|
-
|
|
289
|
-
try:
|
|
290
|
-
# Check if SSL is enabled for registration
|
|
291
|
-
cert_config = self.registration_config.get("certificate", {})
|
|
292
|
-
ssl_config = self.registration_config.get("ssl", {})
|
|
293
|
-
|
|
294
|
-
# FALLBACK: if no explicit registration SSL/certs provided, reuse global SSL config
|
|
295
|
-
if not cert_config and not ssl_config:
|
|
296
|
-
global_ssl = self.config.get("security", {}).get("ssl", {}) or self.config.get("ssl", {})
|
|
297
|
-
if global_ssl:
|
|
298
|
-
# Map global ssl to registration-style configs
|
|
299
|
-
mapped_cert = {}
|
|
300
|
-
if global_ssl.get("cert_file") and global_ssl.get("key_file"):
|
|
301
|
-
mapped_cert = {
|
|
302
|
-
"cert_file": global_ssl.get("cert_file"),
|
|
303
|
-
"key_file": global_ssl.get("key_file"),
|
|
304
|
-
}
|
|
305
|
-
mapped_ssl = {}
|
|
306
|
-
if global_ssl.get("ca_cert"):
|
|
307
|
-
mapped_ssl["ca_cert"] = global_ssl.get("ca_cert")
|
|
308
|
-
if global_ssl.get("verify_client") is not None:
|
|
309
|
-
mapped_ssl["verify_mode"] = (
|
|
310
|
-
"CERT_REQUIRED" if global_ssl.get("verify_client") else "CERT_NONE"
|
|
311
|
-
)
|
|
312
|
-
cert_config = mapped_cert
|
|
313
|
-
ssl_config = mapped_ssl
|
|
314
|
-
|
|
315
|
-
# If still no client certificate specified, raise clear error
|
|
316
|
-
if not cert_config or not cert_config.get("cert_file") or not cert_config.get("key_file"):
|
|
317
|
-
raise ValueError(
|
|
318
|
-
"Client certificate configuration is required for mTLS proxy registration. "
|
|
319
|
-
"Please configure 'proxy_registration.certificate.cert_file' and 'proxy_registration.certificate.key_file' "
|
|
320
|
-
"in your configuration file."
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
get_global_logger().debug(
|
|
324
|
-
f"SSL context creation: cert_config={cert_config}, ssl_config={ssl_config}"
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
# SSL is enabled if certificate config exists or SSL config exists
|
|
328
|
-
if cert_config or ssl_config:
|
|
329
|
-
# Create a custom SSL context based on registration configuration
|
|
330
|
-
ca_file = ssl_config.get("ca_cert") if isinstance(ssl_config, dict) else None
|
|
331
|
-
if ca_file and Path(ca_file).exists():
|
|
332
|
-
context = ssl.create_default_context(cafile=ca_file)
|
|
333
|
-
else:
|
|
334
|
-
# No CA certificate configured - use system default
|
|
335
|
-
context = ssl.create_default_context()
|
|
336
|
-
get_global_logger().warning(
|
|
337
|
-
"No CA certificate configured for proxy registration. "
|
|
338
|
-
"This may cause SSL verification failures if the proxy uses self-signed certificates. "
|
|
339
|
-
"Consider configuring 'proxy_registration.ssl.ca_cert' in your configuration file."
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
# Load client certificates if provided
|
|
343
|
-
if cert_config:
|
|
344
|
-
cert_file = cert_config.get("cert_file")
|
|
345
|
-
key_file = cert_config.get("key_file")
|
|
346
|
-
|
|
347
|
-
if cert_file and key_file:
|
|
348
|
-
context.load_cert_chain(cert_file, key_file)
|
|
349
|
-
get_global_logger().debug(
|
|
350
|
-
f"Loaded client certificates for mTLS: cert={cert_file}, key={key_file}"
|
|
351
|
-
)
|
|
352
|
-
|
|
353
|
-
# Configure SSL verification based on registration settings
|
|
354
|
-
if ssl_config:
|
|
355
|
-
ca_cert_file = ssl_config.get("ca_cert")
|
|
356
|
-
verify_mode = ssl_config.get("verify_mode", "CERT_REQUIRED")
|
|
357
|
-
verify_ssl = ssl_config.get("verify_ssl", True)
|
|
358
|
-
verify_hostname = ssl_config.get("verify_hostname", True) # ✅ Read verify_hostname setting
|
|
359
|
-
|
|
360
|
-
# Load CA certificate if provided
|
|
361
|
-
if ca_cert_file:
|
|
362
|
-
context.load_verify_locations(ca_cert_file)
|
|
363
|
-
get_global_logger().debug(f"Loaded CA certificate: {ca_cert_file}")
|
|
364
|
-
|
|
365
|
-
# Check if verify_ssl is disabled in ssl_config
|
|
366
|
-
if verify_ssl == False:
|
|
367
|
-
context.check_hostname = False
|
|
368
|
-
context.verify_mode = ssl.CERT_NONE
|
|
369
|
-
get_global_logger().debug("SSL verification disabled (verify_ssl=False)")
|
|
370
|
-
elif verify_mode == "CERT_NONE":
|
|
371
|
-
context.check_hostname = False
|
|
372
|
-
context.verify_mode = ssl.CERT_NONE
|
|
373
|
-
get_global_logger().debug("SSL verification disabled (CERT_NONE)")
|
|
374
|
-
elif verify_mode == "CERT_REQUIRED":
|
|
375
|
-
context.check_hostname = verify_hostname # ✅ Use verify_hostname setting
|
|
376
|
-
context.verify_mode = ssl.CERT_REQUIRED
|
|
377
|
-
get_global_logger().debug(f"SSL verification enabled (CERT_REQUIRED), hostname check: {verify_hostname}")
|
|
378
|
-
else:
|
|
379
|
-
# For test environments, default to CERT_NONE to avoid certificate issues
|
|
380
|
-
context.check_hostname = False
|
|
381
|
-
context.verify_mode = ssl.CERT_NONE
|
|
382
|
-
get_global_logger().debug("SSL verification disabled (default for test environment)")
|
|
383
|
-
else:
|
|
384
|
-
# No specific ssl_config, default to CERT_NONE for test environments
|
|
385
|
-
context.check_hostname = False
|
|
386
|
-
context.verify_mode = ssl.CERT_NONE
|
|
387
|
-
get_global_logger().debug("Using CERT_NONE for test environment (no ssl_config)")
|
|
388
|
-
|
|
389
|
-
get_global_logger().info("Created custom SSL context for proxy registration")
|
|
390
|
-
return context
|
|
391
|
-
else:
|
|
392
|
-
get_global_logger().debug(
|
|
393
|
-
"SSL context creation skipped: no cert_config or ssl_config"
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
return None
|
|
397
|
-
except Exception as e:
|
|
398
|
-
get_global_logger().warning(f"Failed to create SSL context: {e}")
|
|
399
|
-
# Don't fail the entire operation, just return None
|
|
400
|
-
return None
|
|
401
|
-
|
|
402
|
-
async def register_server(self) -> bool:
|
|
403
|
-
"""
|
|
404
|
-
Register the server with the proxy using secure authentication.
|
|
405
|
-
|
|
406
|
-
Returns:
|
|
407
|
-
True if registration was successful, False otherwise.
|
|
408
|
-
"""
|
|
409
|
-
if not self.is_enabled():
|
|
410
|
-
get_global_logger().info("Proxy registration is disabled in configuration")
|
|
411
|
-
return False
|
|
412
|
-
|
|
413
|
-
if not self.server_url:
|
|
414
|
-
get_global_logger().error("Server URL not set, cannot register with proxy")
|
|
415
|
-
return False
|
|
416
|
-
|
|
417
|
-
# Normalize server_url for docker host if needed
|
|
418
|
-
try:
|
|
419
|
-
if self.server_url:
|
|
420
|
-
from urllib.parse import urlparse, urlunparse
|
|
421
|
-
import os as _os
|
|
422
|
-
parsed = urlparse(self.server_url)
|
|
423
|
-
if parsed.hostname in ("localhost", "127.0.0.1"):
|
|
424
|
-
docker_addr = _os.getenv("DOCKER_HOST_ADDR", "172.17.0.1")
|
|
425
|
-
port = parsed.port
|
|
426
|
-
if not port:
|
|
427
|
-
port = 443 if parsed.scheme == "https" else 80
|
|
428
|
-
new_netloc = f"{docker_addr}:{port}"
|
|
429
|
-
normalized = urlunparse(parsed._replace(netloc=new_netloc))
|
|
430
|
-
if normalized != self.server_url:
|
|
431
|
-
self.server_url = normalized
|
|
432
|
-
get_global_logger().info(
|
|
433
|
-
f"Normalized server_url for docker host: {self.server_url}"
|
|
434
|
-
)
|
|
435
|
-
except Exception as _e:
|
|
436
|
-
get_global_logger().debug(f"server_url normalization skipped: {_e}")
|
|
437
|
-
|
|
438
|
-
# Prepare registration data with proxy info
|
|
439
|
-
proxy_info = self.registration_config.get("proxy_info", {})
|
|
440
|
-
registration_data = {
|
|
441
|
-
"server_id": self.server_id,
|
|
442
|
-
"uuid": self.uuid,
|
|
443
|
-
"server_url": self.server_url,
|
|
444
|
-
"server_name": self.server_name,
|
|
445
|
-
"description": self.description,
|
|
446
|
-
"version": self.version,
|
|
447
|
-
"capabilities": proxy_info.get("capabilities", ["jsonrpc", "rest"]),
|
|
448
|
-
"endpoints": proxy_info.get(
|
|
449
|
-
"endpoints",
|
|
450
|
-
{"jsonrpc": "/api/jsonrpc", "rest": "/cmd", "health": "/health"},
|
|
451
|
-
),
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
get_global_logger().info(f"Attempting to register server with proxy at {self.proxy_url}")
|
|
455
|
-
get_global_logger().debug(f"Registration data: {registration_data}")
|
|
456
|
-
|
|
457
|
-
# Do not block application startup: single attempt, no sleeps here
|
|
458
|
-
for attempt in range(1):
|
|
459
|
-
try:
|
|
460
|
-
success, result = await self._make_secure_registration_request(
|
|
461
|
-
registration_data
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
if success:
|
|
465
|
-
self.registered = True
|
|
466
|
-
# Safely extract server_key from result
|
|
467
|
-
if isinstance(result, dict):
|
|
468
|
-
self.server_key = result.get("server_key")
|
|
469
|
-
else:
|
|
470
|
-
self.server_key = None
|
|
471
|
-
get_global_logger().info(
|
|
472
|
-
f"✅ Successfully registered with proxy. Server key: {self.server_key}"
|
|
473
|
-
)
|
|
474
|
-
|
|
475
|
-
# Start heartbeat if enabled
|
|
476
|
-
if self.registration_config.get("heartbeat", {}).get(
|
|
477
|
-
"enabled", True
|
|
478
|
-
):
|
|
479
|
-
await self._start_heartbeat()
|
|
480
|
-
|
|
481
|
-
return True
|
|
482
|
-
else:
|
|
483
|
-
# Be robust if result is not a dict
|
|
484
|
-
error_msg = None
|
|
485
|
-
get_global_logger().error(f"DEBUG: result type = {type(result)}, result = {result}")
|
|
486
|
-
if isinstance(result, dict):
|
|
487
|
-
get_global_logger().error(f"DEBUG: result is dict, getting error field")
|
|
488
|
-
error_field = result.get("error", {})
|
|
489
|
-
get_global_logger().error(f"DEBUG: error_field type = {type(error_field)}, error_field = {error_field}")
|
|
490
|
-
if isinstance(error_field, dict):
|
|
491
|
-
error_msg = error_field.get("message", "Unknown error")
|
|
492
|
-
elif isinstance(error_field, str):
|
|
493
|
-
error_msg = error_field
|
|
494
|
-
else:
|
|
495
|
-
error_msg = str(error_field)
|
|
496
|
-
|
|
497
|
-
# Auto-recovery: already registered case → force unregistration then retry once
|
|
498
|
-
error_code = result.get("error_code") or (result.get("error", {}).get("code") if isinstance(result.get("error"), dict) else None)
|
|
499
|
-
already_registered = False
|
|
500
|
-
existing_server_key = None
|
|
501
|
-
# Prefer structured detail if provided
|
|
502
|
-
if isinstance(result.get("details"), dict):
|
|
503
|
-
existing_server_key = result.get("details", {}).get("existing_server_key")
|
|
504
|
-
# Fallback: parse from error message text
|
|
505
|
-
if not existing_server_key and isinstance(error_msg, str) and "already registered as" in error_msg:
|
|
506
|
-
try:
|
|
507
|
-
# Expecting: "... already registered as <server_id>_<copy_number>"
|
|
508
|
-
tail = error_msg.split("already registered as", 1)[1].strip()
|
|
509
|
-
existing_server_key = tail.split()[0]
|
|
510
|
-
except Exception:
|
|
511
|
-
existing_server_key = None
|
|
512
|
-
|
|
513
|
-
if (error_code in ("DUPLICATE_SERVER_URL", "REGISTRATION_ERROR") or already_registered) and existing_server_key:
|
|
514
|
-
try:
|
|
515
|
-
get_global_logger().info(f"Attempting auto-unregistration of existing instance: {existing_server_key}")
|
|
516
|
-
# Build unregistration payload using parsed server_key
|
|
517
|
-
try:
|
|
518
|
-
copy_number = int(existing_server_key.split("_")[-1])
|
|
519
|
-
except Exception:
|
|
520
|
-
copy_number = 1
|
|
521
|
-
unregistration_data = {"server_id": self.server_id, "copy_number": copy_number}
|
|
522
|
-
# Reuse secure unregistration request directly
|
|
523
|
-
unreg_success, _unreg_result = await self._make_secure_unregistration_request(unregistration_data)
|
|
524
|
-
if unreg_success:
|
|
525
|
-
get_global_logger().info("Auto-unregistration succeeded, retrying registration once...")
|
|
526
|
-
# Retry registration once immediately
|
|
527
|
-
retry_success, retry_result = await self._make_secure_registration_request(registration_data)
|
|
528
|
-
if retry_success:
|
|
529
|
-
self.registered = True
|
|
530
|
-
if isinstance(retry_result, dict):
|
|
531
|
-
self.server_key = retry_result.get("server_key")
|
|
532
|
-
else:
|
|
533
|
-
self.server_key = None
|
|
534
|
-
get_global_logger().info(f"✅ Successfully registered after auto-unregistration. Server key: {self.server_key}")
|
|
535
|
-
if self.registration_config.get("heartbeat", {}).get("enabled", True):
|
|
536
|
-
await self._start_heartbeat()
|
|
537
|
-
return True
|
|
538
|
-
else:
|
|
539
|
-
get_global_logger().warning(f"Retry registration failed after auto-unregistration: {retry_result}")
|
|
540
|
-
else:
|
|
541
|
-
get_global_logger().warning(f"Auto-unregistration failed: {_unreg_result}")
|
|
542
|
-
except Exception as _auto_e:
|
|
543
|
-
get_global_logger().warning(f"Auto-unregistration/registration flow error: {_auto_e}")
|
|
544
|
-
else:
|
|
545
|
-
error_msg = str(result)
|
|
546
|
-
get_global_logger().warning(
|
|
547
|
-
f"❌ Registration attempt {attempt + 1} failed: {error_msg}"
|
|
548
|
-
)
|
|
549
|
-
|
|
550
|
-
except Exception as e:
|
|
551
|
-
get_global_logger().error(
|
|
552
|
-
f"❌ Registration attempt {attempt + 1} failed with exception: {e}"
|
|
553
|
-
)
|
|
554
|
-
get_global_logger().error(f"Full traceback: {traceback.format_exc()}")
|
|
555
|
-
|
|
556
|
-
get_global_logger().error(
|
|
557
|
-
f"❌ Failed to register with proxy after {self.retry_attempts} attempts"
|
|
558
|
-
)
|
|
559
|
-
return False
|
|
560
|
-
|
|
561
|
-
async def unregister_server(self) -> bool:
|
|
562
|
-
"""
|
|
563
|
-
Unregister the server from the proxy.
|
|
564
|
-
|
|
565
|
-
Returns:
|
|
566
|
-
True if unregistration was successful, False otherwise.
|
|
567
|
-
"""
|
|
568
|
-
if not self.is_enabled():
|
|
569
|
-
get_global_logger().info("Proxy registration is disabled, skipping unregistration")
|
|
570
|
-
return True
|
|
571
|
-
|
|
572
|
-
if not self.registered or not self.server_key:
|
|
573
|
-
get_global_logger().info("Server not registered with proxy, skipping unregistration")
|
|
574
|
-
return True
|
|
575
|
-
|
|
576
|
-
# Stop heartbeat
|
|
577
|
-
await self._stop_heartbeat()
|
|
578
|
-
|
|
579
|
-
# Extract copy_number from server_key (format: server_id_copy_number)
|
|
580
|
-
try:
|
|
581
|
-
copy_number = int(self.server_key.split("_")[-1])
|
|
582
|
-
except (ValueError, IndexError):
|
|
583
|
-
copy_number = 1
|
|
584
|
-
|
|
585
|
-
unregistration_data = {"server_id": self.server_id, "copy_number": copy_number}
|
|
586
|
-
|
|
587
|
-
get_global_logger().info(f"Attempting to unregister server from proxy at {self.proxy_url}")
|
|
588
|
-
get_global_logger().debug(f"Unregistration data: {unregistration_data}")
|
|
589
|
-
|
|
590
|
-
try:
|
|
591
|
-
success, result = await self._make_secure_unregistration_request(
|
|
592
|
-
unregistration_data
|
|
593
|
-
)
|
|
594
|
-
|
|
595
|
-
if success:
|
|
596
|
-
unregistered = result.get("unregistered", False)
|
|
597
|
-
if unregistered:
|
|
598
|
-
get_global_logger().info("✅ Successfully unregistered from proxy")
|
|
599
|
-
else:
|
|
600
|
-
get_global_logger().warning("⚠️ Server was not found in proxy registry")
|
|
601
|
-
|
|
602
|
-
self.registered = False
|
|
603
|
-
self.server_key = None
|
|
604
|
-
return True
|
|
605
|
-
else:
|
|
606
|
-
error_msg = result.get("error", {}).get("message", "Unknown error")
|
|
607
|
-
get_global_logger().error(f"❌ Failed to unregister from proxy: {error_msg}")
|
|
608
|
-
return False
|
|
609
|
-
|
|
610
|
-
except Exception as e:
|
|
611
|
-
get_global_logger().error(f"❌ Unregistration failed with exception: {e}")
|
|
612
|
-
return False
|
|
613
|
-
|
|
614
|
-
async def _make_secure_registration_request(
|
|
615
|
-
self, data: Dict[str, Any]
|
|
616
|
-
) -> Tuple[bool, Dict[str, Any]]:
|
|
617
|
-
"""
|
|
618
|
-
Make secure registration request to proxy using security framework.
|
|
619
|
-
|
|
620
|
-
Args:
|
|
621
|
-
data: Registration data.
|
|
622
|
-
|
|
623
|
-
Returns:
|
|
624
|
-
Tuple of (success, result).
|
|
625
|
-
"""
|
|
626
|
-
url = urljoin(self.proxy_url, "/register")
|
|
627
|
-
|
|
628
|
-
# Get authentication headers
|
|
629
|
-
headers = self._get_auth_headers()
|
|
630
|
-
headers["Content-Type"] = "application/json"
|
|
631
|
-
|
|
632
|
-
# Create SSL context if needed
|
|
633
|
-
ssl_context = self._create_ssl_context()
|
|
634
|
-
|
|
635
|
-
# Create connector with SSL context
|
|
636
|
-
connector = None
|
|
637
|
-
if ssl_context:
|
|
638
|
-
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
|
639
|
-
|
|
640
|
-
try:
|
|
641
|
-
async with aiohttp.ClientSession(connector=connector) as session:
|
|
642
|
-
async with session.post(
|
|
643
|
-
url,
|
|
644
|
-
json=data,
|
|
645
|
-
headers=headers,
|
|
646
|
-
timeout=aiohttp.ClientTimeout(total=self.timeout),
|
|
647
|
-
) as response:
|
|
648
|
-
try:
|
|
649
|
-
result = await response.json()
|
|
650
|
-
get_global_logger().debug(f"Response JSON parsed successfully: {type(result)} - {result}")
|
|
651
|
-
except Exception as e:
|
|
652
|
-
text_body = await response.text()
|
|
653
|
-
get_global_logger().debug(f"JSON parsing failed: {e}, text_body: {text_body}")
|
|
654
|
-
result = {"success": False, "error": {"code": "NON_JSON_RESPONSE", "message": text_body}}
|
|
655
|
-
|
|
656
|
-
# Validate response headers if security framework available
|
|
657
|
-
if self.client_security:
|
|
658
|
-
self.client_security.validate_server_response(
|
|
659
|
-
dict(response.headers)
|
|
660
|
-
)
|
|
661
|
-
|
|
662
|
-
# Check both HTTP status and JSON success field
|
|
663
|
-
if response.status == 200:
|
|
664
|
-
success = result.get("success", False)
|
|
665
|
-
if not success:
|
|
666
|
-
error_info = result.get("error", {})
|
|
667
|
-
error_msg = error_info.get("message", "Unknown error")
|
|
668
|
-
error_code = error_info.get("code", "UNKNOWN_ERROR")
|
|
669
|
-
|
|
670
|
-
# Handle duplicate server URL as successful registration
|
|
671
|
-
if error_code == "DUPLICATE_SERVER_URL":
|
|
672
|
-
get_global_logger().info(
|
|
673
|
-
f"✅ Server already registered: {error_msg}"
|
|
674
|
-
)
|
|
675
|
-
# Extract server_key from details if available
|
|
676
|
-
details = error_info.get("details", {})
|
|
677
|
-
existing_server_key = details.get("existing_server_key")
|
|
678
|
-
if existing_server_key:
|
|
679
|
-
result["server_key"] = existing_server_key
|
|
680
|
-
get_global_logger().info(
|
|
681
|
-
f"✅ Retrieved existing server key: {existing_server_key}"
|
|
682
|
-
)
|
|
683
|
-
# Return success=True for duplicate registration
|
|
684
|
-
return True, result
|
|
685
|
-
else:
|
|
686
|
-
get_global_logger().warning(
|
|
687
|
-
f"Registration failed: {error_code} - {error_msg}"
|
|
688
|
-
)
|
|
689
|
-
return success, result
|
|
690
|
-
else:
|
|
691
|
-
get_global_logger().warning(
|
|
692
|
-
f"Registration failed with HTTP status: {response.status}"
|
|
693
|
-
)
|
|
694
|
-
# Ensure result is a dict for consistent error handling
|
|
695
|
-
if isinstance(result, str):
|
|
696
|
-
result = {"success": False, "error": {"code": "HTTP_ERROR", "message": result}}
|
|
697
|
-
return False, result
|
|
698
|
-
finally:
|
|
699
|
-
if connector:
|
|
700
|
-
await connector.close()
|
|
701
|
-
|
|
702
|
-
async def _make_secure_unregistration_request(
|
|
703
|
-
self, data: Dict[str, Any]
|
|
704
|
-
) -> Tuple[bool, Dict[str, Any]]:
|
|
705
|
-
"""
|
|
706
|
-
Make secure unregistration request to proxy using security framework.
|
|
707
|
-
|
|
708
|
-
Args:
|
|
709
|
-
data: Unregistration data.
|
|
710
|
-
|
|
711
|
-
Returns:
|
|
712
|
-
Tuple of (success, result).
|
|
713
|
-
"""
|
|
714
|
-
url = urljoin(self.proxy_url, "/unregister")
|
|
715
|
-
|
|
716
|
-
# Get authentication headers
|
|
717
|
-
headers = self._get_auth_headers()
|
|
718
|
-
headers["Content-Type"] = "application/json"
|
|
719
|
-
|
|
720
|
-
# Create SSL context if needed
|
|
721
|
-
ssl_context = self._create_ssl_context()
|
|
722
|
-
|
|
723
|
-
# Create connector with SSL context
|
|
724
|
-
connector = None
|
|
725
|
-
if ssl_context:
|
|
726
|
-
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
|
727
|
-
|
|
728
|
-
try:
|
|
729
|
-
async with aiohttp.ClientSession(connector=connector) as session:
|
|
730
|
-
async with session.post(
|
|
731
|
-
url,
|
|
732
|
-
json=data,
|
|
733
|
-
headers=headers,
|
|
734
|
-
timeout=aiohttp.ClientTimeout(total=self.timeout),
|
|
735
|
-
) as response:
|
|
736
|
-
result = await response.json()
|
|
737
|
-
|
|
738
|
-
# Validate response headers if security framework available
|
|
739
|
-
if self.client_security:
|
|
740
|
-
self.client_security.validate_server_response(
|
|
741
|
-
dict(response.headers)
|
|
742
|
-
)
|
|
743
|
-
|
|
744
|
-
# Check both HTTP status and JSON success field
|
|
745
|
-
if response.status == 200:
|
|
746
|
-
success = result.get("success", False)
|
|
747
|
-
if not success:
|
|
748
|
-
error_info = result.get("error", {})
|
|
749
|
-
error_msg = error_info.get("message", "Unknown error")
|
|
750
|
-
error_code = error_info.get("code", "UNKNOWN_ERROR")
|
|
751
|
-
get_global_logger().warning(
|
|
752
|
-
f"Unregistration failed: {error_code} - {error_msg}"
|
|
753
|
-
)
|
|
754
|
-
return success, result
|
|
755
|
-
else:
|
|
756
|
-
get_global_logger().warning(
|
|
757
|
-
f"Unregistration failed with HTTP status: {response.status}"
|
|
758
|
-
)
|
|
759
|
-
return False, result
|
|
760
|
-
finally:
|
|
761
|
-
if connector:
|
|
762
|
-
await connector.close()
|
|
763
|
-
|
|
764
|
-
async def _start_heartbeat(self) -> None:
|
|
765
|
-
"""Start heartbeat task for keeping registration alive."""
|
|
766
|
-
if self.heartbeat_task and not self.heartbeat_task.done():
|
|
767
|
-
return
|
|
768
|
-
|
|
769
|
-
self.heartbeat_task = asyncio.create_task(self._heartbeat_loop())
|
|
770
|
-
get_global_logger().info("Heartbeat task started")
|
|
771
|
-
|
|
772
|
-
async def _stop_heartbeat(self) -> None:
|
|
773
|
-
"""Stop heartbeat task."""
|
|
774
|
-
if self.heartbeat_task and not self.heartbeat_task.done():
|
|
775
|
-
self.heartbeat_task.cancel()
|
|
776
|
-
try:
|
|
777
|
-
await self.heartbeat_task
|
|
778
|
-
except asyncio.CancelledError:
|
|
779
|
-
pass
|
|
780
|
-
get_global_logger().info("Heartbeat task stopped")
|
|
781
|
-
|
|
782
|
-
async def _heartbeat_loop(self) -> None:
|
|
783
|
-
"""Heartbeat loop to keep registration alive."""
|
|
784
|
-
while self.registered:
|
|
785
|
-
try:
|
|
786
|
-
await asyncio.sleep(self.heartbeat_interval)
|
|
787
|
-
|
|
788
|
-
if not self.registered:
|
|
789
|
-
break
|
|
790
|
-
|
|
791
|
-
# Send heartbeat
|
|
792
|
-
success = await self._send_heartbeat()
|
|
793
|
-
if not success:
|
|
794
|
-
get_global_logger().warning("Heartbeat failed, attempting to re-register")
|
|
795
|
-
await self.register_server()
|
|
796
|
-
|
|
797
|
-
except asyncio.CancelledError:
|
|
798
|
-
break
|
|
799
|
-
except Exception as e:
|
|
800
|
-
get_global_logger().error(f"Heartbeat error: {e}")
|
|
801
|
-
|
|
802
|
-
async def heartbeat(self) -> bool:
|
|
803
|
-
"""
|
|
804
|
-
Public method to send heartbeat to proxy server.
|
|
805
|
-
|
|
806
|
-
Returns:
|
|
807
|
-
True if heartbeat was successful, False otherwise.
|
|
808
|
-
"""
|
|
809
|
-
return await self._send_heartbeat()
|
|
810
|
-
|
|
811
|
-
async def _send_heartbeat(self) -> bool:
|
|
812
|
-
"""Send heartbeat to proxy server."""
|
|
813
|
-
if not self.server_key:
|
|
814
|
-
return False
|
|
815
|
-
|
|
816
|
-
url = urljoin(self.proxy_url, "/heartbeat")
|
|
817
|
-
|
|
818
|
-
# Get authentication headers
|
|
819
|
-
headers = self._get_auth_headers()
|
|
820
|
-
|
|
821
|
-
# Create SSL context if needed
|
|
822
|
-
ssl_context = self._create_ssl_context()
|
|
823
|
-
|
|
824
|
-
# Create connector with SSL context
|
|
825
|
-
connector = aiohttp.TCPConnector(ssl=ssl_context) if ssl_context else None
|
|
826
|
-
|
|
827
|
-
try:
|
|
828
|
-
timeout = aiohttp.ClientTimeout(total=self.timeout)
|
|
829
|
-
async with aiohttp.ClientSession(connector=connector) as session:
|
|
830
|
-
# Prefer GET heartbeat (container exposes GET /heartbeat)
|
|
831
|
-
try:
|
|
832
|
-
async with session.get(url, headers=headers, timeout=timeout) as resp:
|
|
833
|
-
if resp.status == 200:
|
|
834
|
-
get_global_logger().debug("Heartbeat (GET) succeeded")
|
|
835
|
-
return True
|
|
836
|
-
# If method not allowed, fall back to POST
|
|
837
|
-
if resp.status != 405:
|
|
838
|
-
get_global_logger().warning(
|
|
839
|
-
f"Heartbeat (GET) failed with status: {resp.status}"
|
|
840
|
-
)
|
|
841
|
-
except Exception as ge:
|
|
842
|
-
get_global_logger().debug(f"Heartbeat (GET) error: {ge}")
|
|
843
|
-
|
|
844
|
-
# Fallback to POST if GET not supported
|
|
845
|
-
heartbeat_data = {
|
|
846
|
-
"server_id": self.server_id,
|
|
847
|
-
"server_key": self.server_key,
|
|
848
|
-
"timestamp": int(time.time()),
|
|
849
|
-
}
|
|
850
|
-
post_headers = dict(headers)
|
|
851
|
-
post_headers["Content-Type"] = "application/json"
|
|
852
|
-
async with session.post(
|
|
853
|
-
url, json=heartbeat_data, headers=post_headers, timeout=timeout
|
|
854
|
-
) as resp:
|
|
855
|
-
if resp.status == 200:
|
|
856
|
-
get_global_logger().debug("Heartbeat (POST) succeeded")
|
|
857
|
-
return True
|
|
858
|
-
get_global_logger().warning(
|
|
859
|
-
f"Heartbeat (POST) failed with status: {resp.status}"
|
|
860
|
-
)
|
|
861
|
-
return False
|
|
862
|
-
except Exception as e:
|
|
863
|
-
get_global_logger().error(f"Heartbeat error: {e}")
|
|
864
|
-
return False
|
|
865
|
-
finally:
|
|
866
|
-
if connector:
|
|
867
|
-
await connector.close()
|
|
868
|
-
|
|
869
|
-
def get_registration_status(self) -> Dict[str, Any]:
|
|
870
|
-
"""
|
|
871
|
-
Get current registration status.
|
|
872
|
-
|
|
873
|
-
Returns:
|
|
874
|
-
Dictionary with registration status information.
|
|
875
|
-
"""
|
|
876
|
-
status = {
|
|
877
|
-
"enabled": self.is_enabled(),
|
|
878
|
-
"registered": self.registered,
|
|
879
|
-
"server_key": self.server_key,
|
|
880
|
-
"server_url": self.server_url,
|
|
881
|
-
"proxy_url": self.proxy_url,
|
|
882
|
-
"server_id": self.server_id,
|
|
883
|
-
"heartbeat_active": self.heartbeat_task is not None
|
|
884
|
-
and not self.heartbeat_task.done(),
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
# Add security information if available
|
|
888
|
-
if self.client_security:
|
|
889
|
-
status["security_enabled"] = True
|
|
890
|
-
status["ssl_enabled"] = self.client_security.is_ssl_enabled()
|
|
891
|
-
status["auth_methods"] = self.client_security.get_supported_auth_methods()
|
|
892
|
-
|
|
893
|
-
cert_info = self.client_security.get_client_certificate_info()
|
|
894
|
-
if cert_info:
|
|
895
|
-
status["client_certificate"] = cert_info
|
|
896
|
-
else:
|
|
897
|
-
status["security_enabled"] = False
|
|
898
|
-
|
|
899
|
-
return status
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
# Global proxy registration manager instance (will be initialized with config)
|
|
903
|
-
proxy_registration_manager: Optional[ProxyRegistrationManager] = None
|
|
20
|
+
# Global registration manager instance
|
|
21
|
+
_registration_manager: Optional[ProxyRegistrationManager] = None
|
|
904
22
|
|
|
905
23
|
|
|
906
24
|
def initialize_proxy_registration(config: Dict[str, Any]) -> None:
|
|
907
25
|
"""
|
|
908
|
-
Initialize
|
|
26
|
+
Initialize proxy registration with configuration.
|
|
909
27
|
|
|
910
28
|
Args:
|
|
911
29
|
config: Application configuration
|
|
912
30
|
"""
|
|
913
|
-
global
|
|
914
|
-
|
|
31
|
+
global _registration_manager
|
|
32
|
+
_registration_manager = ProxyRegistrationManager(config)
|
|
915
33
|
|
|
916
34
|
|
|
917
35
|
async def register_with_proxy(server_url: str) -> bool:
|
|
918
36
|
"""
|
|
919
|
-
Register
|
|
37
|
+
Register server with proxy.
|
|
920
38
|
|
|
921
39
|
Args:
|
|
922
|
-
server_url:
|
|
40
|
+
server_url: Server URL to register
|
|
923
41
|
|
|
924
42
|
Returns:
|
|
925
|
-
True if registration
|
|
43
|
+
True if registration successful, False otherwise
|
|
926
44
|
"""
|
|
927
|
-
if not
|
|
928
|
-
get_global_logger().error("Proxy registration manager not initialized")
|
|
45
|
+
if not _registration_manager:
|
|
929
46
|
return False
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
return await
|
|
47
|
+
|
|
48
|
+
_registration_manager.set_server_url(server_url)
|
|
49
|
+
return await _registration_manager.register()
|
|
933
50
|
|
|
934
51
|
|
|
935
52
|
async def unregister_from_proxy() -> bool:
|
|
936
53
|
"""
|
|
937
|
-
Unregister
|
|
54
|
+
Unregister server from proxy.
|
|
938
55
|
|
|
939
56
|
Returns:
|
|
940
|
-
True if unregistration
|
|
57
|
+
True if unregistration successful, False otherwise
|
|
941
58
|
"""
|
|
942
|
-
if not
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
return await proxy_registration_manager.unregister_server()
|
|
59
|
+
if not _registration_manager:
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
return await _registration_manager.unregister()
|
|
947
63
|
|
|
948
64
|
|
|
949
65
|
def get_proxy_registration_status() -> Dict[str, Any]:
|
|
@@ -951,9 +67,16 @@ def get_proxy_registration_status() -> Dict[str, Any]:
|
|
|
951
67
|
Get current proxy registration status.
|
|
952
68
|
|
|
953
69
|
Returns:
|
|
954
|
-
Dictionary with registration status information
|
|
70
|
+
Dictionary with registration status information
|
|
955
71
|
"""
|
|
956
|
-
if not
|
|
957
|
-
return {
|
|
958
|
-
|
|
959
|
-
|
|
72
|
+
if not _registration_manager:
|
|
73
|
+
return {
|
|
74
|
+
"enabled": False,
|
|
75
|
+
"registered": False,
|
|
76
|
+
"proxy_url": None,
|
|
77
|
+
"server_url": None,
|
|
78
|
+
"registration_time": None,
|
|
79
|
+
"client_security_available": False,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return _registration_manager.get_registration_status()
|