embed-client 2.0.0.0__py3-none-any.whl → 3.1.0.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.
- embed_client/async_client.py +376 -16
- embed_client/auth.py +491 -0
- embed_client/auth_examples.py +248 -0
- embed_client/client_factory.py +396 -0
- embed_client/client_factory_examples.py +353 -0
- embed_client/config.py +592 -0
- embed_client/config_examples.py +197 -0
- embed_client/example_async_usage.py +578 -90
- embed_client/example_async_usage_ru.py +536 -102
- embed_client/ssl_examples.py +329 -0
- embed_client/ssl_manager.py +475 -0
- embed_client-3.1.0.1.dist-info/METADATA +256 -0
- embed_client-3.1.0.1.dist-info/RECORD +17 -0
- embed_client-3.1.0.1.dist-info/licenses/LICENSE +21 -0
- embed_client-2.0.0.0.dist-info/METADATA +0 -9
- embed_client-2.0.0.0.dist-info/RECORD +0 -8
- {embed_client-2.0.0.0.dist-info → embed_client-3.1.0.1.dist-info}/WHEEL +0 -0
- {embed_client-2.0.0.0.dist-info → embed_client-3.1.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,475 @@
|
|
1
|
+
"""
|
2
|
+
SSL/TLS Manager for embed-client.
|
3
|
+
|
4
|
+
This module provides SSL/TLS management for the embed-client, supporting
|
5
|
+
all security modes including HTTP, HTTPS, and mTLS. It integrates with
|
6
|
+
mcp_security_framework when available and provides fallback implementations.
|
7
|
+
|
8
|
+
Author: Vasiliy Zdanovskiy
|
9
|
+
email: vasilyvz@gmail.com
|
10
|
+
"""
|
11
|
+
|
12
|
+
import logging
|
13
|
+
import ssl
|
14
|
+
import os
|
15
|
+
from pathlib import Path
|
16
|
+
from typing import Any, Dict, List, Optional, Union
|
17
|
+
|
18
|
+
# Try to import mcp_security_framework components
|
19
|
+
try:
|
20
|
+
from mcp_security_framework import (
|
21
|
+
SSLConfig,
|
22
|
+
CertificateInfo,
|
23
|
+
SecurityManager
|
24
|
+
)
|
25
|
+
SECURITY_FRAMEWORK_AVAILABLE = True
|
26
|
+
except ImportError:
|
27
|
+
SECURITY_FRAMEWORK_AVAILABLE = False
|
28
|
+
print("Warning: mcp_security_framework not available. Using fallback SSL/TLS management.")
|
29
|
+
|
30
|
+
# Fallback certificate handling
|
31
|
+
try:
|
32
|
+
from cryptography import x509
|
33
|
+
from cryptography.hazmat.primitives import serialization
|
34
|
+
CRYPTOGRAPHY_AVAILABLE = True
|
35
|
+
except ImportError:
|
36
|
+
CRYPTOGRAPHY_AVAILABLE = False
|
37
|
+
print("Warning: cryptography not available. Limited SSL/TLS functionality.")
|
38
|
+
|
39
|
+
|
40
|
+
class SSLManagerError(Exception):
|
41
|
+
"""Raised when SSL/TLS operations fail."""
|
42
|
+
|
43
|
+
def __init__(self, message: str, error_code: int = -32002):
|
44
|
+
self.message = message
|
45
|
+
self.error_code = error_code
|
46
|
+
super().__init__(self.message)
|
47
|
+
|
48
|
+
|
49
|
+
class ClientSSLManager:
|
50
|
+
"""
|
51
|
+
Client SSL/TLS Manager.
|
52
|
+
|
53
|
+
This class provides SSL/TLS management for the embed-client,
|
54
|
+
supporting all security modes with integration to
|
55
|
+
mcp_security_framework when available.
|
56
|
+
"""
|
57
|
+
|
58
|
+
def __init__(self, config: Dict[str, Any]):
|
59
|
+
"""
|
60
|
+
Initialize SSL/TLS manager.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
config: SSL/TLS configuration dictionary
|
64
|
+
"""
|
65
|
+
self.config = config
|
66
|
+
self.logger = logging.getLogger(__name__)
|
67
|
+
|
68
|
+
# Initialize security framework components if available
|
69
|
+
self.ssl_manager = None
|
70
|
+
self.cert_manager = None
|
71
|
+
|
72
|
+
if SECURITY_FRAMEWORK_AVAILABLE:
|
73
|
+
self._initialize_security_framework()
|
74
|
+
else:
|
75
|
+
self.logger.warning("mcp_security_framework not available, using fallback SSL/TLS management")
|
76
|
+
|
77
|
+
def _initialize_security_framework(self) -> None:
|
78
|
+
"""Initialize mcp_security_framework components."""
|
79
|
+
try:
|
80
|
+
# Create SSL config
|
81
|
+
ssl_config_dict = self.config.get("ssl", {})
|
82
|
+
ssl_config = SSLConfig(
|
83
|
+
enabled=ssl_config_dict.get("enabled", False),
|
84
|
+
cert_file=ssl_config_dict.get("cert_file") if ssl_config_dict.get("cert_file") and os.path.exists(ssl_config_dict.get("cert_file")) else None,
|
85
|
+
key_file=ssl_config_dict.get("key_file") if ssl_config_dict.get("key_file") and os.path.exists(ssl_config_dict.get("key_file")) else None,
|
86
|
+
ca_cert_file=ssl_config_dict.get("ca_cert_file") if ssl_config_dict.get("ca_cert_file") and os.path.exists(ssl_config_dict.get("ca_cert_file")) else None,
|
87
|
+
verify_mode=ssl_config_dict.get("verify_mode", "CERT_REQUIRED"),
|
88
|
+
check_hostname=ssl_config_dict.get("check_hostname", True),
|
89
|
+
check_expiry=ssl_config_dict.get("check_expiry", True)
|
90
|
+
)
|
91
|
+
|
92
|
+
# Create permission config (required by SecurityManager)
|
93
|
+
from mcp_security_framework import PermissionConfig
|
94
|
+
permission_config = PermissionConfig(
|
95
|
+
enabled=False,
|
96
|
+
roles_file="configs/roles.json"
|
97
|
+
)
|
98
|
+
|
99
|
+
# Create security config
|
100
|
+
from mcp_security_framework import SecurityConfig
|
101
|
+
security_config = SecurityConfig(
|
102
|
+
ssl=ssl_config,
|
103
|
+
permissions=permission_config
|
104
|
+
)
|
105
|
+
|
106
|
+
# Initialize managers
|
107
|
+
self.ssl_manager = SecurityManager(security_config)
|
108
|
+
|
109
|
+
self.logger.info("Security framework SSL/TLS components initialized successfully")
|
110
|
+
|
111
|
+
except Exception as e:
|
112
|
+
self.logger.warning(f"Failed to initialize security framework SSL/TLS: {e}")
|
113
|
+
self.ssl_manager = None
|
114
|
+
self.cert_manager = None
|
115
|
+
|
116
|
+
def create_client_ssl_context(self) -> Optional[ssl.SSLContext]:
|
117
|
+
"""
|
118
|
+
Create SSL context for client connections.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
SSL context for client connections or None if SSL is disabled
|
122
|
+
"""
|
123
|
+
# Check if SSL is enabled first
|
124
|
+
if not self.is_ssl_enabled():
|
125
|
+
return None
|
126
|
+
|
127
|
+
try:
|
128
|
+
# Force fallback implementation for CERT_NONE mode
|
129
|
+
ssl_config = self.config.get("ssl", {})
|
130
|
+
if ssl_config.get("verify_mode") == "CERT_NONE":
|
131
|
+
return self._create_client_ssl_context_fallback()
|
132
|
+
elif self.ssl_manager:
|
133
|
+
# Use security framework
|
134
|
+
return self.ssl_manager.create_client_context()
|
135
|
+
else:
|
136
|
+
# Fallback implementation
|
137
|
+
return self._create_client_ssl_context_fallback()
|
138
|
+
|
139
|
+
except Exception as e:
|
140
|
+
self.logger.error(f"Failed to create client SSL context: {e}")
|
141
|
+
raise SSLManagerError(f"Failed to create client SSL context: {e}")
|
142
|
+
|
143
|
+
def _create_client_ssl_context_fallback(self) -> Optional[ssl.SSLContext]:
|
144
|
+
"""Fallback SSL context creation."""
|
145
|
+
ssl_config = self.config.get("ssl", {})
|
146
|
+
|
147
|
+
if not ssl_config.get("enabled", False):
|
148
|
+
return None
|
149
|
+
|
150
|
+
try:
|
151
|
+
# Configure verification
|
152
|
+
verify_mode = ssl_config.get("verify_mode", "CERT_REQUIRED")
|
153
|
+
check_hostname = ssl_config.get("check_hostname", True)
|
154
|
+
|
155
|
+
# Force check_hostname=False for CERT_NONE mode
|
156
|
+
if verify_mode == "CERT_NONE":
|
157
|
+
check_hostname = False
|
158
|
+
|
159
|
+
# Create SSL context based on verification mode
|
160
|
+
if verify_mode == "CERT_NONE":
|
161
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
162
|
+
context.check_hostname = False
|
163
|
+
context.verify_mode = ssl.CERT_NONE
|
164
|
+
else:
|
165
|
+
context = ssl.create_default_context()
|
166
|
+
context.check_hostname = check_hostname
|
167
|
+
if verify_mode == "CERT_OPTIONAL":
|
168
|
+
context.verify_mode = ssl.CERT_OPTIONAL
|
169
|
+
else: # CERT_REQUIRED
|
170
|
+
context.verify_mode = ssl.CERT_REQUIRED
|
171
|
+
|
172
|
+
# Load CA certificate if provided
|
173
|
+
ca_cert_file = ssl_config.get("ca_cert_file")
|
174
|
+
if ca_cert_file and os.path.exists(ca_cert_file):
|
175
|
+
context.load_verify_locations(ca_cert_file)
|
176
|
+
|
177
|
+
# Load client certificate if provided (for mTLS)
|
178
|
+
cert_file = ssl_config.get("cert_file")
|
179
|
+
key_file = ssl_config.get("key_file")
|
180
|
+
if cert_file and key_file and os.path.exists(cert_file) and os.path.exists(key_file):
|
181
|
+
context.load_cert_chain(cert_file, key_file)
|
182
|
+
|
183
|
+
return context
|
184
|
+
|
185
|
+
except Exception as e:
|
186
|
+
raise SSLManagerError(f"Failed to create SSL context: {e}")
|
187
|
+
|
188
|
+
def create_connector(self) -> Optional[Any]:
|
189
|
+
"""
|
190
|
+
Create aiohttp connector with SSL context.
|
191
|
+
|
192
|
+
Returns:
|
193
|
+
aiohttp connector with SSL context or None if SSL is disabled
|
194
|
+
"""
|
195
|
+
try:
|
196
|
+
import aiohttp
|
197
|
+
|
198
|
+
ssl_context = self.create_client_ssl_context()
|
199
|
+
if ssl_context is None:
|
200
|
+
return None
|
201
|
+
|
202
|
+
return aiohttp.TCPConnector(ssl=ssl_context)
|
203
|
+
|
204
|
+
except ImportError:
|
205
|
+
self.logger.error("aiohttp not available for connector creation")
|
206
|
+
return None
|
207
|
+
except Exception as e:
|
208
|
+
self.logger.error(f"Failed to create connector: {e}")
|
209
|
+
return None
|
210
|
+
|
211
|
+
def validate_certificate(self, cert_file: str) -> Dict[str, Any]:
|
212
|
+
"""
|
213
|
+
Validate certificate file.
|
214
|
+
|
215
|
+
Args:
|
216
|
+
cert_file: Path to certificate file
|
217
|
+
|
218
|
+
Returns:
|
219
|
+
Dictionary with validation results
|
220
|
+
"""
|
221
|
+
try:
|
222
|
+
if self.ssl_manager and CRYPTOGRAPHY_AVAILABLE:
|
223
|
+
# Use security framework
|
224
|
+
cert_info = extract_certificate_info(cert_file)
|
225
|
+
return {
|
226
|
+
"valid": True,
|
227
|
+
"subject": str(cert_info.subject),
|
228
|
+
"issuer": str(cert_info.issuer),
|
229
|
+
"not_valid_before": cert_info.not_valid_before.isoformat(),
|
230
|
+
"not_valid_after": cert_info.not_valid_after.isoformat(),
|
231
|
+
"serial_number": str(cert_info.serial_number),
|
232
|
+
"is_self_signed": is_certificate_self_signed(cert_file)
|
233
|
+
}
|
234
|
+
else:
|
235
|
+
# Fallback implementation
|
236
|
+
return self._validate_certificate_fallback(cert_file)
|
237
|
+
|
238
|
+
except Exception as e:
|
239
|
+
self.logger.error(f"Certificate validation failed: {e}")
|
240
|
+
return {
|
241
|
+
"valid": False,
|
242
|
+
"error": str(e)
|
243
|
+
}
|
244
|
+
|
245
|
+
def _validate_certificate_fallback(self, cert_file: str) -> Dict[str, Any]:
|
246
|
+
"""Fallback certificate validation."""
|
247
|
+
if not CRYPTOGRAPHY_AVAILABLE:
|
248
|
+
return {
|
249
|
+
"valid": False,
|
250
|
+
"error": "cryptography not available for certificate validation"
|
251
|
+
}
|
252
|
+
|
253
|
+
try:
|
254
|
+
if not os.path.exists(cert_file):
|
255
|
+
return {
|
256
|
+
"valid": False,
|
257
|
+
"error": "Certificate file not found"
|
258
|
+
}
|
259
|
+
|
260
|
+
# Basic file existence and readability check
|
261
|
+
with open(cert_file, 'rb') as f:
|
262
|
+
cert_data = f.read()
|
263
|
+
|
264
|
+
# Try to parse certificate
|
265
|
+
cert = x509.load_pem_x509_certificate(cert_data)
|
266
|
+
|
267
|
+
return {
|
268
|
+
"valid": True,
|
269
|
+
"subject": str(cert.subject),
|
270
|
+
"issuer": str(cert.issuer),
|
271
|
+
"not_valid_before": cert.not_valid_before.isoformat(),
|
272
|
+
"not_valid_after": cert.not_valid_after.isoformat(),
|
273
|
+
"serial_number": str(cert.serial_number)
|
274
|
+
}
|
275
|
+
|
276
|
+
except Exception as e:
|
277
|
+
return {
|
278
|
+
"valid": False,
|
279
|
+
"error": f"Certificate validation failed: {e}"
|
280
|
+
}
|
281
|
+
|
282
|
+
def get_ssl_config(self) -> Dict[str, Any]:
|
283
|
+
"""
|
284
|
+
Get current SSL configuration.
|
285
|
+
|
286
|
+
Returns:
|
287
|
+
Dictionary with SSL configuration
|
288
|
+
"""
|
289
|
+
return self.config.get("ssl", {})
|
290
|
+
|
291
|
+
def is_ssl_enabled(self) -> bool:
|
292
|
+
"""
|
293
|
+
Check if SSL/TLS is enabled.
|
294
|
+
|
295
|
+
Returns:
|
296
|
+
True if SSL/TLS is enabled, False otherwise
|
297
|
+
"""
|
298
|
+
return self.config.get("ssl", {}).get("enabled", False)
|
299
|
+
|
300
|
+
def is_mtls_enabled(self) -> bool:
|
301
|
+
"""
|
302
|
+
Check if mTLS (mutual TLS) is enabled.
|
303
|
+
|
304
|
+
Returns:
|
305
|
+
True if mTLS is enabled, False otherwise
|
306
|
+
"""
|
307
|
+
ssl_config = self.config.get("ssl", {})
|
308
|
+
return (ssl_config.get("enabled", False) and
|
309
|
+
bool(ssl_config.get("cert_file")) and
|
310
|
+
bool(ssl_config.get("key_file")))
|
311
|
+
|
312
|
+
def get_certificate_info(self, cert_file: str) -> Optional[Dict[str, Any]]:
|
313
|
+
"""
|
314
|
+
Get certificate information.
|
315
|
+
|
316
|
+
Args:
|
317
|
+
cert_file: Path to certificate file
|
318
|
+
|
319
|
+
Returns:
|
320
|
+
Dictionary with certificate information or None if not available
|
321
|
+
"""
|
322
|
+
try:
|
323
|
+
if self.ssl_manager and CRYPTOGRAPHY_AVAILABLE:
|
324
|
+
# Use security framework
|
325
|
+
cert_info = extract_certificate_info(cert_file)
|
326
|
+
return {
|
327
|
+
"subject": str(cert_info.subject),
|
328
|
+
"issuer": str(cert_info.issuer),
|
329
|
+
"not_valid_before": cert_info.not_valid_before.isoformat(),
|
330
|
+
"not_valid_after": cert_info.not_valid_after.isoformat(),
|
331
|
+
"serial_number": str(cert_info.serial_number),
|
332
|
+
"version": cert_info.version,
|
333
|
+
"signature_algorithm": str(cert_info.signature_algorithm_oid),
|
334
|
+
"is_self_signed": is_certificate_self_signed(cert_file)
|
335
|
+
}
|
336
|
+
else:
|
337
|
+
# Fallback implementation
|
338
|
+
return self._get_certificate_info_fallback(cert_file)
|
339
|
+
|
340
|
+
except Exception as e:
|
341
|
+
self.logger.error(f"Failed to get certificate info: {e}")
|
342
|
+
return None
|
343
|
+
|
344
|
+
def _get_certificate_info_fallback(self, cert_file: str) -> Optional[Dict[str, Any]]:
|
345
|
+
"""Fallback certificate info extraction."""
|
346
|
+
if not CRYPTOGRAPHY_AVAILABLE:
|
347
|
+
return None
|
348
|
+
|
349
|
+
try:
|
350
|
+
if not os.path.exists(cert_file):
|
351
|
+
return None
|
352
|
+
|
353
|
+
with open(cert_file, 'rb') as f:
|
354
|
+
cert_data = f.read()
|
355
|
+
|
356
|
+
cert = x509.load_pem_x509_certificate(cert_data)
|
357
|
+
|
358
|
+
return {
|
359
|
+
"subject": str(cert.subject),
|
360
|
+
"issuer": str(cert.issuer),
|
361
|
+
"not_valid_before": cert.not_valid_before.isoformat(),
|
362
|
+
"not_valid_after": cert.not_valid_after.isoformat(),
|
363
|
+
"serial_number": str(cert.serial_number),
|
364
|
+
"version": cert.version.name,
|
365
|
+
"signature_algorithm": str(cert.signature_algorithm_oid)
|
366
|
+
}
|
367
|
+
|
368
|
+
except Exception as e:
|
369
|
+
self.logger.error(f"Failed to extract certificate info: {e}")
|
370
|
+
return None
|
371
|
+
|
372
|
+
def validate_ssl_config(self) -> List[str]:
|
373
|
+
"""
|
374
|
+
Validate SSL configuration.
|
375
|
+
|
376
|
+
Returns:
|
377
|
+
List of validation errors
|
378
|
+
"""
|
379
|
+
errors = []
|
380
|
+
ssl_config = self.config.get("ssl", {})
|
381
|
+
|
382
|
+
if not ssl_config.get("enabled", False):
|
383
|
+
return errors # SSL disabled, no validation needed
|
384
|
+
|
385
|
+
# Check certificate files if mTLS is configured
|
386
|
+
cert_file = ssl_config.get("cert_file")
|
387
|
+
key_file = ssl_config.get("key_file")
|
388
|
+
|
389
|
+
if cert_file and not os.path.exists(cert_file):
|
390
|
+
errors.append(f"Certificate file not found: {cert_file}")
|
391
|
+
|
392
|
+
if key_file and not os.path.exists(key_file):
|
393
|
+
errors.append(f"Key file not found: {key_file}")
|
394
|
+
|
395
|
+
# Check CA certificate if provided
|
396
|
+
ca_cert_file = ssl_config.get("ca_cert_file")
|
397
|
+
if ca_cert_file and not os.path.exists(ca_cert_file):
|
398
|
+
errors.append(f"CA certificate file not found: {ca_cert_file}")
|
399
|
+
|
400
|
+
# Validate certificate if provided
|
401
|
+
if cert_file and os.path.exists(cert_file):
|
402
|
+
validation_result = self.validate_certificate(cert_file)
|
403
|
+
if not validation_result.get("valid", False):
|
404
|
+
errors.append(f"Certificate validation failed: {validation_result.get('error', 'Unknown error')}")
|
405
|
+
|
406
|
+
return errors
|
407
|
+
|
408
|
+
def get_supported_protocols(self) -> List[str]:
|
409
|
+
"""
|
410
|
+
Get list of supported SSL/TLS protocols.
|
411
|
+
|
412
|
+
Returns:
|
413
|
+
List of supported protocol names
|
414
|
+
"""
|
415
|
+
protocols = []
|
416
|
+
|
417
|
+
try:
|
418
|
+
# Check available protocols
|
419
|
+
if hasattr(ssl, 'PROTOCOL_TLSv1_2'):
|
420
|
+
protocols.append("TLSv1.2")
|
421
|
+
if hasattr(ssl, 'PROTOCOL_TLSv1_3'):
|
422
|
+
protocols.append("TLSv1.3")
|
423
|
+
if hasattr(ssl, 'PROTOCOL_TLS'):
|
424
|
+
protocols.append("TLS")
|
425
|
+
|
426
|
+
# Fallback to basic protocols
|
427
|
+
if not protocols:
|
428
|
+
protocols = ["SSLv23", "TLS"]
|
429
|
+
|
430
|
+
except Exception as e:
|
431
|
+
self.logger.warning(f"Failed to detect supported protocols: {e}")
|
432
|
+
protocols = ["TLS"] # Basic fallback
|
433
|
+
|
434
|
+
return protocols
|
435
|
+
|
436
|
+
|
437
|
+
def create_ssl_manager(config: Dict[str, Any]) -> ClientSSLManager:
|
438
|
+
"""
|
439
|
+
Create SSL/TLS manager from configuration.
|
440
|
+
|
441
|
+
Args:
|
442
|
+
config: Configuration dictionary
|
443
|
+
|
444
|
+
Returns:
|
445
|
+
ClientSSLManager instance
|
446
|
+
"""
|
447
|
+
return ClientSSLManager(config)
|
448
|
+
|
449
|
+
|
450
|
+
def create_ssl_context(config: Dict[str, Any]) -> Optional[ssl.SSLContext]:
|
451
|
+
"""
|
452
|
+
Create SSL context from configuration.
|
453
|
+
|
454
|
+
Args:
|
455
|
+
config: Configuration dictionary
|
456
|
+
|
457
|
+
Returns:
|
458
|
+
SSL context or None if SSL is disabled
|
459
|
+
"""
|
460
|
+
ssl_manager = ClientSSLManager(config)
|
461
|
+
return ssl_manager.create_client_ssl_context()
|
462
|
+
|
463
|
+
|
464
|
+
def create_connector(config: Dict[str, Any]) -> Optional[Any]:
|
465
|
+
"""
|
466
|
+
Create aiohttp connector from configuration.
|
467
|
+
|
468
|
+
Args:
|
469
|
+
config: Configuration dictionary
|
470
|
+
|
471
|
+
Returns:
|
472
|
+
aiohttp connector or None if SSL is disabled
|
473
|
+
"""
|
474
|
+
ssl_manager = ClientSSLManager(config)
|
475
|
+
return ssl_manager.create_connector()
|