mcp-proxy-adapter 6.2.36__py3-none-any.whl → 6.3.0__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/core/app_factory.py +127 -86
- mcp_proxy_adapter/core/config_validator.py +92 -55
- mcp_proxy_adapter/core/crl_utils.py +89 -76
- mcp_proxy_adapter/core/security_integration.py +110 -77
- mcp_proxy_adapter/main.py +19 -10
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-6.2.36.dist-info → mcp_proxy_adapter-6.3.0.dist-info}/METADATA +1 -1
- {mcp_proxy_adapter-6.2.36.dist-info → mcp_proxy_adapter-6.3.0.dist-info}/RECORD +12 -12
- {mcp_proxy_adapter-6.2.36.dist-info → mcp_proxy_adapter-6.3.0.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-6.2.36.dist-info → mcp_proxy_adapter-6.3.0.dist-info}/entry_points.txt +0 -0
- {mcp_proxy_adapter-6.2.36.dist-info → mcp_proxy_adapter-6.3.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_proxy_adapter-6.2.36.dist-info → mcp_proxy_adapter-6.3.0.dist-info}/top_level.txt +0 -0
@@ -14,7 +14,6 @@ import os
|
|
14
14
|
import tempfile
|
15
15
|
from pathlib import Path
|
16
16
|
from typing import Optional, Union, Dict, Any
|
17
|
-
from urllib.parse import urlparse
|
18
17
|
import requests
|
19
18
|
from requests.adapters import HTTPAdapter
|
20
19
|
from urllib3.util.retry import Retry
|
@@ -22,12 +21,12 @@ from urllib3.util.retry import Retry
|
|
22
21
|
# Import mcp_security_framework CRL utilities
|
23
22
|
try:
|
24
23
|
from mcp_security_framework.utils.cert_utils import (
|
25
|
-
parse_crl,
|
26
24
|
is_certificate_revoked,
|
27
25
|
validate_certificate_against_crl,
|
28
26
|
is_crl_valid,
|
29
|
-
get_crl_info
|
27
|
+
get_crl_info,
|
30
28
|
)
|
29
|
+
|
31
30
|
SECURITY_FRAMEWORK_AVAILABLE = True
|
32
31
|
except ImportError:
|
33
32
|
SECURITY_FRAMEWORK_AVAILABLE = False
|
@@ -38,57 +37,68 @@ logger = logging.getLogger(__name__)
|
|
38
37
|
class CRLManager:
|
39
38
|
"""
|
40
39
|
Manager for Certificate Revocation Lists (CRL).
|
41
|
-
|
40
|
+
|
42
41
|
Supports both file-based and URL-based CRL sources.
|
43
42
|
Automatically downloads CRL from URLs and caches them locally.
|
44
43
|
"""
|
45
|
-
|
44
|
+
|
46
45
|
def __init__(self, config: Dict[str, Any]):
|
47
46
|
"""
|
48
47
|
Initialize CRL manager.
|
49
|
-
|
48
|
+
|
50
49
|
Args:
|
51
50
|
config: Configuration dictionary containing CRL settings
|
52
51
|
"""
|
53
52
|
self.config = config
|
54
53
|
self.crl_enabled = config.get("crl_enabled", False)
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
54
|
+
|
55
|
+
# Only analyze CRL paths if certificates are enabled
|
56
|
+
certificates_enabled = config.get("certificates_enabled", True)
|
57
|
+
if certificates_enabled and self.crl_enabled:
|
58
|
+
self.crl_path = config.get("crl_path")
|
59
|
+
self.crl_url = config.get("crl_url")
|
60
|
+
self.crl_validity_days = config.get("crl_validity_days", 30)
|
61
|
+
else:
|
62
|
+
# Don't analyze CRL paths if certificates are disabled
|
63
|
+
self.crl_path = None
|
64
|
+
self.crl_url = None
|
65
|
+
self.crl_validity_days = 30
|
66
|
+
|
59
67
|
# Cache for downloaded CRL files
|
60
68
|
self._crl_cache: Dict[str, str] = {}
|
61
|
-
|
69
|
+
|
62
70
|
# Setup HTTP session with retry strategy
|
63
71
|
self._setup_http_session()
|
64
|
-
|
65
|
-
logger.info(
|
66
|
-
|
72
|
+
|
73
|
+
logger.info(
|
74
|
+
f"CRL Manager initialized - enabled: {self.crl_enabled}, certificates_enabled: {certificates_enabled}"
|
75
|
+
)
|
76
|
+
|
67
77
|
def _setup_http_session(self):
|
68
78
|
"""Setup HTTP session with retry strategy for CRL downloads."""
|
69
79
|
self.session = requests.Session()
|
70
|
-
|
80
|
+
|
71
81
|
# Configure retry strategy
|
72
82
|
retry_strategy = Retry(
|
73
83
|
total=3,
|
74
84
|
backoff_factor=1,
|
75
85
|
status_forcelist=[429, 500, 502, 503, 504],
|
76
86
|
)
|
77
|
-
|
87
|
+
|
78
88
|
adapter = HTTPAdapter(max_retries=retry_strategy)
|
79
89
|
self.session.mount("http://", adapter)
|
80
90
|
self.session.mount("https://", adapter)
|
81
|
-
|
91
|
+
|
82
92
|
# Set timeout
|
83
93
|
self.session.timeout = 30
|
84
|
-
|
94
|
+
|
85
95
|
def get_crl_data(self) -> Optional[Union[str, bytes, Path]]:
|
86
96
|
"""
|
87
97
|
Get CRL data from configured source.
|
88
|
-
|
98
|
+
|
89
99
|
Returns:
|
90
100
|
CRL data as string, bytes, or Path, or None if not available
|
91
|
-
|
101
|
+
|
92
102
|
Raises:
|
93
103
|
ValueError: If CRL is enabled but no source is configured
|
94
104
|
FileNotFoundError: If CRL file is not found
|
@@ -97,49 +107,52 @@ class CRLManager:
|
|
97
107
|
if not self.crl_enabled:
|
98
108
|
logger.debug("CRL is disabled, skipping CRL check")
|
99
109
|
return None
|
100
|
-
|
110
|
+
|
101
111
|
# Check if CRL URL is configured
|
102
112
|
if self.crl_url:
|
103
113
|
return self._get_crl_from_url()
|
104
|
-
|
114
|
+
|
105
115
|
# Check if CRL file path is configured
|
106
116
|
if self.crl_path:
|
107
117
|
return self._get_crl_from_file()
|
108
|
-
|
118
|
+
|
109
119
|
# If CRL is enabled but no source is configured, this is an error
|
110
120
|
if self.crl_enabled:
|
111
121
|
raise ValueError("CRL is enabled but neither crl_path nor crl_url is configured")
|
112
|
-
|
122
|
+
|
113
123
|
return None
|
114
|
-
|
124
|
+
|
115
125
|
def _get_crl_from_url(self) -> str:
|
116
126
|
"""
|
117
127
|
Download CRL from URL.
|
118
|
-
|
128
|
+
|
119
129
|
Returns:
|
120
130
|
Path to downloaded CRL file
|
121
|
-
|
131
|
+
|
122
132
|
Raises:
|
123
133
|
requests.RequestException: If download fails
|
124
134
|
ValueError: If downloaded data is not valid CRL
|
125
135
|
"""
|
126
136
|
try:
|
127
137
|
logger.info(f"Downloading CRL from URL: {self.crl_url}")
|
128
|
-
|
138
|
+
|
129
139
|
# Download CRL
|
130
140
|
response = self.session.get(self.crl_url)
|
131
141
|
response.raise_for_status()
|
132
|
-
|
142
|
+
|
133
143
|
# Validate content type
|
134
|
-
content_type = response.headers.get(
|
135
|
-
if
|
144
|
+
content_type = response.headers.get("content-type", "").lower()
|
145
|
+
if (
|
146
|
+
"application/pkix-crl" not in content_type
|
147
|
+
and "application/x-pkcs7-crl" not in content_type
|
148
|
+
):
|
136
149
|
logger.warning(f"Unexpected content type for CRL: {content_type}")
|
137
|
-
|
150
|
+
|
138
151
|
# Save to temporary file
|
139
|
-
with tempfile.NamedTemporaryFile(mode=
|
152
|
+
with tempfile.NamedTemporaryFile(mode="wb", suffix=".crl", delete=False) as temp_file:
|
140
153
|
temp_file.write(response.content)
|
141
154
|
temp_file_path = temp_file.name
|
142
|
-
|
155
|
+
|
143
156
|
# Validate CRL format
|
144
157
|
if SECURITY_FRAMEWORK_AVAILABLE:
|
145
158
|
try:
|
@@ -150,33 +163,33 @@ class CRLManager:
|
|
150
163
|
raise ValueError(f"Downloaded CRL is not valid: {e}")
|
151
164
|
else:
|
152
165
|
logger.warning("mcp_security_framework not available, skipping CRL validation")
|
153
|
-
|
166
|
+
|
154
167
|
# Cache the file path
|
155
168
|
self._crl_cache[self.crl_url] = temp_file_path
|
156
|
-
|
169
|
+
|
157
170
|
return temp_file_path
|
158
|
-
|
171
|
+
|
159
172
|
except requests.RequestException as e:
|
160
173
|
logger.error(f"Failed to download CRL from {self.crl_url}: {e}")
|
161
174
|
raise
|
162
175
|
except Exception as e:
|
163
176
|
logger.error(f"CRL download failed: {e}")
|
164
177
|
raise
|
165
|
-
|
178
|
+
|
166
179
|
def _get_crl_from_file(self) -> str:
|
167
180
|
"""
|
168
181
|
Get CRL from file path.
|
169
|
-
|
182
|
+
|
170
183
|
Returns:
|
171
184
|
Path to CRL file
|
172
|
-
|
185
|
+
|
173
186
|
Raises:
|
174
187
|
FileNotFoundError: If CRL file is not found
|
175
188
|
ValueError: If CRL file is not valid
|
176
189
|
"""
|
177
190
|
if not os.path.exists(self.crl_path):
|
178
191
|
raise FileNotFoundError(f"CRL file not found: {self.crl_path}")
|
179
|
-
|
192
|
+
|
180
193
|
# Validate CRL format
|
181
194
|
if SECURITY_FRAMEWORK_AVAILABLE:
|
182
195
|
try:
|
@@ -186,60 +199,60 @@ class CRLManager:
|
|
186
199
|
raise ValueError(f"CRL file is not valid: {e}")
|
187
200
|
else:
|
188
201
|
logger.warning("mcp_security_framework not available, skipping CRL validation")
|
189
|
-
|
202
|
+
|
190
203
|
return self.crl_path
|
191
|
-
|
204
|
+
|
192
205
|
def is_certificate_revoked(self, cert_path: str) -> bool:
|
193
206
|
"""
|
194
207
|
Check if certificate is revoked according to CRL.
|
195
|
-
|
208
|
+
|
196
209
|
Args:
|
197
210
|
cert_path: Path to certificate file
|
198
|
-
|
211
|
+
|
199
212
|
Returns:
|
200
213
|
True if certificate is revoked, False otherwise
|
201
|
-
|
214
|
+
|
202
215
|
Raises:
|
203
216
|
ValueError: If CRL is enabled but not available
|
204
217
|
FileNotFoundError: If certificate file is not found
|
205
218
|
"""
|
206
219
|
if not self.crl_enabled:
|
207
220
|
return False
|
208
|
-
|
221
|
+
|
209
222
|
if not SECURITY_FRAMEWORK_AVAILABLE:
|
210
223
|
logger.warning("mcp_security_framework not available, skipping CRL check")
|
211
224
|
return False
|
212
|
-
|
225
|
+
|
213
226
|
try:
|
214
227
|
crl_data = self.get_crl_data()
|
215
228
|
if not crl_data:
|
216
229
|
logger.warning("CRL is enabled but no CRL data is available")
|
217
230
|
return False
|
218
|
-
|
231
|
+
|
219
232
|
is_revoked = is_certificate_revoked(cert_path, crl_data)
|
220
|
-
|
233
|
+
|
221
234
|
if is_revoked:
|
222
235
|
logger.warning(f"Certificate is revoked according to CRL: {cert_path}")
|
223
236
|
else:
|
224
237
|
logger.debug(f"Certificate is not revoked according to CRL: {cert_path}")
|
225
|
-
|
238
|
+
|
226
239
|
return is_revoked
|
227
|
-
|
240
|
+
|
228
241
|
except Exception as e:
|
229
242
|
logger.error(f"CRL check failed for certificate {cert_path}: {e}")
|
230
243
|
# For security, consider certificate invalid if CRL check fails
|
231
244
|
return True
|
232
|
-
|
245
|
+
|
233
246
|
def validate_certificate_against_crl(self, cert_path: str) -> Dict[str, Any]:
|
234
247
|
"""
|
235
248
|
Validate certificate against CRL and return detailed status.
|
236
|
-
|
249
|
+
|
237
250
|
Args:
|
238
251
|
cert_path: Path to certificate file
|
239
|
-
|
252
|
+
|
240
253
|
Returns:
|
241
254
|
Dictionary containing validation results
|
242
|
-
|
255
|
+
|
243
256
|
Raises:
|
244
257
|
ValueError: If CRL is enabled but not available
|
245
258
|
FileNotFoundError: If certificate file is not found
|
@@ -249,18 +262,18 @@ class CRLManager:
|
|
249
262
|
"is_revoked": False,
|
250
263
|
"crl_checked": False,
|
251
264
|
"crl_source": None,
|
252
|
-
"message": "CRL check is disabled"
|
265
|
+
"message": "CRL check is disabled",
|
253
266
|
}
|
254
|
-
|
267
|
+
|
255
268
|
if not SECURITY_FRAMEWORK_AVAILABLE:
|
256
269
|
logger.warning("mcp_security_framework not available, skipping CRL validation")
|
257
270
|
return {
|
258
271
|
"is_revoked": False,
|
259
272
|
"crl_checked": False,
|
260
273
|
"crl_source": None,
|
261
|
-
"message": "mcp_security_framework not available"
|
274
|
+
"message": "mcp_security_framework not available",
|
262
275
|
}
|
263
|
-
|
276
|
+
|
264
277
|
try:
|
265
278
|
crl_data = self.get_crl_data()
|
266
279
|
if not crl_data:
|
@@ -269,20 +282,20 @@ class CRLManager:
|
|
269
282
|
"is_revoked": True, # For security, consider invalid if CRL unavailable
|
270
283
|
"crl_checked": False,
|
271
284
|
"crl_source": None,
|
272
|
-
"message": "CRL is enabled but not available"
|
285
|
+
"message": "CRL is enabled but not available",
|
273
286
|
}
|
274
|
-
|
287
|
+
|
275
288
|
# Get CRL source info
|
276
289
|
crl_source = self.crl_url if self.crl_url else self.crl_path
|
277
|
-
|
290
|
+
|
278
291
|
# Validate certificate against CRL
|
279
292
|
result = validate_certificate_against_crl(cert_path, crl_data)
|
280
|
-
|
293
|
+
|
281
294
|
result["crl_checked"] = True
|
282
295
|
result["crl_source"] = crl_source
|
283
|
-
|
296
|
+
|
284
297
|
return result
|
285
|
-
|
298
|
+
|
286
299
|
except Exception as e:
|
287
300
|
logger.error(f"CRL validation failed for certificate {cert_path}: {e}")
|
288
301
|
# For security, consider certificate invalid if CRL validation fails
|
@@ -290,34 +303,34 @@ class CRLManager:
|
|
290
303
|
"is_revoked": True,
|
291
304
|
"crl_checked": False,
|
292
305
|
"crl_source": self.crl_url if self.crl_url else self.crl_path,
|
293
|
-
"message": f"CRL validation failed: {e}"
|
306
|
+
"message": f"CRL validation failed: {e}",
|
294
307
|
}
|
295
|
-
|
308
|
+
|
296
309
|
def get_crl_info(self) -> Optional[Dict[str, Any]]:
|
297
310
|
"""
|
298
311
|
Get information about the configured CRL.
|
299
|
-
|
312
|
+
|
300
313
|
Returns:
|
301
314
|
Dictionary containing CRL information, or None if CRL is not available
|
302
315
|
"""
|
303
316
|
if not self.crl_enabled:
|
304
317
|
return None
|
305
|
-
|
318
|
+
|
306
319
|
if not SECURITY_FRAMEWORK_AVAILABLE:
|
307
320
|
logger.warning("mcp_security_framework not available, cannot get CRL info")
|
308
321
|
return None
|
309
|
-
|
322
|
+
|
310
323
|
try:
|
311
324
|
crl_data = self.get_crl_data()
|
312
325
|
if not crl_data:
|
313
326
|
return None
|
314
|
-
|
327
|
+
|
315
328
|
return get_crl_info(crl_data)
|
316
|
-
|
329
|
+
|
317
330
|
except Exception as e:
|
318
331
|
logger.error(f"Failed to get CRL info: {e}")
|
319
332
|
return None
|
320
|
-
|
333
|
+
|
321
334
|
def cleanup_cache(self):
|
322
335
|
"""Clean up temporary CRL files."""
|
323
336
|
for url, temp_path in self._crl_cache.items():
|
@@ -327,9 +340,9 @@ class CRLManager:
|
|
327
340
|
logger.debug(f"Cleaned up temporary CRL file: {temp_path}")
|
328
341
|
except Exception as e:
|
329
342
|
logger.warning(f"Failed to cleanup temporary CRL file {temp_path}: {e}")
|
330
|
-
|
343
|
+
|
331
344
|
self._crl_cache.clear()
|
332
|
-
|
345
|
+
|
333
346
|
def __del__(self):
|
334
347
|
"""Cleanup when object is destroyed."""
|
335
348
|
self.cleanup_cache()
|