use std::os::unix::fs::PermissionsExt; use serde::{Deserialize, Serialize}; use crate::config::settings::{Validate, ValidationError}; const NGINX_BINARY_PATH_TEMPLATE: &str = "{{nginx_binary_path}}"; const NGINX_DEFAULT_BINARY: &str = "nginx"; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct NginxSettings { #[serde(default = "default_nginx_config_path")] pub nginx_config_path: String, // #[serde(default = "default_nginx_binary_path")] #[serde(default)] pub nginx_binary_path: Option, // commands #[serde(default = "default_nginx_reload_command")] pub override_nginx_reload_command: Vec, #[serde(default = "default_nginx_test_command")] pub override_nginx_test_command: Vec, // timeouts #[serde(default = "default_nginx_reload_timeout_seconds")] pub nginx_reload_timeout_seconds: u64, #[serde(default = "default_nginx_test_timeout_seconds")] pub nginx_test_timeout_seconds: u64, } impl NginxSettings { /// Transforms the reload and test commands by replacing the binary path template with the actual binary path if provided. /// This MUST be called after validation to ensure the binary path is valid and the commands contain the template. pub fn transform_commands(&mut self) { self.override_nginx_reload_command = self.transformed_reload_command(); self.override_nginx_test_command = self.transformed_test_command(); } fn transformed_reload_command(&self) -> Vec { self.override_nginx_reload_command .iter() .map(|cmd| { cmd.replace( NGINX_BINARY_PATH_TEMPLATE, &self .nginx_binary_path .clone() .unwrap_or_else(|| NGINX_DEFAULT_BINARY.into()), ) }) .collect() } fn transformed_test_command(&self) -> Vec { self.override_nginx_test_command .iter() .map(|cmd| { cmd.replace( NGINX_BINARY_PATH_TEMPLATE, &self .nginx_binary_path .clone() .unwrap_or_else(|| NGINX_DEFAULT_BINARY.into()), ) }) .collect() } } impl Validate for NginxSettings { fn validate(&self) -> Result<(), ValidationError> { match &self.nginx_binary_path { Some(path) if path.is_empty() => { return Err("Nginx binary path cannot be empty".into()); } Some(path) if !std::path::Path::new(path).exists() => { return Err(format!("Nginx binary not found: {}", path)); } Some(path) if !std::fs::metadata(path) .map_err(|e| format!("Failed to read nginx binary metadata: {}", e))? .permissions() .mode() & 0o111 != 0 => { return Err(format!("Nginx binary is not executable: {}", path)); } _ => {} } if self.nginx_config_path.is_empty() { return Err("Nginx config path cannot be empty".into()); } if !std::path::Path::new(&self.nginx_config_path).exists() { return Err(format!( "Nginx config file not found: {}", self.nginx_config_path )); } // ensure reload and test commands contain the binary path template if !&self .override_nginx_reload_command .join(" ") .contains(NGINX_BINARY_PATH_TEMPLATE) { return Err(format!( "Nginx reload command must contain the binary path template '{}': {}", NGINX_BINARY_PATH_TEMPLATE, self.override_nginx_reload_command.join(" ") )); } if !&self .override_nginx_test_command .join(" ") .contains(NGINX_BINARY_PATH_TEMPLATE) { return Err(format!( "Nginx test command must contain the binary path template '{}': {}", NGINX_BINARY_PATH_TEMPLATE, self.override_nginx_test_command.join(" ") )); } Ok(()) } } fn default_nginx_config_path() -> String { "/etc/nginx/nginx.conf".into() } fn default_nginx_reload_command() -> Vec { vec![ NGINX_BINARY_PATH_TEMPLATE.to_string(), "-s".to_string(), "reload".to_string(), ] } fn default_nginx_test_command() -> Vec { vec![NGINX_BINARY_PATH_TEMPLATE.to_string(), "-t".to_string()] } fn default_nginx_reload_timeout_seconds() -> u64 { 30 } fn default_nginx_test_timeout_seconds() -> u64 { 30 } #[cfg(test)] mod tests { use std::{fs, os::unix::fs::PermissionsExt, path::Path}; use tempfile::TempDir; use super::*; #[test] fn test_esnure_send_and_sync() { fn assert_send_sync() {} assert_send_sync::(); } fn write_file(path: &Path) { let result = fs::write(path, b"content"); assert!(result.is_ok()); } fn create_exec_file(path: &Path) { write_file(path); let metadata = fs::metadata(path); assert!(metadata.is_ok()); let metadata = metadata.ok(); assert!(metadata.is_some()); let metadata = metadata.unwrap_or_else(|| unreachable!()); let mut perms = metadata.permissions(); perms.set_mode(0o755); let result = fs::set_permissions(path, perms); assert!(result.is_ok()); } fn create_non_exec_file(path: &Path) { write_file(path); let metadata = fs::metadata(path); assert!(metadata.is_ok()); let metadata = metadata.ok(); assert!(metadata.is_some()); let metadata = metadata.unwrap_or_else(|| unreachable!()); let mut perms = metadata.permissions(); perms.set_mode(0o644); let result = fs::set_permissions(path, perms); assert!(result.is_ok()); } #[test] fn nginx_validate_succeeds_for_valid_paths_and_commands() { let temp_dir = TempDir::new(); assert!(temp_dir.is_ok()); let temp_dir = temp_dir.ok(); assert!(temp_dir.is_some()); let temp_dir = temp_dir.unwrap_or_else(|| unreachable!()); let nginx_binary = temp_dir.path().join("nginx"); let nginx_config = temp_dir.path().join("nginx.conf"); create_exec_file(&nginx_binary); write_file(&nginx_config); let nginx = NginxSettings { nginx_config_path: nginx_config.to_string_lossy().to_string(), nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()), override_nginx_reload_command: default_nginx_reload_command(), override_nginx_test_command: default_nginx_test_command(), nginx_reload_timeout_seconds: 30, nginx_test_timeout_seconds: 30, }; assert!(nginx.validate().is_ok()); } #[test] fn nginx_validate_fails_for_non_executable_binary() { let temp_dir = TempDir::new(); assert!(temp_dir.is_ok()); let temp_dir = temp_dir.ok(); assert!(temp_dir.is_some()); let temp_dir = temp_dir.unwrap_or_else(|| unreachable!()); let nginx_binary = temp_dir.path().join("nginx"); let nginx_config = temp_dir.path().join("nginx.conf"); create_non_exec_file(&nginx_binary); write_file(&nginx_config); let nginx = NginxSettings { nginx_config_path: nginx_config.to_string_lossy().to_string(), nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()), override_nginx_reload_command: default_nginx_reload_command(), override_nginx_test_command: default_nginx_test_command(), nginx_reload_timeout_seconds: 30, nginx_test_timeout_seconds: 30, }; let result = nginx.validate(); assert!(result.is_err()); let msg = result.err().unwrap_or_else(|| unreachable!()); assert!(msg.contains("Nginx binary is not executable")); } #[test] fn nginx_validate_fails_when_reload_command_lacks_template() { let temp_dir = TempDir::new(); assert!(temp_dir.is_ok()); let temp_dir = temp_dir.ok(); assert!(temp_dir.is_some()); let temp_dir = temp_dir.unwrap_or_else(|| unreachable!()); let nginx_binary = temp_dir.path().join("nginx"); let nginx_config = temp_dir.path().join("nginx.conf"); create_exec_file(&nginx_binary); write_file(&nginx_config); let nginx = NginxSettings { nginx_config_path: nginx_config.to_string_lossy().to_string(), nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()), override_nginx_reload_command: vec!["nginx".into(), "-s".into(), "reload".into()], override_nginx_test_command: default_nginx_test_command(), nginx_reload_timeout_seconds: 30, nginx_test_timeout_seconds: 30, }; let result = nginx.validate(); assert!(result.is_err()); let msg = result.err().unwrap_or_else(|| unreachable!()); assert!(msg.contains("Nginx reload command must contain the binary path template")); } }