kftray-portforward 0.27.30

KFtray library with port forwarding logic
Documentation
use std::process::Command;

use anyhow::{
    Context,
    Result,
};
use log::{
    info,
    warn,
};
use tokio::fs;

pub async fn install_ca_certificate(ca_cert_der: &[u8]) -> Result<()> {
    let cert_path = get_permanent_cert_path();

    if let Some(parent) = cert_path.parent() {
        tokio::fs::create_dir_all(parent).await.ok();
    }

    let pem_data = format!(
        "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n",
        base64::Engine::encode(&base64::engine::general_purpose::STANDARD, ca_cert_der)
    );

    fs::write(&cert_path, pem_data)
        .await
        .context("Failed to write certificate file")?;

    install_to_keychain(&cert_path).await
}

async fn install_to_keychain(cert_path: &std::path::Path) -> Result<()> {
    info!("Installing CA certificate to System keychain");

    let cert_path_str = cert_path.display().to_string();

    let install_script = format!(
        r#"do shell script "security add-cert -k /Library/Keychains/System.keychain '{}'" with administrator privileges with prompt "KFtray needs administrator privileges to install the SSL certificate to the System keychain for secure HTTPS connections.""#,
        cert_path_str.replace("'", "'\\''")
    );

    let install_output = Command::new("osascript")
        .arg("-e")
        .arg(&install_script)
        .output()
        .context("Failed to install certificate")?;

    let install_stderr = String::from_utf8_lossy(&install_output.stderr);

    if !install_output.status.success()
        && !install_stderr.contains("already exists")
        && !install_stderr.contains("duplicate")
    {
        warn!("Certificate installation failed: {}", install_stderr.trim());
        return Err(anyhow::anyhow!("Certificate installation failed"));
    }

    info!("Configuring certificate trust settings");

    let trust_output = Command::new("security")
        .arg("add-trusted-cert")
        .arg(&cert_path_str)
        .output()
        .context("Failed to configure trust settings")?;

    if trust_output.status.success() {
        info!("CA certificate installed and trusted successfully");
        Ok(())
    } else {
        let trust_stderr = String::from_utf8_lossy(&trust_output.stderr);

        if trust_stderr.contains("User canceled") || trust_stderr.contains("(-128)") {
            warn!("User canceled trust configuration");
            Err(anyhow::anyhow!("User canceled authentication"))
        } else if trust_stderr.contains("already exists") {
            info!("CA certificate trust settings already configured");
            Ok(())
        } else {
            warn!("Trust configuration failed: {}", trust_stderr.trim());
            info!("Certificate installed but trust settings may need manual configuration");
            Ok(())
        }
    }
}

fn get_permanent_cert_path() -> std::path::PathBuf {
    dirs::config_dir()
        .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
        .join("kftray")
        .join("ssl-ca")
        .join("kftray-ca.crt")
}

pub async fn is_ca_installed(_ca_cert_der: &[u8]) -> Result<bool> {
    let output = Command::new("security")
        .arg("find-certificate")
        .arg("-c")
        .arg("kftray Local CA")
        .arg("/Library/Keychains/System.keychain")
        .output()
        .context("Failed to check certificate")?;

    if !output.status.success() {
        return Ok(false);
    }

    let trust_output = Command::new("security").arg("dump-trust-settings").output();

    match trust_output {
        Ok(result) if result.status.success() => {
            let stdout = String::from_utf8_lossy(&result.stdout);
            Ok(stdout.contains("kftray Local CA"))
        }
        _ => Ok(false),
    }
}

pub async fn remove_ca_certificate(_ca_cert_der: &[u8]) -> Result<()> {
    info!("Removing CA certificate");

    let script = r#"do shell script "security delete-certificate -c 'kftray Local CA' /Library/Keychains/System.keychain" with administrator privileges"#;

    let output = Command::new("osascript")
        .arg("-e")
        .arg(script)
        .output()
        .context("Failed to remove certificate")?;

    let stderr = String::from_utf8_lossy(&output.stderr);

    if output.status.success() || stderr.contains("not found") {
        info!("CA certificate removed");
        let cert_path = get_permanent_cert_path();
        let _ = tokio::fs::remove_file(&cert_path).await;
        Ok(())
    } else {
        warn!("Failed to remove certificate: {}", stderr.trim());
        Err(anyhow::anyhow!("Failed to remove certificate"))
    }
}