product-os-proxy 0.0.19

Product OS : Proxy builds on the work of hudsucker, taking it to the next level with a man-in-the-middle proxy server that can tunnel traffic through a VPN utilising Product OS : VPN.
//! Interactive and non-interactive CA trust setup guidance.

#[cfg(not(feature = "interactive"))]
use std::io::Write;
use std::io::{self, IsTerminal};
#[cfg(feature = "interactive")]
use std::path::PathBuf;

use product_os_security::certificates::ManagedCa;
use product_os_utilities::ProductOSError;

use crate::ca::probe::{active_https_self_test, wait_for_proxy_ready};
use crate::ca::trust::TrustProbe;
use crate::ca::trust_api::{manual_trust_steps, probe_trust_report};
use crate::config::{NetworkProxyCertificateAuthorityTrust, NetworkProxyOnMissingTrust};
use crate::error::ProxyError;

/// Run startup trust guidance based on configuration.
pub async fn run_startup_trust_guide(
    managed: &ManagedCa,
    trust_config: &NetworkProxyCertificateAuthorityTrust,
    proxy_host: &str,
    proxy_port: u16,
) -> Result<(), ProductOSError> {
    if !trust_config.verify_on_startup {
        return Ok(());
    }

    let targets = &trust_config.targets;
    if probe_trust_report(managed, targets).all_trusted {
        tracing::info!("MITM root CA is trusted in configured stores");
        return Ok(());
    }

    match trust_config.on_missing_trust {
        NetworkProxyOnMissingTrust::Off => Ok(()),
        NetworkProxyOnMissingTrust::Warn => {
            log_missing_trust(managed, proxy_host, proxy_port, targets);
            Ok(())
        }
        NetworkProxyOnMissingTrust::Guide => {
            if io::stdin().is_terminal() && io::stdout().is_terminal() {
                run_cert_guide_wizard(managed, trust_config, targets, proxy_host, proxy_port).await
            } else {
                log_missing_trust(managed, proxy_host, proxy_port, targets);
                Ok(())
            }
        }
        NetworkProxyOnMissingTrust::Block => {
            log_missing_trust(managed, proxy_host, proxy_port, targets);
            Err(ProductOSError::from(ProxyError::Generic(
                "MITM root CA is not trusted; set onMissingTrust to warn or run pos-proxy cert guide"
                    .into(),
            )))
        }
    }
}

/// Run the active HTTPS probe after the proxy is listening.
pub async fn run_post_startup_active_probe(
    managed: &ManagedCa,
    trust_config: &NetworkProxyCertificateAuthorityTrust,
    proxy_host: &str,
    proxy_port: u16,
) -> Result<(), ProductOSError> {
    if !trust_config.active_https_probe {
        return Ok(());
    }

    let targets = &trust_config.targets;
    if !probe_trust_report(managed, targets).all_trusted {
        tracing::debug!(
            "skipping active HTTPS probe because configured trust stores are incomplete"
        );
        return Ok(());
    }

    wait_for_proxy_ready(proxy_host, proxy_port)
        .await
        .map_err(|e| ProductOSError::from(ProxyError::Generic(e)))?;

    match active_https_self_test(managed, proxy_host, proxy_port).await {
        Ok(()) => {
            tracing::info!("MITM root CA active HTTPS self-test passed");
            Ok(())
        }
        Err(err) => {
            tracing::warn!(error = %err, "MITM active HTTPS self-test failed");
            if trust_config.on_missing_trust == NetworkProxyOnMissingTrust::Block {
                Err(ProductOSError::from(ProxyError::Generic(err)))
            } else {
                Ok(())
            }
        }
    }
}

/// Interactive Proxyman-style wizard (dialoguer when `interactive` feature enabled).
pub async fn run_cert_guide_wizard(
    managed: &ManagedCa,
    trust_config: &NetworkProxyCertificateAuthorityTrust,
    targets: &[String],
    proxy_host: &str,
    proxy_port: u16,
) -> Result<(), ProductOSError> {
    print_status_header(managed, targets);

    #[cfg(feature = "interactive")]
    {
        return run_dialoguer_wizard(managed, trust_config, targets, proxy_host, proxy_port).await;
    }

    #[cfg(not(feature = "interactive"))]
    {
        let _ = (proxy_host, proxy_port);
        run_simple_prompt_guide(managed, trust_config, targets)
    }
}

#[cfg(feature = "interactive")]
async fn run_dialoguer_wizard(
    managed: &ManagedCa,
    trust_config: &NetworkProxyCertificateAuthorityTrust,
    targets: &[String],
    proxy_host: &str,
    proxy_port: u16,
) -> Result<(), ProductOSError> {
    use dialoguer::{Confirm, Select};

    loop {
        let options = [
            "Auto-install and trust (requires administrator permission)",
            "Show manual installation steps",
            "Export CA to Desktop",
            "Re-check trust status",
            "Run active HTTPS self-test through proxy",
            "Skip for now",
        ];
        let choice = Select::new()
            .with_prompt("Product OS Proxy CA setup")
            .items(&options)
            .default(0)
            .interact()
            .map_err(|e| ProductOSError::from(ProxyError::Generic(e.to_string())))?;

        match choice {
            0 if trust_config.allow_auto_install => {
                let proceed = Confirm::new()
                    .with_prompt("Proceed with automatic trust installation?")
                    .default(false)
                    .interact()
                    .map_err(|e| ProductOSError::from(ProxyError::Generic(e.to_string())))?;
                if proceed {
                    auto_install_all(managed, targets)?;
                    if probe_trust_report(managed, targets).all_trusted {
                        println!("CA installed and trusted.");
                        return Ok(());
                    }
                    println!("Trust incomplete — try manual steps or export the CA.");
                }
            }
            0 => println!("Auto-install disabled in configuration (allowAutoInstall=false)."),
            1 => print_manual_steps(managed),
            2 => export_ca_to_desktop(managed)?,
            3 => print_status_header(managed, targets),
            4 => {
                if let Err(e) = wait_for_proxy_ready(proxy_host, proxy_port).await {
                    println!("Proxy not reachable: {e}");
                } else {
                    match active_https_self_test(managed, proxy_host, proxy_port).await {
                        Ok(()) => println!("Active HTTPS self-test passed."),
                        Err(e) => println!("Active HTTPS self-test failed: {e}"),
                    }
                }
            }
            _ => return Ok(()),
        }
    }
}

#[cfg(not(feature = "interactive"))]
fn run_simple_prompt_guide(
    managed: &ManagedCa,
    trust_config: &NetworkProxyCertificateAuthorityTrust,
    targets: &[String],
) -> Result<(), ProductOSError> {
    let stdout = io::stdout();
    let mut out = stdout.lock();

    if trust_config.allow_auto_install {
        writeln!(
            out,
            "\nAuto-install requires administrator permission. Proceed? [y/N]: "
        )
        .ok();
        out.flush().ok();
        let mut line = String::new();
        if io::stdin().read_line(&mut line).is_ok() && line.trim().eq_ignore_ascii_case("y") {
            auto_install_all(managed, targets)?;
            if probe_trust_report(managed, targets).all_trusted {
                writeln!(out, "CA installed and trusted.").ok();
                return Ok(());
            }
        }
    }

    print_manual_steps(managed);
    Ok(())
}

fn auto_install_all(managed: &ManagedCa, targets: &[String]) -> Result<(), ProductOSError> {
    let results = crate::ca::trust_api::auto_install_targets_internal(managed, targets);
    for r in &results {
        if !r.ok {
            return Err(ProductOSError::from(ProxyError::Generic(
                r.error
                    .clone()
                    .unwrap_or_else(|| format!("install failed for {}", r.name)),
            )));
        }
    }
    Ok(())
}

#[cfg(feature = "interactive")]
fn export_ca_to_desktop(managed: &ManagedCa) -> Result<(), ProductOSError> {
    let desktop = std::env::var("HOME")
        .map(|h| PathBuf::from(h).join("Desktop"))
        .or_else(|_| std::env::var("USERPROFILE").map(|h| PathBuf::from(h).join("Desktop")))
        .map_err(|_| {
            ProductOSError::from(ProxyError::Generic("could not resolve Desktop".into()))
        })?;
    let dest = desktop.join("product-os-proxy-ca.pem");
    std::fs::write(&dest, managed.cert_pem()).map_err(|e| {
        ProductOSError::from(ProxyError::Generic(format!("failed to export CA: {e}")))
    })?;
    println!("Exported CA to {}", dest.display());
    println!(
        "Fingerprint: {}",
        managed
            .fingerprint_sha256()
            .unwrap_or_else(|_| "unknown".into())
    );
    Ok(())
}

fn print_status_header(managed: &ManagedCa, targets: &[String]) {
    println!("CA path: {}", managed.cert_path().display());
    println!(
        "Fingerprint: {}",
        managed
            .fingerprint_sha256()
            .unwrap_or_else(|_| "unknown".into())
    );
    for target in targets {
        println!("{target}: {}", TrustProbe::check_target(managed, target));
    }
}

fn log_missing_trust(managed: &ManagedCa, proxy_host: &str, proxy_port: u16, targets: &[String]) {
    let fingerprint = managed
        .fingerprint_sha256()
        .unwrap_or_else(|_| "unknown".into());
    tracing::warn!(
        cert_path = %managed.cert_path().display(),
        fingerprint = %fingerprint,
        setup_url = %format!("http://{proxy_host}:{proxy_port}/_product-os/setup"),
        "MITM root CA is not trusted"
    );
    for target in targets {
        let status = TrustProbe::check_target(managed, target);
        tracing::warn!(target = %target, status = %status, "trust status");
    }
    tracing::warn!("Run `pos-proxy cert guide` to install and trust the CA");
}

fn print_manual_steps(managed: &ManagedCa) {
    println!("\nManual installation steps:");
    for step in manual_trust_steps(managed) {
        println!("{step}");
    }
}