deepctl-cmd-debug-network 0.1.0__tar.gz
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.
- deepctl_cmd_debug_network-0.1.0/PKG-INFO +26 -0
- deepctl_cmd_debug_network-0.1.0/README.md +3 -0
- deepctl_cmd_debug_network-0.1.0/pyproject.toml +43 -0
- deepctl_cmd_debug_network-0.1.0/setup.cfg +4 -0
- deepctl_cmd_debug_network-0.1.0/src/deepctl_cmd_debug_network/__init__.py +5 -0
- deepctl_cmd_debug_network-0.1.0/src/deepctl_cmd_debug_network/command.py +982 -0
- deepctl_cmd_debug_network-0.1.0/src/deepctl_cmd_debug_network/models.py +121 -0
- deepctl_cmd_debug_network-0.1.0/src/deepctl_cmd_debug_network.egg-info/PKG-INFO +26 -0
- deepctl_cmd_debug_network-0.1.0/src/deepctl_cmd_debug_network.egg-info/SOURCES.txt +11 -0
- deepctl_cmd_debug_network-0.1.0/src/deepctl_cmd_debug_network.egg-info/dependency_links.txt +1 -0
- deepctl_cmd_debug_network-0.1.0/src/deepctl_cmd_debug_network.egg-info/entry_points.txt +2 -0
- deepctl_cmd_debug_network-0.1.0/src/deepctl_cmd_debug_network.egg-info/requires.txt +6 -0
- deepctl_cmd_debug_network-0.1.0/src/deepctl_cmd_debug_network.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepctl-cmd-debug-network
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Network debug subcommand for deepctl
|
|
5
|
+
Author-email: Deepgram <devrel@deepgram.com>
|
|
6
|
+
Maintainer-email: Deepgram <devrel@deepgram.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: deepgram,cli,debug,network,diagnostics
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: deepctl-core>=0.1.0
|
|
18
|
+
Requires-Dist: click>=8.0.0
|
|
19
|
+
Requires-Dist: rich>=13.0.0
|
|
20
|
+
Requires-Dist: pydantic>=2.0.0
|
|
21
|
+
Requires-Dist: httpx>=0.24.0
|
|
22
|
+
Requires-Dist: requests<3.0,>=2.31
|
|
23
|
+
|
|
24
|
+
# deepctl-cmd-debug-network
|
|
25
|
+
|
|
26
|
+
Network debug subcommand for the Deepgram CLI (deepctl). This package provides the `network` subcommand under the `debug` command group, allowing users to debug network issues or problems connecting to api.deepgram.com
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "deepctl-cmd-debug-network"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Network debug subcommand for deepctl"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{ name = "Deepgram", email = "devrel@deepgram.com" }]
|
|
12
|
+
maintainers = [{ name = "Deepgram", email = "devrel@deepgram.com" }]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
]
|
|
21
|
+
keywords = ["deepgram", "cli", "debug", "network", "diagnostics"]
|
|
22
|
+
requires-python = ">=3.10"
|
|
23
|
+
dependencies = [
|
|
24
|
+
"deepctl-core>=0.1.0",
|
|
25
|
+
"click>=8.0.0",
|
|
26
|
+
"rich>=13.0.0",
|
|
27
|
+
"pydantic>=2.0.0",
|
|
28
|
+
"httpx>=0.24.0",
|
|
29
|
+
"requests>=2.31,<3.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
# Internal commands not meant to be called directly
|
|
34
|
+
|
|
35
|
+
[project.entry-points."deepctl.subcommands.debug"]
|
|
36
|
+
network = "deepctl_cmd_debug_network.command:NetworkCommand"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools]
|
|
39
|
+
package-dir = { "" = "src" }
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
include = ["deepctl_cmd_debug_network*"]
|
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
"""Network debug command for deepctl."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import socket
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import warnings
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
import urllib3
|
|
15
|
+
from deepctl_core import AuthManager, BaseCommand, Config, DeepgramClient
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
19
|
+
|
|
20
|
+
from .models import (
|
|
21
|
+
CertificateInfo,
|
|
22
|
+
CommandExecutionResult,
|
|
23
|
+
DNSResult,
|
|
24
|
+
EndpointTestResult,
|
|
25
|
+
NetworkDebugResult,
|
|
26
|
+
PythonRequestsTest,
|
|
27
|
+
RevocationEndpointTest,
|
|
28
|
+
TLSTestResult,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
console = Console()
|
|
32
|
+
|
|
33
|
+
# Default Deepgram endpoints to test
|
|
34
|
+
DEEPGRAM_ENDPOINTS = {
|
|
35
|
+
"api": "https://api.deepgram.com",
|
|
36
|
+
"auth": "https://auth.dx.deepgram.com",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class NetworkCommand(BaseCommand):
|
|
41
|
+
"""Debug network connectivity issues with Deepgram services."""
|
|
42
|
+
|
|
43
|
+
name = "network"
|
|
44
|
+
help = "Debug network connectivity issues with Deepgram services"
|
|
45
|
+
short_help = "Debug network issues"
|
|
46
|
+
|
|
47
|
+
# Network debug doesn't require auth
|
|
48
|
+
requires_auth = False
|
|
49
|
+
requires_project = False
|
|
50
|
+
ci_friendly = True
|
|
51
|
+
|
|
52
|
+
def get_arguments(self) -> list[dict[str, Any]]:
|
|
53
|
+
"""Get command arguments and options."""
|
|
54
|
+
return [
|
|
55
|
+
{
|
|
56
|
+
"names": ["--endpoint", "-e"],
|
|
57
|
+
"help": (
|
|
58
|
+
"Specific endpoint to test " "(default: api.deepgram.com)"
|
|
59
|
+
),
|
|
60
|
+
"type": str,
|
|
61
|
+
"is_option": True,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"names": ["--verbose", "-v"],
|
|
65
|
+
"help": "Show detailed diagnostic information",
|
|
66
|
+
"is_flag": True,
|
|
67
|
+
"is_option": True,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"names": ["--skip-commands"],
|
|
71
|
+
"help": "Skip system command execution (openssl, curl, etc.)",
|
|
72
|
+
"is_flag": True,
|
|
73
|
+
"is_option": True,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"names": ["--timeout"],
|
|
77
|
+
"help": (
|
|
78
|
+
"Timeout in seconds for network operations "
|
|
79
|
+
"(default: 10)"
|
|
80
|
+
),
|
|
81
|
+
"type": int,
|
|
82
|
+
"default": 10,
|
|
83
|
+
"is_option": True,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"names": ["--simulate-blocked-crl"],
|
|
87
|
+
"help": "Simulate blocked CRL/OCSP endpoints (for testing)",
|
|
88
|
+
"is_flag": True,
|
|
89
|
+
"is_option": True,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"names": ["--save-report"],
|
|
93
|
+
"help": "Save full diagnostic report to file",
|
|
94
|
+
"type": str,
|
|
95
|
+
"is_option": True,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"names": ["--test-websocket"],
|
|
99
|
+
"help": "Also test WebSocket connectivity (wss://)",
|
|
100
|
+
"is_flag": True,
|
|
101
|
+
"is_option": True,
|
|
102
|
+
},
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
def handle(
|
|
106
|
+
self,
|
|
107
|
+
config: Config,
|
|
108
|
+
auth_manager: AuthManager,
|
|
109
|
+
client: DeepgramClient,
|
|
110
|
+
**kwargs: Any,
|
|
111
|
+
) -> Any:
|
|
112
|
+
"""Handle network debug command execution."""
|
|
113
|
+
endpoint = kwargs.get("endpoint") or "api.deepgram.com"
|
|
114
|
+
verbose = kwargs.get("verbose", False)
|
|
115
|
+
skip_commands = kwargs.get("skip_commands", False)
|
|
116
|
+
timeout = kwargs.get("timeout", 10)
|
|
117
|
+
simulate_blocked_crl = kwargs.get("simulate_blocked_crl", False)
|
|
118
|
+
save_report = kwargs.get("save_report")
|
|
119
|
+
test_websocket = kwargs.get("test_websocket", False)
|
|
120
|
+
|
|
121
|
+
# Initialize result
|
|
122
|
+
result = NetworkDebugResult(status="success")
|
|
123
|
+
|
|
124
|
+
# Show header
|
|
125
|
+
console.print(
|
|
126
|
+
Panel.fit(
|
|
127
|
+
"[bold cyan]🔍 Deepgram Network Diagnostics[/bold cyan]\n\n"
|
|
128
|
+
f"[dim]Testing connectivity to:[/dim] "
|
|
129
|
+
f"[yellow]{endpoint}[/yellow]\n"
|
|
130
|
+
"[dim]Running comprehensive TLS/SSL analysis...[/dim]",
|
|
131
|
+
title="Network Debug",
|
|
132
|
+
border_style="cyan",
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
with Progress(
|
|
137
|
+
SpinnerColumn(),
|
|
138
|
+
TextColumn("[progress.description]{task.description}"),
|
|
139
|
+
console=console,
|
|
140
|
+
) as progress:
|
|
141
|
+
|
|
142
|
+
# 1. Collect environment information
|
|
143
|
+
task = progress.add_task(
|
|
144
|
+
"Collecting environment information...", total=None
|
|
145
|
+
)
|
|
146
|
+
self._collect_environment_info(result)
|
|
147
|
+
progress.remove_task(task)
|
|
148
|
+
|
|
149
|
+
# 2. DNS Resolution
|
|
150
|
+
task = progress.add_task(
|
|
151
|
+
"Performing DNS resolution...", total=None
|
|
152
|
+
)
|
|
153
|
+
self._test_dns_resolution(endpoint, result, timeout)
|
|
154
|
+
progress.remove_task(task)
|
|
155
|
+
|
|
156
|
+
# 3. Basic connectivity test
|
|
157
|
+
task = progress.add_task(
|
|
158
|
+
"Testing basic connectivity...", total=None
|
|
159
|
+
)
|
|
160
|
+
self._test_basic_connectivity(endpoint, result, timeout)
|
|
161
|
+
progress.remove_task(task)
|
|
162
|
+
|
|
163
|
+
# 4. TLS/SSL analysis
|
|
164
|
+
if not skip_commands:
|
|
165
|
+
task = progress.add_task(
|
|
166
|
+
"Analyzing TLS/SSL certificates...", total=None
|
|
167
|
+
)
|
|
168
|
+
self._analyze_tls_certificates(
|
|
169
|
+
endpoint, result, verbose, simulate_blocked_crl
|
|
170
|
+
)
|
|
171
|
+
progress.remove_task(task)
|
|
172
|
+
|
|
173
|
+
# 5. Python requests tests
|
|
174
|
+
task = progress.add_task(
|
|
175
|
+
"Testing Python requests library...", total=None
|
|
176
|
+
)
|
|
177
|
+
self._test_python_requests(endpoint, result, timeout)
|
|
178
|
+
progress.remove_task(task)
|
|
179
|
+
|
|
180
|
+
# 6. System command tests
|
|
181
|
+
if not skip_commands:
|
|
182
|
+
task = progress.add_task(
|
|
183
|
+
"Running system diagnostics...", total=None
|
|
184
|
+
)
|
|
185
|
+
self._run_system_diagnostics(endpoint, result, verbose)
|
|
186
|
+
progress.remove_task(task)
|
|
187
|
+
|
|
188
|
+
# 7. WebSocket test (if requested)
|
|
189
|
+
if test_websocket:
|
|
190
|
+
task = progress.add_task(
|
|
191
|
+
"Testing WebSocket connectivity...", total=None
|
|
192
|
+
)
|
|
193
|
+
self._test_websocket_connectivity(endpoint, result, timeout)
|
|
194
|
+
progress.remove_task(task)
|
|
195
|
+
|
|
196
|
+
# Display results
|
|
197
|
+
self._display_results(result, verbose)
|
|
198
|
+
|
|
199
|
+
# Generate recommendations
|
|
200
|
+
self._generate_recommendations(result)
|
|
201
|
+
|
|
202
|
+
# Save report if requested
|
|
203
|
+
if save_report:
|
|
204
|
+
self._save_report(result, save_report, verbose)
|
|
205
|
+
|
|
206
|
+
return result
|
|
207
|
+
|
|
208
|
+
def _collect_environment_info(self, result: NetworkDebugResult) -> None:
|
|
209
|
+
"""Collect environment information."""
|
|
210
|
+
result.environment_info = {
|
|
211
|
+
"python_version": sys.version,
|
|
212
|
+
"platform": sys.platform,
|
|
213
|
+
"requests_version": requests.__version__,
|
|
214
|
+
"urllib3_version": getattr(urllib3, "__version__", "unknown"),
|
|
215
|
+
"proxy_env_vars": {
|
|
216
|
+
"http_proxy": os.environ.get("http_proxy", "Not set"),
|
|
217
|
+
"https_proxy": os.environ.get("https_proxy", "Not set"),
|
|
218
|
+
"no_proxy": os.environ.get("no_proxy", "Not set"),
|
|
219
|
+
"HTTP_PROXY": os.environ.get("HTTP_PROXY", "Not set"),
|
|
220
|
+
"HTTPS_PROXY": os.environ.get("HTTPS_PROXY", "Not set"),
|
|
221
|
+
"NO_PROXY": os.environ.get("NO_PROXY", "Not set"),
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Check if proxy is configured
|
|
226
|
+
if any(
|
|
227
|
+
os.environ.get(var)
|
|
228
|
+
for var in [
|
|
229
|
+
"http_proxy",
|
|
230
|
+
"https_proxy",
|
|
231
|
+
"HTTP_PROXY",
|
|
232
|
+
"HTTPS_PROXY",
|
|
233
|
+
]
|
|
234
|
+
):
|
|
235
|
+
result.proxy_detected = True
|
|
236
|
+
result.proxy_settings = result.environment_info["proxy_env_vars"]
|
|
237
|
+
|
|
238
|
+
def _test_dns_resolution(
|
|
239
|
+
self, hostname: str, result: NetworkDebugResult, timeout: int
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Test DNS resolution for the hostname."""
|
|
242
|
+
dns_result = DNSResult(hostname=hostname, resolved=False)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
start_time = time.time()
|
|
246
|
+
ip_addresses = socket.getaddrinfo(hostname, None)
|
|
247
|
+
dns_result.resolution_time_ms = (time.time() - start_time) * 1000
|
|
248
|
+
|
|
249
|
+
# Extract unique IP addresses
|
|
250
|
+
ips = list({str(addr[4][0]) for addr in ip_addresses})
|
|
251
|
+
dns_result.ip_addresses = ips
|
|
252
|
+
dns_result.resolved = True
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
dns_result.error = str(e)
|
|
256
|
+
result.network_issues_detected = True
|
|
257
|
+
|
|
258
|
+
result.dns_results[hostname] = dns_result
|
|
259
|
+
|
|
260
|
+
def _test_basic_connectivity(
|
|
261
|
+
self, hostname: str, result: NetworkDebugResult, timeout: int
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Test basic HTTPS connectivity."""
|
|
264
|
+
endpoint_test = EndpointTestResult(
|
|
265
|
+
name="HTTPS", url=f"https://{hostname}", reachable=False
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
start_time = time.time()
|
|
270
|
+
response = requests.get(
|
|
271
|
+
f"https://{hostname}/", timeout=timeout, allow_redirects=False
|
|
272
|
+
)
|
|
273
|
+
endpoint_test.response_time_ms = (time.time() - start_time) * 1000
|
|
274
|
+
endpoint_test.status_code = response.status_code
|
|
275
|
+
endpoint_test.reachable = True
|
|
276
|
+
endpoint_test.ssl_valid = True
|
|
277
|
+
|
|
278
|
+
except requests.exceptions.SSLError as e:
|
|
279
|
+
endpoint_test.error = f"SSL Error: {e!s}"
|
|
280
|
+
endpoint_test.ssl_valid = False
|
|
281
|
+
result.network_issues_detected = True
|
|
282
|
+
except Exception as e:
|
|
283
|
+
endpoint_test.error = str(e)
|
|
284
|
+
result.network_issues_detected = True
|
|
285
|
+
|
|
286
|
+
result.endpoint_results.append(endpoint_test)
|
|
287
|
+
|
|
288
|
+
def _analyze_tls_certificates(
|
|
289
|
+
self,
|
|
290
|
+
hostname: str,
|
|
291
|
+
result: NetworkDebugResult,
|
|
292
|
+
verbose: bool,
|
|
293
|
+
simulate_blocked_crl: bool = False,
|
|
294
|
+
) -> None:
|
|
295
|
+
"""Analyze TLS certificates and check revocation endpoints."""
|
|
296
|
+
tls_result = TLSTestResult(
|
|
297
|
+
hostname=hostname, port=443, connected=False
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Get certificate chain using openssl
|
|
301
|
+
try:
|
|
302
|
+
# First, get the full certificate chain
|
|
303
|
+
cmd = [
|
|
304
|
+
"openssl",
|
|
305
|
+
"s_client",
|
|
306
|
+
"-connect",
|
|
307
|
+
f"{hostname}:443",
|
|
308
|
+
"-showcerts",
|
|
309
|
+
]
|
|
310
|
+
proc = subprocess.run(
|
|
311
|
+
cmd, input=b"", capture_output=True, timeout=10
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
if verbose:
|
|
315
|
+
tls_result.raw_openssl_output = proc.stdout.decode()
|
|
316
|
+
|
|
317
|
+
if proc.returncode == 0:
|
|
318
|
+
tls_result.connected = True
|
|
319
|
+
|
|
320
|
+
# Parse certificates from output
|
|
321
|
+
self._parse_certificate_chain(proc.stdout.decode(), tls_result)
|
|
322
|
+
|
|
323
|
+
# Test revocation endpoints
|
|
324
|
+
self._test_revocation_endpoints(
|
|
325
|
+
tls_result, result, simulate_blocked_crl
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
else:
|
|
329
|
+
tls_result.chain_errors.append(
|
|
330
|
+
f"OpenSSL connection failed: {proc.stderr.decode()}"
|
|
331
|
+
)
|
|
332
|
+
result.network_issues_detected = True
|
|
333
|
+
|
|
334
|
+
except subprocess.TimeoutExpired:
|
|
335
|
+
tls_result.chain_errors.append("OpenSSL command timed out")
|
|
336
|
+
result.network_issues_detected = True
|
|
337
|
+
except Exception as e:
|
|
338
|
+
tls_result.chain_errors.append(f"Error running OpenSSL: {e!s}")
|
|
339
|
+
result.network_issues_detected = True
|
|
340
|
+
|
|
341
|
+
result.tls_test_results[hostname] = tls_result
|
|
342
|
+
|
|
343
|
+
def _parse_certificate_chain(
|
|
344
|
+
self, openssl_output: str, tls_result: TLSTestResult
|
|
345
|
+
) -> None:
|
|
346
|
+
"""Parse certificate chain from OpenSSL output."""
|
|
347
|
+
# Extract certificates
|
|
348
|
+
cert_pattern = re.compile(
|
|
349
|
+
r"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----",
|
|
350
|
+
re.DOTALL,
|
|
351
|
+
)
|
|
352
|
+
certificates = cert_pattern.findall(openssl_output)
|
|
353
|
+
|
|
354
|
+
for i, cert_pem in enumerate(certificates):
|
|
355
|
+
cert_info = CertificateInfo(subject="", issuer="")
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
# Get certificate details
|
|
359
|
+
proc = subprocess.run(
|
|
360
|
+
[
|
|
361
|
+
"openssl",
|
|
362
|
+
"x509",
|
|
363
|
+
"-noout",
|
|
364
|
+
"-subject",
|
|
365
|
+
"-issuer",
|
|
366
|
+
"-ext",
|
|
367
|
+
"crlDistributionPoints,authorityInfoAccess",
|
|
368
|
+
],
|
|
369
|
+
input=cert_pem.encode(),
|
|
370
|
+
capture_output=True,
|
|
371
|
+
timeout=5,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
if proc.returncode == 0:
|
|
375
|
+
output = proc.stdout.decode()
|
|
376
|
+
|
|
377
|
+
# Parse subject and issuer
|
|
378
|
+
subject_match = re.search(r"subject=(.+)", output)
|
|
379
|
+
issuer_match = re.search(r"issuer=(.+)", output)
|
|
380
|
+
|
|
381
|
+
if subject_match:
|
|
382
|
+
cert_info.subject = subject_match.group(1).strip()
|
|
383
|
+
if issuer_match:
|
|
384
|
+
cert_info.issuer = issuer_match.group(1).strip()
|
|
385
|
+
|
|
386
|
+
# Parse CRL distribution points
|
|
387
|
+
crl_matches = re.findall(r"URI:(.+)", output)
|
|
388
|
+
for url in crl_matches:
|
|
389
|
+
url = url.strip()
|
|
390
|
+
if url.startswith("http://") and "c.lencr.org" in url:
|
|
391
|
+
cert_info.crl_distribution_points.append(url)
|
|
392
|
+
|
|
393
|
+
# Parse OCSP and CA issuer URLs
|
|
394
|
+
ocsp_matches = re.findall(r"OCSP - URI:(.+)", output)
|
|
395
|
+
cert_info.ocsp_urls = [url.strip() for url in ocsp_matches]
|
|
396
|
+
|
|
397
|
+
ca_issuer_matches = re.findall(
|
|
398
|
+
r"CA Issuers - URI:(.+)", output
|
|
399
|
+
)
|
|
400
|
+
cert_info.ca_issuer_urls = [
|
|
401
|
+
url.strip() for url in ca_issuer_matches
|
|
402
|
+
]
|
|
403
|
+
|
|
404
|
+
tls_result.certificate_chain.append(cert_info)
|
|
405
|
+
|
|
406
|
+
except Exception as e:
|
|
407
|
+
tls_result.chain_errors.append(
|
|
408
|
+
f"Error parsing certificate {i}: {e!s}"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def _test_revocation_endpoints(
|
|
412
|
+
self,
|
|
413
|
+
tls_result: TLSTestResult,
|
|
414
|
+
result: NetworkDebugResult,
|
|
415
|
+
simulate_blocked_crl: bool = False,
|
|
416
|
+
) -> None:
|
|
417
|
+
"""Test accessibility of CRL/OCSP/CA issuer endpoints."""
|
|
418
|
+
all_endpoints = []
|
|
419
|
+
|
|
420
|
+
for cert in tls_result.certificate_chain:
|
|
421
|
+
# Add OCSP endpoints
|
|
422
|
+
for url in cert.ocsp_urls:
|
|
423
|
+
all_endpoints.append((url, "ocsp"))
|
|
424
|
+
|
|
425
|
+
# Add CRL endpoints
|
|
426
|
+
for url in cert.crl_distribution_points:
|
|
427
|
+
all_endpoints.append((url, "crl"))
|
|
428
|
+
|
|
429
|
+
# Add CA issuer endpoints
|
|
430
|
+
for url in cert.ca_issuer_urls:
|
|
431
|
+
all_endpoints.append((url, "ca_issuer"))
|
|
432
|
+
|
|
433
|
+
# Test each unique endpoint
|
|
434
|
+
tested_urls = set()
|
|
435
|
+
for url, endpoint_type in all_endpoints:
|
|
436
|
+
if url in tested_urls:
|
|
437
|
+
continue
|
|
438
|
+
tested_urls.add(url)
|
|
439
|
+
|
|
440
|
+
endpoint_test = RevocationEndpointTest(
|
|
441
|
+
url=url, endpoint_type=endpoint_type, accessible=False
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Simulate blocked CRL/OCSP endpoints for testing
|
|
445
|
+
if simulate_blocked_crl and endpoint_type in ["crl", "ocsp"]:
|
|
446
|
+
endpoint_test.error = (
|
|
447
|
+
"Connection timed out (simulated corporate "
|
|
448
|
+
"firewall blocking)"
|
|
449
|
+
)
|
|
450
|
+
endpoint_test.accessible = False
|
|
451
|
+
result.network_issues_detected = True
|
|
452
|
+
else:
|
|
453
|
+
try:
|
|
454
|
+
start_time = time.time()
|
|
455
|
+
response = requests.head(
|
|
456
|
+
url, timeout=5, allow_redirects=True
|
|
457
|
+
)
|
|
458
|
+
endpoint_test.response_time_ms = (
|
|
459
|
+
time.time() - start_time
|
|
460
|
+
) * 1000
|
|
461
|
+
endpoint_test.status_code = response.status_code
|
|
462
|
+
endpoint_test.accessible = response.status_code < 400
|
|
463
|
+
|
|
464
|
+
except Exception as e:
|
|
465
|
+
endpoint_test.error = str(e)
|
|
466
|
+
result.network_issues_detected = True
|
|
467
|
+
|
|
468
|
+
tls_result.revocation_endpoints.append(endpoint_test)
|
|
469
|
+
|
|
470
|
+
def _test_python_requests(
|
|
471
|
+
self, hostname: str, result: NetworkDebugResult, timeout: int
|
|
472
|
+
) -> None:
|
|
473
|
+
"""Test connectivity using Python requests library."""
|
|
474
|
+
# Test with SSL verification enabled
|
|
475
|
+
test_with_verify = PythonRequestsTest(
|
|
476
|
+
url=f"https://{hostname}/", success=False, ssl_verify_enabled=True
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
start_time = time.time()
|
|
481
|
+
response = requests.get(
|
|
482
|
+
f"https://{hostname}/",
|
|
483
|
+
timeout=timeout,
|
|
484
|
+
verify=True,
|
|
485
|
+
allow_redirects=False,
|
|
486
|
+
)
|
|
487
|
+
test_with_verify.response_time_ms = (
|
|
488
|
+
time.time() - start_time
|
|
489
|
+
) * 1000
|
|
490
|
+
test_with_verify.status_code = response.status_code
|
|
491
|
+
test_with_verify.success = True
|
|
492
|
+
|
|
493
|
+
except requests.exceptions.SSLError as e:
|
|
494
|
+
test_with_verify.error = f"SSL verification failed: {e!s}"
|
|
495
|
+
result.network_issues_detected = True
|
|
496
|
+
except Exception as e:
|
|
497
|
+
test_with_verify.error = str(e)
|
|
498
|
+
|
|
499
|
+
result.python_requests_tests.append(test_with_verify)
|
|
500
|
+
|
|
501
|
+
# Test with SSL verification disabled (for comparison)
|
|
502
|
+
test_without_verify = PythonRequestsTest(
|
|
503
|
+
url=f"https://{hostname}/", success=False, ssl_verify_enabled=False
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
try:
|
|
507
|
+
# Disable SSL warnings temporarily
|
|
508
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
509
|
+
|
|
510
|
+
start_time = time.time()
|
|
511
|
+
response = requests.get(
|
|
512
|
+
f"https://{hostname}/",
|
|
513
|
+
timeout=timeout,
|
|
514
|
+
verify=False,
|
|
515
|
+
allow_redirects=False,
|
|
516
|
+
)
|
|
517
|
+
test_without_verify.response_time_ms = (
|
|
518
|
+
time.time() - start_time
|
|
519
|
+
) * 1000
|
|
520
|
+
test_without_verify.status_code = response.status_code
|
|
521
|
+
test_without_verify.success = True
|
|
522
|
+
|
|
523
|
+
except Exception as e:
|
|
524
|
+
test_without_verify.error = str(e)
|
|
525
|
+
finally:
|
|
526
|
+
# Re-enable warnings
|
|
527
|
+
warnings.resetwarnings()
|
|
528
|
+
|
|
529
|
+
result.python_requests_tests.append(test_without_verify)
|
|
530
|
+
|
|
531
|
+
def _run_system_diagnostics(
|
|
532
|
+
self, hostname: str, result: NetworkDebugResult, verbose: bool
|
|
533
|
+
) -> None:
|
|
534
|
+
"""Run system diagnostic commands."""
|
|
535
|
+
commands = [
|
|
536
|
+
# Basic connectivity
|
|
537
|
+
["ping", "-c", "3", hostname],
|
|
538
|
+
# Traceroute (platform-specific)
|
|
539
|
+
(
|
|
540
|
+
["traceroute", "-m", "10", hostname]
|
|
541
|
+
if sys.platform != "win32"
|
|
542
|
+
else ["tracert", "-h", "10", hostname]
|
|
543
|
+
),
|
|
544
|
+
# OpenSSL connection test
|
|
545
|
+
[
|
|
546
|
+
"openssl",
|
|
547
|
+
"s_client",
|
|
548
|
+
"-connect",
|
|
549
|
+
f"{hostname}:443",
|
|
550
|
+
"-servername",
|
|
551
|
+
hostname,
|
|
552
|
+
],
|
|
553
|
+
# Curl test
|
|
554
|
+
["curl", "-I", "--connect-timeout", "10", f"https://{hostname}/"],
|
|
555
|
+
# Curl with verbose SSL info
|
|
556
|
+
["curl", "-v", "--connect-timeout", "10", f"https://{hostname}/"],
|
|
557
|
+
]
|
|
558
|
+
|
|
559
|
+
for cmd in commands:
|
|
560
|
+
# Skip commands that don't exist on the system
|
|
561
|
+
if not self._command_exists(cmd[0]):
|
|
562
|
+
continue
|
|
563
|
+
|
|
564
|
+
cmd_result = CommandExecutionResult(
|
|
565
|
+
command=" ".join(cmd), success=False, exit_code=-1
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
start_time = time.time()
|
|
570
|
+
|
|
571
|
+
# Special handling for openssl s_client (needs input)
|
|
572
|
+
if cmd[0] == "openssl" and "s_client" in cmd:
|
|
573
|
+
proc = subprocess.run(
|
|
574
|
+
cmd, input=b"", capture_output=True, timeout=15
|
|
575
|
+
)
|
|
576
|
+
else:
|
|
577
|
+
proc = subprocess.run(cmd, capture_output=True, timeout=15)
|
|
578
|
+
|
|
579
|
+
cmd_result.execution_time_ms = (
|
|
580
|
+
time.time() - start_time
|
|
581
|
+
) * 1000
|
|
582
|
+
cmd_result.exit_code = proc.returncode
|
|
583
|
+
cmd_result.success = proc.returncode == 0
|
|
584
|
+
|
|
585
|
+
# Limit output size for non-verbose mode
|
|
586
|
+
if verbose or cmd_result.success:
|
|
587
|
+
cmd_result.stdout = proc.stdout.decode()[:5000]
|
|
588
|
+
cmd_result.stderr = proc.stderr.decode()[:5000]
|
|
589
|
+
else:
|
|
590
|
+
cmd_result.stdout = proc.stdout.decode()[:1000]
|
|
591
|
+
cmd_result.stderr = proc.stderr.decode()[:1000]
|
|
592
|
+
|
|
593
|
+
except subprocess.TimeoutExpired:
|
|
594
|
+
cmd_result.stderr = "Command timed out"
|
|
595
|
+
except Exception as e:
|
|
596
|
+
cmd_result.stderr = str(e)
|
|
597
|
+
|
|
598
|
+
result.command_results.append(cmd_result)
|
|
599
|
+
|
|
600
|
+
def _command_exists(self, command: str) -> bool:
|
|
601
|
+
"""Check if a command exists on the system."""
|
|
602
|
+
try:
|
|
603
|
+
subprocess.run(
|
|
604
|
+
(
|
|
605
|
+
["which", command]
|
|
606
|
+
if sys.platform != "win32"
|
|
607
|
+
else ["where", command]
|
|
608
|
+
),
|
|
609
|
+
capture_output=True,
|
|
610
|
+
check=False,
|
|
611
|
+
)
|
|
612
|
+
return True
|
|
613
|
+
except Exception:
|
|
614
|
+
return False
|
|
615
|
+
|
|
616
|
+
def _test_websocket_connectivity(
|
|
617
|
+
self, hostname: str, result: NetworkDebugResult, timeout: int
|
|
618
|
+
) -> None:
|
|
619
|
+
"""Test WebSocket connectivity to the endpoint."""
|
|
620
|
+
ws_test = EndpointTestResult(
|
|
621
|
+
name="WebSocket",
|
|
622
|
+
url=f"wss://{hostname}/v1/listen",
|
|
623
|
+
reachable=False,
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
# Test with curl if available
|
|
627
|
+
if self._command_exists("curl"):
|
|
628
|
+
cmd_result = CommandExecutionResult(
|
|
629
|
+
command=(
|
|
630
|
+
f"curl -i -N -H 'Connection: Upgrade' "
|
|
631
|
+
f"-H 'Upgrade: websocket' "
|
|
632
|
+
f"-H 'Sec-WebSocket-Key: test' "
|
|
633
|
+
f"-H 'Sec-WebSocket-Version: 13' "
|
|
634
|
+
f"https://{hostname}/v1/listen"
|
|
635
|
+
),
|
|
636
|
+
success=False,
|
|
637
|
+
exit_code=-1,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
try:
|
|
641
|
+
start_time = time.time()
|
|
642
|
+
proc = subprocess.run(
|
|
643
|
+
[
|
|
644
|
+
"curl",
|
|
645
|
+
"-i",
|
|
646
|
+
"-N",
|
|
647
|
+
"-H",
|
|
648
|
+
"Connection: Upgrade",
|
|
649
|
+
"-H",
|
|
650
|
+
"Upgrade: websocket",
|
|
651
|
+
"-H",
|
|
652
|
+
"Sec-WebSocket-Key: test",
|
|
653
|
+
"-H",
|
|
654
|
+
"Sec-WebSocket-Version: 13",
|
|
655
|
+
f"https://{hostname}/v1/listen",
|
|
656
|
+
],
|
|
657
|
+
capture_output=True,
|
|
658
|
+
timeout=timeout,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
cmd_result.execution_time_ms = (
|
|
662
|
+
time.time() - start_time
|
|
663
|
+
) * 1000
|
|
664
|
+
cmd_result.exit_code = proc.returncode
|
|
665
|
+
cmd_result.success = proc.returncode == 0
|
|
666
|
+
cmd_result.stdout = proc.stdout.decode()[:1000]
|
|
667
|
+
cmd_result.stderr = proc.stderr.decode()[:1000]
|
|
668
|
+
|
|
669
|
+
# Check if we got a valid WebSocket upgrade response
|
|
670
|
+
if "HTTP" in cmd_result.stdout and (
|
|
671
|
+
"101" in cmd_result.stdout or "401" in cmd_result.stdout
|
|
672
|
+
):
|
|
673
|
+
ws_test.reachable = True
|
|
674
|
+
ws_test.status_code = (
|
|
675
|
+
101 if "101" in cmd_result.stdout else 401
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
except Exception as e:
|
|
679
|
+
cmd_result.stderr = str(e)
|
|
680
|
+
|
|
681
|
+
result.command_results.append(cmd_result)
|
|
682
|
+
|
|
683
|
+
result.endpoint_results.append(ws_test)
|
|
684
|
+
|
|
685
|
+
def _save_report(
|
|
686
|
+
self, result: NetworkDebugResult, filename: str, verbose: bool
|
|
687
|
+
) -> None:
|
|
688
|
+
"""Save diagnostic report to file."""
|
|
689
|
+
import json
|
|
690
|
+
from datetime import datetime
|
|
691
|
+
|
|
692
|
+
try:
|
|
693
|
+
# Prepare report data
|
|
694
|
+
report = {
|
|
695
|
+
"timestamp": datetime.now().isoformat(),
|
|
696
|
+
"result": result.model_dump(),
|
|
697
|
+
"verbose": verbose,
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
# Save to file
|
|
701
|
+
with open(filename, "w") as f:
|
|
702
|
+
json.dump(report, f, indent=2)
|
|
703
|
+
|
|
704
|
+
console.print(f"\n[green]✅ Report saved to: {filename}[/green]")
|
|
705
|
+
|
|
706
|
+
except Exception as e:
|
|
707
|
+
console.print(f"\n[red]❌ Failed to save report: {e!s}[/red]")
|
|
708
|
+
|
|
709
|
+
def _display_results(
|
|
710
|
+
self, result: NetworkDebugResult, verbose: bool
|
|
711
|
+
) -> None:
|
|
712
|
+
"""Display diagnostic results."""
|
|
713
|
+
console.print("\n[bold cyan]📊 Diagnostic Results[/bold cyan]\n")
|
|
714
|
+
|
|
715
|
+
# DNS Results
|
|
716
|
+
if result.dns_results:
|
|
717
|
+
console.print("[bold]DNS Resolution:[/bold]")
|
|
718
|
+
for hostname, dns_result in result.dns_results.items():
|
|
719
|
+
if dns_result.resolved:
|
|
720
|
+
console.print(
|
|
721
|
+
f" ✅ {hostname} → "
|
|
722
|
+
f"{', '.join(dns_result.ip_addresses)}"
|
|
723
|
+
)
|
|
724
|
+
if dns_result.resolution_time_ms:
|
|
725
|
+
console.print(
|
|
726
|
+
f" [dim]Resolution time: "
|
|
727
|
+
f"{dns_result.resolution_time_ms:.2f}ms[/dim]"
|
|
728
|
+
)
|
|
729
|
+
else:
|
|
730
|
+
console.print(f" ❌ {hostname} - {dns_result.error}")
|
|
731
|
+
console.print()
|
|
732
|
+
|
|
733
|
+
# Basic Connectivity
|
|
734
|
+
if result.endpoint_results:
|
|
735
|
+
console.print("[bold]Basic Connectivity:[/bold]")
|
|
736
|
+
for endpoint in result.endpoint_results:
|
|
737
|
+
if endpoint.reachable:
|
|
738
|
+
console.print(f" ✅ {endpoint.name} - {endpoint.url}")
|
|
739
|
+
if endpoint.response_time_ms:
|
|
740
|
+
console.print(
|
|
741
|
+
f" [dim]Response time: "
|
|
742
|
+
f"{endpoint.response_time_ms:.2f}ms[/dim]"
|
|
743
|
+
)
|
|
744
|
+
else:
|
|
745
|
+
console.print(f" ❌ {endpoint.name} - {endpoint.error}")
|
|
746
|
+
console.print()
|
|
747
|
+
|
|
748
|
+
# TLS/SSL Analysis
|
|
749
|
+
if result.tls_test_results:
|
|
750
|
+
console.print("[bold]TLS/SSL Certificate Chain:[/bold]")
|
|
751
|
+
for hostname, tls_result in result.tls_test_results.items():
|
|
752
|
+
if tls_result.connected:
|
|
753
|
+
console.print(f" ✅ Connected to {hostname}:443")
|
|
754
|
+
|
|
755
|
+
# Show certificate chain
|
|
756
|
+
for i, cert in enumerate(tls_result.certificate_chain):
|
|
757
|
+
console.print(
|
|
758
|
+
f"\n [bold]Certificate #{i + 1}:[/bold]"
|
|
759
|
+
)
|
|
760
|
+
console.print(
|
|
761
|
+
f" Subject: [yellow]{cert.subject}[/yellow]"
|
|
762
|
+
)
|
|
763
|
+
console.print(
|
|
764
|
+
f" Issuer: [cyan]{cert.issuer}[/cyan]"
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
if verbose:
|
|
768
|
+
if cert.ocsp_urls:
|
|
769
|
+
console.print(
|
|
770
|
+
f" OCSP URLs: "
|
|
771
|
+
f"{', '.join(cert.ocsp_urls)}"
|
|
772
|
+
)
|
|
773
|
+
if cert.ca_issuer_urls:
|
|
774
|
+
console.print(
|
|
775
|
+
f" CA Issuer URLs: "
|
|
776
|
+
f"{', '.join(cert.ca_issuer_urls)}"
|
|
777
|
+
)
|
|
778
|
+
if cert.crl_distribution_points:
|
|
779
|
+
crl_urls = ", ".join(
|
|
780
|
+
cert.crl_distribution_points
|
|
781
|
+
)
|
|
782
|
+
console.print(f" CRL URLs: {crl_urls}")
|
|
783
|
+
|
|
784
|
+
# Show revocation endpoint test results
|
|
785
|
+
if tls_result.revocation_endpoints:
|
|
786
|
+
console.print(
|
|
787
|
+
"\n [bold]Revocation Endpoint Tests:[/bold]"
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
# Group by type
|
|
791
|
+
by_type: dict[str, list[Any]] = {}
|
|
792
|
+
for rev_endpoint in tls_result.revocation_endpoints:
|
|
793
|
+
if rev_endpoint.endpoint_type not in by_type:
|
|
794
|
+
by_type[rev_endpoint.endpoint_type] = []
|
|
795
|
+
by_type[rev_endpoint.endpoint_type].append(
|
|
796
|
+
rev_endpoint
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
for endpoint_type, endpoints in by_type.items():
|
|
800
|
+
console.print(
|
|
801
|
+
f"\n [yellow]{endpoint_type.upper()} "
|
|
802
|
+
f"Endpoints:[/yellow]"
|
|
803
|
+
)
|
|
804
|
+
for endpoint in endpoints:
|
|
805
|
+
if endpoint.accessible:
|
|
806
|
+
console.print(f" ✅ {endpoint.url}")
|
|
807
|
+
if verbose and endpoint.response_time_ms:
|
|
808
|
+
resp_time = (
|
|
809
|
+
f"{endpoint.response_time_ms:.2f}"
|
|
810
|
+
)
|
|
811
|
+
console.print(
|
|
812
|
+
f" [dim]Response time: "
|
|
813
|
+
f"{resp_time}ms[/dim]"
|
|
814
|
+
)
|
|
815
|
+
else:
|
|
816
|
+
console.print(f" ❌ {endpoint.url}")
|
|
817
|
+
if endpoint.error:
|
|
818
|
+
console.print(
|
|
819
|
+
f" [red]Error: "
|
|
820
|
+
f"{endpoint.error}[/red]"
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
else:
|
|
824
|
+
console.print(f" ❌ Failed to connect to {hostname}:443")
|
|
825
|
+
for error in tls_result.chain_errors:
|
|
826
|
+
console.print(f" [red]{error}[/red]")
|
|
827
|
+
console.print()
|
|
828
|
+
|
|
829
|
+
# Python Requests Tests
|
|
830
|
+
if result.python_requests_tests:
|
|
831
|
+
console.print("[bold]Python Requests Library Tests:[/bold]")
|
|
832
|
+
for test in result.python_requests_tests:
|
|
833
|
+
verify_status = (
|
|
834
|
+
"with SSL verification"
|
|
835
|
+
if test.ssl_verify_enabled
|
|
836
|
+
else "without SSL verification"
|
|
837
|
+
)
|
|
838
|
+
if test.success:
|
|
839
|
+
console.print(
|
|
840
|
+
f" ✅ Successfully connected {verify_status}"
|
|
841
|
+
)
|
|
842
|
+
if test.response_time_ms:
|
|
843
|
+
console.print(
|
|
844
|
+
f" [dim]Response time: "
|
|
845
|
+
f"{test.response_time_ms:.2f}ms[/dim]"
|
|
846
|
+
)
|
|
847
|
+
else:
|
|
848
|
+
console.print(f" ❌ Failed to connect {verify_status}")
|
|
849
|
+
if test.error:
|
|
850
|
+
console.print(f" [red]{test.error}[/red]")
|
|
851
|
+
console.print()
|
|
852
|
+
|
|
853
|
+
# Proxy Detection Warning
|
|
854
|
+
if result.proxy_settings:
|
|
855
|
+
console.print(
|
|
856
|
+
"[bold yellow]⚠️ Proxy Configuration Detected:[/bold yellow]"
|
|
857
|
+
)
|
|
858
|
+
for var, value in result.proxy_settings.items():
|
|
859
|
+
if value and value != "Not set":
|
|
860
|
+
console.print(f" {var}: {value}")
|
|
861
|
+
console.print()
|
|
862
|
+
|
|
863
|
+
# Command Results (in verbose mode)
|
|
864
|
+
if verbose and result.command_results:
|
|
865
|
+
console.print("[bold]System Command Results:[/bold]")
|
|
866
|
+
for cmd_result in result.command_results:
|
|
867
|
+
status = "✅" if cmd_result.success else "❌"
|
|
868
|
+
console.print(
|
|
869
|
+
f"\n {status} Command: [cyan]{cmd_result.command}[/cyan]"
|
|
870
|
+
)
|
|
871
|
+
console.print(f" Exit code: {cmd_result.exit_code}")
|
|
872
|
+
if cmd_result.stdout.strip():
|
|
873
|
+
console.print(" [dim]Output:[/dim]")
|
|
874
|
+
for line in cmd_result.stdout.strip().split("\n")[:10]:
|
|
875
|
+
console.print(f" {line}")
|
|
876
|
+
if cmd_result.stderr.strip():
|
|
877
|
+
console.print(" [dim red]Errors:[/dim red]")
|
|
878
|
+
for line in cmd_result.stderr.strip().split("\n")[:5]:
|
|
879
|
+
console.print(f" {line}")
|
|
880
|
+
|
|
881
|
+
def _generate_recommendations(self, result: NetworkDebugResult) -> None:
|
|
882
|
+
"""Generate recommendations based on diagnostic results."""
|
|
883
|
+
recommendations = []
|
|
884
|
+
|
|
885
|
+
# Check DNS issues
|
|
886
|
+
for hostname, dns_result in result.dns_results.items():
|
|
887
|
+
if not dns_result.resolved:
|
|
888
|
+
recommendations.append(
|
|
889
|
+
f"❌ DNS resolution failed for {hostname}. "
|
|
890
|
+
f"Check your DNS settings and network connectivity."
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
# Check TLS/SSL issues
|
|
894
|
+
for hostname, tls_result in result.tls_test_results.items():
|
|
895
|
+
if not tls_result.connected:
|
|
896
|
+
recommendations.append(
|
|
897
|
+
f"❌ Failed to establish TLS connection to {hostname}. "
|
|
898
|
+
f"This may indicate a firewall or network issue."
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
# Check revocation endpoint accessibility
|
|
902
|
+
blocked_endpoints = []
|
|
903
|
+
for endpoint in tls_result.revocation_endpoints:
|
|
904
|
+
if not endpoint.accessible:
|
|
905
|
+
blocked_endpoints.append(endpoint)
|
|
906
|
+
|
|
907
|
+
if blocked_endpoints:
|
|
908
|
+
recommendations.append(
|
|
909
|
+
"⚠️ Certificate revocation endpoints are blocked. "
|
|
910
|
+
"This is a security issue that prevents proper "
|
|
911
|
+
"certificate validation."
|
|
912
|
+
)
|
|
913
|
+
recommendations.append(
|
|
914
|
+
" To fix this, ensure the following domains are "
|
|
915
|
+
"accessible on port 80 (HTTP):"
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
# Group by domain
|
|
919
|
+
domains = set()
|
|
920
|
+
for endpoint in blocked_endpoints:
|
|
921
|
+
parsed = urlparse(endpoint.url)
|
|
922
|
+
domains.add(parsed.netloc)
|
|
923
|
+
|
|
924
|
+
for domain in sorted(domains):
|
|
925
|
+
recommendations.append(f" • {domain}")
|
|
926
|
+
|
|
927
|
+
recommendations.append(
|
|
928
|
+
" These endpoints are required by Let's Encrypt for "
|
|
929
|
+
"certificate validation."
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
# Check Python SSL issues
|
|
933
|
+
ssl_verify_failed = False
|
|
934
|
+
ssl_noverify_success = False
|
|
935
|
+
|
|
936
|
+
for test in result.python_requests_tests:
|
|
937
|
+
if (
|
|
938
|
+
test.ssl_verify_enabled
|
|
939
|
+
and not test.success
|
|
940
|
+
and "SSL" in str(test.error)
|
|
941
|
+
):
|
|
942
|
+
ssl_verify_failed = True
|
|
943
|
+
elif not test.ssl_verify_enabled and test.success:
|
|
944
|
+
ssl_noverify_success = True
|
|
945
|
+
|
|
946
|
+
if ssl_verify_failed and ssl_noverify_success:
|
|
947
|
+
recommendations.append(
|
|
948
|
+
"⚠️ Python requests fails with SSL verification but "
|
|
949
|
+
"succeeds without it."
|
|
950
|
+
)
|
|
951
|
+
recommendations.append(
|
|
952
|
+
" This typically indicates certificate validation "
|
|
953
|
+
"issues, possibly due to:"
|
|
954
|
+
)
|
|
955
|
+
recommendations.append(
|
|
956
|
+
" • Blocked certificate revocation endpoints (see above)"
|
|
957
|
+
)
|
|
958
|
+
recommendations.append(" • Missing or outdated CA certificates")
|
|
959
|
+
recommendations.append(" • Corporate proxy interference")
|
|
960
|
+
|
|
961
|
+
# Check proxy configuration
|
|
962
|
+
if result.proxy_detected:
|
|
963
|
+
recommendations.append(
|
|
964
|
+
"ℹ️ Proxy configuration detected. Ensure your proxy "
|
|
965
|
+
"allows access to:"
|
|
966
|
+
)
|
|
967
|
+
recommendations.append(" • api.deepgram.com (port 443)")
|
|
968
|
+
recommendations.append(
|
|
969
|
+
" • Certificate validation endpoints (port 80)"
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
# Display recommendations
|
|
973
|
+
if recommendations:
|
|
974
|
+
console.print("\n[bold yellow]📋 Recommendations:[/bold yellow]\n")
|
|
975
|
+
for rec in recommendations:
|
|
976
|
+
console.print(f" {rec}")
|
|
977
|
+
else:
|
|
978
|
+
console.print(
|
|
979
|
+
"\n[bold green]✅ All network diagnostics passed![/bold green]"
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
result.recommendations = recommendations
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Data models for network debug command."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from deepctl_core import BaseResult
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EndpointTestResult(BaseModel):
|
|
10
|
+
"""Result of testing a single endpoint."""
|
|
11
|
+
|
|
12
|
+
name: str
|
|
13
|
+
url: str
|
|
14
|
+
reachable: bool
|
|
15
|
+
status_code: int | None = None
|
|
16
|
+
response_time_ms: float | None = None
|
|
17
|
+
error: str | None = None
|
|
18
|
+
ssl_valid: bool = True
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DNSResult(BaseModel):
|
|
22
|
+
"""DNS resolution result."""
|
|
23
|
+
|
|
24
|
+
hostname: str
|
|
25
|
+
resolved: bool
|
|
26
|
+
ip_addresses: list[str] = Field(default_factory=list)
|
|
27
|
+
resolution_time_ms: float | None = None
|
|
28
|
+
error: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CertificateInfo(BaseModel):
|
|
32
|
+
"""Information about a single certificate in the chain."""
|
|
33
|
+
|
|
34
|
+
subject: str
|
|
35
|
+
issuer: str
|
|
36
|
+
not_before: str | None = None
|
|
37
|
+
not_after: str | None = None
|
|
38
|
+
serial_number: str | None = None
|
|
39
|
+
signature_algorithm: str | None = None
|
|
40
|
+
is_self_signed: bool = False
|
|
41
|
+
ocsp_urls: list[str] = Field(default_factory=list)
|
|
42
|
+
ca_issuer_urls: list[str] = Field(default_factory=list)
|
|
43
|
+
crl_distribution_points: list[str] = Field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RevocationEndpointTest(BaseModel):
|
|
47
|
+
"""Result of testing a revocation/CA endpoint."""
|
|
48
|
+
|
|
49
|
+
url: str
|
|
50
|
+
endpoint_type: str # 'ocsp', 'crl', or 'ca_issuer'
|
|
51
|
+
accessible: bool
|
|
52
|
+
status_code: int | None = None
|
|
53
|
+
response_time_ms: float | None = None
|
|
54
|
+
error: str | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TLSTestResult(BaseModel):
|
|
58
|
+
"""Result of TLS/SSL connectivity test."""
|
|
59
|
+
|
|
60
|
+
hostname: str
|
|
61
|
+
port: int
|
|
62
|
+
connected: bool
|
|
63
|
+
tls_version: str | None = None
|
|
64
|
+
cipher_suite: str | None = None
|
|
65
|
+
certificate_chain: list[CertificateInfo] = Field(default_factory=list)
|
|
66
|
+
revocation_endpoints: list[RevocationEndpointTest] = Field(
|
|
67
|
+
default_factory=list
|
|
68
|
+
)
|
|
69
|
+
chain_valid: bool = False
|
|
70
|
+
chain_errors: list[str] = Field(default_factory=list)
|
|
71
|
+
raw_openssl_output: str | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PythonRequestsTest(BaseModel):
|
|
75
|
+
"""Result of testing with Python requests library."""
|
|
76
|
+
|
|
77
|
+
url: str
|
|
78
|
+
success: bool
|
|
79
|
+
status_code: int | None = None
|
|
80
|
+
response_time_ms: float | None = None
|
|
81
|
+
ssl_verify_enabled: bool = True
|
|
82
|
+
error: str | None = None
|
|
83
|
+
ssl_info: dict[str, Any] | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CommandExecutionResult(BaseModel):
|
|
87
|
+
"""Result of executing a system command."""
|
|
88
|
+
|
|
89
|
+
command: str
|
|
90
|
+
success: bool
|
|
91
|
+
exit_code: int
|
|
92
|
+
stdout: str = ""
|
|
93
|
+
stderr: str = ""
|
|
94
|
+
execution_time_ms: float | None = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class NetworkDebugResult(BaseResult):
|
|
98
|
+
"""Result from network debug command execution."""
|
|
99
|
+
|
|
100
|
+
dns_results: dict[str, DNSResult] = Field(default_factory=dict)
|
|
101
|
+
endpoint_results: list[EndpointTestResult] = Field(default_factory=list)
|
|
102
|
+
tls_test_results: dict[str, TLSTestResult] = Field(default_factory=dict)
|
|
103
|
+
python_requests_tests: list[PythonRequestsTest] = Field(
|
|
104
|
+
default_factory=list
|
|
105
|
+
)
|
|
106
|
+
command_results: list[CommandExecutionResult] = Field(default_factory=list)
|
|
107
|
+
proxy_detected: bool = False
|
|
108
|
+
proxy_settings: dict[str, str] | None = None
|
|
109
|
+
network_issues_detected: bool = False
|
|
110
|
+
recommendations: list[str] = Field(default_factory=list)
|
|
111
|
+
environment_info: dict[str, Any] = Field(default_factory=dict)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class DeepgramEndpoint(BaseModel):
|
|
115
|
+
"""Information about a Deepgram API endpoint."""
|
|
116
|
+
|
|
117
|
+
name: str
|
|
118
|
+
url: str
|
|
119
|
+
description: str
|
|
120
|
+
protocol: str = "https" # https or wss
|
|
121
|
+
region: str | None = None
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deepctl-cmd-debug-network
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Network debug subcommand for deepctl
|
|
5
|
+
Author-email: Deepgram <devrel@deepgram.com>
|
|
6
|
+
Maintainer-email: Deepgram <devrel@deepgram.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: deepgram,cli,debug,network,diagnostics
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: deepctl-core>=0.1.0
|
|
18
|
+
Requires-Dist: click>=8.0.0
|
|
19
|
+
Requires-Dist: rich>=13.0.0
|
|
20
|
+
Requires-Dist: pydantic>=2.0.0
|
|
21
|
+
Requires-Dist: httpx>=0.24.0
|
|
22
|
+
Requires-Dist: requests<3.0,>=2.31
|
|
23
|
+
|
|
24
|
+
# deepctl-cmd-debug-network
|
|
25
|
+
|
|
26
|
+
Network debug subcommand for the Deepgram CLI (deepctl). This package provides the `network` subcommand under the `debug` command group, allowing users to debug network issues or problems connecting to api.deepgram.com
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/deepctl_cmd_debug_network/__init__.py
|
|
4
|
+
src/deepctl_cmd_debug_network/command.py
|
|
5
|
+
src/deepctl_cmd_debug_network/models.py
|
|
6
|
+
src/deepctl_cmd_debug_network.egg-info/PKG-INFO
|
|
7
|
+
src/deepctl_cmd_debug_network.egg-info/SOURCES.txt
|
|
8
|
+
src/deepctl_cmd_debug_network.egg-info/dependency_links.txt
|
|
9
|
+
src/deepctl_cmd_debug_network.egg-info/entry_points.txt
|
|
10
|
+
src/deepctl_cmd_debug_network.egg-info/requires.txt
|
|
11
|
+
src/deepctl_cmd_debug_network.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
deepctl_cmd_debug_network
|