From c5e82d4101284949afb140c64cc15db2789addc1 Mon Sep 17 00:00:00 2001 From: Cylae <13425054+Cylae@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:25:04 +0000 Subject: [PATCH] Refactor config loading, optimize dashboard, and fix Nextcloud config security - Refactor `Config::load` and `Config::load_async` to support `/opt/server_manager/config.yaml` as a fallback path and correctly manage caching with file path awareness. - Optimize web dashboard by wrapping blocking `sysinfo` refresh in `tokio::task::spawn_blocking` to prevent blocking the async runtime. - Fix potential security issue in Nextcloud configuration by escaping `$` characters in generated PHP autoconfig to prevent variable interpolation. - Bump version to 1.0.8 in `Cargo.toml` and `README.md`. --- README.md | 2 +- server_manager/Cargo.lock | 2 +- server_manager/Cargo.toml | 2 +- server_manager/src/core/config.rs | 141 +++++++++++++++++++--------- server_manager/src/interface/web.rs | 83 ++++++++-------- server_manager/src/services/apps.rs | 2 +- 6 files changed, 149 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 6187960..1d57e34 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Server Manager - Next-Gen Media Server Orchestrator 🚀 -![Server Manager Banner](https://img.shields.io/badge/Status-Tested-brightgreen) ![Version](https://img.shields.io/badge/Version-1.0.7-blue) ![Rust](https://img.shields.io/badge/Built%20With-Rust-orange) ![Docker](https://img.shields.io/badge/Powered%20By-Docker-blue) +![Server Manager Banner](https://img.shields.io/badge/Status-Tested-brightgreen) ![Version](https://img.shields.io/badge/Version-1.0.8-blue) ![Rust](https://img.shields.io/badge/Built%20With-Rust-orange) ![Docker](https://img.shields.io/badge/Powered%20By-Docker-blue) **Server Manager** is a powerful and intelligent tool written in Rust to deploy, manage, and optimize a complete personal media and cloud server stack. It detects your hardware and automatically configures 28 Docker services for optimal performance. diff --git a/server_manager/Cargo.lock b/server_manager/Cargo.lock index 9d73bac..88fc67f 100644 --- a/server_manager/Cargo.lock +++ b/server_manager/Cargo.lock @@ -1288,7 +1288,7 @@ dependencies = [ [[package]] name = "server_manager" -version = "1.0.7" +version = "1.0.8" dependencies = [ "anyhow", "async-trait", diff --git a/server_manager/Cargo.toml b/server_manager/Cargo.toml index 475f80e..59a6547 100644 --- a/server_manager/Cargo.toml +++ b/server_manager/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "server_manager" -version = "1.0.7" +version = "1.0.8" edition = "2021" [dependencies] diff --git a/server_manager/src/core/config.rs b/server_manager/src/core/config.rs index c57c2a7..61ca5d5 100644 --- a/server_manager/src/core/config.rs +++ b/server_manager/src/core/config.rs @@ -3,7 +3,7 @@ use log::info; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::OnceLock; use std::time::SystemTime; use tokio::sync::RwLock; @@ -12,6 +12,7 @@ use tokio::sync::RwLock; struct CachedConfig { config: Config, last_mtime: Option, + loaded_path: Option, } static CONFIG_CACHE: OnceLock> = OnceLock::new(); @@ -25,8 +26,18 @@ pub struct Config { impl Config { pub fn load() -> Result { let path = Path::new("config.yaml"); - if path.exists() { - let content = fs::read_to_string(path).context("Failed to read config.yaml")?; + let fallback_path = Path::new("/opt/server_manager/config.yaml"); + + let load_path = if path.exists() { + Some(path) + } else if fallback_path.exists() { + Some(fallback_path) + } else { + None + }; + + if let Some(p) = load_path { + let content = fs::read_to_string(p).context("Failed to read config.yaml")?; // If empty file, return default if content.trim().is_empty() { return Ok(Config::default()); @@ -42,70 +53,116 @@ impl Config { RwLock::new(CachedConfig { config: Config::default(), last_mtime: None, + loaded_path: None, }) }); + // Determine current priority path + let path = Path::new("config.yaml"); + let fallback_path = Path::new("/opt/server_manager/config.yaml"); + + // Check for existence asynchronously + let current_path = if tokio::fs::try_exists(path).await.unwrap_or(false) { + Some(path.to_path_buf()) + } else if tokio::fs::try_exists(fallback_path).await.unwrap_or(false) { + Some(fallback_path.to_path_buf()) + } else { + None + }; + // Fast path: Optimistic read { let guard = cache.read().await; - if let Some(cached_mtime) = guard.last_mtime { - // Check if file still matches - if let Ok(metadata) = tokio::fs::metadata("config.yaml").await { - if let Ok(modified) = metadata.modified() { - if modified == cached_mtime { - return Ok(guard.config.clone()); + if let Some(p) = ¤t_path { + // If we are looking at the same file as cached + if guard.loaded_path.as_ref() == Some(p) { + if let Some(cached_mtime) = guard.last_mtime { + // Check if file still matches + if let Ok(metadata) = tokio::fs::metadata(p).await { + if let Ok(modified) = metadata.modified() { + if modified == cached_mtime { + return Ok(guard.config.clone()); + } + } } } } + } else if guard.loaded_path.is_none() { + // No file exists and cache has no file -> return default + return Ok(guard.config.clone()); } } // Slow path: Update cache let mut guard = cache.write().await; - // Check metadata again (double-checked locking pattern) - let metadata_res = tokio::fs::metadata("config.yaml").await; - - match metadata_res { - Ok(metadata) => { - let modified = metadata.modified().unwrap_or(SystemTime::now()); - - if let Some(cached_mtime) = guard.last_mtime { - if modified == cached_mtime { - return Ok(guard.config.clone()); + // Re-evaluate path under lock (though unlikely to race in this context, good practice) + let current_path_2 = if tokio::fs::try_exists(path).await.unwrap_or(false) { + Some(path.to_path_buf()) + } else if tokio::fs::try_exists(fallback_path).await.unwrap_or(false) { + Some(fallback_path.to_path_buf()) + } else { + None + }; + + if let Some(p) = current_path_2 { + let metadata_res = tokio::fs::metadata(&p).await; + match metadata_res { + Ok(metadata) => { + let modified = metadata.modified().unwrap_or(SystemTime::now()); + + // Check if cache is already up to date for this path + if guard.loaded_path.as_ref() == Some(&p) { + if let Some(cached_mtime) = guard.last_mtime { + if modified == cached_mtime { + return Ok(guard.config.clone()); + } + } } - } - // Load file - match tokio::fs::read_to_string("config.yaml").await { - Ok(content) => { - let config = if content.trim().is_empty() { - Config::default() - } else { - serde_yaml_ng::from_str(&content) - .context("Failed to parse config.yaml")? - }; - - guard.config = config.clone(); - guard.last_mtime = Some(modified); - Ok(config) + // Load file + match tokio::fs::read_to_string(&p).await { + Ok(content) => { + let config = if content.trim().is_empty() { + Config::default() + } else { + serde_yaml_ng::from_str(&content) + .context("Failed to parse config.yaml")? + }; + + guard.config = config.clone(); + guard.last_mtime = Some(modified); + guard.loaded_path = Some(p); + Ok(config) + } + Err(e) => Err(anyhow::Error::new(e).context("Failed to read config.yaml")), } - Err(e) => Err(anyhow::Error::new(e).context("Failed to read config.yaml")), } + Err(e) => Err(anyhow::Error::new(e).context("Failed to read config metadata")), } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - // File not found -> Default - guard.config = Config::default(); - guard.last_mtime = None; - Ok(guard.config.clone()) - } - Err(e) => Err(anyhow::Error::new(e).context("Failed to read config metadata")), + } else { + // File not found -> Default + guard.config = Config::default(); + guard.last_mtime = None; + guard.loaded_path = None; + Ok(guard.config.clone()) } } pub fn save(&self) -> Result<()> { + // Prefer saving to /opt/server_manager if it exists/is writable (checked by parent dir existence), else CWD + let target = if Path::new("/opt/server_manager").exists() { + Path::new("/opt/server_manager/config.yaml") + } else { + Path::new("config.yaml") + }; + let content = serde_yaml_ng::to_string(self)?; - fs::write("config.yaml", content).context("Failed to write config.yaml")?; + fs::write(target, content).context("Failed to write config.yaml")?; + + // Invalidate cache implicitly? + // Ideally we should update the cache here or invalidate it. + // Since the cache checks mtime, it will pick up the change on next read. Ok(()) } diff --git a/server_manager/src/interface/web.rs b/server_manager/src/interface/web.rs index 1f677c3..a45744c 100644 --- a/server_manager/src/interface/web.rs +++ b/server_manager/src/interface/web.rs @@ -331,43 +331,52 @@ async fn dashboard(State(state): State, session: Session) -> impl I let config = state.get_config().await; // System Stats - let mut sys = state.system.lock().unwrap(); - let now = SystemTime::now(); - let mut last_refresh = state.last_system_refresh.lock().unwrap(); - - // Throttle refresh to max once every 500ms - if now - .duration_since(*last_refresh) - .unwrap_or_default() - .as_millis() - > 500 - { - sys.refresh_cpu(); - sys.refresh_memory(); - sys.refresh_disks(); - *last_refresh = now; - } - let ram_used = sys.used_memory() / 1024 / 1024; // MB - let ram_total = sys.total_memory() / 1024 / 1024; // MB - let swap_used = sys.used_swap() / 1024 / 1024; // MB - let swap_total = sys.total_swap() / 1024 / 1024; // MB - let cpu_usage = sys.global_cpu_info().cpu_usage(); - - // Simple Disk Usage (Root or fallback) - let mut disk_total = 0; - let mut disk_used = 0; - - let target_disk = sys - .disks() - .iter() - .find(|d| d.mount_point() == std::path::Path::new("/")) - .or_else(|| sys.disks().first()); - - if let Some(disk) = target_disk { - disk_total = disk.total_space() / 1024 / 1024 / 1024; // GB - disk_used = (disk.total_space() - disk.available_space()) / 1024 / 1024 / 1024; // GB - } - drop(sys); // Release lock explicitely + let state_clone = state.clone(); + let (ram_used, ram_total, swap_used, swap_total, cpu_usage, disk_total, disk_used) = + tokio::task::spawn_blocking(move || { + let mut sys = state_clone.system.lock().unwrap(); + let now = SystemTime::now(); + let mut last_refresh = state_clone.last_system_refresh.lock().unwrap(); + + // Throttle refresh to max once every 500ms + if now + .duration_since(*last_refresh) + .unwrap_or_default() + .as_millis() + > 500 + { + sys.refresh_cpu(); + sys.refresh_memory(); + sys.refresh_disks(); + *last_refresh = now; + } + let ram_used = sys.used_memory() / 1024 / 1024; // MB + let ram_total = sys.total_memory() / 1024 / 1024; // MB + let swap_used = sys.used_swap() / 1024 / 1024; // MB + let swap_total = sys.total_swap() / 1024 / 1024; // MB + let cpu_usage = sys.global_cpu_info().cpu_usage(); + + // Simple Disk Usage (Root or fallback) + let mut disk_total = 0; + let mut disk_used = 0; + + let target_disk = sys + .disks() + .iter() + .find(|d| d.mount_point() == std::path::Path::new("/")) + .or_else(|| sys.disks().first()); + + if let Some(disk) = target_disk { + disk_total = disk.total_space() / 1024 / 1024 / 1024; // GB + disk_used = (disk.total_space() - disk.available_space()) / 1024 / 1024 / 1024; // GB + } + + ( + ram_used, ram_total, swap_used, swap_total, cpu_usage, disk_total, disk_used, + ) + }) + .await + .unwrap_or_default(); let mut html = String::with_capacity(8192); write_html_head(&mut html, "Dashboard - Server Manager"); diff --git a/server_manager/src/services/apps.rs b/server_manager/src/services/apps.rs index ff924f6..a07b855 100644 --- a/server_manager/src/services/apps.rs +++ b/server_manager/src/services/apps.rs @@ -190,7 +190,7 @@ impl Service for NextcloudService { return Ok(()); } - let escape_php = |s: &str| s.replace('\\', "\\\\").replace('"', "\\\""); + let escape_php = |s: &str| s.replace('\\', "\\\\").replace('"', "\\\"").replace('$', "\\$"); let db_pass = escape_php(&secrets.nextcloud_db_password.clone().unwrap_or_default()); let admin_pass = escape_php(&secrets.nextcloud_admin_password.clone().unwrap_or_default());