diff --git a/nice.py b/nice.py index d4df9f0..382ec8c 100644 --- a/nice.py +++ b/nice.py @@ -39,6 +39,9 @@ from starlette.middleware.base import BaseHTTPMiddleware import typer import logging import subprocess +import shutil +from urllib.parse import urlparse +import socket import platform import importlib.util @@ -393,6 +396,46 @@ def clear_anonymized_files(): return +def ssl_check_cmd(url: str) -> str: + """Run openssl s_client -showcerts against the given URL host and return output. + + Falls back to a minimal Python-based peer certificate fetch if openssl is not available. + """ + try: + parsed = urlparse(url) + host = parsed.hostname or url + port = parsed.port or (443 if parsed.scheme in ("https", "") else 80) + except Exception: + host = url + port = 443 + + openssl_path = shutil.which("openssl") + if openssl_path: + cmd = [openssl_path, "s_client", "-showcerts", "-connect", f"{host}:{port}", "-servername", host] + try: + proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + out = proc.stdout + "\n" + proc.stderr + return out + except Exception as e: + return f"OpenSSL check failed: {e}\n" + else: + # Fallback: use Python ssl to fetch the peer certificate (leaf only) + import ssl + + try: + ctx = ssl.create_default_context() + with ctx.wrap_socket( + socket.socket(socket.AF_INET), server_hostname=host + ) as s: + s.settimeout(10) + s.connect((host, port)) + der = s.getpeercert(True) + pem = ssl.DER_cert_to_PEM_cert(der) + return pem + except Exception as e: + return f"Python SSL fallback failed: {e}\n" + + def upload_files(q, rqst): # global rqst#, uploaded_files @@ -681,6 +724,16 @@ def main_page(): ui.button("Clear anon path", on_click=clear_anonymized_files).tooltip( "Delete all files in ANON_PATH" ) + async def ssl_check_ui() -> None: + # Run the openssl/Python SSL check in a thread and show output in a dialog + output = await run.cpu_bound(ssl_check_cmd, BASE_SITE_URL) + with ui.dialog() as d: + with ui.card().classes("p-4"): + ui.label("SSL Check output").classes("text-h6") + ui.code(output).props("white-space: pre-wrap") + ui.button("Close", on_click=d.close) + + ui.button("SSL check", on_click=ssl_check_ui).tooltip("Run openssl s_client -showcerts against the configured site and show output") def shutdown_app(): app.shutdown() @@ -790,13 +843,26 @@ def launch_app( if files: # Create a temporary combined CA bundle file (persisted) tmp = tempfile.NamedTemporaryFile(delete=False, prefix="nice_uploader_ca_", suffix=".pem") + included = [] try: + import base64 + for f in files: try: with open(f, "rb") as fh: - tmp.write(fh.read()) - # Ensure certs are separated - tmp.write(b"\n") + data = fh.read() + # If the file already contains a PEM block, write as-is + if b"-----BEGIN CERTIFICATE-----" in data: + tmp.write(data) + tmp.write(b"\n") + included.append(str(f)) + else: + # Assume DER/binary and convert to PEM by base64-encoding + b64 = base64.encodebytes(data) + tmp.write(b"-----BEGIN CERTIFICATE-----\n") + tmp.write(b64) + tmp.write(b"-----END CERTIFICATE-----\n") + included.append(str(f) + " (converted from DER)") except Exception: # skip unreadable files logger.debug(f"Skipping unreadable cert file: {f}") @@ -804,6 +870,7 @@ def launch_app( tmp.close() VERIFY_SSL = str(Path(tmp.name)) logger.info(f"Using bundled CA bundle dir combined to {VERIFY_SSL}") + logger.debug(f"Bundled cert files: {included}") except Exception as e: try: tmp.close()