mcp-proxy-adapter 6.2.18__py3-none-any.whl → 6.2.22__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/__main__.py +1 -1
- 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 -9
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +99 -24
- mcp_proxy_adapter/config.py +1 -1
- mcp_proxy_adapter/core/protocol_manager.py +33 -5
- mcp_proxy_adapter/examples/__init__.py +1 -1
- mcp_proxy_adapter/examples/generate_test_configs.py +30 -9
- 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/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.18.dist-info → mcp_proxy_adapter-6.2.22.dist-info}/METADATA +9 -11
- {mcp_proxy_adapter-6.2.18.dist-info → mcp_proxy_adapter-6.2.22.dist-info}/RECORD +22 -20
- mcp_proxy_adapter-6.2.22.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-6.2.18.dist-info → mcp_proxy_adapter-6.2.22.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.2.18.dist-info → mcp_proxy_adapter-6.2.22.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-6.2.18.dist-info → mcp_proxy_adapter-6.2.22.dist-info}/top_level.txt +0 -0
@@ -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():
|
@@ -21,6 +21,7 @@ from typing import Dict, List, Optional, Any
|
|
21
21
|
from dataclasses import dataclass
|
22
22
|
import aiohttp
|
23
23
|
from aiohttp import ClientSession, ClientTimeout, TCPConnector
|
24
|
+
|
24
25
|
# Add project root to path for imports
|
25
26
|
project_root = Path(__file__).parent.parent.parent
|
26
27
|
current_dir = Path(__file__).parent
|
@@ -28,6 +29,26 @@ parent_dir = current_dir.parent
|
|
28
29
|
sys.path.insert(0, str(project_root))
|
29
30
|
sys.path.insert(0, str(current_dir))
|
30
31
|
sys.path.insert(0, str(parent_dir))
|
32
|
+
|
33
|
+
# Import mcp_security_framework components
|
34
|
+
try:
|
35
|
+
from mcp_security_framework import SSLManager, CertificateManager
|
36
|
+
from mcp_security_framework.schemas.config import SSLConfig
|
37
|
+
_MCP_SECURITY_AVAILABLE = True
|
38
|
+
print("✅ mcp_security_framework available")
|
39
|
+
except ImportError:
|
40
|
+
_MCP_SECURITY_AVAILABLE = False
|
41
|
+
print("⚠️ mcp_security_framework not available, falling back to standard SSL")
|
42
|
+
|
43
|
+
# Import cryptography components
|
44
|
+
try:
|
45
|
+
from cryptography import x509
|
46
|
+
from cryptography.hazmat.primitives import serialization
|
47
|
+
_CRYPTOGRAPHY_AVAILABLE = True
|
48
|
+
print("✅ cryptography available")
|
49
|
+
except ImportError:
|
50
|
+
_CRYPTOGRAPHY_AVAILABLE = False
|
51
|
+
print("⚠️ cryptography not available, SSL validation will be limited")
|
31
52
|
@dataclass
|
32
53
|
class TestResult:
|
33
54
|
"""Test result data class."""
|
@@ -45,11 +66,33 @@ class SecurityTestClient:
|
|
45
66
|
"""Initialize security test client."""
|
46
67
|
self.base_url = base_url
|
47
68
|
self.session: Optional[ClientSession] = None
|
48
|
-
|
49
|
-
#
|
50
|
-
self.security_manager = None
|
69
|
+
|
70
|
+
# Initialize security managers if available
|
51
71
|
self.ssl_manager = None
|
52
|
-
self.
|
72
|
+
self.cert_manager = None
|
73
|
+
self._security_available = _MCP_SECURITY_AVAILABLE
|
74
|
+
self._crypto_available = _CRYPTOGRAPHY_AVAILABLE
|
75
|
+
|
76
|
+
if self._security_available:
|
77
|
+
try:
|
78
|
+
# Initialize SSL manager with default config
|
79
|
+
ssl_config = SSLConfig(
|
80
|
+
enabled=True,
|
81
|
+
cert_file=None,
|
82
|
+
key_file=None,
|
83
|
+
ca_cert_file=None,
|
84
|
+
verify_mode="CERT_NONE", # For testing
|
85
|
+
min_tls_version="TLSv1.2"
|
86
|
+
)
|
87
|
+
self.ssl_manager = SSLManager(ssl_config)
|
88
|
+
print("✅ SSL Manager initialized with mcp_security_framework")
|
89
|
+
except Exception as e:
|
90
|
+
print(f"⚠️ Failed to initialize SSL Manager: {e}")
|
91
|
+
self._security_available = False
|
92
|
+
|
93
|
+
if not self._security_available:
|
94
|
+
print("ℹ️ Using standard SSL library for testing")
|
95
|
+
self.ssl_manager = None
|
53
96
|
self.test_results: List[TestResult] = []
|
54
97
|
# Test tokens
|
55
98
|
self.test_tokens = {
|
@@ -85,14 +128,33 @@ class SecurityTestClient:
|
|
85
128
|
return self
|
86
129
|
def create_ssl_context_for_mtls(self) -> ssl.SSLContext:
|
87
130
|
"""Create SSL context for mTLS connections."""
|
88
|
-
|
89
|
-
|
131
|
+
if self.ssl_manager and self._security_available:
|
132
|
+
try:
|
133
|
+
# Use mcp_security_framework for mTLS
|
134
|
+
cert_file = "./certs/user_cert.pem"
|
135
|
+
key_file = "./certs/user_key.pem"
|
136
|
+
ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
|
137
|
+
|
138
|
+
return self.ssl_manager.create_client_context(
|
139
|
+
ca_cert_file=ca_cert_file if os.path.exists(ca_cert_file) else None,
|
140
|
+
client_cert_file=cert_file if os.path.exists(cert_file) else None,
|
141
|
+
client_key_file=key_file if os.path.exists(key_file) else None,
|
142
|
+
verify_mode="CERT_NONE", # For testing
|
143
|
+
min_version="TLSv1.2"
|
144
|
+
)
|
145
|
+
except Exception as e:
|
146
|
+
print(f"⚠️ Failed to create mTLS context with mcp_security_framework: {e}")
|
147
|
+
print("ℹ️ Falling back to standard SSL")
|
148
|
+
|
149
|
+
# Fallback to standard SSL
|
150
|
+
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
151
|
+
# For mTLS testing - client needs to present certificate to server
|
90
152
|
ssl_context.check_hostname = False
|
91
|
-
ssl_context.verify_mode = ssl.CERT_NONE
|
92
|
-
# Load client certificate and key
|
153
|
+
ssl_context.verify_mode = ssl.CERT_NONE # Don't verify server cert for testing
|
154
|
+
# Load client certificate and key for mTLS
|
93
155
|
cert_file = "./certs/user_cert.pem"
|
94
156
|
key_file = "./certs/user_key.pem"
|
95
|
-
ca_cert_file = "./certs/
|
157
|
+
ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
|
96
158
|
if os.path.exists(cert_file) and os.path.exists(key_file):
|
97
159
|
ssl_context.load_cert_chain(certfile=cert_file, keyfile=key_file)
|
98
160
|
if os.path.exists(ca_cert_file):
|
@@ -106,6 +168,21 @@ class SecurityTestClient:
|
|
106
168
|
key_file: Optional[str] = None,
|
107
169
|
ca_cert_file: Optional[str] = None) -> ssl.SSLContext:
|
108
170
|
"""Create SSL context for client."""
|
171
|
+
if self.ssl_manager and self._security_available:
|
172
|
+
try:
|
173
|
+
# Use mcp_security_framework for SSL context creation
|
174
|
+
return self.ssl_manager.create_client_context(
|
175
|
+
ca_cert_file=ca_cert_file if ca_cert_file and os.path.exists(ca_cert_file) else None,
|
176
|
+
client_cert_file=cert_file if cert_file and os.path.exists(cert_file) else None,
|
177
|
+
client_key_file=key_file if key_file and os.path.exists(key_file) else None,
|
178
|
+
verify_mode="CERT_NONE", # For testing
|
179
|
+
min_version="TLSv1.2"
|
180
|
+
)
|
181
|
+
except Exception as e:
|
182
|
+
print(f"⚠️ Failed to create SSL context with mcp_security_framework: {e}")
|
183
|
+
print("ℹ️ Falling back to standard SSL")
|
184
|
+
|
185
|
+
# Fallback to standard SSL
|
109
186
|
ssl_context = ssl.create_default_context()
|
110
187
|
# For testing with self-signed certificates
|
111
188
|
ssl_context.check_hostname = False
|
@@ -121,8 +198,10 @@ class SecurityTestClient:
|
|
121
198
|
"""Create authentication headers."""
|
122
199
|
headers = {"Content-Type": "application/json"}
|
123
200
|
if auth_type == "api_key":
|
124
|
-
token = kwargs.get("token", "test-token-123")
|
125
|
-
|
201
|
+
token = kwargs.get("token", "test-token-123")
|
202
|
+
# Provide both common header styles to maximize compatibility
|
203
|
+
headers["X-API-Key"] = token
|
204
|
+
headers["Authorization"] = f"Bearer {token}"
|
126
205
|
elif auth_type == "basic":
|
127
206
|
username = kwargs.get("username", "admin")
|
128
207
|
password = kwargs.get("password", "password")
|
@@ -306,39 +385,104 @@ class SecurityTestClient:
|
|
306
385
|
start_time = time.time()
|
307
386
|
test_name = f"Negative Auth ({auth_type})"
|
308
387
|
try:
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
388
|
+
if auth_type == "certificate":
|
389
|
+
# For mTLS, test with invalid/expired certificate or no certificate
|
390
|
+
import aiohttp
|
391
|
+
from aiohttp import ClientTimeout, TCPConnector
|
392
|
+
import ssl
|
393
|
+
|
394
|
+
# Create SSL context with wrong certificate (should be rejected)
|
395
|
+
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
396
|
+
ssl_context.check_hostname = False
|
397
|
+
# Don't load any client certificate - this should cause rejection
|
398
|
+
# Load CA certificate for server verification
|
399
|
+
ca_cert_file = "./certs/mcp_proxy_adapter_ca_ca.crt"
|
400
|
+
if os.path.exists(ca_cert_file):
|
401
|
+
ssl_context.load_verify_locations(cafile=ca_cert_file)
|
402
|
+
ssl_context.verify_mode = ssl.CERT_NONE # Don't verify server cert for testing
|
403
|
+
|
404
|
+
connector = TCPConnector(ssl=ssl_context)
|
405
|
+
timeout = ClientTimeout(total=10) # Shorter timeout
|
406
|
+
|
407
|
+
try:
|
408
|
+
async with aiohttp.ClientSession(timeout=timeout, connector=connector) as temp_session:
|
409
|
+
data = {
|
410
|
+
"jsonrpc": "2.0",
|
411
|
+
"method": "echo",
|
412
|
+
"params": {"message": "Should fail without certificate"},
|
413
|
+
"id": 3
|
414
|
+
}
|
415
|
+
async with temp_session.post(f"{server_url}/cmd", json=data) as response:
|
416
|
+
duration = time.time() - start_time
|
417
|
+
# If we get here, the server accepted the connection without proper certificate
|
418
|
+
# This is actually a security issue - server should reject
|
419
|
+
return TestResult(
|
420
|
+
test_name=test_name,
|
421
|
+
server_url=server_url,
|
422
|
+
auth_type=auth_type,
|
423
|
+
success=False,
|
424
|
+
status_code=response.status,
|
425
|
+
error_message=f"SECURITY ISSUE: mTLS server accepted connection without client certificate (status: {response.status})",
|
426
|
+
duration=duration
|
427
|
+
)
|
428
|
+
except (aiohttp.ClientError, aiohttp.ServerDisconnectedError, asyncio.TimeoutError) as e:
|
429
|
+
# This is expected - server should reject connections without proper certificate
|
430
|
+
duration = time.time() - start_time
|
333
431
|
return TestResult(
|
334
432
|
test_name=test_name,
|
335
433
|
server_url=server_url,
|
336
434
|
auth_type=auth_type,
|
337
|
-
success=
|
338
|
-
status_code=
|
339
|
-
|
435
|
+
success=True,
|
436
|
+
status_code=0,
|
437
|
+
response_data={"expected": "connection_rejected", "error": str(e)},
|
340
438
|
duration=duration
|
341
439
|
)
|
440
|
+
else:
|
441
|
+
# For other auth types, use invalid token
|
442
|
+
headers = self.create_auth_headers("api_key", token="invalid-token-999")
|
443
|
+
data = {
|
444
|
+
"jsonrpc": "2.0",
|
445
|
+
"method": "echo",
|
446
|
+
"params": {"message": "Should fail"},
|
447
|
+
"id": 3
|
448
|
+
}
|
449
|
+
async with self.session.post(f"{server_url}/cmd",
|
450
|
+
headers=headers,
|
451
|
+
json=data) as response:
|
452
|
+
duration = time.time() - start_time
|
453
|
+
# Expect 401 only when auth is enforced
|
454
|
+
expects_auth = auth_type in ("api_key", "certificate", "basic")
|
455
|
+
if expects_auth and response.status == 401:
|
456
|
+
return TestResult(
|
457
|
+
test_name=test_name,
|
458
|
+
server_url=server_url,
|
459
|
+
auth_type=auth_type,
|
460
|
+
success=True,
|
461
|
+
status_code=response.status,
|
462
|
+
response_data={"expected": "authentication_failure"},
|
463
|
+
duration=duration
|
464
|
+
)
|
465
|
+
elif not expects_auth and response.status == 200:
|
466
|
+
# Security disabled: negative auth should not fail
|
467
|
+
return TestResult(
|
468
|
+
test_name=test_name,
|
469
|
+
server_url=server_url,
|
470
|
+
auth_type=auth_type,
|
471
|
+
success=True,
|
472
|
+
status_code=response.status,
|
473
|
+
response_data={"expected": "no_auth_required"},
|
474
|
+
duration=duration
|
475
|
+
)
|
476
|
+
else:
|
477
|
+
return TestResult(
|
478
|
+
test_name=test_name,
|
479
|
+
server_url=server_url,
|
480
|
+
auth_type=auth_type,
|
481
|
+
success=False,
|
482
|
+
status_code=response.status,
|
483
|
+
error_message=f"Unexpected status for negative auth: {response.status}",
|
484
|
+
duration=duration
|
485
|
+
)
|
342
486
|
except Exception as e:
|
343
487
|
duration = time.time() - start_time
|
344
488
|
return TestResult(
|
mcp_proxy_adapter/main.py
CHANGED
@@ -7,6 +7,7 @@ email: vasilyvz@gmail.com
|
|
7
7
|
"""
|
8
8
|
|
9
9
|
import sys
|
10
|
+
import ssl
|
10
11
|
import hypercorn.asyncio
|
11
12
|
import hypercorn.config
|
12
13
|
import asyncio
|
@@ -32,7 +33,10 @@ def main():
|
|
32
33
|
config = Config(config_path=args.config)
|
33
34
|
else:
|
34
35
|
config = Config()
|
35
|
-
|
36
|
+
|
37
|
+
print(f"DEBUG main.py: config type: {type(config)}")
|
38
|
+
print(f"DEBUG main.py: config.get_all() type: {type(config.get_all())}")
|
39
|
+
|
36
40
|
# Create application
|
37
41
|
app = create_app(app_config=config)
|
38
42
|
|
@@ -70,8 +74,14 @@ def main():
|
|
70
74
|
config_hypercorn.ca_certs = ssl_ca_cert
|
71
75
|
|
72
76
|
if verify_client:
|
73
|
-
|
77
|
+
# For mTLS, require client certificates
|
78
|
+
config_hypercorn.set_cert_reqs(ssl.CERT_REQUIRED)
|
74
79
|
config_hypercorn.verify_mode = ssl.CERT_REQUIRED
|
80
|
+
print("🔐 mTLS: Client certificate verification enabled")
|
81
|
+
else:
|
82
|
+
# For regular HTTPS without client verification
|
83
|
+
config_hypercorn.set_cert_reqs(ssl.CERT_NONE)
|
84
|
+
config_hypercorn.verify_mode = ssl.CERT_NONE
|
75
85
|
|
76
86
|
print(f"🔐 Starting HTTPS server with hypercorn...")
|
77
87
|
else:
|