#!/usr/bin/env python3
"""Standalone SSL check utility to be frozen for testing.

Usage:
  python ssl_check.py --url https://www.penracourses.org.uk --cert-dir ./cert

What it does:
  - Runs `openssl s_client -showcerts` against the host (if openssl on PATH).
  - Extracts PEM cert blocks from the output and inspects each with `openssl x509`.
  - Loads cert files from --cert-dir (PEM or DER) and converts DER to PEM as needed.
  - Compares server chain Issuers with Subjects of bundled certs and reports which CA certs are missing.

This script is intentionally small and self-contained so it can be frozen with PyInstaller
for quick testing on Windows where the system openssl may be too old.
"""

from __future__ import annotations

import argparse
import base64
import re
import shutil
import os
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import List, Tuple


def find_openssl() -> str | None:
    return shutil.which("openssl")


def run_openssl_s_client(host: str, port: int = 443, tls_flag: str | None = None, timeout: int = 60) -> str:
    openssl = find_openssl()
    if not openssl:
        raise RuntimeError("openssl not found on PATH")
    cmd = [openssl, "s_client", "-showcerts", "-connect", f"{host}:{port}", "-servername", host]
    if tls_flag:
        cmd.append(tls_flag)
    # give a newline so s_client won't wait for interactive input
    proc = subprocess.run(cmd, input="\n", capture_output=True, text=True, timeout=timeout)
    return proc.stdout + "\n" + proc.stderr


def fetch_cert_via_proxy(host: str, port: int, proxy: str, timeout: int = 20) -> List[str]:
    """Connect to proxy, issue CONNECT, perform TLS handshake and return PEM of the leaf cert."""
    from urllib.parse import urlparse
    import socket
    import ssl
    import base64

    parsed = urlparse(proxy)
    proxy_host = parsed.hostname
    proxy_port = parsed.port or (443 if parsed.scheme == 'https' else 80)

    auth_header = None
    if parsed.username:
        user = parsed.username
        pwd = parsed.password or ""
        cred = f"{user}:{pwd}".encode("utf-8")
        auth_header = base64.b64encode(cred).decode("ascii")

    s = socket.create_connection((proxy_host, proxy_port), timeout=timeout)
    try:
        connect_req = f"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n"
        if auth_header:
            connect_req += f"Proxy-Authorization: Basic {auth_header}\r\n"
        connect_req += "\r\n"
        s.sendall(connect_req.encode("ascii"))

        # read response headers
        resp = b""
        while b"\r\n\r\n" not in resp:
            chunk = s.recv(4096)
            if not chunk:
                break
            resp += chunk
            if len(resp) > 65536:
                break

        header = resp.split(b"\r\n\r\n", 1)[0].decode("ascii", errors="ignore")
        # Simple status check
        first_line = header.splitlines()[0] if header.splitlines() else ""
        if not first_line.startswith("HTTP/") or ("200" not in first_line):
            raise RuntimeError(f"Proxy CONNECT failed: {first_line}")

        # Wrap the socket with SSL and fetch peer cert (leaf)
        ctx = ssl.create_default_context()
        with ctx.wrap_socket(s, server_hostname=host) as ss:
            der = ss.getpeercert(True)
            pem = ssl.DER_cert_to_PEM_cert(der)
            return [pem]
    except Exception:
        s.close()
        raise


def extract_pem_blocks(s: str) -> List[str]:
    pattern = re.compile(r"-----BEGIN CERTIFICATE-----(?:.|\n)*?-----END CERTIFICATE-----", re.M)
    return pattern.findall(s)


def write_pems_to_temp(pems: List[str]) -> List[Path]:
    out = []
    for i, pem in enumerate(pems):
        f = Path(tempfile.gettempdir()) / f"ssl_check_cert_{i}.pem"
        f.write_text(pem)
        out.append(f)
    return out


def inspect_cert(path: Path) -> Tuple[str, str, str]:
    """Return (subject, issuer, fingerprint) by calling openssl x509."""
    openssl = find_openssl()
    if not openssl:
        raise RuntimeError("openssl not found on PATH")
    proc = subprocess.run([openssl, "x509", "-in", str(path), "-noout", "-subject", "-issuer", "-fingerprint"], capture_output=True, text=True)
    out = proc.stdout + proc.stderr
    subj = re.search(r"subject=\s*(.*)", out)
    issu = re.search(r"issuer=\s*(.*)", out)
    fp = re.search(r"Fingerprint=([A-F0-9:]+)", out)
    return (subj.group(1).strip() if subj else "", issu.group(1).strip() if issu else "", fp.group(1).strip() if fp else "")


def load_cert_files_from_dir(cert_dir: Path) -> List[Path]:
    files = []
    if not cert_dir.exists():
        return files
    for pattern in ("*.pem", "*.crt", "*.cer"):
        files.extend(sorted(cert_dir.glob(pattern)))

    out = []
    for f in files:
        data = f.read_bytes()
        if b"-----BEGIN CERTIFICATE-----" in data:
            # already PEM
            out.append(f)
            continue
        # assume DER -> convert to PEM
        b64 = base64.encodebytes(data).decode("ascii")
        pem = "-----BEGIN CERTIFICATE-----\n" + b64 + "-----END CERTIFICATE-----\n"
        tmp = Path(tempfile.gettempdir()) / f"ssl_check_bundle_{f.name}.pem"
        tmp.write_text(pem)
        out.append(tmp)
    return out


def main(argv: List[str]) -> int:
    p = argparse.ArgumentParser()
    p.add_argument("--url", required=True, help="URL or host to check (eg https://www.penracourses.org.uk)")
    p.add_argument("--cert-dir", default="cert", help="Directory with bundled certs")
    p.add_argument("--force-tls12", action="store_true", help="Force -tls1_2 on openssl if initial attempt failed")
    args = p.parse_args(argv)

    # parse host/port
    from urllib.parse import urlparse

    parsed = urlparse(args.url if "//" in args.url else "//" + args.url)
    host = parsed.hostname or args.url
    port = parsed.port or 443

    openssl = find_openssl()
    if not openssl:
        print("openssl not found on PATH; please install Git for Windows or OpenSSL and re-run.")
        # we still attempt Python fallback but chain won't be available
    else:
        print(f"Using openssl: {openssl}")

    out = ""
    if openssl:
        try:
            out = run_openssl_s_client(host, port)
            if "tlsv1 alert protocol version" in out or "SSL23_GET_SERVER_HELLO" in out:
                print("Server rejected legacy TLS; retrying with -tls1_2")
                out2 = run_openssl_s_client(host, port, tls_flag="-tls1_2")
                out = out + "\n--- retry result ---\n" + out2
        except subprocess.TimeoutExpired:
            print("OpenSSL check timed out")
        except Exception as e:
            print(f"OpenSSL check failed: {e}")

    # If openssl couldn't fetch a chain (or wasn't available), attempt a proxy-aware
    # fetch if HTTPS_PROXY or HTTP_PROXY is set. This helps in corporate networks
    # where direct TLS is blocked and a proxy must be used.
    if not extract_pem_blocks(out):
        proxy = None
        for key in ("HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"):
            proxy = os.environ.get(key)
            if proxy:
                break
        if proxy:
            try:
                print(f"Attempting proxy CONNECT via {proxy}")
                pems = fetch_cert_via_proxy(host, port, proxy)
                if pems:
                    print(f"Fetched {len(pems)} PEM(s) via proxy")
                    server_paths = write_pems_to_temp(pems)
                    server_info = [inspect_cert(p) for p in server_paths]
                    print("Server chain (proxy fetched):")
                    for i, (s, iss, fp) in enumerate(server_info):
                        print(f"[{i}] SUBJECT: {s}")
                        print(f"    ISSUER: {iss}")
                        print(f"    FP: {fp}")
                else:
                    print("Proxy CONNECT succeeded but no certs found")
            except Exception as e:
                print(f"Proxy CONNECT fetch failed: {e}")

    # extract certs from openssl output if any
    pems = extract_pem_blocks(out)
    if pems:
        print(f"Found {len(pems)} PEM blocks in openssl output")
        server_paths = write_pems_to_temp(pems)
        server_info = [inspect_cert(p) for p in server_paths]
        print("Server chain:")
        for i, (s, iss, fp) in enumerate(server_info):
            print(f"[{i}] SUBJECT: {s}")
            print(f"    ISSUER: {iss}")
            print(f"    FP: {fp}")
    else:
        print("No certificate chain found from openssl (server may have rejected the handshake or openssl is incompatible)")
        server_info = []

    # load bundled certs
    cert_dir = Path(args.cert_dir)
    bundled = load_cert_files_from_dir(cert_dir)
    print(f"Found {len(bundled)} bundled cert files in {cert_dir}")
    bundled_info = [inspect_cert(p) for p in bundled]
    subjects = {info[0]: p for info, p in zip(bundled_info, bundled)}

    # Compare: ensure each server issuer is present in bundled subjects
    missing = set()
    for s, iss, fp in server_info:
        if iss and iss not in subjects:
            missing.add(iss)

    if not server_info:
        print("No server certs to compare; check network/openssl compatibility")
        return 2

    if not missing:
        print("All server issuers are present in the bundled certs. OK")
        return 0
    else:
        print("Missing issuer certificates in bundled certs:")
        for m in missing:
            print(" - ", m)
        return 3


if __name__ == "__main__":
    try:
        rc = main(sys.argv[1:])
    except Exception as e:
        print(f"ssl_check failed: {e}")
        rc = 1
    sys.exit(rc)
