xbp 0.9.4

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use anyhow::{Context, Result};
use regex::Regex;
use std::fs;
use std::path::PathBuf;
use tokio::process::Command;
use tracing::{debug, warn, info};
use crate::logging::{log_error, log_info, log_success, get_prefix};

pub async fn setup_nginx(domain: &str, port: u16, debug: bool) -> Result<()> {
    let _ = log_info("nginx", &format!("Setting up Nginx for {} on port {}", domain, port), None).await;

    let config_content = format!(
        "server {{
    server_name {};

    location / {{
        proxy_pass http://127.0.0.1:{};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }}
}}",
        domain, port
    );

    let available_path = format!("/etc/nginx/sites-available/{}", domain);
    let enabled_path = format!("/etc/nginx/sites-enabled/{}", domain);

    // Write config file (requires sudo, so we write to a temp file and move it)
    let escaped_content = config_content.replace("'", "'\\''");
    
    let write_cmd = format!("echo '{}' | sudo tee {}", escaped_content, available_path);
    
    if debug {
        debug!("Executing: {}", write_cmd);
    }

    let output = Command::new("sh")
        .arg("-c")
        .arg(&write_cmd)
        .output()
        .await
        .context("Failed to write Nginx config")?;

    if !output.status.success() {
        return Err(anyhow::anyhow!("Failed to write config: {}", String::from_utf8_lossy(&output.stderr)));
    }

    // Link to sites-enabled
    let link_cmd = format!("sudo ln -sf {} {}", available_path, enabled_path);
    if debug { debug!("Executing: {}", link_cmd); }
    let output = Command::new("sh").arg("-c").arg(&link_cmd).output().await?;
    if !output.status.success() {
        return Err(anyhow::anyhow!("Failed to link config: {}", String::from_utf8_lossy(&output.stderr)));
    }

    // Test configuration
    let test_cmd = "sudo nginx -t";
    if debug { debug!("Executing: {}", test_cmd); }
    let output = Command::new("sh").arg("-c").arg(test_cmd).output().await?;
    if !output.status.success() {
        let _ = log_error("nginx", "Nginx configuration test failed", Some(&String::from_utf8_lossy(&output.stderr))).await;
        return Err(anyhow::anyhow!("Nginx config test failed"));
    }

    // Run certbot
    let _ = log_info("nginx", "Running certbot...", None).await;
    // Reload Nginx
    let reload_cmd = "sudo systemctl reload nginx";
    if debug { debug!("Executing: {}", reload_cmd); }
    let output = Command::new("sh").arg("-c").arg(reload_cmd).output().await?;
    if !output.status.success() {
        return Err(anyhow::anyhow!("Failed to reload Nginx: {}", String::from_utf8_lossy(&output.stderr)));
    }

    let _ = log_success("nginx", &format!("Successfully setup {}", domain), None).await;
    Ok(())
}

pub async fn list_nginx(debug: bool) -> Result<()> {
    let sites_enabled = PathBuf::from("/etc/nginx/sites-enabled");
    if !sites_enabled.exists() {
        warn!("/etc/nginx/sites-enabled does not exist");
        return Ok(());
    }

    let mut entries = fs::read_dir(sites_enabled)?;
    // Relaxed regex to handle whitespace and potential variations
    let server_name_regex = Regex::new(r"server_name\s+([^;]+);").unwrap();
    // Relaxed regex for proxy_pass to handle different localhost representations and spacing
    let proxy_pass_regex = Regex::new(r"proxy_pass\s+http://(?:127\.0\.0\.1|localhost):(\d+)(?:;|/)").unwrap();

    let prefix = get_prefix();
    info!("{}{:<30} {:<10}", prefix, "DOMAIN", "PORT");
    info!("{}{:-<40}", prefix, "");

    while let Some(entry) = entries.next() {
        let entry = entry?;
        let path = entry.path();
        if path.is_file() || path.is_symlink() {
            let content = fs::read_to_string(&path).unwrap_or_default();
            
            if debug {
                debug!("Analyzing config file: {}", path.display());
                debug!("Content preview: {:.100}...", content);
            }

            let domain = server_name_regex.captures(&content)
                .map(|c| c.get(1).map_or("", |m| m.as_str().trim()))
                .unwrap_or("unknown");
            
            let port = proxy_pass_regex.captures(&content)
                .map(|c| c.get(1).map_or("", |m| m.as_str()))
                .unwrap_or("-");

            if debug && (domain == "unknown" || port == "-") {
                 debug!("Failed to detect domain or port in {}. Domain: {}, Port: {}", path.display(), domain, port);
                 debug!("Full content:\n{}", content);
            }

            if !domain.is_empty() && domain != "_" {
                 info!("{}{:<30} {:<10}", prefix, domain, port);
            }
        }
    }
    Ok(())
}

pub async fn update_nginx(domain: &str, port: u16, debug: bool) -> Result<()> {
    let available_path = format!("/etc/nginx/sites-available/{}", domain);
    
    let content = match fs::read_to_string(&available_path) {
        Ok(c) => c,
        Err(_) => {
            let output = Command::new("sudo").arg("cat").arg(&available_path).output().await?;
            if !output.status.success() {
                return Err(anyhow::anyhow!("Config for {} not found", domain));
            }
            String::from_utf8_lossy(&output.stdout).to_string()
        }
    };

    if debug {
        debug!("Original Nginx Config for {}:\n{}", domain, content);
    }

    let proxy_pass_regex = Regex::new(r"(proxy_pass\s+http://(?:127\.0\.0\.1|localhost):)(\d+)(.*)").unwrap();
    
    if !proxy_pass_regex.is_match(&content) {
         if debug {
             debug!("Regex failed to match proxy_pass in content.");
         }
         return Err(anyhow::anyhow!("Could not find proxy_pass directive in config"));
    }

    let new_content = proxy_pass_regex.replace_all(&content, |caps: &regex::Captures| {
        format!("{}{}{}", &caps[1], port, &caps[3])
    });

    if debug {
        debug!("New Nginx Config Preview for {}:\n{}", domain, new_content);
    }

    let escaped_content = new_content.replace("'", "'\\''");
    let write_cmd = format!("echo '{}' | sudo tee {}", escaped_content, available_path);
    
    let output = Command::new("sh").arg("-c").arg(&write_cmd).output().await?;
    if !output.status.success() {
        return Err(anyhow::anyhow!("Failed to update config: {}", String::from_utf8_lossy(&output.stderr)));
    }

    let _ = log_success("nginx", &format!("Updated {} to port {}", domain, port), None).await;
    
    let output = Command::new("sudo").arg("systemctl").arg("reload").arg("nginx").output().await?;
    if !output.status.success() {
        warn!("Failed to reload nginx: {}", String::from_utf8_lossy(&output.stderr));
    } else {
        let _ = log_info("nginx", "Nginx reloaded", None).await;
    }

    Ok(())
}