gopher-mcp 0.2.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.
- gopher_mcp/__init__.py +18 -0
- gopher_mcp/__main__.py +23 -0
- gopher_mcp/certificate_errors.py +562 -0
- gopher_mcp/client_certs.py +388 -0
- gopher_mcp/fingerprints.py +439 -0
- gopher_mcp/gemini_client.py +501 -0
- gopher_mcp/gemini_tls.py +349 -0
- gopher_mcp/gopher_client.py +408 -0
- gopher_mcp/http_server.py +153 -0
- gopher_mcp/models.py +722 -0
- gopher_mcp/security.py +311 -0
- gopher_mcp/security_policy.py +479 -0
- gopher_mcp/server.py +144 -0
- gopher_mcp/tofu.py +311 -0
- gopher_mcp/tools.py +89 -0
- gopher_mcp/utils.py +1419 -0
- gopher_mcp-0.2.1.dist-info/METADATA +501 -0
- gopher_mcp-0.2.1.dist-info/RECORD +21 -0
- gopher_mcp-0.2.1.dist-info/WHEEL +4 -0
- gopher_mcp-0.2.1.dist-info/entry_points.txt +2 -0
- gopher_mcp-0.2.1.dist-info/licenses/LICENSE +21 -0
gopher_mcp/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Gopher MCP - A Model Context Protocol server for Gopher and Gemini protocols.
|
|
2
|
+
|
|
3
|
+
This package provides a cross-platform MCP server that allows LLMs to browse
|
|
4
|
+
Gopher and Gemini resources safely and efficiently.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.2.1"
|
|
8
|
+
__author__ = "Gopher MCP Team"
|
|
9
|
+
__email__ = "team@gopher-mcp.dev"
|
|
10
|
+
__license__ = "MIT"
|
|
11
|
+
|
|
12
|
+
from .server import gopher_fetch, gemini_fetch, mcp
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"mcp",
|
|
16
|
+
"gopher_fetch",
|
|
17
|
+
"gemini_fetch",
|
|
18
|
+
]
|
gopher_mcp/__main__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Main entry point for the Gopher MCP server."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from .server import mcp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
"""Run the main entry point."""
|
|
10
|
+
try:
|
|
11
|
+
if len(sys.argv) > 1 and sys.argv[1] == "--http":
|
|
12
|
+
# HTTP transport not yet implemented in FastMCP
|
|
13
|
+
print("HTTP transport not yet supported", file=sys.stderr)
|
|
14
|
+
sys.exit(1)
|
|
15
|
+
else:
|
|
16
|
+
# FastMCP handles its own event loop
|
|
17
|
+
mcp.run(transport="stdio")
|
|
18
|
+
except KeyboardInterrupt:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
"""Certificate validation error handling for Gemini protocol."""
|
|
2
|
+
|
|
3
|
+
import ssl
|
|
4
|
+
import time
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Dict, List, Optional, Any
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
|
|
11
|
+
logger = structlog.get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CertificateErrorType(Enum):
|
|
15
|
+
"""Types of certificate validation errors."""
|
|
16
|
+
|
|
17
|
+
# TLS handshake errors
|
|
18
|
+
TLS_HANDSHAKE_FAILED = "tls_handshake_failed"
|
|
19
|
+
TLS_VERSION_NOT_SUPPORTED = "tls_version_not_supported"
|
|
20
|
+
TLS_CIPHER_NOT_SUPPORTED = "tls_cipher_not_supported"
|
|
21
|
+
TLS_SNI_FAILED = "tls_sni_failed"
|
|
22
|
+
TLS_CONNECTION_TIMEOUT = "tls_connection_timeout"
|
|
23
|
+
|
|
24
|
+
# Certificate validation errors
|
|
25
|
+
CERTIFICATE_EXPIRED = "certificate_expired"
|
|
26
|
+
CERTIFICATE_NOT_YET_VALID = "certificate_not_yet_valid"
|
|
27
|
+
CERTIFICATE_REVOKED = "certificate_revoked"
|
|
28
|
+
CERTIFICATE_INVALID_SIGNATURE = "certificate_invalid_signature"
|
|
29
|
+
CERTIFICATE_UNTRUSTED_ROOT = "certificate_untrusted_root"
|
|
30
|
+
CERTIFICATE_HOSTNAME_MISMATCH = "certificate_hostname_mismatch"
|
|
31
|
+
CERTIFICATE_CHAIN_INVALID = "certificate_chain_invalid"
|
|
32
|
+
|
|
33
|
+
# TOFU-specific errors
|
|
34
|
+
TOFU_CERTIFICATE_CHANGED = "tofu_certificate_changed"
|
|
35
|
+
TOFU_FINGERPRINT_MISMATCH = "tofu_fingerprint_mismatch"
|
|
36
|
+
TOFU_STORAGE_ERROR = "tofu_storage_error"
|
|
37
|
+
TOFU_VALIDATION_FAILED = "tofu_validation_failed"
|
|
38
|
+
|
|
39
|
+
# Client certificate errors
|
|
40
|
+
CLIENT_CERT_NOT_FOUND = "client_cert_not_found"
|
|
41
|
+
CLIENT_CERT_EXPIRED = "client_cert_expired"
|
|
42
|
+
CLIENT_CERT_INVALID = "client_cert_invalid"
|
|
43
|
+
CLIENT_CERT_KEY_MISMATCH = "client_cert_key_mismatch"
|
|
44
|
+
CLIENT_CERT_REJECTED = "client_cert_rejected"
|
|
45
|
+
|
|
46
|
+
# General errors
|
|
47
|
+
CERTIFICATE_PARSE_ERROR = "certificate_parse_error"
|
|
48
|
+
CERTIFICATE_ENCODING_ERROR = "certificate_encoding_error"
|
|
49
|
+
UNKNOWN_CERTIFICATE_ERROR = "unknown_certificate_error"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CertificateErrorSeverity(Enum):
|
|
53
|
+
"""Severity levels for certificate errors."""
|
|
54
|
+
|
|
55
|
+
LOW = "low"
|
|
56
|
+
MEDIUM = "medium"
|
|
57
|
+
HIGH = "high"
|
|
58
|
+
CRITICAL = "critical"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class CertificateError:
|
|
63
|
+
"""Represents a certificate validation error."""
|
|
64
|
+
|
|
65
|
+
error_type: CertificateErrorType
|
|
66
|
+
message: str
|
|
67
|
+
severity: CertificateErrorSeverity
|
|
68
|
+
host: str
|
|
69
|
+
port: int
|
|
70
|
+
details: Dict[str, Any]
|
|
71
|
+
timestamp: float
|
|
72
|
+
recoverable: bool = False
|
|
73
|
+
recovery_suggestions: Optional[List[str]] = None
|
|
74
|
+
|
|
75
|
+
def __post_init__(self) -> None:
|
|
76
|
+
"""Initialize recovery suggestions if not provided."""
|
|
77
|
+
if self.recovery_suggestions is None:
|
|
78
|
+
self.recovery_suggestions = []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CertificateErrorHandler:
|
|
82
|
+
"""Handles certificate validation errors and provides recovery suggestions."""
|
|
83
|
+
|
|
84
|
+
def __init__(self) -> None:
|
|
85
|
+
"""Initialize error handler."""
|
|
86
|
+
self._error_counts: Dict[str, int] = {}
|
|
87
|
+
self._last_errors: Dict[str, CertificateError] = {}
|
|
88
|
+
|
|
89
|
+
def handle_ssl_error(
|
|
90
|
+
self,
|
|
91
|
+
error: ssl.SSLError,
|
|
92
|
+
host: str,
|
|
93
|
+
port: int,
|
|
94
|
+
context: Optional[Dict[str, Any]] = None,
|
|
95
|
+
) -> CertificateError:
|
|
96
|
+
"""Handle SSL/TLS errors and convert to CertificateError.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
error: SSL error from Python ssl module
|
|
100
|
+
host: Target hostname
|
|
101
|
+
port: Target port
|
|
102
|
+
context: Additional context information
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
CertificateError with appropriate type and recovery suggestions
|
|
106
|
+
"""
|
|
107
|
+
context = context or {}
|
|
108
|
+
error_str = str(error).lower()
|
|
109
|
+
|
|
110
|
+
# Determine error type based on SSL error
|
|
111
|
+
if "certificate verify failed" in error_str:
|
|
112
|
+
if "certificate has expired" in error_str:
|
|
113
|
+
error_type = CertificateErrorType.CERTIFICATE_EXPIRED
|
|
114
|
+
severity = CertificateErrorSeverity.HIGH
|
|
115
|
+
recoverable = True
|
|
116
|
+
suggestions = [
|
|
117
|
+
"Check if the server certificate has expired",
|
|
118
|
+
"Contact the server administrator",
|
|
119
|
+
"Verify system clock is correct",
|
|
120
|
+
"Consider using TOFU mode if appropriate",
|
|
121
|
+
]
|
|
122
|
+
elif "hostname" in error_str or "name" in error_str:
|
|
123
|
+
error_type = CertificateErrorType.CERTIFICATE_HOSTNAME_MISMATCH
|
|
124
|
+
severity = CertificateErrorSeverity.HIGH
|
|
125
|
+
recoverable = True
|
|
126
|
+
suggestions = [
|
|
127
|
+
f"Verify you're connecting to the correct hostname: {host}",
|
|
128
|
+
"Check for typos in the hostname",
|
|
129
|
+
"Verify DNS resolution is correct",
|
|
130
|
+
"Check if the server uses a different certificate name",
|
|
131
|
+
]
|
|
132
|
+
elif "self signed" in error_str or "untrusted" in error_str:
|
|
133
|
+
error_type = CertificateErrorType.CERTIFICATE_UNTRUSTED_ROOT
|
|
134
|
+
severity = CertificateErrorSeverity.MEDIUM
|
|
135
|
+
recoverable = True
|
|
136
|
+
suggestions = [
|
|
137
|
+
"This is common in Gemini - consider using TOFU validation",
|
|
138
|
+
"Verify the certificate fingerprint manually",
|
|
139
|
+
"Check if this is the expected self-signed certificate",
|
|
140
|
+
"Contact the server administrator for certificate details",
|
|
141
|
+
]
|
|
142
|
+
else:
|
|
143
|
+
error_type = CertificateErrorType.CERTIFICATE_INVALID_SIGNATURE
|
|
144
|
+
severity = CertificateErrorSeverity.HIGH
|
|
145
|
+
recoverable = False
|
|
146
|
+
suggestions = [
|
|
147
|
+
"Certificate signature validation failed",
|
|
148
|
+
"This may indicate a security issue",
|
|
149
|
+
"Do not proceed without verification",
|
|
150
|
+
"Contact the server administrator",
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
elif "timeout" in error_str or "timed out" in error_str:
|
|
154
|
+
error_type = CertificateErrorType.TLS_CONNECTION_TIMEOUT
|
|
155
|
+
severity = CertificateErrorSeverity.MEDIUM
|
|
156
|
+
recoverable = True
|
|
157
|
+
suggestions = [
|
|
158
|
+
"Connection timed out during TLS handshake",
|
|
159
|
+
"Check network connectivity",
|
|
160
|
+
"Try increasing connection timeout",
|
|
161
|
+
"Verify the server is responding",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
elif "handshake failure" in error_str or "handshake" in error_str:
|
|
165
|
+
error_type = CertificateErrorType.TLS_HANDSHAKE_FAILED
|
|
166
|
+
severity = CertificateErrorSeverity.HIGH
|
|
167
|
+
recoverable = True
|
|
168
|
+
suggestions = [
|
|
169
|
+
"TLS handshake failed - check TLS version compatibility",
|
|
170
|
+
"Verify cipher suite compatibility",
|
|
171
|
+
"Check if client certificate is required",
|
|
172
|
+
"Try with different TLS configuration",
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
elif "protocol" in error_str or "version" in error_str:
|
|
176
|
+
error_type = CertificateErrorType.TLS_VERSION_NOT_SUPPORTED
|
|
177
|
+
severity = CertificateErrorSeverity.HIGH
|
|
178
|
+
recoverable = True
|
|
179
|
+
suggestions = [
|
|
180
|
+
"TLS version not supported by server",
|
|
181
|
+
"Try with TLS 1.2 or 1.3",
|
|
182
|
+
"Check server TLS configuration",
|
|
183
|
+
"Update TLS client configuration",
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
else:
|
|
187
|
+
error_type = CertificateErrorType.UNKNOWN_CERTIFICATE_ERROR
|
|
188
|
+
severity = CertificateErrorSeverity.MEDIUM
|
|
189
|
+
recoverable = False
|
|
190
|
+
suggestions = [
|
|
191
|
+
"Unknown SSL/TLS error occurred",
|
|
192
|
+
"Check server logs for more details",
|
|
193
|
+
"Verify network connectivity",
|
|
194
|
+
"Contact support if issue persists",
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
cert_error = CertificateError(
|
|
198
|
+
error_type=error_type,
|
|
199
|
+
message=f"SSL/TLS error: {str(error)}",
|
|
200
|
+
severity=severity,
|
|
201
|
+
host=host,
|
|
202
|
+
port=port,
|
|
203
|
+
details={
|
|
204
|
+
"ssl_error": str(error),
|
|
205
|
+
"ssl_error_type": type(error).__name__,
|
|
206
|
+
"context": context,
|
|
207
|
+
},
|
|
208
|
+
timestamp=time.time(),
|
|
209
|
+
recoverable=recoverable,
|
|
210
|
+
recovery_suggestions=suggestions,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
self._record_error(cert_error)
|
|
214
|
+
return cert_error
|
|
215
|
+
|
|
216
|
+
def handle_tofu_error(
|
|
217
|
+
self,
|
|
218
|
+
error_type: CertificateErrorType,
|
|
219
|
+
message: str,
|
|
220
|
+
host: str,
|
|
221
|
+
port: int,
|
|
222
|
+
details: Optional[Dict[str, Any]] = None,
|
|
223
|
+
) -> CertificateError:
|
|
224
|
+
"""Handle TOFU-specific errors.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
error_type: Type of TOFU error
|
|
228
|
+
message: Error message
|
|
229
|
+
host: Target hostname
|
|
230
|
+
port: Target port
|
|
231
|
+
details: Additional error details
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
CertificateError with TOFU-specific recovery suggestions
|
|
235
|
+
"""
|
|
236
|
+
details = details or {}
|
|
237
|
+
|
|
238
|
+
if error_type == CertificateErrorType.TOFU_CERTIFICATE_CHANGED:
|
|
239
|
+
severity = CertificateErrorSeverity.CRITICAL
|
|
240
|
+
recoverable = True
|
|
241
|
+
suggestions = [
|
|
242
|
+
"Certificate has changed since first connection",
|
|
243
|
+
"This could indicate a security issue or server update",
|
|
244
|
+
"Verify the change is legitimate with the server administrator",
|
|
245
|
+
"Check the new certificate fingerprint manually",
|
|
246
|
+
"Only accept if you trust the new certificate",
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
elif error_type == CertificateErrorType.TOFU_FINGERPRINT_MISMATCH:
|
|
250
|
+
severity = CertificateErrorSeverity.HIGH
|
|
251
|
+
recoverable = True
|
|
252
|
+
suggestions = [
|
|
253
|
+
"Certificate fingerprint does not match stored value",
|
|
254
|
+
"Verify the certificate fingerprint manually",
|
|
255
|
+
"Check if the server certificate was updated",
|
|
256
|
+
"Contact the server administrator for verification",
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
elif error_type == CertificateErrorType.TOFU_STORAGE_ERROR:
|
|
260
|
+
severity = CertificateErrorSeverity.MEDIUM
|
|
261
|
+
recoverable = True
|
|
262
|
+
suggestions = [
|
|
263
|
+
"Error accessing TOFU certificate storage",
|
|
264
|
+
"Check file permissions for certificate store",
|
|
265
|
+
"Verify disk space availability",
|
|
266
|
+
"Check certificate store path configuration",
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
else:
|
|
270
|
+
severity = CertificateErrorSeverity.MEDIUM
|
|
271
|
+
recoverable = True
|
|
272
|
+
suggestions = [
|
|
273
|
+
"TOFU validation failed",
|
|
274
|
+
"Check TOFU configuration",
|
|
275
|
+
"Verify certificate storage is accessible",
|
|
276
|
+
"Try clearing TOFU cache if appropriate",
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
cert_error = CertificateError(
|
|
280
|
+
error_type=error_type,
|
|
281
|
+
message=message,
|
|
282
|
+
severity=severity,
|
|
283
|
+
host=host,
|
|
284
|
+
port=port,
|
|
285
|
+
details=details,
|
|
286
|
+
timestamp=time.time(),
|
|
287
|
+
recoverable=recoverable,
|
|
288
|
+
recovery_suggestions=suggestions,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
self._record_error(cert_error)
|
|
292
|
+
return cert_error
|
|
293
|
+
|
|
294
|
+
def handle_client_certificate_error(
|
|
295
|
+
self,
|
|
296
|
+
error_type: CertificateErrorType,
|
|
297
|
+
message: str,
|
|
298
|
+
host: str,
|
|
299
|
+
port: int,
|
|
300
|
+
cert_path: Optional[str] = None,
|
|
301
|
+
details: Optional[Dict[str, Any]] = None,
|
|
302
|
+
) -> CertificateError:
|
|
303
|
+
"""Handle client certificate errors.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
error_type: Type of client certificate error
|
|
307
|
+
message: Error message
|
|
308
|
+
host: Target hostname
|
|
309
|
+
port: Target port
|
|
310
|
+
cert_path: Path to client certificate
|
|
311
|
+
details: Additional error details
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
CertificateError with client certificate recovery suggestions
|
|
315
|
+
"""
|
|
316
|
+
details = details or {}
|
|
317
|
+
if cert_path:
|
|
318
|
+
details["cert_path"] = cert_path
|
|
319
|
+
|
|
320
|
+
if error_type == CertificateErrorType.CLIENT_CERT_NOT_FOUND:
|
|
321
|
+
severity = CertificateErrorSeverity.MEDIUM
|
|
322
|
+
recoverable = True
|
|
323
|
+
suggestions = [
|
|
324
|
+
"Client certificate not found",
|
|
325
|
+
"Check certificate file path",
|
|
326
|
+
"Generate a new client certificate if needed",
|
|
327
|
+
"Verify certificate file permissions",
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
elif error_type == CertificateErrorType.CLIENT_CERT_EXPIRED:
|
|
331
|
+
severity = CertificateErrorSeverity.MEDIUM
|
|
332
|
+
recoverable = True
|
|
333
|
+
suggestions = [
|
|
334
|
+
"Client certificate has expired",
|
|
335
|
+
"Generate a new client certificate",
|
|
336
|
+
"Check certificate validity period",
|
|
337
|
+
"Update certificate before expiry",
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
elif error_type == CertificateErrorType.CLIENT_CERT_REJECTED:
|
|
341
|
+
severity = CertificateErrorSeverity.HIGH
|
|
342
|
+
recoverable = True
|
|
343
|
+
suggestions = [
|
|
344
|
+
"Server rejected the client certificate",
|
|
345
|
+
"Verify certificate is valid for this server",
|
|
346
|
+
"Check certificate scope and permissions",
|
|
347
|
+
"Contact server administrator for certificate requirements",
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
else:
|
|
351
|
+
severity = CertificateErrorSeverity.MEDIUM
|
|
352
|
+
recoverable = True
|
|
353
|
+
suggestions = [
|
|
354
|
+
"Client certificate error",
|
|
355
|
+
"Check certificate file format",
|
|
356
|
+
"Verify certificate and key match",
|
|
357
|
+
"Generate a new certificate if needed",
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
cert_error = CertificateError(
|
|
361
|
+
error_type=error_type,
|
|
362
|
+
message=message,
|
|
363
|
+
severity=severity,
|
|
364
|
+
host=host,
|
|
365
|
+
port=port,
|
|
366
|
+
details=details,
|
|
367
|
+
timestamp=time.time(),
|
|
368
|
+
recoverable=recoverable,
|
|
369
|
+
recovery_suggestions=suggestions,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
self._record_error(cert_error)
|
|
373
|
+
return cert_error
|
|
374
|
+
|
|
375
|
+
def handle_certificate_expiry(
|
|
376
|
+
self,
|
|
377
|
+
host: str,
|
|
378
|
+
port: int,
|
|
379
|
+
not_before: str,
|
|
380
|
+
not_after: str,
|
|
381
|
+
current_time: Optional[float] = None,
|
|
382
|
+
) -> CertificateError:
|
|
383
|
+
"""Handle certificate expiry errors.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
host: Target hostname
|
|
387
|
+
port: Target port
|
|
388
|
+
not_before: Certificate validity start
|
|
389
|
+
not_after: Certificate validity end
|
|
390
|
+
current_time: Current timestamp (defaults to now)
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
CertificateError for expiry issue
|
|
394
|
+
"""
|
|
395
|
+
current_time = current_time or time.time()
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
# Parse certificate dates (assuming ISO format or timestamp)
|
|
399
|
+
if not_before.isdigit():
|
|
400
|
+
not_before_ts = float(not_before)
|
|
401
|
+
else:
|
|
402
|
+
# Try to parse as timestamp, if that fails, raise error
|
|
403
|
+
raise ValueError(f"Invalid date format: {not_before}")
|
|
404
|
+
|
|
405
|
+
if not_after.isdigit():
|
|
406
|
+
not_after_ts = float(not_after)
|
|
407
|
+
# Check if certificate has expired
|
|
408
|
+
if current_time > not_after_ts:
|
|
409
|
+
# Certificate has expired - create error immediately
|
|
410
|
+
cert_error = CertificateError(
|
|
411
|
+
error_type=CertificateErrorType.CERTIFICATE_EXPIRED,
|
|
412
|
+
message=f"Certificate expired (expired on {not_after})",
|
|
413
|
+
severity=CertificateErrorSeverity.HIGH,
|
|
414
|
+
host=host,
|
|
415
|
+
port=port,
|
|
416
|
+
details={
|
|
417
|
+
"not_before": not_before,
|
|
418
|
+
"not_after": not_after,
|
|
419
|
+
"current_time": current_time,
|
|
420
|
+
},
|
|
421
|
+
timestamp=time.time(),
|
|
422
|
+
recoverable=True,
|
|
423
|
+
recovery_suggestions=[
|
|
424
|
+
"Certificate has expired",
|
|
425
|
+
"Contact server administrator for certificate renewal",
|
|
426
|
+
"Check if server has updated certificate",
|
|
427
|
+
"Verify system clock is correct",
|
|
428
|
+
],
|
|
429
|
+
)
|
|
430
|
+
self._record_error(cert_error)
|
|
431
|
+
return cert_error
|
|
432
|
+
else:
|
|
433
|
+
# Try to parse as timestamp, if that fails, raise error
|
|
434
|
+
raise ValueError(f"Invalid date format: {not_after}")
|
|
435
|
+
|
|
436
|
+
if current_time < not_before_ts:
|
|
437
|
+
error_type = CertificateErrorType.CERTIFICATE_NOT_YET_VALID
|
|
438
|
+
message = f"Certificate not yet valid (valid from {not_before})"
|
|
439
|
+
suggestions = [
|
|
440
|
+
"Certificate is not yet valid",
|
|
441
|
+
"Check system clock is correct",
|
|
442
|
+
"Wait until certificate becomes valid",
|
|
443
|
+
"Contact server administrator if clock is correct",
|
|
444
|
+
]
|
|
445
|
+
else:
|
|
446
|
+
error_type = CertificateErrorType.CERTIFICATE_EXPIRED
|
|
447
|
+
message = f"Certificate expired (expired on {not_after})"
|
|
448
|
+
suggestions = [
|
|
449
|
+
"Certificate has expired",
|
|
450
|
+
"Contact server administrator for certificate renewal",
|
|
451
|
+
"Check if server has updated certificate",
|
|
452
|
+
"Verify system clock is correct",
|
|
453
|
+
]
|
|
454
|
+
|
|
455
|
+
except (ValueError, TypeError):
|
|
456
|
+
error_type = CertificateErrorType.CERTIFICATE_PARSE_ERROR
|
|
457
|
+
message = "Could not parse certificate validity dates"
|
|
458
|
+
suggestions = [
|
|
459
|
+
"Certificate date parsing failed",
|
|
460
|
+
"Certificate may be malformed",
|
|
461
|
+
"Contact server administrator",
|
|
462
|
+
"Check certificate format",
|
|
463
|
+
]
|
|
464
|
+
|
|
465
|
+
cert_error = CertificateError(
|
|
466
|
+
error_type=error_type,
|
|
467
|
+
message=message,
|
|
468
|
+
severity=CertificateErrorSeverity.HIGH,
|
|
469
|
+
host=host,
|
|
470
|
+
port=port,
|
|
471
|
+
details={
|
|
472
|
+
"not_before": not_before,
|
|
473
|
+
"not_after": not_after,
|
|
474
|
+
"current_time": current_time,
|
|
475
|
+
},
|
|
476
|
+
timestamp=time.time(),
|
|
477
|
+
recoverable=True,
|
|
478
|
+
recovery_suggestions=suggestions,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
self._record_error(cert_error)
|
|
482
|
+
return cert_error
|
|
483
|
+
|
|
484
|
+
def _record_error(self, error: CertificateError) -> None:
|
|
485
|
+
"""Record error for tracking and analysis.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
error: Certificate error to record
|
|
489
|
+
"""
|
|
490
|
+
host_key = f"{error.host}:{error.port}"
|
|
491
|
+
|
|
492
|
+
# Update error count
|
|
493
|
+
self._error_counts[host_key] = self._error_counts.get(host_key, 0) + 1
|
|
494
|
+
|
|
495
|
+
# Store last error
|
|
496
|
+
self._last_errors[host_key] = error
|
|
497
|
+
|
|
498
|
+
# Log error
|
|
499
|
+
logger.error(
|
|
500
|
+
"Certificate validation error",
|
|
501
|
+
error_type=error.error_type.value,
|
|
502
|
+
severity=error.severity.value,
|
|
503
|
+
host=error.host,
|
|
504
|
+
port=error.port,
|
|
505
|
+
message=error.message,
|
|
506
|
+
recoverable=error.recoverable,
|
|
507
|
+
error_count=self._error_counts[host_key],
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
def get_error_statistics(self) -> Dict[str, Any]:
|
|
511
|
+
"""Get error statistics for monitoring.
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
Dictionary with error statistics
|
|
515
|
+
"""
|
|
516
|
+
total_errors = sum(self._error_counts.values())
|
|
517
|
+
error_types: Dict[str, int] = {}
|
|
518
|
+
|
|
519
|
+
for error in self._last_errors.values():
|
|
520
|
+
error_type = error.error_type.value
|
|
521
|
+
error_types[error_type] = error_types.get(error_type, 0) + 1
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
"total_errors": total_errors,
|
|
525
|
+
"unique_hosts": len(self._error_counts),
|
|
526
|
+
"error_types": error_types,
|
|
527
|
+
"hosts_with_errors": list(self._error_counts.keys()),
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
def get_last_error(self, host: str, port: int) -> Optional[CertificateError]:
|
|
531
|
+
"""Get the last error for a specific host.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
host: Target hostname
|
|
535
|
+
port: Target port
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Last CertificateError for the host or None
|
|
539
|
+
"""
|
|
540
|
+
host_key = f"{host}:{port}"
|
|
541
|
+
return self._last_errors.get(host_key)
|
|
542
|
+
|
|
543
|
+
def clear_errors(
|
|
544
|
+
self, host: Optional[str] = None, port: Optional[int] = None
|
|
545
|
+
) -> None:
|
|
546
|
+
"""Clear error records.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
host: Specific host to clear (clears all if None)
|
|
550
|
+
port: Specific port to clear (clears all if None)
|
|
551
|
+
"""
|
|
552
|
+
if host and port:
|
|
553
|
+
host_key = f"{host}:{port}"
|
|
554
|
+
self._error_counts.pop(host_key, None)
|
|
555
|
+
self._last_errors.pop(host_key, None)
|
|
556
|
+
else:
|
|
557
|
+
self._error_counts.clear()
|
|
558
|
+
self._last_errors.clear()
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# Global error handler instance
|
|
562
|
+
certificate_error_handler = CertificateErrorHandler()
|