mcp-proxy-adapter 6.4.43__py3-none-any.whl → 6.4.44__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.
@@ -1,677 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Security Test Runner for MCP Proxy Adapter
4
- This script runs comprehensive security tests against all server configurations:
5
- - Basic HTTP
6
- - HTTP + Token authentication
7
- - HTTPS
8
- - HTTPS + Token authentication
9
- - mTLS
10
- Author: Vasiliy Zdanovskiy
11
- email: vasilyvz@gmail.com
12
- """
13
- import asyncio
14
- import json
15
- import os
16
- import signal
17
- import socket
18
- import subprocess
19
- import sys
20
- import time
21
- from pathlib import Path
22
- from typing import Dict, List, Optional, Any, Tuple
23
-
24
- import psutil
25
- import requests
26
-
27
- # Import security test client with proper module path
28
- try:
29
- from mcp_proxy_adapter.examples.security_test_client import (
30
- SecurityTestClient,
31
- TestResult,
32
- )
33
- except ImportError:
34
- # Fallback to local import if package import fails
35
- current_dir = Path(__file__).parent
36
- sys.path.insert(0, str(current_dir))
37
- from security_test_client import (
38
- SecurityTestClient,
39
- TestResult,
40
- )
41
-
42
-
43
- class SecurityTestRunner:
44
- """Main test runner for security testing."""
45
-
46
- def __init__(self):
47
- """Initialize test runner."""
48
- self.servers = {}
49
- self.proxy_server = None
50
- self.server_logs = {}
51
- self.proxy_log = None
52
- self.test_results = {}
53
- # Base and proxy ports - each test gets its own port range
54
- self.base_port = 20020
55
- self.proxy_port = 20010
56
- # Server configurations with SEPARATE ports for each test
57
- self.configs = {
58
- "basic_http": {
59
- "config": "configs/http_simple.json",
60
- "port": 20020, # Dedicated port
61
- "url": "http://127.0.0.1:20020",
62
- "auth": "none",
63
- },
64
- "http_token": {
65
- "config": "configs/http_token.json",
66
- "port": 20021, # Dedicated port
67
- "url": "http://127.0.0.1:20021",
68
- "auth": "api_key",
69
- },
70
- "https": {
71
- "config": "configs/https_simple.json",
72
- "port": 20022, # Dedicated port
73
- "url": "https://127.0.0.1:20022",
74
- "auth": "none",
75
- },
76
- "https_token": {
77
- "config": "configs/https_token.json",
78
- "port": 20023, # Dedicated port
79
- "url": "https://127.0.0.1:20023",
80
- "auth": "api_key",
81
- },
82
- "mtls": {
83
- "config": "configs/mtls_no_roles.json",
84
- "port": 20024, # Dedicated port
85
- "url": "https://127.0.0.1:20024",
86
- "auth": "certificate",
87
- },
88
- }
89
-
90
- def _port_in_use(self, port: int, host: str = "127.0.0.1") -> bool:
91
- try:
92
- with socket.create_connection((host, port), timeout=0.5):
93
- return True
94
- except Exception:
95
- return False
96
-
97
- def _pids_on_port(self, port: int) -> List[int]:
98
- pids: List[int] = []
99
- try:
100
- for proc in psutil.process_iter(attrs=["pid"]):
101
- try:
102
- connections = proc.connections(kind="inet")
103
- for c in connections:
104
- if c.laddr and c.laddr.port == port:
105
- pids.append(proc.pid)
106
- break
107
- except (psutil.NoSuchProcess, psutil.AccessDenied):
108
- pass
109
- except Exception:
110
- pass
111
- return list(set(pids))
112
-
113
- def ensure_ports_free(self, ports: List[int]) -> None:
114
- for port in ports:
115
- pids = self._pids_on_port(port)
116
- for pid in pids:
117
- try:
118
- psutil.Process(pid).terminate()
119
- except Exception:
120
- pass
121
- time.sleep(0.3)
122
- for pid in pids:
123
- try:
124
- if psutil.pid_exists(pid):
125
- psutil.Process(pid).kill()
126
- except Exception:
127
- pass
128
-
129
- def wait_for_http(self, url: str, timeout_sec: float = 8.0) -> bool:
130
- end = time.time() + timeout_sec
131
- candidates = ["/health", "/proxy/health"]
132
- while time.time() < end:
133
- for path in candidates:
134
- health_url = url.rstrip("/") + path
135
- try:
136
- resp = requests.get(health_url, timeout=1.0, verify=False)
137
- if resp.status_code == 200:
138
- return True
139
- except Exception:
140
- pass
141
- time.sleep(0.2)
142
- return False
143
-
144
- def wait_for_port(self, port: int, timeout_sec: float = 8.0) -> bool:
145
- end = time.time() + timeout_sec
146
- while time.time() < end:
147
- if self._port_in_use(port):
148
- return True
149
- time.sleep(0.2)
150
- return False
151
-
152
- def get_all_ports(self) -> List[int]:
153
- ports = [self.proxy_port]
154
- for cfg in self.configs.values():
155
- ports.append(cfg["port"])
156
- return list(sorted(set(ports)))
157
-
158
- def check_ports_available(self, ports: List[int]) -> Tuple[bool, List[int]]:
159
- """
160
- Check if all ports in the list are available.
161
- Returns (True, []) if all ports are free, (False, occupied_ports) otherwise.
162
- """
163
- occupied_ports = []
164
- for port in ports:
165
- if self._port_in_use(port):
166
- occupied_ports.append(port)
167
- return len(occupied_ports) == 0, occupied_ports
168
-
169
- def _validate_file(self, base: Path, path_value: Optional[str]) -> Tuple[bool, str]:
170
- if not path_value:
171
- return True, ""
172
- p = Path(path_value)
173
- if not p.is_absolute():
174
- p = base / p
175
- return p.exists(), str(p)
176
-
177
- def validate_config_files(self) -> bool:
178
- ok = True
179
- base = Path.cwd()
180
- missing: List[str] = []
181
- for name, cfg in self.configs.items():
182
- cfg_path = Path(cfg["config"]).resolve()
183
- try:
184
- with open(cfg_path, "r", encoding="utf-8") as f:
185
- data = json.load(f)
186
- ssl = data.get("ssl", {})
187
- for key in ("cert_file", "key_file", "ca_cert"):
188
- exists, abs_path = self._validate_file(base, ssl.get(key))
189
- if (
190
- ssl.get("enabled")
191
- and key in ("cert_file", "key_file")
192
- and not exists
193
- ):
194
- ok = False
195
- missing.append(f"{name}:{key} -> {abs_path}")
196
- sec = data.get("security", {})
197
- perms = sec.get("permissions", {})
198
- exists, abs_path = self._validate_file(base, perms.get("roles_file"))
199
- if sec.get("enabled") and perms.get("enabled") and not exists:
200
- ok = False
201
- missing.append(f"{name}:roles_file -> {abs_path}")
202
- except Exception as e:
203
- ok = False
204
- missing.append(f"{name}: cannot read {cfg_path} ({e})")
205
- if not ok:
206
- print("❌ CONFIG VALIDATION FAILED. Missing files:")
207
- for m in missing:
208
- print(" -", m)
209
- else:
210
- print("✅ Configuration file paths validated")
211
- return ok
212
-
213
- def check_prerequisites(self) -> bool:
214
- """Check if all prerequisites are met."""
215
- print("🔍 Checking prerequisites...")
216
- # Check if we're in the right directory
217
- if not Path("configs").exists():
218
- print(
219
- "❌ configs directory not found. Please run from the test environment root directory."
220
- )
221
- return False
222
- # Check if certificates exist
223
- cert_files = [
224
- "certs/mcp_proxy_adapter_ca_ca.crt",
225
- "certs/localhost_server.crt",
226
- "keys/localhost_server.key",
227
- ]
228
-
229
- missing_certs = []
230
- # Check if roles.json exists
231
- roles_file = "configs/roles.json"
232
- if not os.path.exists(roles_file):
233
- missing_certs.append(f"Missing roles file: {roles_file}")
234
- for cert_file in cert_files:
235
- if not Path(cert_file).exists():
236
- missing_certs.append(cert_file)
237
- if missing_certs:
238
- print(f"❌ Missing certificates: {missing_certs}")
239
- print(
240
- "💡 Run: python -m mcp_proxy_adapter.examples.setup_test_environment to generate certificates"
241
- )
242
- return False
243
- print("✅ Prerequisites check passed")
244
- return True
245
-
246
- def start_server(
247
- self, name: str, config_path: str, port: int
248
- ) -> Optional[subprocess.Popen]:
249
- """Start a server in background."""
250
- try:
251
- print(f"🚀 Starting {name} server on port {port}...")
252
-
253
- # Always ensure port is free before starting server
254
- if self._port_in_use(port):
255
- print(f"🧹 Port {port} is in use, freeing it...")
256
- self.ensure_ports_free([port])
257
- time.sleep(1) # Give time for port to be freed
258
-
259
- # Check again after freeing
260
- if self._port_in_use(port):
261
- print(
262
- f"❌ Port {port} still in use after cleanup, cannot start {name}"
263
- )
264
- return None
265
-
266
- # Start server in background
267
- logs_dir = Path("logs")
268
- logs_dir.mkdir(exist_ok=True)
269
- log_path = logs_dir / f"{name}.log"
270
- log_file = open(log_path, "wb")
271
- self.server_logs[name] = log_file
272
- process = subprocess.Popen(
273
- [
274
- sys.executable,
275
- "-m",
276
- "mcp_proxy_adapter.main",
277
- "--config",
278
- config_path,
279
- ],
280
- stdout=log_file,
281
- stderr=subprocess.STDOUT,
282
- )
283
- # Wait a bit for server to start
284
- time.sleep(3)
285
- # Check if process is still running
286
- if process.poll() is None:
287
- print(f"✅ {name} server started (PID: {process.pid})")
288
- return process
289
- else:
290
- print(f"❌ Failed to start {name} server (see logs/{name}.log)")
291
- return None
292
- except Exception as e:
293
- print(f"❌ Error starting {name} server: {e}")
294
- return None
295
-
296
- def stop_server(self, name: str, process: subprocess.Popen):
297
- """Stop a server."""
298
- try:
299
- print(f"🛑 Stopping {name} server (PID: {process.pid})...")
300
- process.terminate()
301
- # Wait for graceful shutdown
302
- try:
303
- process.wait(timeout=5)
304
- print(f"✅ {name} server stopped")
305
- except subprocess.TimeoutExpired:
306
- print(f"⚠️ Force killing {name} server")
307
- process.kill()
308
- process.wait()
309
- except Exception as e:
310
- print(f"❌ Error stopping {name} server: {e}")
311
- finally:
312
- try:
313
- lf = self.server_logs.pop(name, None)
314
- if lf:
315
- lf.close()
316
- except Exception:
317
- pass
318
-
319
- def start_proxy_server(self) -> bool:
320
- """Start the proxy server for server registration."""
321
- try:
322
- print("🚀 Starting proxy server...")
323
-
324
- # Ensure proxy port is free
325
- if self._port_in_use(self.proxy_port):
326
- print(f"🧹 Proxy port {self.proxy_port} is in use, freeing it...")
327
- self.ensure_ports_free([self.proxy_port])
328
- time.sleep(1)
329
-
330
- if self._port_in_use(self.proxy_port):
331
- print(f"❌ Proxy port {self.proxy_port} still in use after cleanup")
332
- return False
333
-
334
- # Find the proxy server script
335
- proxy_script = Path(__file__).parent / "run_proxy_server.py"
336
- if not proxy_script.exists():
337
- # Try alternative path
338
- proxy_script = Path.cwd() / "run_proxy_server.py"
339
- if not proxy_script.exists():
340
- print("❌ Proxy server script not found")
341
- return False
342
-
343
- # Start proxy server
344
- cmd = [
345
- sys.executable,
346
- str(proxy_script),
347
- "--host",
348
- "127.0.0.1",
349
- "--port",
350
- str(self.proxy_port),
351
- ]
352
- logs_dir = Path("logs")
353
- logs_dir.mkdir(exist_ok=True)
354
- proxy_log_path = logs_dir / "proxy_server.log"
355
- self.proxy_log = open(proxy_log_path, "wb")
356
- process = subprocess.Popen(
357
- cmd, stdout=self.proxy_log, stderr=subprocess.STDOUT, cwd=Path.cwd()
358
- )
359
-
360
- # Check readiness
361
- if process.poll() is None and self.wait_for_http(
362
- f"http://127.0.0.1:{self.proxy_port}"
363
- ):
364
- self.proxy_server = process
365
- print(
366
- "✅ Proxy server started successfully (PID: {})".format(process.pid)
367
- )
368
- return True
369
- else:
370
- print("❌ Failed to start proxy server (see logs/proxy_server.log)")
371
- return False
372
-
373
- except Exception as e:
374
- print(f"❌ Error starting proxy server: {e}")
375
- return False
376
-
377
- def stop_proxy_server(self):
378
- """Stop the proxy server."""
379
- if self.proxy_server:
380
- try:
381
- print(
382
- "🛑 Stopping proxy server (PID: {})...".format(
383
- self.proxy_server.pid
384
- )
385
- )
386
- self.proxy_server.terminate()
387
- try:
388
- self.proxy_server.wait(timeout=5)
389
- print("✅ Proxy server stopped")
390
- except subprocess.TimeoutExpired:
391
- print("⚠️ Force killing proxy server")
392
- self.proxy_server.kill()
393
- self.proxy_server.wait()
394
- except Exception as e:
395
- print(f"❌ Error stopping proxy server: {e}")
396
- finally:
397
- self.proxy_server = None
398
- try:
399
- if self.proxy_log:
400
- self.proxy_log.close()
401
- self.proxy_log = None
402
- except Exception:
403
- pass
404
-
405
- async def test_server(self, name: str, config: Dict[str, Any]) -> List[TestResult]:
406
- """Test a specific server configuration."""
407
- print(f"\n🧪 Testing {name} server...")
408
- print("=" * 50)
409
- # Create client with appropriate SSL context
410
- if config["auth"] == "certificate":
411
- # For mTLS, create client with certificate-based SSL context
412
- client = SecurityTestClient(config["url"])
413
- # Override SSL context for mTLS
414
- client.create_ssl_context = client.create_ssl_context_for_mtls
415
- async with client as client_session:
416
- # Pass correct token for api_key authentication
417
- if config["auth"] == "api_key":
418
- results = await client_session.run_security_tests(
419
- config["url"], config["auth"], token="admin-secret-key"
420
- )
421
- else:
422
- results = await client_session.run_security_tests(
423
- config["url"], config["auth"]
424
- )
425
- else:
426
- # For other auth types, use default SSL context
427
- async with SecurityTestClient(config["url"]) as client:
428
- # Pass correct token for api_key authentication
429
- if config["auth"] == "api_key":
430
- results = await client.run_security_tests(
431
- config["url"], config["auth"], token="admin-secret-key"
432
- )
433
- else:
434
- results = await client.run_security_tests(
435
- config["url"], config["auth"]
436
- )
437
- # Print summary for this server
438
- passed = sum(1 for r in results if r.success)
439
- total = len(results)
440
- print(f"\n📊 {name} Results: {passed}/{total} tests passed")
441
- return results
442
-
443
- async def run_all_tests(self) -> Dict[str, List[TestResult]]:
444
- """Run tests against all server configurations."""
445
- print("🚀 Starting comprehensive security testing")
446
- print("=" * 60)
447
- # Start all servers with verification and abort on failure
448
- for name, config in self.configs.items():
449
- process = self.start_server(name, config["config"], config["port"])
450
- if not process:
451
- print(f"❌ {name} failed to start. Aborting.")
452
- return {}
453
- url = config["url"]
454
- ready = False
455
- if name == "mtls":
456
- ready = self.wait_for_port(config["port"], timeout_sec=8.0)
457
- else:
458
- ready = self.wait_for_http(url, timeout_sec=8.0)
459
- if not ready:
460
- print(f"❌ {name} did not become ready. Aborting.")
461
- return {}
462
- self.servers[name] = process
463
- print("\n✅ All servers started and verified. Proceeding to client tests...")
464
- # Test each server
465
- all_results = {}
466
- for name, config in self.configs.items():
467
- if name in self.servers:
468
- try:
469
- results = await self.test_server(name, config)
470
- all_results[name] = results
471
- except Exception as e:
472
- print(f"❌ Error testing {name}: {e}")
473
- all_results[name] = []
474
- else:
475
- print(f"⚠️ Skipping {name} tests (server not running)")
476
- all_results[name] = []
477
- return all_results
478
-
479
- def print_final_summary(self, all_results: Dict[str, List[TestResult]]):
480
- """Print final test summary."""
481
- print("\n" + "=" * 80)
482
- print("📊 FINAL SECURITY TEST SUMMARY")
483
- print("=" * 80)
484
- total_tests = 0
485
- total_passed = 0
486
- for server_name, results in all_results.items():
487
- if results:
488
- passed = sum(1 for r in results if r.success)
489
- total = len(results)
490
- total_tests += total
491
- total_passed += passed
492
- status = "✅ PASS" if passed == total else "❌ FAIL"
493
- print(f"{status} {server_name.upper()}: {passed}/{total} tests passed")
494
- # Show failed tests
495
- failed_tests = [r for r in results if not r.success]
496
- for test in failed_tests:
497
- print(f" ❌ {test.test_name}: {test.error_message}")
498
- else:
499
- print(f"⚠️ SKIP {server_name.upper()}: No tests run")
500
- print("\n" + "-" * 80)
501
- print(f"OVERALL: {total_passed}/{total_tests} tests passed")
502
- if total_tests > 0:
503
- success_rate = (total_passed / total_tests) * 100
504
- print(f"SUCCESS RATE: {success_rate:.1f}%")
505
- # Overall status
506
- if total_passed == total_tests and total_tests > 0:
507
- print("🎉 ALL TESTS PASSED!")
508
- print("\n" + "=" * 60)
509
- print("✅ SECURITY TESTS COMPLETED SUCCESSFULLY")
510
- print("=" * 60)
511
- print("\n📋 NEXT STEPS:")
512
- print("1. Start basic framework example:")
513
- print(
514
- " python -m mcp_proxy_adapter.examples.basic_framework.main --config configs/https_simple.json"
515
- )
516
- print("\n2. Start full application example:")
517
- print(
518
- " python -m mcp_proxy_adapter.examples.full_application.main --config configs/mtls_with_roles.json"
519
- )
520
- print("\n3. Test with custom configurations:")
521
- print(
522
- " python -m mcp_proxy_adapter.examples.basic_framework.main --config configs/http_simple.json"
523
- )
524
- print("=" * 60)
525
- elif total_passed > 0:
526
- print("⚠️ SOME TESTS FAILED")
527
- print("\n🔧 TROUBLESHOOTING:")
528
- print("1. Check if proxy server is running:")
529
- print(" python /path/to/run_proxy_server.py --host 127.0.0.1 --port 3004")
530
- print("\n2. Check if certificates are generated:")
531
- print(" python -m mcp_proxy_adapter.examples.generate_certificates")
532
- print("\n3. Verify configuration files exist:")
533
- print(
534
- " python -m mcp_proxy_adapter.examples.generate_test_configs --output-dir configs"
535
- )
536
- print("\n4. Check if ports are available (3004, 8000-8005)")
537
- print("=" * 60)
538
- else:
539
- print("❌ ALL TESTS FAILED")
540
- print("\n🔧 TROUBLESHOOTING:")
541
- print("1. Run setup test environment:")
542
- print(" python -m mcp_proxy_adapter.examples.setup_test_environment")
543
- print("\n2. Generate certificates:")
544
- print(" python -m mcp_proxy_adapter.examples.generate_certificates")
545
- print("\n3. Generate configurations:")
546
- print(
547
- " python -m mcp_proxy_adapter.examples.generate_test_configs --output-dir configs"
548
- )
549
- print("\n4. Start proxy server manually if needed:")
550
- print(" python /path/to/run_proxy_server.py --host 127.0.0.1 --port 3004")
551
- print("=" * 60)
552
-
553
- def cleanup(self):
554
- """Cleanup all running servers and proxy."""
555
- print("\n🧹 Cleaning up...")
556
- # Stop test servers
557
- for name, process in self.servers.items():
558
- self.stop_server(name, process)
559
- self.servers.clear()
560
- # Stop proxy server
561
- self.stop_proxy_server()
562
-
563
- def signal_handler(self, signum, frame):
564
- """Handle interrupt signals."""
565
- print(f"\n⚠️ Received signal {signum}, cleaning up...")
566
- self.cleanup()
567
- sys.exit(0)
568
-
569
- async def run(self):
570
- """Main run method."""
571
- # Set up signal handlers
572
- signal.signal(signal.SIGINT, self.signal_handler)
573
- signal.signal(signal.SIGTERM, self.signal_handler)
574
- try:
575
- # FIRST: Check ALL ports at the very beginning
576
- print("\n🔍 STEP 1: Complete Port Availability Check")
577
- all_ports = self.get_all_ports()
578
- print(f"📋 Required ports: {all_ports}")
579
-
580
- # Check if ALL ports are available
581
- ports_available, occupied_ports = self.check_ports_available(all_ports)
582
- if not ports_available:
583
- print(f"❌ CRITICAL: Ports are occupied: {occupied_ports}")
584
- print("🧹 Attempting to free occupied ports...")
585
-
586
- if not self.ensure_ports_free(all_ports):
587
- print("❌ FAILED: Could not free occupied ports. Aborting tests.")
588
- print("💡 Manual cleanup required:")
589
- for port in occupied_ports:
590
- pids = self._pids_on_port(port)
591
- if pids:
592
- print(f" Port {port}: PIDs {pids}")
593
- return False
594
- else:
595
- print("✅ Ports freed successfully")
596
- else:
597
- print("✅ All required ports are available")
598
-
599
- # Check prerequisites
600
- if not self.check_prerequisites():
601
- return False
602
-
603
- # Validate config file paths
604
- if not self.validate_config_files():
605
- return False
606
-
607
- # Start proxy server first
608
- print("\n🚀 Starting proxy server for server registration...")
609
- if not self.start_proxy_server():
610
- print("❌ Cannot proceed without proxy server")
611
- return False
612
-
613
- # Wait for proxy server to be fully ready
614
- print("⏳ Waiting for proxy server to be ready...")
615
- time.sleep(3)
616
-
617
- # Run all tests
618
- all_results = await self.run_all_tests()
619
- # Print summary
620
- self.print_final_summary(all_results)
621
- return True
622
- except Exception as e:
623
- print(f"❌ Test runner error: {e}")
624
- return False
625
- finally:
626
- # Always cleanup
627
- self.cleanup()
628
-
629
-
630
- def main():
631
- """Main function."""
632
- import argparse
633
-
634
- parser = argparse.ArgumentParser(
635
- description="Security Test Runner for MCP Proxy Adapter"
636
- )
637
- parser.add_argument("--config", help="Test specific configuration")
638
- parser.add_argument(
639
- "--no-cleanup", action="store_true", help="Don't cleanup servers after tests"
640
- )
641
- parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
642
- parser.parse_args()
643
-
644
- # Determine the correct configs directory
645
- current_dir = Path.cwd()
646
- if (current_dir / "configs").exists():
647
- # We're in the test environment root directory
648
- configs_dir = current_dir / "configs"
649
- os.chdir(current_dir) # Stay in current directory
650
- elif (Path(__file__).parent.parent / "configs").exists():
651
- # We're running from package installation, configs is relative to examples
652
- configs_dir = Path(__file__).parent.parent / "configs"
653
- os.chdir(Path(__file__).parent.parent) # Change to parent of examples
654
- else:
655
- # Try to find configs relative to examples directory
656
- examples_dir = Path(__file__).parent
657
- configs_dir = examples_dir / "configs"
658
- os.chdir(examples_dir)
659
-
660
- print(f"🔍 Using configs directory: {configs_dir}")
661
- print(f"🔍 Working directory: {Path.cwd()}")
662
-
663
- # Create and run test runner
664
- runner = SecurityTestRunner()
665
- try:
666
- success = asyncio.run(runner.run())
667
- sys.exit(0 if success else 1)
668
- except KeyboardInterrupt:
669
- print("\n⚠️ Interrupted by user")
670
- sys.exit(1)
671
- except Exception as e:
672
- print(f"❌ Unexpected error: {e}")
673
- sys.exit(1)
674
-
675
-
676
- if __name__ == "__main__":
677
- main()