local-ssl 0.1.0

Local HTTPS certificate generation for development — pair with local-dns
Documentation
use colored::Colorize;
use std::path::Path;
use std::process::Command;

pub fn install_ca(cert_path: &Path) -> Result<(), String> {
    if cfg!(target_os = "macos") {
        macos_install(cert_path)
    } else if cfg!(target_os = "windows") {
        windows_install(cert_path)
    } else {
        linux_install(cert_path)
    }
}

pub fn is_ca_trusted(cert_path: &Path) -> bool {
    if cfg!(target_os = "macos") {
        macos_trust_check(cert_path)
    } else if cfg!(target_os = "linux") {
        linux_trust_check(cert_path)
    } else {
        false
    }
}

fn macos_trust_check(cert_path: &Path) -> bool {
    let pem = match std::fs::read_to_string(cert_path) {
        Ok(c) => c,
        Err(_) => return false,
    };
    let cn = pem
        .lines()
        .find(|l| l.contains("CN="))
        .and_then(|l| l.split("CN=").nth(1))
        .map(|s| s.trim_end_matches('"'))
        .unwrap_or("unknown");
    Command::new("security")
        .args([
            "find-certificate",
            "-c",
            cn,
            "/Library/Keychains/System.keychain",
        ])
        .output()
        .ok()
        .is_some_and(|o| o.status.success())
}

fn linux_trust_check(cert_path: &Path) -> bool {
    let hash = Command::new("openssl")
        .args(["x509", "-hash", "-noout", "-in"])
        .arg(cert_path)
        .output()
        .ok()
        .and_then(|o| (o.status.success()).then(|| String::from_utf8(o.stdout).ok()))
        .flatten()
        .map(|s| s.trim().to_string());

    let hash = match hash {
        Some(h) if !h.is_empty() => h,
        _ => return false,
    };

    let dirs = [
        "/etc/ssl/certs",
        "/etc/pki/ca-trust/source/anchors",
        "/usr/share/pki/trust/anchors",
        "/etc/ca-certificates/trust-source/anchors",
    ];

    dirs.iter().any(|d| {
        Path::new(d).join(format!("{hash}.0")).exists()
            || Path::new(d).join(format!("{hash}.p11-kit")).exists()
    })
}

fn linux_install(cert_path: &Path) -> Result<(), String> {
    let cert =
        std::fs::read_to_string(cert_path).map_err(|e| format!("Cannot read CA cert: {e}"))?;

    let dest = if Path::new("/usr/local/share/ca-certificates").is_dir() {
        "/usr/local/share/ca-certificates/local-ssl.crt"
    } else if Path::new("/etc/pki/ca-trust/source/anchors").is_dir() {
        "/etc/pki/ca-trust/source/anchors/local-ssl.pem"
    } else if Path::new("/usr/share/pki/trust/anchors").is_dir() {
        "/usr/share/pki/trust/anchors/local-ssl.pem"
    } else if Path::new("/etc/ca-certificates/trust-source/anchors").is_dir() {
        "/etc/ca-certificates/trust-source/anchors/local-ssl.crt"
    } else {
        return Err("Unsupported Linux distro — install CA manually.".into());
    };

    std::fs::write(dest, &cert).map_err(|e| format!("Cannot write CA cert to {dest}: {e}"))?;
    println!("  {} Copied CA to {}", "".green(), dest.cyan());

    let update_cmds: &[&[&str]] = &[
        &["update-ca-certificates"],
        &["update-ca-trust"],
        &["trust", "extract-compat"],
    ];

    for args in update_cmds {
        if which(args[0]) {
            let out = Command::new(args[0])
                .args(&args[1..])
                .output()
                .map_err(|e| format!("Cannot run {}: {e}", args[0]))?;
            if out.status.success() {
                return Ok(());
            } else {
                let stderr = String::from_utf8_lossy(&out.stderr);
                eprintln!("  {} {} failed, trying next...", "".yellow(), args[0]);
                if !stderr.is_empty() {
                    eprintln!("    {}", stderr.trim());
                }
            }
        }
    }

    Err(
        "CA cert copied but trust DB update failed — run manually:\n  sudo update-ca-certificates"
            .into(),
    )
}

fn macos_install(cert_path: &Path) -> Result<(), String> {
    let s = Command::new("security")
        .args([
            "add-trusted-cert",
            "-d",
            "-r",
            "trustRoot",
            "-k",
            "/Library/Keychains/System.keychain",
            &cert_path.to_string_lossy(),
        ])
        .status()
        .map_err(|e| format!("Cannot add CA to keychain: {e}"))?;
    if s.success() {
        Ok(())
    } else {
        Err("Failed to install CA in System keychain".into())
    }
}

fn windows_install(cert_path: &Path) -> Result<(), String> {
    let s = Command::new("certutil")
        .args(["-addstore", "Root", &cert_path.to_string_lossy()])
        .status()
        .map_err(|e| format!("Cannot install CA: {e}"))?;
    if s.success() {
        Ok(())
    } else {
        Err("Failed to install CA in Root store".into())
    }
}

fn which(name: &str) -> bool {
    Command::new("which")
        .arg(name)
        .output()
        .ok()
        .is_some_and(|o| o.status.success())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_which_existing_command() {
        assert!(which("echo"));
    }

    #[test]
    fn test_which_nonexistent_command() {
        assert!(!which("this-command-definitely-does-not-exist"));
    }
}