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);
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)));
}
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)));
}
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"));
}
let _ = log_info("nginx", "Running certbot...", None).await;
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)?;
let server_name_regex = Regex::new(r"server_name\s+([^;]+);").unwrap();
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: ®ex::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(())
}