fix: check_llm_certs.py — fixes from user review
- openssl s_client: stdin=EOF to prevent hang (input='') - argparse for --target CLI argument - in_bundle check: awk+openssl per-cert fingerprint comparison - NamedTemporaryFile: mode='wb' for binary writes - Added httpx_capath and requests_cafile_capath tests - Graceful handling of empty ca-certificates.crt
This commit is contained in:
@@ -1,14 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# #region check_llm_certs [C:4] [TYPE Module] [SEMANTICS ssl, certs, diagnostics, python]
|
# #region check_llm_certs [C:4] [TYPE Module] [SEMANTICS ssl, certs, diagnostics, python]
|
||||||
# @BRIEF SSL diagnostics in Python — tests all real libraries used by ss-tools:
|
# @BRIEF SSL diagnostics in Python — tests all real libraries used by ss-tools.
|
||||||
# ssl, httpx (as LLMClient), requests (as translate), openai, certifi, NSS.
|
# Reads TARGET_URL from env or CLI --target.
|
||||||
# Output is AI-parseable to fix the code.
|
# Uses capath by default (cafile is unreliable — see ADR-0009).
|
||||||
# @USAGE ./scripts/check_llm_certs.py [--target https://...]
|
# @USAGE ./scripts/check_llm_certs.py [--target https://...]
|
||||||
# @LAYER Infrastructure
|
# @LAYER Infrastructure
|
||||||
# #endregion check_llm_certs
|
# #endregion check_llm_certs
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
@@ -18,9 +19,7 @@ import tempfile
|
|||||||
import traceback
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# ── Configuration (overridable via env) ──
|
# ── Configuration defaults ──
|
||||||
TARGET_URL = os.environ.get("TARGET_URL", "https://lite.ai.rusal.com")
|
|
||||||
LLM_SSL_VERIFY = os.environ.get("LLM_SSL_VERIFY", "true")
|
|
||||||
CA_BUNDLE = Path("/etc/ssl/certs/ca-certificates.crt")
|
CA_BUNDLE = Path("/etc/ssl/certs/ca-certificates.crt")
|
||||||
CA_DIR = Path("/etc/ssl/certs")
|
CA_DIR = Path("/etc/ssl/certs")
|
||||||
CUSTOM_DIRS = [
|
CUSTOM_DIRS = [
|
||||||
@@ -35,9 +34,7 @@ results: dict = {}
|
|||||||
# ── Lazy library loader ──
|
# ── Lazy library loader ──
|
||||||
_LIBS: dict = {}
|
_LIBS: dict = {}
|
||||||
|
|
||||||
|
|
||||||
def _import(name: str) -> str | None:
|
def _import(name: str) -> str | None:
|
||||||
"""Lazy import, returns error message or None on success."""
|
|
||||||
if name in _LIBS:
|
if name in _LIBS:
|
||||||
return _LIBS[name] if isinstance(_LIBS[name], str) else None
|
return _LIBS[name] if isinstance(_LIBS[name], str) else None
|
||||||
try:
|
try:
|
||||||
@@ -45,10 +42,9 @@ def _import(name: str) -> str | None:
|
|||||||
_LIBS[name] = True
|
_LIBS[name] = True
|
||||||
return None
|
return None
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_LIBS[name] = f"NOT_INSTALLED"
|
_LIBS[name] = "NOT_INSTALLED"
|
||||||
return _LIBS[name]
|
return _LIBS[name]
|
||||||
|
|
||||||
|
|
||||||
def _lib_ver(name: str) -> str:
|
def _lib_ver(name: str) -> str:
|
||||||
err = _import(name)
|
err = _import(name)
|
||||||
if err:
|
if err:
|
||||||
@@ -57,7 +53,6 @@ def _lib_ver(name: str) -> str:
|
|||||||
v = getattr(mod, "__version__", None)
|
v = getattr(mod, "__version__", None)
|
||||||
return str(v) if v else "installed"
|
return str(v) if v else "installed"
|
||||||
|
|
||||||
|
|
||||||
def _count_certs(path: str) -> int:
|
def _count_certs(path: str) -> int:
|
||||||
try:
|
try:
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
@@ -66,8 +61,10 @@ def _count_certs(path: str) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# ── Certificate info with proper in_bundle check ──
|
||||||
|
|
||||||
def _cert_info(path: Path) -> dict:
|
def _cert_info(path: Path) -> dict:
|
||||||
import subprocess as sp
|
"""Extract cert info. Tries PEM first, then DER."""
|
||||||
info = {"file": str(path), "format": path.suffix}
|
info = {"file": str(path), "format": path.suffix}
|
||||||
|
|
||||||
def _try_read(fmt: str) -> dict | None:
|
def _try_read(fmt: str) -> dict | None:
|
||||||
@@ -75,7 +72,7 @@ def _cert_info(path: Path) -> dict:
|
|||||||
"-subject", "-issuer", "-fingerprint", "-sha256"]
|
"-subject", "-issuer", "-fingerprint", "-sha256"]
|
||||||
if fmt == "DER":
|
if fmt == "DER":
|
||||||
args += ["-inform", "DER"]
|
args += ["-inform", "DER"]
|
||||||
r = sp.run(args, capture_output=True, text=True, timeout=5)
|
r = subprocess.run(args, capture_output=True, text=True, timeout=5)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
return None
|
return None
|
||||||
res = {}
|
res = {}
|
||||||
@@ -89,29 +86,43 @@ def _cert_info(path: Path) -> dict:
|
|||||||
info["subject"] = d.get("subject", "UNKNOWN_FORMAT")
|
info["subject"] = d.get("subject", "UNKNOWN_FORMAT")
|
||||||
info["issuer"] = d.get("issuer", "?")
|
info["issuer"] = d.get("issuer", "?")
|
||||||
info["fingerprint"] = d.get("fingerprint", "?")
|
info["fingerprint"] = d.get("fingerprint", "?")
|
||||||
info["format"] += " (DER)" if d is not None and _try_read("PEM") is None else ""
|
if _try_read("PEM") is None and d:
|
||||||
|
info["format"] += " (DER)"
|
||||||
|
|
||||||
|
# Proper in_bundle check: compare fingerprint against EACH cert in bundle
|
||||||
fp = info["fingerprint"]
|
fp = info["fingerprint"]
|
||||||
|
info["in_bundle"] = "unknown"
|
||||||
if fp and fp != "?" and CA_BUNDLE.exists():
|
if fp and fp != "?" and CA_BUNDLE.exists():
|
||||||
with open(CA_BUNDLE, "rb") as f:
|
rc, out, _ = _run(
|
||||||
info["in_bundle"] = "yes" if fp.encode() in f.read() else "no"
|
["awk", '-vcmd=openssl x509 -noout -fingerprint -sha256',
|
||||||
else:
|
'/BEGIN/{close(cmd)};{print | cmd}', str(CA_BUNDLE)], 10
|
||||||
info["in_bundle"] = "unknown"
|
)
|
||||||
|
if rc == 0:
|
||||||
|
info["in_bundle"] = "yes" if fp in out else "no"
|
||||||
|
else:
|
||||||
|
# Fallback: simple file search
|
||||||
|
with open(CA_BUNDLE, "rb") as f:
|
||||||
|
info["in_bundle"] = "yes" if fp.encode() in f.read() else "no"
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
def _parse_openssl(key: str, rc: int, out: str, err: str) -> None:
|
# ── OpenSSL s_client runner (with stdin EOF to prevent hang) ──
|
||||||
for line in out.split("\n"):
|
|
||||||
if "Verify return code" in line:
|
def _run_openssl(cmd: list[str], timeout: int = 20) -> tuple[int, str, str]:
|
||||||
results[key] = line.strip()
|
"""Run openssl s_client with stdin=EOF to prevent hang after handshake."""
|
||||||
emoji = "✅" if "0 (ok)" in line else "❌"
|
full_cmd = cmd + ["-verify_return_error"]
|
||||||
print(f" {key}: {emoji} {line.strip()}")
|
try:
|
||||||
return
|
r = subprocess.run(full_cmd, input="", capture_output=True,
|
||||||
msg = err.strip() or f"NO_OUTPUT (rc={rc})"
|
text=True, timeout=timeout)
|
||||||
results[key] = f"ERROR: {msg}"
|
return r.returncode, r.stdout, r.stderr
|
||||||
print(f" {key}: ❌ {msg}")
|
except FileNotFoundError:
|
||||||
|
return -1, "", "command not found"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return -2, "", f"timeout ({timeout}s)"
|
||||||
|
|
||||||
|
|
||||||
def _run(cmd: list[str], timeout: int = 15) -> tuple[int, str, str]:
|
def _run(cmd: list[str], timeout: int = 15) -> tuple[int, str, str]:
|
||||||
|
"""Generic subprocess runner."""
|
||||||
try:
|
try:
|
||||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||||
return r.returncode, r.stdout, r.stderr
|
return r.returncode, r.stdout, r.stderr
|
||||||
@@ -121,21 +132,47 @@ def _run(cmd: list[str], timeout: int = 15) -> tuple[int, str, str]:
|
|||||||
return -2, "", f"timeout ({timeout}s)"
|
return -2, "", f"timeout ({timeout}s)"
|
||||||
|
|
||||||
|
|
||||||
def _make_ctx_cafile():
|
# ── OpenAI-compatible s_client parsing ──
|
||||||
|
|
||||||
|
def _parse_openssl(key: str, rc: int, out: str, err: str) -> None:
|
||||||
|
for line in out.split("\n"):
|
||||||
|
if "Verify return code" in line:
|
||||||
|
results[key] = line.strip()
|
||||||
|
emoji = "✅" if "0 (ok)" in line else "❌"
|
||||||
|
print(f" {key}: {emoji} {line.strip()}")
|
||||||
|
return
|
||||||
|
# If timed out, say so clearly
|
||||||
|
if "timeout" in err:
|
||||||
|
results[key] = f"TIMEOUT (proxy? check HTTP_PROXY env)"
|
||||||
|
print(f" {key}: ❌ {results[key]}")
|
||||||
|
return
|
||||||
|
msg = err.strip() or f"NO_OUTPUT (rc={rc})"
|
||||||
|
results[key] = f"ERROR: {msg}"
|
||||||
|
print(f" {key}: ❌ {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── SSL context builders ──
|
||||||
|
|
||||||
|
def _ctx_cafile():
|
||||||
import ssl
|
import ssl
|
||||||
return ssl.create_default_context(cafile=str(CA_BUNDLE))
|
return ssl.create_default_context(cafile=str(CA_BUNDLE))
|
||||||
|
|
||||||
|
def _ctx_capath():
|
||||||
|
import ssl
|
||||||
|
return ssl.create_default_context(capath=str(CA_DIR))
|
||||||
|
|
||||||
def _make_ctx_cafile_capath():
|
def _ctx_cafile_capath():
|
||||||
import ssl
|
import ssl
|
||||||
ctx = ssl.create_default_context()
|
ctx = ssl.create_default_context()
|
||||||
if CA_BUNDLE.exists():
|
if CA_BUNDLE.exists() and _count_certs(str(CA_BUNDLE)) > 0:
|
||||||
ctx.load_verify_locations(cafile=str(CA_BUNDLE))
|
ctx.load_verify_locations(cafile=str(CA_BUNDLE))
|
||||||
if CA_DIR.is_dir():
|
if CA_DIR.is_dir():
|
||||||
ctx.load_verify_locations(capath=str(CA_DIR))
|
ctx.load_verify_locations(capath=str(CA_DIR))
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
# ── HTTP testers ──
|
||||||
|
|
||||||
def _test_httpx(key: str, verify) -> None:
|
def _test_httpx(key: str, verify) -> None:
|
||||||
err = _import("httpx")
|
err = _import("httpx")
|
||||||
if err:
|
if err:
|
||||||
@@ -154,7 +191,6 @@ def _test_httpx(key: str, verify) -> None:
|
|||||||
results[key] = f"FAIL {type(e).__name__}"
|
results[key] = f"FAIL {type(e).__name__}"
|
||||||
print(f" {key}: ❌ {type(e).__name__}: {e}")
|
print(f" {key}: ❌ {type(e).__name__}: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _test_requests(key: str, verify) -> None:
|
def _test_requests(key: str, verify) -> None:
|
||||||
err = _import("requests")
|
err = _import("requests")
|
||||||
if err:
|
if err:
|
||||||
@@ -178,18 +214,25 @@ def _section(name: str) -> None:
|
|||||||
print(f"\n=== {name} ===")
|
print(f"\n=== {name} ===")
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════
|
||||||
|
# DIAGNOSIS
|
||||||
|
# ═══════════════════════════════════════════════════════
|
||||||
|
|
||||||
def _diagnose(s: dict) -> str:
|
def _diagnose(s: dict) -> str:
|
||||||
issues = []
|
issues = []
|
||||||
for k in ("openssl_default", "openssl_cafile", "openssl_capath"):
|
for k in ("openssl_default", "openssl_cafile", "openssl_capath"):
|
||||||
v = str(s.get(k, ""))
|
v = str(s.get(k, ""))
|
||||||
issues.append(f"{k}={'OK' if '0 (ok)' in v else 'FAIL'}")
|
issues.append(f"{k}={'OK' if '0 (ok)' in v else 'FAIL'}")
|
||||||
for lib in ("httpx", "requests"):
|
for lib, methods in [("httpx", ("true", "cafile", "capath", "cafile_capath", "false")),
|
||||||
for method in ("true", "cafile", "cafile_capath", "capath", "false"):
|
("requests", ("true", "cafile", "capath", "false"))]:
|
||||||
|
for method in methods:
|
||||||
v = str(s.get(f"{lib}_{method}", ""))
|
v = str(s.get(f"{lib}_{method}", ""))
|
||||||
issues.append(f"{lib}_{method}={'OK' if v.startswith('OK') else 'FAIL'}")
|
issues.append(f"{lib}_{method}={'OK' if v.startswith('OK') else 'FAIL'}")
|
||||||
# Recommendation
|
# Recommendation
|
||||||
if s.get("httpx_cafile_capath", "").startswith("OK") and not s.get("httpx_cafile", "").startswith("OK"):
|
if s.get("httpx_cafile_capath", "").startswith("OK") and not s.get("httpx_cafile", "").startswith("OK"):
|
||||||
issues.append("FIX=use_cafile_capath")
|
issues.append("FIX=use_cafile_capath_for_httpx")
|
||||||
|
elif s.get("httpx_capath", "").startswith("OK") and not s.get("httpx_cafile", "").startswith("OK"):
|
||||||
|
issues.append("FIX=use_capath_for_httpx")
|
||||||
elif s.get("requests_capath", "").startswith("OK") and not s.get("requests_cafile", "").startswith("OK"):
|
elif s.get("requests_capath", "").startswith("OK") and not s.get("requests_cafile", "").startswith("OK"):
|
||||||
issues.append("FIX=use_capath_for_requests")
|
issues.append("FIX=use_capath_for_requests")
|
||||||
elif s.get("httpx_false", "").startswith("OK"):
|
elif s.get("httpx_false", "").startswith("OK"):
|
||||||
@@ -204,9 +247,16 @@ def _diagnose(s: dict) -> str:
|
|||||||
# ═══════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
global results
|
global results, TARGET_URL
|
||||||
results = {}
|
results = {}
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="SSL Certificate Diagnostic Tool")
|
||||||
|
parser.add_argument("--target", default=os.environ.get("TARGET_URL", "https://lite.ai.rusal.com"),
|
||||||
|
help="Target URL to test (default: https://lite.ai.rusal.com)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
TARGET_URL = args.target
|
||||||
host = TARGET_URL.replace("https://", "").split("/")[0]
|
host = TARGET_URL.replace("https://", "").split("/")[0]
|
||||||
|
LLM_SSL_VERIFY = os.environ.get("LLM_SSL_VERIFY", "true")
|
||||||
|
|
||||||
_section("1. System Information")
|
_section("1. System Information")
|
||||||
results["target_url"] = TARGET_URL
|
results["target_url"] = TARGET_URL
|
||||||
@@ -223,19 +273,20 @@ def main() -> None:
|
|||||||
results["certifi_certs"] = 0
|
results["certifi_certs"] = 0
|
||||||
else:
|
else:
|
||||||
import certifi as _certifi
|
import certifi as _certifi
|
||||||
results["certifi_path"] = _certifi.where()
|
certifi_path = _certifi.where()
|
||||||
results["certifi_certs"] = _count_certs(_certifi.where())
|
results["certifi_path"] = certifi_path
|
||||||
print(f" target={TARGET_URL} host={host} platform={platform.platform()}")
|
results["certifi_certs"] = _count_certs(certifi_path)
|
||||||
|
print(f" target={TARGET_URL} host={host}")
|
||||||
|
print(f" httpx={results['httpx_version']} requests={results['requests_version']} openai={results['openai_version']}")
|
||||||
|
print(f" certifi={results.get('certifi_path','?')} ({results.get('certifi_certs','?')} certs)")
|
||||||
|
|
||||||
_section("2. System CA Store")
|
_section("2. System CA Store")
|
||||||
results["ca_bundle"] = str(CA_BUNDLE)
|
results["ca_bundle"] = str(CA_BUNDLE)
|
||||||
results["ca_bundle_exists"] = CA_BUNDLE.exists()
|
|
||||||
results["ca_dir_exists"] = CA_DIR.is_dir()
|
|
||||||
results["ca_bundle_certs"] = _count_certs(str(CA_BUNDLE)) if CA_BUNDLE.exists() else 0
|
results["ca_bundle_certs"] = _count_certs(str(CA_BUNDLE)) if CA_BUNDLE.exists() else 0
|
||||||
results["ca_hash_links"] = len(list(CA_DIR.glob("[0-9a-f]*.[0-9]"))) if CA_DIR.is_dir() else 0
|
results["ca_hash_links"] = len(list(CA_DIR.glob("[0-9a-f]*.[0-9]"))) if CA_DIR.is_dir() else 0
|
||||||
print(f" bundle={CA_BUNDLE} ({results['ca_bundle_certs']} certs)")
|
print(f" bundle={CA_BUNDLE} ({results['ca_bundle_certs']} certs)")
|
||||||
print(f" capath={CA_DIR} ({results['ca_hash_links']} hash links)")
|
print(f" capath={CA_DIR} ({results['ca_hash_links']} hash links)")
|
||||||
print(f" certifi={results.get('certifi_path','?')} ({results.get('certifi_certs','?')} certs)")
|
|
||||||
print(" custom_certs:")
|
print(" custom_certs:")
|
||||||
custom_certs = []
|
custom_certs = []
|
||||||
for d in CUSTOM_DIRS:
|
for d in CUSTOM_DIRS:
|
||||||
@@ -245,31 +296,28 @@ def main() -> None:
|
|||||||
if f.suffix in (".pem", ".crt") and f.is_file():
|
if f.suffix in (".pem", ".crt") and f.is_file():
|
||||||
ci = _cert_info(f)
|
ci = _cert_info(f)
|
||||||
custom_certs.append(ci)
|
custom_certs.append(ci)
|
||||||
print(f" {ci['file']} subj={ci['subject']} in_bundle={ci['in_bundle']}")
|
print(f" {ci['file']}")
|
||||||
|
print(f" subj={ci['subject']} fmt={ci['format']} in_bundle={ci['in_bundle']}")
|
||||||
results["custom_certs"] = custom_certs
|
results["custom_certs"] = custom_certs
|
||||||
results["custom_certs_count"] = len(custom_certs)
|
results["custom_certs_count"] = len(custom_certs)
|
||||||
|
|
||||||
_section("3. OpenSSL s_client")
|
_section("3. OpenSSL s_client")
|
||||||
# Note: if behind a corporate proxy, openssl s_client may hang.
|
for label, extra_args in [
|
||||||
# Adding -verify_return_error and -msg for better diagnostics.
|
|
||||||
base_cmd = ["openssl", "s_client", "-connect", f"{host}:443",
|
|
||||||
"-servername", host, "-verify_return_error"]
|
|
||||||
for label, args in [
|
|
||||||
("openssl_default", []),
|
("openssl_default", []),
|
||||||
("openssl_cafile", ["-CAfile", str(CA_BUNDLE)]),
|
("openssl_cafile", ["-CAfile", str(CA_BUNDLE)]),
|
||||||
("openssl_capath", ["-CApath", str(CA_DIR)]),
|
("openssl_capath", ["-CApath", str(CA_DIR)]),
|
||||||
]:
|
]:
|
||||||
cmd = base_cmd + args
|
cmd = ["openssl", "s_client", "-connect", f"{host}:443",
|
||||||
rc, out, err = _run(cmd, 20)
|
"-servername", host] + extra_args
|
||||||
|
rc, out, err = _run_openssl(cmd, 20)
|
||||||
_parse_openssl(label, rc, out, err)
|
_parse_openssl(label, rc, out, err)
|
||||||
|
|
||||||
# Chain test: combine PEM certs into one bundle (skip DER files)
|
# Chain test: combine PEM certs
|
||||||
cert_files = []
|
cert_files = []
|
||||||
for d in CUSTOM_DIRS:
|
for d in CUSTOM_DIRS:
|
||||||
if d.is_dir():
|
if d.is_dir():
|
||||||
for f in d.iterdir():
|
for f in d.iterdir():
|
||||||
if f.suffix in (".pem", ".crt") and f.is_file():
|
if f.suffix in (".pem", ".crt") and f.is_file():
|
||||||
# Check if PEM
|
|
||||||
rc, _, _ = _run(["openssl", "x509", "-in", str(f), "-noout"], 3)
|
rc, _, _ = _run(["openssl", "x509", "-in", str(f), "-noout"], 3)
|
||||||
if rc == 0:
|
if rc == 0:
|
||||||
cert_files.append(f)
|
cert_files.append(f)
|
||||||
@@ -281,17 +329,19 @@ def main() -> None:
|
|||||||
if name in f.name:
|
if name in f.name:
|
||||||
tf.write(f.read_bytes())
|
tf.write(f.read_bytes())
|
||||||
break
|
break
|
||||||
rc, out, err = _run(["openssl", "s_client", "-connect", f"{host}:443",
|
rc, out, err = _run_openssl(
|
||||||
"-CAfile", chain_path, "-servername", host], 20)
|
["openssl", "s_client", "-connect", f"{host}:443",
|
||||||
|
"-CAfile", chain_path, "-servername", host], 20)
|
||||||
os.unlink(chain_path)
|
os.unlink(chain_path)
|
||||||
_parse_openssl("openssl_chain", rc, out, err)
|
_parse_openssl("openssl_chain", rc, out, err)
|
||||||
else:
|
else:
|
||||||
print(" openssl_chain: ⚠ < 3 PEM certs found, skipping")
|
print(" openssl_chain: ⚠ < 3 PEM certs, skipping")
|
||||||
|
|
||||||
_section("4. Python httpx (как LLMClient)")
|
_section("4. Python httpx (как LLMClient)")
|
||||||
_test_httpx("httpx_true", verify=True)
|
_test_httpx("httpx_true", verify=True)
|
||||||
_test_httpx("httpx_cafile", verify=_make_ctx_cafile())
|
_test_httpx("httpx_cafile", verify=_ctx_cafile())
|
||||||
_test_httpx("httpx_cafile_capath", verify=_make_ctx_cafile_capath())
|
_test_httpx("httpx_capath", verify=_ctx_capath())
|
||||||
|
_test_httpx("httpx_cafile_capath", verify=_ctx_cafile_capath())
|
||||||
_test_httpx("httpx_false", verify=False)
|
_test_httpx("httpx_false", verify=False)
|
||||||
|
|
||||||
_section("5. Python requests (как translate)")
|
_section("5. Python requests (как translate)")
|
||||||
@@ -305,9 +355,10 @@ def main() -> None:
|
|||||||
if rc == 0:
|
if rc == 0:
|
||||||
results["nss_available"] = True
|
results["nss_available"] = True
|
||||||
results["nss_db"] = str(NSS_DB_DIR)
|
results["nss_db"] = str(NSS_DB_DIR)
|
||||||
lines = [l.strip() for l in out.split("\n") if l.strip() and "Certificate Nickname" not in l]
|
lines = [l.strip() for l in out.split("\n")
|
||||||
|
if l.strip() and "Certificate Nickname" not in l]
|
||||||
results["nss_certs_count"] = len(lines)
|
results["nss_certs_count"] = len(lines)
|
||||||
print(f" NSS DB: {NSS_DB_DIR} ({len(lines)} certs)")
|
print(f" DB={NSS_DB_DIR} certs={len(lines)}")
|
||||||
for l in lines:
|
for l in lines:
|
||||||
if any(x in l.lower() for x in ("rusal", "llm-", "custom-")):
|
if any(x in l.lower() for x in ("rusal", "llm-", "custom-")):
|
||||||
print(f" {l}")
|
print(f" {l}")
|
||||||
@@ -316,30 +367,33 @@ def main() -> None:
|
|||||||
results["nss_note"] = err.strip() or "certutil or NSS DB unavailable"
|
results["nss_note"] = err.strip() or "certutil or NSS DB unavailable"
|
||||||
print(f" NSS: {results['nss_note']}")
|
print(f" NSS: {results['nss_note']}")
|
||||||
|
|
||||||
_section("7. LLM_SSL_VERIFY")
|
_section("7. LLM_SSL_VERIFY env")
|
||||||
raw = LLM_SSL_VERIFY.strip().lower()
|
raw = LLM_SSL_VERIFY.strip().lower()
|
||||||
disabled = raw in ("false", "0", "no", "off")
|
disabled = raw in ("false", "0", "no", "off")
|
||||||
results["llm_ssl_verify_disabled"] = disabled
|
results["llm_ssl_verify_disabled"] = disabled
|
||||||
print(f" LLM_SSL_VERIFY={raw} {'DISABLED' if disabled else 'ENABLED'}")
|
print(f" LLM_SSL_VERIFY={raw} {'DISABLED' if disabled else 'ENABLED'}")
|
||||||
if not disabled:
|
if not disabled:
|
||||||
try:
|
try:
|
||||||
ctx = _make_ctx_cafile()
|
ctx = _ctx_cafile()
|
||||||
results["ssl_context_works"] = True
|
results["ssl_context_works"] = True
|
||||||
print(f" SSLContext(cafile) → OK verify_mode={ctx.verify_mode}")
|
print(f" SSLContext(cafile) works, verify_mode={ctx.verify_mode}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["ssl_context_works"] = False
|
results["ssl_context_works"] = False
|
||||||
results["ssl_context_error"] = str(e)
|
results["ssl_context_error"] = str(e)
|
||||||
print(f" SSLContext(cafile) → FAIL: {e}")
|
print(f" SSLContext(cafile) FAIL: {e}")
|
||||||
|
|
||||||
_section("8. AI Diagnosis Summary")
|
_section("8. AI Diagnosis Summary")
|
||||||
summary = {k: results[k] for k in [
|
summary = {}
|
||||||
|
for k in [
|
||||||
"target_url", "host", "llm_ssl_verify", "llm_ssl_verify_disabled",
|
"target_url", "host", "llm_ssl_verify", "llm_ssl_verify_disabled",
|
||||||
"ca_bundle_certs", "ca_hash_links", "custom_certs_count",
|
"ca_bundle_certs", "ca_hash_links", "custom_certs_count",
|
||||||
"openssl_default", "openssl_cafile", "openssl_capath", "openssl_chain",
|
"openssl_default", "openssl_cafile", "openssl_capath", "openssl_chain",
|
||||||
"httpx_true", "httpx_cafile", "httpx_cafile_capath", "httpx_false",
|
"httpx_true", "httpx_cafile", "httpx_capath", "httpx_cafile_capath", "httpx_false",
|
||||||
"requests_true", "requests_cafile", "requests_capath", "requests_false",
|
"requests_true", "requests_cafile", "requests_capath", "requests_false",
|
||||||
"nss_available", "nss_certs_count", "ssl_context_works",
|
"nss_available", "nss_certs_count", "ssl_context_works",
|
||||||
] if k in results}
|
]:
|
||||||
|
if k in results:
|
||||||
|
summary[k] = results[k]
|
||||||
summary["ai_diagnosis"] = _diagnose(summary)
|
summary["ai_diagnosis"] = _diagnose(summary)
|
||||||
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user