mcp-proxy-adapter 6.2.21__py3-none-any.whl → 6.2.23__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 +3 -0
- mcp_proxy_adapter/api/middleware/__init__.py +7 -7
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +59 -12
- mcp_proxy_adapter/api/middleware/unified_security.py +42 -6
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +99 -24
- mcp_proxy_adapter/core/protocol_manager.py +33 -5
- mcp_proxy_adapter/examples/create_certificates_simple.py +77 -9
- mcp_proxy_adapter/examples/generate_test_configs.py +44 -23
- mcp_proxy_adapter/examples/run_full_test_suite.py +125 -0
- mcp_proxy_adapter/examples/run_proxy_server.py +9 -7
- mcp_proxy_adapter/examples/run_security_tests.py +187 -36
- mcp_proxy_adapter/examples/security_test_client.py +182 -38
- mcp_proxy_adapter/examples/setup_test_environment.py +12 -0
- mcp_proxy_adapter/main.py +12 -2
- mcp_proxy_adapter/utils/config_generator.py +740 -0
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.2.21.dist-info → mcp_proxy_adapter-6.2.23.dist-info}/METADATA +9 -11
- {mcp_proxy_adapter-6.2.21.dist-info → mcp_proxy_adapter-6.2.23.dist-info}/RECORD +22 -19
- mcp_proxy_adapter-6.2.23.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-6.2.21.dist-info → mcp_proxy_adapter-6.2.23.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.2.21.dist-info → mcp_proxy_adapter-6.2.23.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-6.2.21.dist-info → mcp_proxy_adapter-6.2.23.dist-info}/top_level.txt +0 -0
@@ -9,14 +9,14 @@ import json
|
|
9
9
|
import os
|
10
10
|
import argparse
|
11
11
|
from typing import Dict, Any
|
12
|
-
def generate_http_simple_config(port: int =
|
12
|
+
def generate_http_simple_config(port: int = 20000, certs_dir: str = "./certs", keys_dir: str = "./keys") -> Dict[str, Any]:
|
13
13
|
"""Generate HTTP configuration without authorization."""
|
14
14
|
return {
|
15
15
|
"server": {"host": "127.0.0.1", "port": port},
|
16
16
|
"ssl": {"enabled": False},
|
17
17
|
"security": {"enabled": False},
|
18
18
|
"registration": {
|
19
|
-
"enabled":
|
19
|
+
"enabled": False,
|
20
20
|
"auth_method": "token",
|
21
21
|
"server_url": "http://127.0.0.1:3004/proxy",
|
22
22
|
"token": {"enabled": True, "token": "proxy_registration_token_123"},
|
@@ -29,14 +29,25 @@ def generate_http_simple_config(port: int = 8000) -> Dict[str, Any]:
|
|
29
29
|
},
|
30
30
|
"protocols": {"enabled": True, "allowed_protocols": ["http"]}
|
31
31
|
}
|
32
|
-
def generate_http_token_config(port: int =
|
32
|
+
def generate_http_token_config(port: int = 20001, certs_dir: str = "./certs", keys_dir: str = "./keys") -> Dict[str, Any]:
|
33
33
|
"""Generate HTTP configuration with token authorization."""
|
34
34
|
return {
|
35
35
|
"server": {"host": "127.0.0.1", "port": port},
|
36
36
|
"ssl": {"enabled": False},
|
37
37
|
"security": {
|
38
38
|
"enabled": True,
|
39
|
-
"auth": {
|
39
|
+
"auth": {
|
40
|
+
"enabled": True,
|
41
|
+
"methods": ["api_key"],
|
42
|
+
# Map API tokens to roles for testing
|
43
|
+
"api_keys": {
|
44
|
+
"test-token-123": "admin",
|
45
|
+
"user-token-456": "user",
|
46
|
+
"readonly-token-123": "readonly",
|
47
|
+
"guest-token-123": "guest",
|
48
|
+
"proxy-token-123": "proxy"
|
49
|
+
}
|
50
|
+
},
|
40
51
|
"permissions": {"enabled": True, "roles_file": "./roles.json"}
|
41
52
|
},
|
42
53
|
"registration": {
|
@@ -50,7 +61,7 @@ def generate_http_token_config(port: int = 8001) -> Dict[str, Any]:
|
|
50
61
|
},
|
51
62
|
"protocols": {"enabled": True, "allowed_protocols": ["http"]}
|
52
63
|
}
|
53
|
-
def generate_https_simple_config(port: int =
|
64
|
+
def generate_https_simple_config(port: int = 20002, certs_dir: str = "./certs", keys_dir: str = "./keys") -> Dict[str, Any]:
|
54
65
|
"""Generate HTTPS configuration without client certificate verification and authorization."""
|
55
66
|
return {
|
56
67
|
"server": {"host": "127.0.0.1", "port": port},
|
@@ -71,7 +82,7 @@ def generate_https_simple_config(port: int = 8002) -> Dict[str, Any]:
|
|
71
82
|
},
|
72
83
|
"protocols": {"enabled": True, "allowed_protocols": ["http", "https"]}
|
73
84
|
}
|
74
|
-
def generate_https_token_config(port: int =
|
85
|
+
def generate_https_token_config(port: int = 20003, certs_dir: str = "./certs", keys_dir: str = "./keys") -> Dict[str, Any]:
|
75
86
|
"""Generate HTTPS configuration without client certificate verification with token authorization."""
|
76
87
|
return {
|
77
88
|
"server": {"host": "127.0.0.1", "port": port},
|
@@ -82,7 +93,17 @@ def generate_https_token_config(port: int = 8003) -> Dict[str, Any]:
|
|
82
93
|
},
|
83
94
|
"security": {
|
84
95
|
"enabled": True,
|
85
|
-
"auth": {
|
96
|
+
"auth": {
|
97
|
+
"enabled": True,
|
98
|
+
"methods": ["api_key"],
|
99
|
+
"api_keys": {
|
100
|
+
"test-token-123": "admin",
|
101
|
+
"user-token-456": "user",
|
102
|
+
"readonly-token-123": "readonly",
|
103
|
+
"guest-token-123": "guest",
|
104
|
+
"proxy-token-123": "proxy"
|
105
|
+
}
|
106
|
+
},
|
86
107
|
"permissions": {"enabled": True, "roles_file": "./roles.json"}
|
87
108
|
},
|
88
109
|
"registration": {
|
@@ -96,7 +117,7 @@ def generate_https_token_config(port: int = 8003) -> Dict[str, Any]:
|
|
96
117
|
},
|
97
118
|
"protocols": {"enabled": True, "allowed_protocols": ["http", "https"]}
|
98
119
|
}
|
99
|
-
def generate_mtls_no_roles_config(port: int =
|
120
|
+
def generate_mtls_no_roles_config(port: int = 20004, certs_dir: str = "./certs", keys_dir: str = "./keys") -> Dict[str, Any]:
|
100
121
|
"""Generate mTLS configuration without roles."""
|
101
122
|
return {
|
102
123
|
"server": {"host": "127.0.0.1", "port": port},
|
@@ -104,7 +125,7 @@ def generate_mtls_no_roles_config(port: int = 8004) -> Dict[str, Any]:
|
|
104
125
|
"enabled": True,
|
105
126
|
"cert_file": "./certs/localhost_server.crt",
|
106
127
|
"key_file": "./keys/localhost_server.key",
|
107
|
-
"ca_cert": "./certs/
|
128
|
+
"ca_cert": "./certs/mcp_proxy_adapter_ca_ca.crt",
|
108
129
|
"verify_client": True
|
109
130
|
},
|
110
131
|
"security": {
|
@@ -114,7 +135,7 @@ def generate_mtls_no_roles_config(port: int = 8004) -> Dict[str, Any]:
|
|
114
135
|
},
|
115
136
|
"protocols": {"enabled": True, "allowed_protocols": ["https", "mtls"]}
|
116
137
|
}
|
117
|
-
def generate_mtls_with_roles_config(port: int =
|
138
|
+
def generate_mtls_with_roles_config(port: int = 20005, certs_dir: str = "./certs", keys_dir: str = "./keys") -> Dict[str, Any]:
|
118
139
|
"""Generate mTLS configuration with roles."""
|
119
140
|
return {
|
120
141
|
"server": {"host": "127.0.0.1", "port": port},
|
@@ -122,7 +143,7 @@ def generate_mtls_with_roles_config(port: int = 8005) -> Dict[str, Any]:
|
|
122
143
|
"enabled": True,
|
123
144
|
"cert_file": "./certs/localhost_server.crt",
|
124
145
|
"key_file": "./keys/localhost_server.key",
|
125
|
-
"ca_cert": "./certs/
|
146
|
+
"ca_cert": "./certs/mcp_proxy_adapter_ca_ca.crt",
|
126
147
|
"verify_client": True
|
127
148
|
},
|
128
149
|
"registration": {
|
@@ -160,7 +181,7 @@ def generate_roles_config() -> Dict[str, Any]:
|
|
160
181
|
"heartbeat",
|
161
182
|
"discover"
|
162
183
|
],
|
163
|
-
"tokens": []
|
184
|
+
"tokens": ["test-token-123"]
|
164
185
|
},
|
165
186
|
"user": {
|
166
187
|
"description": "User role with limited access",
|
@@ -172,7 +193,7 @@ def generate_roles_config() -> Dict[str, Any]:
|
|
172
193
|
"heartbeat",
|
173
194
|
"discover"
|
174
195
|
],
|
175
|
-
"tokens": []
|
196
|
+
"tokens": ["user-token-456"]
|
176
197
|
},
|
177
198
|
"readonly": {
|
178
199
|
"description": "Read-only role",
|
@@ -180,7 +201,7 @@ def generate_roles_config() -> Dict[str, Any]:
|
|
180
201
|
"read",
|
181
202
|
"discover"
|
182
203
|
],
|
183
|
-
"tokens": []
|
204
|
+
"tokens": ["readonly-token-123"]
|
184
205
|
},
|
185
206
|
"guest": {
|
186
207
|
"description": "Guest role with read-only access",
|
@@ -188,7 +209,7 @@ def generate_roles_config() -> Dict[str, Any]:
|
|
188
209
|
"read",
|
189
210
|
"discover"
|
190
211
|
],
|
191
|
-
"tokens": []
|
212
|
+
"tokens": ["guest-token-123"]
|
192
213
|
},
|
193
214
|
"proxy": {
|
194
215
|
"description": "Proxy role for registration",
|
@@ -198,18 +219,18 @@ def generate_roles_config() -> Dict[str, Any]:
|
|
198
219
|
"heartbeat",
|
199
220
|
"discover"
|
200
221
|
],
|
201
|
-
"tokens": []
|
222
|
+
"tokens": ["proxy-token-123"]
|
202
223
|
}
|
203
224
|
}
|
204
|
-
def generate_all_configs(output_dir: str) -> None:
|
225
|
+
def generate_all_configs(output_dir: str, certs_dir: str = "./certs", keys_dir: str = "./keys") -> None:
|
205
226
|
"""Generate all 6 configuration types and save them to files."""
|
206
227
|
configs = {
|
207
|
-
"http_simple": generate_http_simple_config(
|
208
|
-
"http_token": generate_http_token_config(
|
209
|
-
"https_simple": generate_https_simple_config(
|
210
|
-
"https_token": generate_https_token_config(
|
211
|
-
"mtls_no_roles": generate_mtls_no_roles_config(
|
212
|
-
"mtls_with_roles": generate_mtls_with_roles_config(
|
228
|
+
"http_simple": generate_http_simple_config(20000, certs_dir, keys_dir),
|
229
|
+
"http_token": generate_http_token_config(20001, certs_dir, keys_dir),
|
230
|
+
"https_simple": generate_https_simple_config(20002, certs_dir, keys_dir),
|
231
|
+
"https_token": generate_https_token_config(20003, certs_dir, keys_dir),
|
232
|
+
"mtls_no_roles": generate_mtls_no_roles_config(20004, certs_dir, keys_dir),
|
233
|
+
"mtls_with_roles": generate_mtls_with_roles_config(20005, certs_dir, keys_dir)
|
213
234
|
}
|
214
235
|
# Ensure output directory exists
|
215
236
|
os.makedirs(output_dir, exist_ok=True)
|
@@ -0,0 +1,125 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Full Test Suite Runner for MCP Proxy Adapter
|
4
|
+
This script automatically runs the complete test suite:
|
5
|
+
1. Setup test environment
|
6
|
+
2. Generate configurations
|
7
|
+
3. Create certificates
|
8
|
+
4. Run security tests
|
9
|
+
|
10
|
+
Author: Vasiliy Zdanovskiy
|
11
|
+
email: vasilyvz@gmail.com
|
12
|
+
"""
|
13
|
+
import sys
|
14
|
+
import subprocess
|
15
|
+
import os
|
16
|
+
from pathlib import Path
|
17
|
+
|
18
|
+
|
19
|
+
def run_command(cmd: list, description: str) -> bool:
|
20
|
+
"""Run a command and return success status."""
|
21
|
+
try:
|
22
|
+
print(f"\n🚀 {description}")
|
23
|
+
print("=" * 60)
|
24
|
+
|
25
|
+
# Change to script directory if running from package
|
26
|
+
script_dir = Path(__file__).parent
|
27
|
+
if script_dir.name == "examples":
|
28
|
+
os.chdir(script_dir)
|
29
|
+
|
30
|
+
result = subprocess.run(
|
31
|
+
cmd,
|
32
|
+
capture_output=False,
|
33
|
+
text=True,
|
34
|
+
check=True,
|
35
|
+
cwd=script_dir
|
36
|
+
)
|
37
|
+
print(f"✅ {description} completed successfully")
|
38
|
+
return True
|
39
|
+
except subprocess.CalledProcessError as e:
|
40
|
+
print(f"❌ {description} failed:")
|
41
|
+
print(f" Command: {' '.join(cmd)}")
|
42
|
+
print(f" Error: {e.stderr}")
|
43
|
+
return False
|
44
|
+
except Exception as e:
|
45
|
+
print(f"❌ {description} failed: {e}")
|
46
|
+
return False
|
47
|
+
|
48
|
+
|
49
|
+
def main():
|
50
|
+
"""Run the complete test suite."""
|
51
|
+
print("🧪 MCP Proxy Adapter - Full Test Suite")
|
52
|
+
print("=" * 60)
|
53
|
+
|
54
|
+
# Check if we're in the right directory
|
55
|
+
current_dir = Path.cwd()
|
56
|
+
if not (current_dir / "setup_test_environment.py").exists():
|
57
|
+
print("❌ Please run this script from the examples directory")
|
58
|
+
return 1
|
59
|
+
|
60
|
+
success = True
|
61
|
+
|
62
|
+
# 1. Setup test environment
|
63
|
+
if not run_command([
|
64
|
+
sys.executable, "-m", "mcp_proxy_adapter.examples.setup_test_environment",
|
65
|
+
"--output-dir", "."
|
66
|
+
], "Setting up test environment"):
|
67
|
+
success = False
|
68
|
+
|
69
|
+
# 2. Generate configurations
|
70
|
+
if success and not run_command([
|
71
|
+
sys.executable, "-m", "mcp_proxy_adapter.examples.generate_test_configs",
|
72
|
+
"--output-dir", "configs"
|
73
|
+
], "Generating test configurations"):
|
74
|
+
success = False
|
75
|
+
|
76
|
+
# 3. Create certificates
|
77
|
+
if success and not run_command([
|
78
|
+
sys.executable, "-m", "mcp_proxy_adapter.examples.create_certificates_simple"
|
79
|
+
], "Creating certificates"):
|
80
|
+
success = False
|
81
|
+
|
82
|
+
# 4. Copy roles.json to root directory
|
83
|
+
if success:
|
84
|
+
import shutil
|
85
|
+
from pathlib import Path
|
86
|
+
roles_file = Path("configs/roles.json")
|
87
|
+
if roles_file.exists():
|
88
|
+
shutil.copy2(roles_file, "roles.json")
|
89
|
+
print("✅ Copied roles.json to root directory")
|
90
|
+
else:
|
91
|
+
success = False
|
92
|
+
print("❌ roles.json not found in configs directory")
|
93
|
+
|
94
|
+
# 5. Run security tests
|
95
|
+
if success and not run_command([
|
96
|
+
sys.executable, "-m", "mcp_proxy_adapter.examples.run_security_tests"
|
97
|
+
], "Running security tests"):
|
98
|
+
success = False
|
99
|
+
|
100
|
+
# Final result
|
101
|
+
print("\n" + "=" * 60)
|
102
|
+
if success:
|
103
|
+
print("🎉 FULL TEST SUITE COMPLETED SUCCESSFULLY!")
|
104
|
+
print("=" * 60)
|
105
|
+
print("\n📋 SUMMARY:")
|
106
|
+
print("✅ Test environment setup")
|
107
|
+
print("✅ Configuration generation")
|
108
|
+
print("✅ Certificate creation")
|
109
|
+
print("✅ Roles configuration")
|
110
|
+
print("✅ Security testing")
|
111
|
+
print("\n🚀 All systems are working correctly!")
|
112
|
+
return 0
|
113
|
+
else:
|
114
|
+
print("❌ FULL TEST SUITE FAILED!")
|
115
|
+
print("=" * 60)
|
116
|
+
print("\n🔧 TROUBLESHOOTING:")
|
117
|
+
print("1. Check the error messages above")
|
118
|
+
print("2. Ensure you have write permissions")
|
119
|
+
print("3. Make sure ports 20000-20010 are free")
|
120
|
+
print("4. Check if mcp_security_framework is installed")
|
121
|
+
return 1
|
122
|
+
|
123
|
+
|
124
|
+
if __name__ == "__main__":
|
125
|
+
exit(main())
|
@@ -19,7 +19,7 @@ from datetime import datetime, timedelta
|
|
19
19
|
|
20
20
|
from fastapi import FastAPI, HTTPException
|
21
21
|
from pydantic import BaseModel
|
22
|
-
import
|
22
|
+
from mcp_proxy_adapter.core.server_adapter import UnifiedServerRunner
|
23
23
|
|
24
24
|
|
25
25
|
# Simple in-memory storage for registered adapters
|
@@ -130,13 +130,15 @@ def main() -> None:
|
|
130
130
|
print(" POST /proxy/heartbeat - Heartbeat from adapter")
|
131
131
|
print("⚡ Press Ctrl+C to stop\n")
|
132
132
|
|
133
|
-
# Run server
|
134
|
-
|
133
|
+
# Run server via unified runner (hypercorn under the hood)
|
134
|
+
runner = UnifiedServerRunner()
|
135
|
+
runner.run_server(
|
135
136
|
app,
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
137
|
+
{
|
138
|
+
"host": args.host,
|
139
|
+
"port": args.port,
|
140
|
+
"log_level": args.log_level,
|
141
|
+
},
|
140
142
|
)
|
141
143
|
|
142
144
|
|
@@ -14,11 +14,15 @@ import asyncio
|
|
14
14
|
import json
|
15
15
|
import os
|
16
16
|
import signal
|
17
|
+
import socket
|
17
18
|
import subprocess
|
18
19
|
import sys
|
19
20
|
import time
|
20
21
|
from pathlib import Path
|
21
|
-
from typing import Dict, List, Optional, Any
|
22
|
+
from typing import Dict, List, Optional, Any, Tuple
|
23
|
+
|
24
|
+
import psutil
|
25
|
+
import requests
|
22
26
|
# Import security test client with proper module path
|
23
27
|
from mcp_proxy_adapter.examples.security_test_client import SecurityTestClient, TestResult
|
24
28
|
class SecurityTestRunner:
|
@@ -27,39 +31,149 @@ class SecurityTestRunner:
|
|
27
31
|
"""Initialize test runner."""
|
28
32
|
self.servers = {}
|
29
33
|
self.proxy_server = None
|
34
|
+
self.server_logs = {}
|
35
|
+
self.proxy_log = None
|
30
36
|
self.test_results = {}
|
37
|
+
# Base and proxy ports
|
38
|
+
self.base_port = 20000
|
39
|
+
self.proxy_port = 20010
|
40
|
+
# Server configurations with ports starting from 20000
|
31
41
|
self.configs = {
|
32
42
|
"basic_http": {
|
33
43
|
"config": "configs/http_simple.json",
|
34
|
-
"port":
|
35
|
-
"url": "http://localhost:
|
44
|
+
"port": self.base_port + 0,
|
45
|
+
"url": f"http://localhost:{self.base_port + 0}",
|
36
46
|
"auth": "none"
|
37
47
|
},
|
38
48
|
"http_token": {
|
39
49
|
"config": "configs/http_token.json",
|
40
|
-
"port":
|
41
|
-
"url": "http://localhost:
|
50
|
+
"port": self.base_port + 1,
|
51
|
+
"url": f"http://localhost:{self.base_port + 1}",
|
42
52
|
"auth": "api_key"
|
43
53
|
},
|
44
54
|
"https": {
|
45
55
|
"config": "configs/https_simple.json",
|
46
|
-
"port":
|
47
|
-
"url": "https://localhost:
|
56
|
+
"port": self.base_port + 2,
|
57
|
+
"url": f"https://localhost:{self.base_port + 2}",
|
48
58
|
"auth": "none"
|
49
59
|
},
|
50
60
|
"https_token": {
|
51
61
|
"config": "configs/https_token.json",
|
52
|
-
"port":
|
53
|
-
"url": "https://localhost:
|
62
|
+
"port": self.base_port + 3,
|
63
|
+
"url": f"https://localhost:{self.base_port + 3}",
|
54
64
|
"auth": "api_key"
|
55
65
|
},
|
56
66
|
"mtls": {
|
57
67
|
"config": "configs/mtls_no_roles.json",
|
58
|
-
"port":
|
59
|
-
"url": "https://localhost:
|
68
|
+
"port": self.base_port + 4,
|
69
|
+
"url": f"https://localhost:{self.base_port + 4}",
|
60
70
|
"auth": "certificate"
|
61
71
|
}
|
62
72
|
}
|
73
|
+
|
74
|
+
def _port_in_use(self, port: int, host: str = "127.0.0.1") -> bool:
|
75
|
+
try:
|
76
|
+
with socket.create_connection((host, port), timeout=0.5):
|
77
|
+
return True
|
78
|
+
except Exception:
|
79
|
+
return False
|
80
|
+
|
81
|
+
def _pids_on_port(self, port: int) -> List[int]:
|
82
|
+
pids: List[int] = []
|
83
|
+
try:
|
84
|
+
for proc in psutil.process_iter(attrs=["pid", "connections"]):
|
85
|
+
for c in proc.connections(kind="inet"):
|
86
|
+
if c.laddr and c.laddr.port == port:
|
87
|
+
pids.append(proc.pid)
|
88
|
+
break
|
89
|
+
except Exception:
|
90
|
+
pass
|
91
|
+
return list(set(pids))
|
92
|
+
|
93
|
+
def ensure_ports_free(self, ports: List[int]) -> None:
|
94
|
+
for port in ports:
|
95
|
+
pids = self._pids_on_port(port)
|
96
|
+
for pid in pids:
|
97
|
+
try:
|
98
|
+
psutil.Process(pid).terminate()
|
99
|
+
except Exception:
|
100
|
+
pass
|
101
|
+
time.sleep(0.3)
|
102
|
+
for pid in pids:
|
103
|
+
try:
|
104
|
+
if psutil.pid_exists(pid):
|
105
|
+
psutil.Process(pid).kill()
|
106
|
+
except Exception:
|
107
|
+
pass
|
108
|
+
|
109
|
+
def wait_for_http(self, url: str, timeout_sec: float = 8.0) -> bool:
|
110
|
+
end = time.time() + timeout_sec
|
111
|
+
candidates = ["/health", "/proxy/health"]
|
112
|
+
while time.time() < end:
|
113
|
+
for path in candidates:
|
114
|
+
health_url = url.rstrip("/") + path
|
115
|
+
try:
|
116
|
+
resp = requests.get(health_url, timeout=1.0, verify=False)
|
117
|
+
if resp.status_code == 200:
|
118
|
+
return True
|
119
|
+
except Exception:
|
120
|
+
pass
|
121
|
+
time.sleep(0.2)
|
122
|
+
return False
|
123
|
+
|
124
|
+
def wait_for_port(self, port: int, timeout_sec: float = 8.0) -> bool:
|
125
|
+
end = time.time() + timeout_sec
|
126
|
+
while time.time() < end:
|
127
|
+
if self._port_in_use(port):
|
128
|
+
return True
|
129
|
+
time.sleep(0.2)
|
130
|
+
return False
|
131
|
+
|
132
|
+
def get_all_ports(self) -> List[int]:
|
133
|
+
ports = [self.proxy_port]
|
134
|
+
for cfg in self.configs.values():
|
135
|
+
ports.append(cfg["port"])
|
136
|
+
return list(sorted(set(ports)))
|
137
|
+
|
138
|
+
def _validate_file(self, base: Path, path_value: Optional[str]) -> Tuple[bool, str]:
|
139
|
+
if not path_value:
|
140
|
+
return True, ""
|
141
|
+
p = Path(path_value)
|
142
|
+
if not p.is_absolute():
|
143
|
+
p = base / p
|
144
|
+
return p.exists(), str(p)
|
145
|
+
|
146
|
+
def validate_config_files(self) -> bool:
|
147
|
+
ok = True
|
148
|
+
base = Path.cwd()
|
149
|
+
missing: List[str] = []
|
150
|
+
for name, cfg in self.configs.items():
|
151
|
+
cfg_path = Path(cfg["config"]).resolve()
|
152
|
+
try:
|
153
|
+
with open(cfg_path, "r", encoding="utf-8") as f:
|
154
|
+
data = json.load(f)
|
155
|
+
ssl = data.get("ssl", {})
|
156
|
+
for key in ("cert_file", "key_file", "ca_cert"):
|
157
|
+
exists, abs_path = self._validate_file(base, ssl.get(key))
|
158
|
+
if ssl.get("enabled") and key in ("cert_file", "key_file") and not exists:
|
159
|
+
ok = False
|
160
|
+
missing.append(f"{name}:{key} -> {abs_path}")
|
161
|
+
sec = data.get("security", {})
|
162
|
+
perms = sec.get("permissions", {})
|
163
|
+
exists, abs_path = self._validate_file(base, perms.get("roles_file"))
|
164
|
+
if sec.get("enabled") and perms.get("enabled") and not exists:
|
165
|
+
ok = False
|
166
|
+
missing.append(f"{name}:roles_file -> {abs_path}")
|
167
|
+
except Exception as e:
|
168
|
+
ok = False
|
169
|
+
missing.append(f"{name}: cannot read {cfg_path} ({e})")
|
170
|
+
if not ok:
|
171
|
+
print("❌ CONFIG VALIDATION FAILED. Missing files:")
|
172
|
+
for m in missing:
|
173
|
+
print(" -", m)
|
174
|
+
else:
|
175
|
+
print("✅ Configuration file paths validated")
|
176
|
+
return ok
|
63
177
|
def check_prerequisites(self) -> bool:
|
64
178
|
"""Check if all prerequisites are met."""
|
65
179
|
print("🔍 Checking prerequisites...")
|
@@ -69,7 +183,7 @@ class SecurityTestRunner:
|
|
69
183
|
return False
|
70
184
|
# Check if certificates exist
|
71
185
|
cert_files = [
|
72
|
-
|
186
|
+
"certs/mcp_proxy_adapter_ca_ca.crt",
|
73
187
|
"certs/localhost_server.crt",
|
74
188
|
"keys/localhost_server.key"
|
75
189
|
]
|
@@ -87,11 +201,26 @@ class SecurityTestRunner:
|
|
87
201
|
"""Start a server in background."""
|
88
202
|
try:
|
89
203
|
print(f"🚀 Starting {name} server on port {port}...")
|
204
|
+
# Ensure the port is free just before starting
|
205
|
+
self.ensure_ports_free([port])
|
206
|
+
if self._port_in_use(port):
|
207
|
+
print(f"⚠️ Port {port} still busy, waiting...")
|
208
|
+
if not self.wait_for_port(port, timeout_sec=1.5):
|
209
|
+
# After wait_for_port True means busy; we invert logic here
|
210
|
+
pass
|
211
|
+
# If still busy after attempts, abort this server start
|
212
|
+
if self._port_in_use(port):
|
213
|
+
print(f"❌ Port {port} is in use, cannot start {name}")
|
214
|
+
return None
|
90
215
|
# Start server in background
|
216
|
+
logs_dir = Path("logs"); logs_dir.mkdir(exist_ok=True)
|
217
|
+
log_path = logs_dir / f"{name}.log"
|
218
|
+
log_file = open(log_path, "wb")
|
219
|
+
self.server_logs[name] = log_file
|
91
220
|
process = subprocess.Popen([
|
92
221
|
sys.executable, "-m", "mcp_proxy_adapter.main",
|
93
222
|
"--config", config_path
|
94
|
-
], stdout=
|
223
|
+
], stdout=log_file, stderr=subprocess.STDOUT)
|
95
224
|
# Wait a bit for server to start
|
96
225
|
time.sleep(3)
|
97
226
|
# Check if process is still running
|
@@ -99,10 +228,7 @@ class SecurityTestRunner:
|
|
99
228
|
print(f"✅ {name} server started (PID: {process.pid})")
|
100
229
|
return process
|
101
230
|
else:
|
102
|
-
|
103
|
-
print(f"❌ Failed to start {name} server:")
|
104
|
-
print(f"STDOUT: {stdout.decode()}")
|
105
|
-
print(f"STDERR: {stderr.decode()}")
|
231
|
+
print(f"❌ Failed to start {name} server (see logs/{name}.log)")
|
106
232
|
return None
|
107
233
|
except Exception as e:
|
108
234
|
print(f"❌ Error starting {name} server: {e}")
|
@@ -122,6 +248,13 @@ class SecurityTestRunner:
|
|
122
248
|
process.wait()
|
123
249
|
except Exception as e:
|
124
250
|
print(f"❌ Error stopping {name} server: {e}")
|
251
|
+
finally:
|
252
|
+
try:
|
253
|
+
lf = self.server_logs.pop(name, None)
|
254
|
+
if lf:
|
255
|
+
lf.close()
|
256
|
+
except Exception:
|
257
|
+
pass
|
125
258
|
|
126
259
|
def start_proxy_server(self) -> bool:
|
127
260
|
"""Start the proxy server for server registration."""
|
@@ -134,27 +267,24 @@ class SecurityTestRunner:
|
|
134
267
|
return False
|
135
268
|
|
136
269
|
# Start proxy server
|
137
|
-
cmd = [sys.executable, str(proxy_script), "--host", "127.0.0.1", "--port",
|
270
|
+
cmd = [sys.executable, str(proxy_script), "--host", "127.0.0.1", "--port", str(self.proxy_port)]
|
271
|
+
logs_dir = Path("logs"); logs_dir.mkdir(exist_ok=True)
|
272
|
+
proxy_log_path = logs_dir / "proxy_server.log"
|
273
|
+
self.proxy_log = open(proxy_log_path, "wb")
|
138
274
|
process = subprocess.Popen(
|
139
275
|
cmd,
|
140
|
-
stdout=
|
141
|
-
stderr=subprocess.
|
276
|
+
stdout=self.proxy_log,
|
277
|
+
stderr=subprocess.STDOUT,
|
142
278
|
cwd=Path.cwd()
|
143
279
|
)
|
144
280
|
|
145
|
-
#
|
146
|
-
|
147
|
-
|
148
|
-
# Check if process is still running
|
149
|
-
if process.poll() is None:
|
281
|
+
# Check readiness
|
282
|
+
if process.poll() is None and self.wait_for_http(f"http://127.0.0.1:{self.proxy_port}"):
|
150
283
|
self.proxy_server = process
|
151
284
|
print("✅ Proxy server started successfully (PID: {})".format(process.pid))
|
152
285
|
return True
|
153
286
|
else:
|
154
|
-
|
155
|
-
print("❌ Failed to start proxy server:")
|
156
|
-
print("STDOUT:", stdout.decode())
|
157
|
-
print("STDERR:", stderr.decode())
|
287
|
+
print("❌ Failed to start proxy server (see logs/proxy_server.log)")
|
158
288
|
return False
|
159
289
|
|
160
290
|
except Exception as e:
|
@@ -178,6 +308,12 @@ class SecurityTestRunner:
|
|
178
308
|
print(f"❌ Error stopping proxy server: {e}")
|
179
309
|
finally:
|
180
310
|
self.proxy_server = None
|
311
|
+
try:
|
312
|
+
if self.proxy_log:
|
313
|
+
self.proxy_log.close()
|
314
|
+
self.proxy_log = None
|
315
|
+
except Exception:
|
316
|
+
pass
|
181
317
|
async def test_server(self, name: str, config: Dict[str, Any]) -> List[TestResult]:
|
182
318
|
"""Test a specific server configuration."""
|
183
319
|
print(f"\n🧪 Testing {name} server...")
|
@@ -209,16 +345,23 @@ class SecurityTestRunner:
|
|
209
345
|
"""Run tests against all server configurations."""
|
210
346
|
print("🚀 Starting comprehensive security testing")
|
211
347
|
print("=" * 60)
|
212
|
-
# Start all servers
|
348
|
+
# Start all servers with verification and abort on failure
|
213
349
|
for name, config in self.configs.items():
|
214
350
|
process = self.start_server(name, config["config"], config["port"])
|
215
|
-
if process:
|
216
|
-
|
351
|
+
if not process:
|
352
|
+
print(f"❌ {name} failed to start. Aborting.")
|
353
|
+
return {}
|
354
|
+
url = config["url"]
|
355
|
+
ready = False
|
356
|
+
if name == "mtls":
|
357
|
+
ready = self.wait_for_port(config["port"], timeout_sec=8.0)
|
217
358
|
else:
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
359
|
+
ready = self.wait_for_http(url, timeout_sec=8.0)
|
360
|
+
if not ready:
|
361
|
+
print(f"❌ {name} did not become ready. Aborting.")
|
362
|
+
return {}
|
363
|
+
self.servers[name] = process
|
364
|
+
print("\n✅ All servers started and verified. Proceeding to client tests...")
|
222
365
|
# Test each server
|
223
366
|
all_results = {}
|
224
367
|
for name, config in self.configs.items():
|
@@ -320,6 +463,14 @@ class SecurityTestRunner:
|
|
320
463
|
if not self.check_prerequisites():
|
321
464
|
return False
|
322
465
|
|
466
|
+
# Free ports before run
|
467
|
+
print("\n🧹 Freeing ports before startup...")
|
468
|
+
self.ensure_ports_free(self.get_all_ports())
|
469
|
+
|
470
|
+
# Validate config file paths
|
471
|
+
if not self.validate_config_files():
|
472
|
+
return False
|
473
|
+
|
323
474
|
# Start proxy server first
|
324
475
|
print("\n🚀 Starting proxy server for server registration...")
|
325
476
|
if not self.start_proxy_server():
|