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.
@@ -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,3 @@
1
+ # deepctl-cmd-debug-network
2
+
3
+ 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Network debug subcommand for deepctl."""
2
+
3
+ from .command import NetworkCommand
4
+
5
+ __all__ = ["NetworkCommand"]
@@ -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,2 @@
1
+ [deepctl.subcommands.debug]
2
+ network = deepctl_cmd_debug_network.command:NetworkCommand
@@ -0,0 +1,6 @@
1
+ deepctl-core>=0.1.0
2
+ click>=8.0.0
3
+ rich>=13.0.0
4
+ pydantic>=2.0.0
5
+ httpx>=0.24.0
6
+ requests<3.0,>=2.31