mcp-proxy-adapter 6.0.0__py3-none-any.whl → 6.1.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.
- mcp_proxy_adapter/api/app.py +174 -80
- mcp_proxy_adapter/api/handlers.py +16 -5
- mcp_proxy_adapter/api/middleware/__init__.py +9 -4
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
- mcp_proxy_adapter/api/middleware/factory.py +36 -12
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +32 -13
- mcp_proxy_adapter/api/middleware/unified_security.py +160 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +83 -0
- mcp_proxy_adapter/commands/__init__.py +7 -1
- mcp_proxy_adapter/commands/base.py +7 -4
- mcp_proxy_adapter/commands/builtin_commands.py +8 -2
- mcp_proxy_adapter/commands/command_registry.py +8 -0
- mcp_proxy_adapter/commands/echo_command.py +81 -0
- mcp_proxy_adapter/commands/help_command.py +21 -14
- mcp_proxy_adapter/commands/proxy_registration_command.py +326 -185
- mcp_proxy_adapter/commands/role_test_command.py +141 -0
- mcp_proxy_adapter/commands/security_command.py +488 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +2 -2
- mcp_proxy_adapter/commands/token_management_command.py +1 -1
- mcp_proxy_adapter/config.py +81 -21
- mcp_proxy_adapter/core/app_factory.py +326 -0
- mcp_proxy_adapter/core/client_security.py +384 -0
- mcp_proxy_adapter/core/logging.py +8 -3
- mcp_proxy_adapter/core/mtls_asgi.py +156 -0
- mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
- mcp_proxy_adapter/core/protocol_manager.py +139 -8
- mcp_proxy_adapter/core/proxy_client.py +602 -0
- mcp_proxy_adapter/core/proxy_registration.py +299 -47
- mcp_proxy_adapter/core/security_adapter.py +12 -15
- mcp_proxy_adapter/core/security_integration.py +285 -0
- mcp_proxy_adapter/core/server_adapter.py +345 -0
- mcp_proxy_adapter/core/server_engine.py +364 -0
- mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
- mcp_proxy_adapter/docs/EN/TROUBLESHOOTING.md +285 -0
- mcp_proxy_adapter/docs/RU/TROUBLESHOOTING.md +285 -0
- mcp_proxy_adapter/examples/README.md +230 -97
- mcp_proxy_adapter/examples/README_EN.md +258 -0
- mcp_proxy_adapter/examples/SECURITY_TESTING.md +455 -0
- mcp_proxy_adapter/examples/basic_framework/configs/http_auth.json +37 -0
- mcp_proxy_adapter/examples/basic_framework/configs/http_simple.json +23 -0
- mcp_proxy_adapter/examples/basic_framework/configs/https_auth.json +43 -0
- mcp_proxy_adapter/examples/basic_framework/configs/https_no_protocol_middleware.json +36 -0
- mcp_proxy_adapter/examples/basic_framework/configs/https_simple.json +29 -0
- mcp_proxy_adapter/examples/basic_framework/configs/mtls_no_protocol_middleware.json +34 -0
- mcp_proxy_adapter/examples/basic_framework/configs/mtls_no_roles.json +39 -0
- mcp_proxy_adapter/examples/basic_framework/configs/mtls_simple.json +35 -0
- mcp_proxy_adapter/examples/basic_framework/configs/mtls_with_roles.json +45 -0
- mcp_proxy_adapter/examples/basic_framework/main.py +63 -0
- mcp_proxy_adapter/examples/basic_framework/roles.json +21 -0
- mcp_proxy_adapter/examples/cert_config.json +9 -0
- mcp_proxy_adapter/examples/certs/admin.crt +32 -0
- mcp_proxy_adapter/examples/certs/admin.key +52 -0
- mcp_proxy_adapter/examples/certs/admin_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/admin_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/ca_cert.pem +23 -0
- mcp_proxy_adapter/examples/certs/ca_cert.srl +1 -0
- mcp_proxy_adapter/examples/certs/ca_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/cert_config.json +9 -0
- mcp_proxy_adapter/examples/certs/client.crt +32 -0
- mcp_proxy_adapter/examples/certs/client.key +52 -0
- mcp_proxy_adapter/examples/certs/client_admin.crt +32 -0
- mcp_proxy_adapter/examples/certs/client_admin.key +52 -0
- mcp_proxy_adapter/examples/certs/client_user.crt +32 -0
- mcp_proxy_adapter/examples/certs/client_user.key +52 -0
- mcp_proxy_adapter/examples/certs/guest_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/guest_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/mcp_proxy_adapter_ca_ca.crt +23 -0
- mcp_proxy_adapter/examples/certs/proxy_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/proxy_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/readonly.crt +32 -0
- mcp_proxy_adapter/examples/certs/readonly.key +52 -0
- mcp_proxy_adapter/examples/certs/readonly_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/readonly_key.pem +28 -0
- mcp_proxy_adapter/examples/certs/server.crt +32 -0
- mcp_proxy_adapter/examples/certs/server.key +52 -0
- mcp_proxy_adapter/examples/certs/server_cert.pem +32 -0
- mcp_proxy_adapter/examples/certs/server_key.pem +52 -0
- mcp_proxy_adapter/examples/certs/test_ca_ca.crt +20 -0
- mcp_proxy_adapter/examples/certs/user.crt +32 -0
- mcp_proxy_adapter/examples/certs/user.key +52 -0
- mcp_proxy_adapter/examples/certs/user_cert.pem +21 -0
- mcp_proxy_adapter/examples/certs/user_key.pem +28 -0
- mcp_proxy_adapter/examples/client_configs/api_key_client.json +13 -0
- mcp_proxy_adapter/examples/client_configs/basic_auth_client.json +13 -0
- mcp_proxy_adapter/examples/client_configs/certificate_client.json +22 -0
- mcp_proxy_adapter/examples/client_configs/jwt_client.json +15 -0
- mcp_proxy_adapter/examples/client_configs/no_auth_client.json +9 -0
- mcp_proxy_adapter/examples/commands/__init__.py +1 -0
- mcp_proxy_adapter/examples/create_certificates_simple.py +307 -0
- mcp_proxy_adapter/examples/debug_request_state.py +144 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +205 -0
- mcp_proxy_adapter/examples/demo_client.py +341 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +99 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +106 -0
- mcp_proxy_adapter/examples/full_application/configs/http_auth.json +37 -0
- mcp_proxy_adapter/examples/full_application/configs/http_simple.json +23 -0
- mcp_proxy_adapter/examples/full_application/configs/https_auth.json +39 -0
- mcp_proxy_adapter/examples/full_application/configs/https_simple.json +25 -0
- mcp_proxy_adapter/examples/full_application/configs/mtls_no_roles.json +39 -0
- mcp_proxy_adapter/examples/full_application/configs/mtls_with_roles.json +45 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +97 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +95 -0
- mcp_proxy_adapter/examples/full_application/main.py +138 -0
- mcp_proxy_adapter/examples/full_application/roles.json +21 -0
- mcp_proxy_adapter/examples/generate_all_certificates.py +429 -0
- mcp_proxy_adapter/examples/generate_certificates.py +121 -0
- mcp_proxy_adapter/examples/keys/ca_key.pem +28 -0
- mcp_proxy_adapter/examples/keys/mcp_proxy_adapter_ca_ca.key +28 -0
- mcp_proxy_adapter/examples/keys/test_ca_ca.key +28 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log +220 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.1 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.2 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.3 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.4 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter.log.5 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log +220 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.1 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.2 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.3 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.4 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_access.log.5 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log +2 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.1 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.2 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.3 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.4 +1 -0
- mcp_proxy_adapter/examples/logs/mcp_proxy_adapter_error.log.5 +1 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +401 -0
- mcp_proxy_adapter/examples/roles.json +38 -0
- mcp_proxy_adapter/examples/run_example.py +81 -0
- mcp_proxy_adapter/examples/run_security_tests.py +326 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +300 -0
- mcp_proxy_adapter/examples/security_test_client.py +743 -0
- mcp_proxy_adapter/examples/server_configs/config_basic_http.json +204 -0
- mcp_proxy_adapter/examples/server_configs/config_http_token.json +238 -0
- mcp_proxy_adapter/examples/server_configs/config_https.json +215 -0
- mcp_proxy_adapter/examples/server_configs/config_https_token.json +231 -0
- mcp_proxy_adapter/examples/server_configs/config_mtls.json +215 -0
- mcp_proxy_adapter/examples/server_configs/config_proxy_registration.json +250 -0
- mcp_proxy_adapter/examples/server_configs/config_simple.json +46 -0
- mcp_proxy_adapter/examples/server_configs/roles.json +38 -0
- mcp_proxy_adapter/examples/test_config_generator.py +110 -0
- mcp_proxy_adapter/examples/test_examples.py +344 -0
- mcp_proxy_adapter/examples/universal_client.py +628 -0
- mcp_proxy_adapter/main.py +21 -10
- mcp_proxy_adapter/utils/config_generator.py +727 -0
- mcp_proxy_adapter/version.py +5 -2
- mcp_proxy_adapter-6.1.1.dist-info/METADATA +205 -0
- mcp_proxy_adapter-6.1.1.dist-info/RECORD +197 -0
- mcp_proxy_adapter-6.1.1.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.1.dist-info}/licenses/LICENSE +2 -2
- mcp_proxy_adapter/api/middleware/auth.py +0 -146
- mcp_proxy_adapter/api/middleware/auth_adapter.py +0 -235
- mcp_proxy_adapter/api/middleware/mtls_adapter.py +0 -305
- mcp_proxy_adapter/api/middleware/mtls_middleware.py +0 -296
- mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
- mcp_proxy_adapter/api/middleware/rate_limit_adapter.py +0 -241
- mcp_proxy_adapter/api/middleware/roles_adapter.py +0 -365
- mcp_proxy_adapter/api/middleware/roles_middleware.py +0 -381
- mcp_proxy_adapter/api/middleware/security.py +0 -376
- mcp_proxy_adapter/api/middleware/token_auth_middleware.py +0 -261
- mcp_proxy_adapter/examples/__init__.py +0 -7
- mcp_proxy_adapter/examples/basic_server/README.md +0 -60
- mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
- mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
- mcp_proxy_adapter/examples/basic_server/config.json +0 -70
- mcp_proxy_adapter/examples/basic_server/config_all_protocols.json +0 -54
- mcp_proxy_adapter/examples/basic_server/config_http.json +0 -70
- mcp_proxy_adapter/examples/basic_server/config_http_only.json +0 -52
- mcp_proxy_adapter/examples/basic_server/config_https.json +0 -58
- mcp_proxy_adapter/examples/basic_server/config_mtls.json +0 -58
- mcp_proxy_adapter/examples/basic_server/config_ssl.json +0 -46
- mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
- mcp_proxy_adapter/examples/basic_server/server.py +0 -114
- mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
- mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -566
- mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
- mcp_proxy_adapter/examples/custom_commands/auto_commands/test_command.py +0 -105
- mcp_proxy_adapter/examples/custom_commands/catalog/commands/test_command.py +0 -129
- mcp_proxy_adapter/examples/custom_commands/config.json +0 -118
- mcp_proxy_adapter/examples/custom_commands/config_all_protocols.json +0 -46
- mcp_proxy_adapter/examples/custom_commands/config_https_only.json +0 -46
- mcp_proxy_adapter/examples/custom_commands/config_https_transport.json +0 -33
- mcp_proxy_adapter/examples/custom_commands/config_mtls_only.json +0 -46
- mcp_proxy_adapter/examples/custom_commands/config_mtls_transport.json +0 -33
- mcp_proxy_adapter/examples/custom_commands/config_single_transport.json +0 -33
- mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
- mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
- mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
- mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
- mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
- mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
- mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
- mcp_proxy_adapter/examples/custom_commands/full_help_response.json +0 -1
- mcp_proxy_adapter/examples/custom_commands/generated_openapi.json +0 -629
- mcp_proxy_adapter/examples/custom_commands/get_openapi.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
- mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
- mcp_proxy_adapter/examples/custom_commands/loadable_commands/test_ignored.py +0 -129
- mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/proxy_connection_manager.py +0 -278
- mcp_proxy_adapter/examples/custom_commands/server.py +0 -252
- mcp_proxy_adapter/examples/custom_commands/simple_openapi_server.py +0 -75
- mcp_proxy_adapter/examples/custom_commands/start_server_with_proxy_manager.py +0 -299
- mcp_proxy_adapter/examples/custom_commands/start_server_with_registration.py +0 -278
- mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
- mcp_proxy_adapter/examples/custom_commands/test_openapi.py +0 -27
- mcp_proxy_adapter/examples/custom_commands/test_registry.py +0 -23
- mcp_proxy_adapter/examples/custom_commands/test_simple.py +0 -19
- mcp_proxy_adapter/examples/custom_project_example/README.md +0 -103
- mcp_proxy_adapter/examples/custom_project_example/README_EN.md +0 -103
- mcp_proxy_adapter/examples/deployment/README.md +0 -49
- mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
- mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
- mcp_proxy_adapter/examples/deployment/config.json +0 -29
- mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
- mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
- mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
- mcp_proxy_adapter/examples/deployment/run.sh +0 -43
- mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
- mcp_proxy_adapter/examples/simple_custom_commands/README.md +0 -149
- mcp_proxy_adapter/examples/simple_custom_commands/README_EN.md +0 -149
- mcp_proxy_adapter/schemas/base_schema.json +0 -114
- mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
- mcp_proxy_adapter/schemas/roles_schema.json +0 -162
- mcp_proxy_adapter/tests/__init__.py +0 -0
- mcp_proxy_adapter/tests/api/__init__.py +0 -3
- mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
- mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
- mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
- mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
- mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
- mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
- mcp_proxy_adapter/tests/commands/__init__.py +0 -3
- mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
- mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
- mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
- mcp_proxy_adapter/tests/conftest.py +0 -131
- mcp_proxy_adapter/tests/functional/__init__.py +0 -3
- mcp_proxy_adapter/tests/functional/test_api.py +0 -253
- mcp_proxy_adapter/tests/integration/__init__.py +0 -3
- mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
- mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
- mcp_proxy_adapter/tests/performance/__init__.py +0 -3
- mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
- mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
- mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
- mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
- mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
- mcp_proxy_adapter/tests/test_base_command.py +0 -123
- mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
- mcp_proxy_adapter/tests/test_command_registry.py +0 -281
- mcp_proxy_adapter/tests/test_config.py +0 -127
- mcp_proxy_adapter/tests/test_utils.py +0 -65
- mcp_proxy_adapter/tests/unit/__init__.py +0 -3
- mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
- mcp_proxy_adapter/tests/unit/test_config.py +0 -270
- mcp_proxy_adapter-6.0.0.dist-info/METADATA +0 -201
- mcp_proxy_adapter-6.0.0.dist-info/RECORD +0 -179
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.0.0.dist-info → mcp_proxy_adapter-6.1.1.dist-info}/top_level.txt +0 -0
@@ -13,9 +13,10 @@ from fastapi import FastAPI
|
|
13
13
|
from mcp_proxy_adapter.core.logging import logger
|
14
14
|
from mcp_proxy_adapter.core.security_factory import SecurityFactory
|
15
15
|
from .base import BaseMiddleware
|
16
|
-
from .
|
16
|
+
from .unified_security import UnifiedSecurityMiddleware
|
17
17
|
from .error_handling import ErrorHandlingMiddleware
|
18
18
|
from .logging import LoggingMiddleware
|
19
|
+
from .user_info_middleware import UserInfoMiddleware
|
19
20
|
|
20
21
|
|
21
22
|
class MiddlewareFactory:
|
@@ -40,12 +41,12 @@ class MiddlewareFactory:
|
|
40
41
|
|
41
42
|
logger.info("Middleware factory initialized")
|
42
43
|
|
43
|
-
def create_security_middleware(self) -> Optional[
|
44
|
+
def create_security_middleware(self) -> Optional[UnifiedSecurityMiddleware]:
|
44
45
|
"""
|
45
|
-
Create security middleware.
|
46
|
+
Create unified security middleware.
|
46
47
|
|
47
48
|
Returns:
|
48
|
-
|
49
|
+
UnifiedSecurityMiddleware instance or None if creation failed
|
49
50
|
"""
|
50
51
|
try:
|
51
52
|
security_config = self.config.get("security", {})
|
@@ -54,14 +55,14 @@ class MiddlewareFactory:
|
|
54
55
|
logger.info("Security middleware disabled by configuration")
|
55
56
|
return None
|
56
57
|
|
57
|
-
middleware =
|
58
|
+
middleware = UnifiedSecurityMiddleware(self.app, self.config)
|
58
59
|
self.middleware_stack.append(middleware)
|
59
60
|
|
60
|
-
logger.info("
|
61
|
+
logger.info("Unified security middleware created successfully")
|
61
62
|
return middleware
|
62
63
|
|
63
64
|
except Exception as e:
|
64
|
-
logger.error(f"Failed to create security middleware: {e}")
|
65
|
+
logger.error(f"Failed to create unified security middleware: {e}")
|
65
66
|
return None
|
66
67
|
|
67
68
|
def create_error_handling_middleware(self) -> Optional[ErrorHandlingMiddleware]:
|
@@ -106,6 +107,24 @@ class MiddlewareFactory:
|
|
106
107
|
logger.error(f"Failed to create logging middleware: {e}")
|
107
108
|
return None
|
108
109
|
|
110
|
+
def create_user_info_middleware(self) -> Optional[UserInfoMiddleware]:
|
111
|
+
"""
|
112
|
+
Create user info middleware.
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
UserInfoMiddleware instance or None if creation failed
|
116
|
+
"""
|
117
|
+
try:
|
118
|
+
middleware = UserInfoMiddleware(self.app, self.config)
|
119
|
+
self.middleware_stack.append(middleware)
|
120
|
+
|
121
|
+
logger.info("User info middleware created successfully")
|
122
|
+
return middleware
|
123
|
+
|
124
|
+
except Exception as e:
|
125
|
+
logger.error(f"Failed to create user info middleware: {e}")
|
126
|
+
return None
|
127
|
+
|
109
128
|
|
110
129
|
|
111
130
|
def create_all_middleware(self) -> List[BaseMiddleware]:
|
@@ -132,6 +151,11 @@ class MiddlewareFactory:
|
|
132
151
|
if logging_middleware:
|
133
152
|
middleware_list.append(logging_middleware)
|
134
153
|
|
154
|
+
# Create user info middleware
|
155
|
+
user_info_middleware = self.create_user_info_middleware()
|
156
|
+
if user_info_middleware:
|
157
|
+
middleware_list.append(user_info_middleware)
|
158
|
+
|
135
159
|
logger.info(f"Created {len(middleware_list)} middleware components")
|
136
160
|
return middleware_list
|
137
161
|
|
@@ -152,14 +176,14 @@ class MiddlewareFactory:
|
|
152
176
|
return middleware
|
153
177
|
return None
|
154
178
|
|
155
|
-
def get_security_middleware(self) -> Optional[
|
179
|
+
def get_security_middleware(self) -> Optional[UnifiedSecurityMiddleware]:
|
156
180
|
"""
|
157
|
-
Get security middleware instance.
|
181
|
+
Get unified security middleware instance.
|
158
182
|
|
159
183
|
Returns:
|
160
|
-
|
184
|
+
UnifiedSecurityMiddleware instance or None if not found
|
161
185
|
"""
|
162
|
-
return self.get_middleware_by_type(
|
186
|
+
return self.get_middleware_by_type(UnifiedSecurityMiddleware)
|
163
187
|
|
164
188
|
def validate_middleware_config(self) -> bool:
|
165
189
|
"""
|
@@ -213,7 +237,7 @@ class MiddlewareFactory:
|
|
213
237
|
middleware_type = type(middleware).__name__
|
214
238
|
info["middleware_types"].append(middleware_type)
|
215
239
|
|
216
|
-
if isinstance(middleware,
|
240
|
+
if isinstance(middleware, UnifiedSecurityMiddleware):
|
217
241
|
info["security_enabled"] = True
|
218
242
|
|
219
243
|
return info
|
@@ -4,12 +4,12 @@ Protocol middleware module.
|
|
4
4
|
This module provides middleware for validating protocol access based on configuration.
|
5
5
|
"""
|
6
6
|
|
7
|
-
from typing import Callable
|
7
|
+
from typing import Callable, Dict, Any, Optional
|
8
8
|
from fastapi import Request, Response
|
9
9
|
from starlette.middleware.base import BaseHTTPMiddleware
|
10
10
|
from starlette.responses import JSONResponse
|
11
11
|
|
12
|
-
from mcp_proxy_adapter.core.protocol_manager import
|
12
|
+
from mcp_proxy_adapter.core.protocol_manager import get_protocol_manager
|
13
13
|
from mcp_proxy_adapter.core.logging import logger
|
14
14
|
|
15
15
|
|
@@ -21,16 +21,29 @@ class ProtocolMiddleware(BaseHTTPMiddleware):
|
|
21
21
|
based on the protocol configuration.
|
22
22
|
"""
|
23
23
|
|
24
|
-
def __init__(self, app,
|
24
|
+
def __init__(self, app, app_config: Optional[Dict[str, Any]] = None):
|
25
25
|
"""
|
26
26
|
Initialize protocol middleware.
|
27
27
|
|
28
28
|
Args:
|
29
29
|
app: FastAPI application
|
30
|
-
|
30
|
+
app_config: Application configuration dictionary (optional)
|
31
31
|
"""
|
32
32
|
super().__init__(app)
|
33
|
-
self.
|
33
|
+
self.app_config = app_config
|
34
|
+
# Get protocol manager with current configuration
|
35
|
+
self.protocol_manager = get_protocol_manager(app_config)
|
36
|
+
|
37
|
+
def update_config(self, new_config: Dict[str, Any]):
|
38
|
+
"""
|
39
|
+
Update configuration and reload protocol manager.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
new_config: New configuration dictionary
|
43
|
+
"""
|
44
|
+
self.app_config = new_config
|
45
|
+
self.protocol_manager = get_protocol_manager(new_config)
|
46
|
+
logger.info("Protocol middleware configuration updated")
|
34
47
|
|
35
48
|
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
36
49
|
"""
|
@@ -116,20 +129,26 @@ class ProtocolMiddleware(BaseHTTPMiddleware):
|
|
116
129
|
return "http"
|
117
130
|
|
118
131
|
|
119
|
-
def setup_protocol_middleware(app,
|
132
|
+
def setup_protocol_middleware(app, app_config: Optional[Dict[str, Any]] = None):
|
120
133
|
"""
|
121
134
|
Setup protocol middleware for FastAPI application.
|
122
135
|
|
123
136
|
Args:
|
124
137
|
app: FastAPI application
|
125
|
-
|
138
|
+
app_config: Application configuration dictionary (optional)
|
126
139
|
"""
|
127
|
-
if
|
128
|
-
|
140
|
+
# Check if protocol management is enabled
|
141
|
+
if app_config is None:
|
142
|
+
from mcp_proxy_adapter.config import config
|
143
|
+
app_config = config.get_all()
|
144
|
+
|
145
|
+
protocols_config = app_config.get("protocols", {})
|
146
|
+
enabled = protocols_config.get("enabled", True)
|
129
147
|
|
130
|
-
|
131
|
-
|
132
|
-
app
|
148
|
+
if enabled:
|
149
|
+
# Create protocol middleware with current configuration
|
150
|
+
middleware = ProtocolMiddleware(app, app_config)
|
151
|
+
app.add_middleware(ProtocolMiddleware, app_config=app_config)
|
133
152
|
logger.info("Protocol middleware added to application")
|
134
153
|
else:
|
135
|
-
logger.
|
154
|
+
logger.info("Protocol management is disabled, skipping protocol middleware")
|
@@ -0,0 +1,160 @@
|
|
1
|
+
"""
|
2
|
+
Unified Security Middleware - Direct Framework Integration
|
3
|
+
|
4
|
+
This middleware now directly uses mcp_security_framework components
|
5
|
+
instead of custom implementations.
|
6
|
+
|
7
|
+
Author: Vasiliy Zdanovskiy
|
8
|
+
email: vasilyvz@gmail.com
|
9
|
+
"""
|
10
|
+
|
11
|
+
import time
|
12
|
+
import logging
|
13
|
+
from typing import Dict, Any, Optional, Callable, Awaitable
|
14
|
+
from fastapi import Request, Response
|
15
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
16
|
+
|
17
|
+
# Direct import from framework
|
18
|
+
try:
|
19
|
+
from mcp_security_framework.middleware.fastapi_middleware import FastAPISecurityMiddleware
|
20
|
+
from mcp_security_framework import SecurityManager
|
21
|
+
from mcp_security_framework.schemas.config import SecurityConfig
|
22
|
+
SECURITY_FRAMEWORK_AVAILABLE = True
|
23
|
+
except ImportError:
|
24
|
+
SECURITY_FRAMEWORK_AVAILABLE = False
|
25
|
+
FastAPISecurityMiddleware = None
|
26
|
+
SecurityManager = None
|
27
|
+
SecurityConfig = None
|
28
|
+
|
29
|
+
from mcp_proxy_adapter.core.logging import logger
|
30
|
+
from mcp_proxy_adapter.core.security_integration import create_security_integration
|
31
|
+
|
32
|
+
|
33
|
+
class SecurityValidationError(Exception):
|
34
|
+
"""Security validation error."""
|
35
|
+
|
36
|
+
def __init__(self, message: str, error_code: int):
|
37
|
+
self.message = message
|
38
|
+
self.error_code = error_code
|
39
|
+
super().__init__(self.message)
|
40
|
+
|
41
|
+
|
42
|
+
class UnifiedSecurityMiddleware(BaseHTTPMiddleware):
|
43
|
+
"""
|
44
|
+
Unified security middleware using mcp_security_framework.
|
45
|
+
|
46
|
+
This middleware now directly uses the security framework's FastAPI middleware
|
47
|
+
and components instead of custom implementations.
|
48
|
+
"""
|
49
|
+
|
50
|
+
def __init__(self, app, config: Dict[str, Any]):
|
51
|
+
"""
|
52
|
+
Initialize unified security middleware.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
app: FastAPI application
|
56
|
+
config: mcp_proxy_adapter configuration dictionary
|
57
|
+
"""
|
58
|
+
super().__init__(app)
|
59
|
+
self.config = config
|
60
|
+
|
61
|
+
# Create security integration
|
62
|
+
try:
|
63
|
+
self.security_integration = create_security_integration(config)
|
64
|
+
# Use framework's FastAPI middleware
|
65
|
+
self.framework_middleware = self.security_integration.create_fastapi_middleware(app)
|
66
|
+
logger.info("Using mcp_security_framework FastAPI middleware")
|
67
|
+
except Exception as e:
|
68
|
+
logger.error(f"Security framework integration failed: {e}")
|
69
|
+
# Instead of raising error, log warning and continue without security
|
70
|
+
logger.warning("Continuing without security framework - some security features will be disabled")
|
71
|
+
self.security_integration = None
|
72
|
+
self.framework_middleware = None
|
73
|
+
|
74
|
+
logger.info("Unified security middleware initialized")
|
75
|
+
|
76
|
+
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
77
|
+
"""
|
78
|
+
Process request using framework middleware.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
request: Request object
|
82
|
+
call_next: Next handler
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
Response object
|
86
|
+
"""
|
87
|
+
try:
|
88
|
+
# Use framework middleware if available
|
89
|
+
if self.framework_middleware is not None:
|
90
|
+
return await self.framework_middleware.dispatch(request, call_next)
|
91
|
+
else:
|
92
|
+
# Fallback: continue without security middleware
|
93
|
+
logger.debug("Security framework not available, continuing without security checks")
|
94
|
+
return await call_next(request)
|
95
|
+
|
96
|
+
except SecurityValidationError as e:
|
97
|
+
# Handle security validation errors
|
98
|
+
return await self._handle_security_error(request, e)
|
99
|
+
except Exception as e:
|
100
|
+
# Handle other errors
|
101
|
+
logger.error(f"Unexpected error in unified security middleware: {e}")
|
102
|
+
return await self._handle_general_error(request, e)
|
103
|
+
|
104
|
+
|
105
|
+
|
106
|
+
async def _handle_security_error(self, request: Request, error: SecurityValidationError) -> Response:
|
107
|
+
"""
|
108
|
+
Handle security validation errors.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
request: Request object
|
112
|
+
error: Security validation error
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
Error response
|
116
|
+
"""
|
117
|
+
from fastapi.responses import JSONResponse
|
118
|
+
|
119
|
+
error_response = {
|
120
|
+
"error": {
|
121
|
+
"code": error.error_code,
|
122
|
+
"message": error.message,
|
123
|
+
"type": "security_validation_error"
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
logger.warning(f"Security validation failed: {error.message}")
|
128
|
+
|
129
|
+
return JSONResponse(
|
130
|
+
status_code=error.error_code,
|
131
|
+
content=error_response
|
132
|
+
)
|
133
|
+
|
134
|
+
async def _handle_general_error(self, request: Request, error: Exception) -> Response:
|
135
|
+
"""
|
136
|
+
Handle general errors.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
request: Request object
|
140
|
+
error: General error
|
141
|
+
|
142
|
+
Returns:
|
143
|
+
Error response
|
144
|
+
"""
|
145
|
+
from fastapi.responses import JSONResponse
|
146
|
+
|
147
|
+
error_response = {
|
148
|
+
"error": {
|
149
|
+
"code": 500,
|
150
|
+
"message": "Internal server error",
|
151
|
+
"type": "general_error"
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
logger.error(f"General error in security middleware: {error}")
|
156
|
+
|
157
|
+
return JSONResponse(
|
158
|
+
status_code=500,
|
159
|
+
content=error_response
|
160
|
+
)
|
@@ -0,0 +1,83 @@
|
|
1
|
+
"""
|
2
|
+
User Info Middleware
|
3
|
+
|
4
|
+
This middleware extracts user information from authentication headers
|
5
|
+
and sets it in request.state for use by commands.
|
6
|
+
|
7
|
+
Author: Vasiliy Zdanovskiy
|
8
|
+
email: vasilyvz@gmail.com
|
9
|
+
"""
|
10
|
+
|
11
|
+
import logging
|
12
|
+
from typing import Dict, Any, Optional, Callable, Awaitable
|
13
|
+
from fastapi import Request, Response
|
14
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
15
|
+
|
16
|
+
from mcp_proxy_adapter.core.logging import logger
|
17
|
+
|
18
|
+
|
19
|
+
class UserInfoMiddleware(BaseHTTPMiddleware):
|
20
|
+
"""
|
21
|
+
Middleware for setting user information in request.state.
|
22
|
+
|
23
|
+
This middleware extracts user information from authentication headers
|
24
|
+
and sets it in request.state for use by commands.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(self, app, config: Dict[str, Any]):
|
28
|
+
"""
|
29
|
+
Initialize user info middleware.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
app: FastAPI application
|
33
|
+
config: Configuration dictionary
|
34
|
+
"""
|
35
|
+
super().__init__(app)
|
36
|
+
self.config = config
|
37
|
+
|
38
|
+
# Get API keys configuration
|
39
|
+
security_config = config.get("security", {})
|
40
|
+
auth_config = security_config.get("auth", {})
|
41
|
+
self.api_keys = auth_config.get("api_keys", {})
|
42
|
+
|
43
|
+
logger.info("User info middleware initialized")
|
44
|
+
|
45
|
+
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
46
|
+
"""
|
47
|
+
Process request and set user info in request.state.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
request: Request object
|
51
|
+
call_next: Next handler
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
Response object
|
55
|
+
"""
|
56
|
+
# Extract API key from headers
|
57
|
+
api_key = request.headers.get("X-API-Key")
|
58
|
+
|
59
|
+
if api_key and api_key in self.api_keys:
|
60
|
+
# Get user info from API key configuration
|
61
|
+
user_config = self.api_keys[api_key]
|
62
|
+
|
63
|
+
# Set user info in request.state
|
64
|
+
request.state.user = {
|
65
|
+
"id": api_key,
|
66
|
+
"role": user_config.get("roles", ["guest"])[0] if user_config.get("roles") else "guest",
|
67
|
+
"roles": user_config.get("roles", ["guest"]),
|
68
|
+
"permissions": user_config.get("permissions", ["read"])
|
69
|
+
}
|
70
|
+
|
71
|
+
logger.debug(f"Set user info for {api_key}: {request.state.user}")
|
72
|
+
else:
|
73
|
+
# Set default guest user info
|
74
|
+
request.state.user = {
|
75
|
+
"id": None,
|
76
|
+
"role": "guest",
|
77
|
+
"roles": ["guest"],
|
78
|
+
"permissions": ["read"]
|
79
|
+
}
|
80
|
+
|
81
|
+
logger.debug("Set default guest user info")
|
82
|
+
|
83
|
+
return await call_next(request)
|
@@ -12,6 +12,9 @@ from mcp_proxy_adapter.commands.certificate_management_command import Certificat
|
|
12
12
|
from mcp_proxy_adapter.commands.key_management_command import KeyManagementCommand
|
13
13
|
from mcp_proxy_adapter.commands.cert_monitor_command import CertMonitorCommand
|
14
14
|
from mcp_proxy_adapter.commands.transport_management_command import TransportManagementCommand
|
15
|
+
from mcp_proxy_adapter.commands.role_test_command import RoleTestCommand
|
16
|
+
from mcp_proxy_adapter.commands.echo_command import EchoCommand
|
17
|
+
from mcp_proxy_adapter.commands.proxy_registration_command import ProxyRegistrationCommand
|
15
18
|
|
16
19
|
__all__ = [
|
17
20
|
"Command",
|
@@ -27,5 +30,8 @@ __all__ = [
|
|
27
30
|
"CertificateManagementCommand",
|
28
31
|
"KeyManagementCommand",
|
29
32
|
"CertMonitorCommand",
|
30
|
-
"TransportManagementCommand"
|
33
|
+
"TransportManagementCommand",
|
34
|
+
"RoleTestCommand",
|
35
|
+
"EchoCommand",
|
36
|
+
"ProxyRegistrationCommand"
|
31
37
|
]
|
@@ -48,7 +48,7 @@ class Command(ABC):
|
|
48
48
|
Execute command with the specified parameters.
|
49
49
|
|
50
50
|
Args:
|
51
|
-
**kwargs: Command parameters.
|
51
|
+
**kwargs: Command parameters including optional 'context' parameter.
|
52
52
|
|
53
53
|
Returns:
|
54
54
|
Command result.
|
@@ -153,11 +153,14 @@ class Command(ABC):
|
|
153
153
|
Runs the command with the specified arguments.
|
154
154
|
|
155
155
|
Args:
|
156
|
-
**kwargs: Command arguments.
|
156
|
+
**kwargs: Command arguments including optional 'context' parameter.
|
157
157
|
|
158
158
|
Returns:
|
159
159
|
Command result.
|
160
160
|
"""
|
161
|
+
# Extract context from kwargs
|
162
|
+
context = kwargs.pop('context', {}) if 'context' in kwargs else {}
|
163
|
+
|
161
164
|
try:
|
162
165
|
logger.debug(f"Running command {cls.__name__} with params: {kwargs}")
|
163
166
|
|
@@ -185,8 +188,8 @@ class Command(ABC):
|
|
185
188
|
command = command_class()
|
186
189
|
validated_params = command.validate_params(kwargs)
|
187
190
|
|
188
|
-
# Execute command with validated parameters
|
189
|
-
result = await command.execute(**validated_params)
|
191
|
+
# Execute command with validated parameters and context
|
192
|
+
result = await command.execute(**validated_params, context=context)
|
190
193
|
|
191
194
|
logger.debug(f"Command {cls.__name__} executed successfully")
|
192
195
|
return result
|
@@ -17,6 +17,8 @@ from mcp_proxy_adapter.commands.unload_command import UnloadCommand
|
|
17
17
|
from mcp_proxy_adapter.commands.plugins_command import PluginsCommand
|
18
18
|
from mcp_proxy_adapter.commands.transport_management_command import TransportManagementCommand
|
19
19
|
from mcp_proxy_adapter.commands.proxy_registration_command import ProxyRegistrationCommand
|
20
|
+
from mcp_proxy_adapter.commands.echo_command import EchoCommand
|
21
|
+
from mcp_proxy_adapter.commands.role_test_command import RoleTestCommand
|
20
22
|
from mcp_proxy_adapter.core.logging import logger
|
21
23
|
|
22
24
|
|
@@ -39,7 +41,9 @@ def register_builtin_commands() -> int:
|
|
39
41
|
UnloadCommand,
|
40
42
|
PluginsCommand,
|
41
43
|
TransportManagementCommand,
|
42
|
-
ProxyRegistrationCommand
|
44
|
+
ProxyRegistrationCommand,
|
45
|
+
EchoCommand,
|
46
|
+
RoleTestCommand
|
43
47
|
]
|
44
48
|
|
45
49
|
registered_count = 0
|
@@ -85,5 +89,7 @@ def get_builtin_commands_list() -> list:
|
|
85
89
|
UnloadCommand,
|
86
90
|
PluginsCommand,
|
87
91
|
TransportManagementCommand,
|
88
|
-
ProxyRegistrationCommand
|
92
|
+
ProxyRegistrationCommand,
|
93
|
+
EchoCommand,
|
94
|
+
RoleTestCommand
|
89
95
|
]
|
@@ -660,6 +660,14 @@ class CommandRegistry:
|
|
660
660
|
except Exception as e:
|
661
661
|
logger.error(f"❌ Failed to initialize logging: {e}")
|
662
662
|
|
663
|
+
# Step 2.5: Reload protocol manager configuration
|
664
|
+
try:
|
665
|
+
from mcp_proxy_adapter.core.protocol_manager import protocol_manager
|
666
|
+
protocol_manager.reload_config()
|
667
|
+
logger.info("✅ Protocol manager configuration reloaded")
|
668
|
+
except Exception as e:
|
669
|
+
logger.error(f"❌ Failed to reload protocol manager: {e}")
|
670
|
+
|
663
671
|
# Step 3: Clear all commands (always clear for consistency)
|
664
672
|
self.clear()
|
665
673
|
|
@@ -0,0 +1,81 @@
|
|
1
|
+
"""
|
2
|
+
Author: Vasiliy Zdanovskiy
|
3
|
+
email: vasilyvz@gmail.com
|
4
|
+
|
5
|
+
Echo command for testing purposes.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
from typing import Any, Dict, Optional
|
10
|
+
|
11
|
+
from mcp_proxy_adapter.commands.base import Command
|
12
|
+
from mcp_proxy_adapter.commands.result import SuccessResult
|
13
|
+
|
14
|
+
|
15
|
+
class EchoCommandResult(SuccessResult):
|
16
|
+
"""Result for echo command."""
|
17
|
+
|
18
|
+
def __init__(self, message: str, timestamp: Optional[str] = None):
|
19
|
+
data = {"message": message}
|
20
|
+
if timestamp:
|
21
|
+
data["timestamp"] = timestamp
|
22
|
+
super().__init__(data=data, message=message)
|
23
|
+
|
24
|
+
@classmethod
|
25
|
+
def get_schema(cls) -> Dict[str, Any]:
|
26
|
+
"""Get JSON schema for result."""
|
27
|
+
return {
|
28
|
+
"type": "object",
|
29
|
+
"properties": {
|
30
|
+
"success": {"type": "boolean"},
|
31
|
+
"data": {
|
32
|
+
"type": "object",
|
33
|
+
"properties": {
|
34
|
+
"message": {"type": "string"},
|
35
|
+
"timestamp": {"type": "string", "nullable": True}
|
36
|
+
}
|
37
|
+
},
|
38
|
+
"message": {"type": "string"}
|
39
|
+
},
|
40
|
+
"required": ["success", "data"]
|
41
|
+
}
|
42
|
+
|
43
|
+
|
44
|
+
class EchoCommand(Command):
|
45
|
+
"""Echo command for testing purposes."""
|
46
|
+
|
47
|
+
name = "echo"
|
48
|
+
version = "1.0.0"
|
49
|
+
descr = "Echo command for testing"
|
50
|
+
category = "testing"
|
51
|
+
author = "Vasiliy Zdanovskiy"
|
52
|
+
email = "vasilyvz@gmail.com"
|
53
|
+
result_class = EchoCommandResult
|
54
|
+
|
55
|
+
async def execute(self, **kwargs) -> EchoCommandResult:
|
56
|
+
"""Execute echo command."""
|
57
|
+
message = kwargs.get("message", "Hello, World!")
|
58
|
+
timestamp = kwargs.get("timestamp")
|
59
|
+
|
60
|
+
# Simulate some processing time
|
61
|
+
await asyncio.sleep(0.001)
|
62
|
+
|
63
|
+
return EchoCommandResult(message=message, timestamp=timestamp)
|
64
|
+
|
65
|
+
def get_schema(self) -> Dict[str, Any]:
|
66
|
+
"""Get command schema."""
|
67
|
+
return {
|
68
|
+
"type": "object",
|
69
|
+
"properties": {
|
70
|
+
"message": {
|
71
|
+
"type": "string",
|
72
|
+
"description": "Message to echo",
|
73
|
+
"default": "Hello, World!"
|
74
|
+
},
|
75
|
+
"timestamp": {
|
76
|
+
"type": "string",
|
77
|
+
"description": "Optional timestamp",
|
78
|
+
"nullable": True
|
79
|
+
}
|
80
|
+
}
|
81
|
+
}
|