use crate::logging::{get_prefix, log_error, log_info, log_success};
use anyhow::{Context, Result};
use regex::Regex;
use std::fs;
use std::path::PathBuf;
use tokio::process::Command;
use tracing::{debug, info, warn};
#[derive(Debug, Clone)]
pub struct NginxSiteInfo {
pub domain: String,
pub path: PathBuf,
pub upstream_ports: Vec<u16>,
pub listen_ports: Vec<u16>,
pub content: Option<String>,
}
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 = inspect_nginx_configs(false)?;
if sites.is_empty() {
warn!("No NGINX site configs found");
return Ok(());
}
let prefix = get_prefix();
info!(
"{}{:<30} {:<14} {:<14} {}",
prefix, "DOMAIN", "UPSTREAM", "LISTEN", "PATH"
);
info!("{}{:-<90}", prefix, "");
for site in sites {
if debug {
debug!(
"NGINX site: domain={}, upstream_ports={:?}, listen_ports={:?}, path={}",
site.domain,
site.upstream_ports,
site.listen_ports,
site.path.display()
);
}
info!(
"{}{:<30} {:<14} {:<14} {}",
prefix,
site.domain,
join_ports(&site.upstream_ports),
join_ports(&site.listen_ports),
site.path.display()
);
}
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(())
}
pub fn inspect_nginx_configs(include_content: bool) -> Result<Vec<NginxSiteInfo>> {
let mut results = Vec::new();
let mut seen = std::collections::HashSet::new();
for dir in nginx_config_directories() {
if !dir.exists() {
continue;
}
for entry in fs::read_dir(&dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() && !path.is_symlink() {
continue;
}
let canonical = path.canonicalize().unwrap_or_else(|_| path.clone());
if !seen.insert(canonical) {
continue;
}
let content = fs::read_to_string(&path).unwrap_or_default();
let site = parse_nginx_site(&path, &content, include_content);
results.push(site);
}
}
results.sort_by(|a, b| a.domain.cmp(&b.domain));
Ok(results)
}
pub async fn show_nginx(domain: Option<&str>, debug: bool) -> Result<()> {
let mut sites = inspect_nginx_configs(true)?;
if let Some(domain) = domain {
sites.retain(|site| site.domain == domain);
}
if sites.is_empty() {
return Err(anyhow::anyhow!("No matching NGINX config found"));
}
for site in sites {
println!("Domain: {}", site.domain);
println!("Path: {}", site.path.display());
println!("Upstream Ports: {}", join_ports(&site.upstream_ports));
println!("Listen Ports: {}", join_ports(&site.listen_ports));
println!("{}", "-".repeat(80));
if let Some(content) = site.content {
println!("{}", content);
}
if debug {
println!("{}", "=".repeat(80));
}
}
Ok(())
}
pub async fn edit_nginx(domain: &str, _debug: bool) -> Result<()> {
let site = inspect_nginx_configs(false)?
.into_iter()
.find(|site| site.domain == domain)
.ok_or_else(|| anyhow::anyhow!("No matching NGINX config found for {}", domain))?;
println!("Opening {}", site.path.display());
crate::utils::open_path_with_editor(&site.path)
.map_err(|e| anyhow::anyhow!("Failed to open editor: {}", e))?;
Ok(())
}
fn parse_nginx_site(path: &PathBuf, content: &str, include_content: bool) -> NginxSiteInfo {
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 listen_regex = Regex::new(r"listen\s+(\d+)").unwrap();
let domain = server_name_regex
.captures(content)
.and_then(|captures| {
captures
.get(1)
.map(|value| value.as_str().trim().to_string())
})
.unwrap_or_else(|| {
path.file_name()
.and_then(|name| name.to_str())
.unwrap_or("unknown")
.to_string()
});
let upstream_ports = proxy_pass_regex
.captures_iter(content)
.filter_map(|captures| {
captures
.get(1)
.and_then(|value| value.as_str().parse::<u16>().ok())
})
.collect::<Vec<u16>>();
let listen_ports = listen_regex
.captures_iter(content)
.filter_map(|captures| {
captures
.get(1)
.and_then(|value| value.as_str().parse::<u16>().ok())
})
.collect::<Vec<u16>>();
NginxSiteInfo {
domain,
path: path.clone(),
upstream_ports,
listen_ports,
content: include_content.then(|| content.to_string()),
}
}
fn join_ports(ports: &[u16]) -> String {
if ports.is_empty() {
"-".to_string()
} else {
ports
.iter()
.map(|port| port.to_string())
.collect::<Vec<_>>()
.join(",")
}
}
fn nginx_config_directories() -> Vec<PathBuf> {
vec![
PathBuf::from("/etc/nginx/sites-enabled"),
PathBuf::from("/etc/nginx/sites-available"),
]
}