From 395c00434389195540162e7a4bb0815a1d49a9e7 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sat, 19 Jul 2025 08:37:01 -0500 Subject: [PATCH] ok --- Cargo.lock | 137 ++++++++++++++++++++++ Cargo.toml | 3 + src/app_manager.rs | 267 ------------------------------------------- src/main.rs | 152 +++++++++++++++++++++--- src/server_config.rs | 5 + src/service.rs | 65 +++++++++++ test.js | 48 -------- 7 files changed, 349 insertions(+), 328 deletions(-) delete mode 100644 src/app_manager.rs create mode 100644 src/service.rs delete mode 100644 test.js diff --git a/Cargo.lock b/Cargo.lock index a505e05..b643a07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,6 +266,29 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97493a391b4b18ee918675fb8663e53646fd09321c58b46afa04e8ce2499c869" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2af3eac944c12cdf4423eab70d310da0a8e5851a18ffb192c0a5e3f7ae1663" +dependencies = [ + "darling", + "ident_case", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "brotli" version = "8.0.1" @@ -358,6 +381,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -415,6 +473,12 @@ dependencies = [ "syn", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -544,6 +608,12 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "http" version = "0.2.12" @@ -664,6 +734,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -712,6 +788,15 @@ dependencies = [ "libc", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -734,6 +819,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.174" @@ -1035,6 +1126,12 @@ version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "ryu" version = "1.0.20" @@ -1052,9 +1149,12 @@ name = "sentryx-server" version = "0.1.0" dependencies = [ "actix-web", + "lazy_static", + "regex", "serde", "serde_json", "sysinfo", + "systemctl", "tungstenite", ] @@ -1156,6 +1256,31 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.104" @@ -1192,6 +1317,18 @@ dependencies = [ "windows", ] +[[package]] +name = "systemctl" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c8ce0ab2b5159ff3bd1ad03ac4f84d406438f1c42f1e83c0698b1c8041365ee" +dependencies = [ + "bon", + "itertools", + "strum", + "strum_macros", +] + [[package]] name = "thiserror" version = "2.0.12" diff --git a/Cargo.toml b/Cargo.toml index 2085c57..c55bcd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,10 @@ edition = "2021" [dependencies] actix-web = "4.11.0" +lazy_static = "1.5.0" +regex = "1.11.1" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" sysinfo = "0.36.0" +systemctl = "0.5.0" tungstenite = "0.27.0" diff --git a/src/app_manager.rs b/src/app_manager.rs deleted file mode 100644 index 7952175..0000000 --- a/src/app_manager.rs +++ /dev/null @@ -1,267 +0,0 @@ -use std::env; -use std::fs; -use std::fs::remove_file; -use std::path::PathBuf; -use std::process::Command; - -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct App { - pub repo: String, - pub branch: String, - pub install_command: String, - pub run_command: String, -} - -impl App { - /// Converts URL to "user/repo" - pub fn id(&self) -> Option { - let url = self.repo.trim_end_matches(".git"); - - if url.starts_with("git@") { - // Format: git@github.com:usr/repo - url.split_once(':').map(|(_, path)| path.to_string()) - } else if url.starts_with("https://") || url.starts_with("http://") { - // Format: https://github.com/usr/repo - url.split_once("github.com/") - .map(|(_, path)| path.to_string()) - } else { - None - } - } - - /// Converts URL to "user-repo" - pub fn id_system(&self) -> Option { - let url = self.repo.trim_end_matches(".git"); - - let repo_path = if url.starts_with("git@") { - url.split_once(':').map(|(_, path)| path) - } else if url.starts_with("https://") || url.starts_with("http://") { - url.split_once("github.com/").map(|(_, path)| path) - } else { - None - }?; - - Some(repo_path.replace('/', "-")) - } - - pub fn repo_folder_name(&self) -> String { - self.repo - .trim_end_matches(".git") - .rsplit('/') - .next() - .unwrap_or("unknown") - .to_string() - } -} - -fn home_dir() -> PathBuf { - let home = env::var("HOME").expect("Failed to get HOME environment variable"); - PathBuf::from(home) -} - -fn apps_dir() -> PathBuf { - let home = env::var("HOME").expect("Failed to get HOME environment variable"); - PathBuf::from(home).join("sentryx/apps") -} - -fn apps_file_path() -> PathBuf { - apps_dir().join("apps.json") -} - -fn update_systemd() { - Command::new("systemctl") - .arg("--user") - .arg("daemon-reload") - .status() - .expect("Failed to reload systemd daemon"); -} - -pub fn init() -> Vec { - let path = apps_file_path(); - - if !path.exists() { - let _ = fs::create_dir_all(path.parent().unwrap()); - let _ = fs::write(&path, "[]"); - } - - let content = fs::read_to_string(&path).unwrap_or_else(|_| "[]".to_string()); - serde_json::from_str(&content).unwrap_or_else(|_| vec![]) -} - -pub fn save_apps(apps: &[App]) { - let path = apps_file_path(); - let json = serde_json::to_string_pretty(apps).expect("Failed to serialize apps"); - - if let Some(parent) = path.parent() { - let _ = fs::create_dir_all(parent); - } - - fs::write(path, json).expect("Failed to write apps.json"); -} - -pub fn uninstall_app(apps: &mut Vec, index: usize) { - if index >= apps.len() { - eprintln!("Invalid app index: {}", index); - return; - } - - let app = &apps[index]; - let repo_dir = apps_dir().join(app.repo_folder_name()); - - if repo_dir.exists() { - if let Err(e) = fs::remove_dir_all(&repo_dir) { - eprintln!("Failed to remove directory {:?}: {}", repo_dir, e); - } else { - println!("Removed app: {}", app.repo_folder_name()); - } - } - - let user_systemd_dir = home_dir().join(".config/systemd/user"); - - let service_path = user_systemd_dir.join(format!( - "{}.service", - app.id_system().expect("Unable to obtain id") - )); - - println!("Removing service file: {:?}", service_path); - - remove_file(service_path).unwrap_or_else(|e| { - eprintln!("Failed to remove service file: {}", e); - }); - - update_systemd(); - - apps.remove(index); - save_apps(apps); -} - -pub fn install_app(apps: &mut Vec, app: App) { - let path = apps_dir(); - let repo_name = app.repo_folder_name(); - - // Ensure apps directory exists - if let Err(e) = fs::create_dir_all(&path) { - eprintln!("Failed to create apps directory: {}", e); - return; - } - - if let Err(e) = env::set_current_dir(&path) { - eprintln!("Failed to switch to apps directory: {}", e); - return; - } - - println!("Installing app: {}", app.id().expect("Unable to obtain id")); - println!("→ Cloning branch `{}` from `{}`", app.branch, app.repo); - - let clone_status = Command::new("git") - .arg("clone") - .arg("-b") - .arg(&app.branch) - .arg(&app.repo) - .status(); - - if !matches!(clone_status, Ok(s) if s.success()) { - eprintln!("Failed to clone repository"); - return; - } - - let repo_path = path.join(&repo_name); - if let Err(e) = env::set_current_dir(&repo_path) { - eprintln!("Failed to change into cloned repo directory: {}", e); - return; - } - - println!( - "→ Running install command in {:?}", - env::current_dir().unwrap() - ); - - let install_status = Command::new("bash") - .arg("-c") - .arg(&app.install_command) - .status(); - - if !matches!(install_status, Ok(s) if s.success()) { - eprintln!("Install command failed"); - } - - let service_contents = format!( - "[Unit]\nDescription={repo_name} (Installed with SentryX)\nAfter=network.target\n\n[Service]\nWorkingDirectory=/home/server/saturn-server\nExecStart={}\nRestart=always\nRestartSec=5\nStandardOutput=journal\nStandardError=journal\n\n[Install]\nWantedBy=multi-user.target", - app.run_command, - ); - - let user_systemd_dir = home_dir().join(".config/systemd/user"); - - fs::create_dir_all(&user_systemd_dir).expect("Failed to create user systemd dir"); - - let service_path = user_systemd_dir.join(format!( - "{}.service", - app.id_system().expect("Unable to obtain id") - )); - - println!("→ Creating service file: {service_path:?}"); - - if let Err(e) = fs::write(&service_path, service_contents) { - eprintln!( - "Failed to write service file, make sure you run this server with sudo permissions: {}", - e - ); - return; - } - - update_systemd(); - - // Add to app list and save - apps.push(app); - save_apps(apps); -} - -pub fn toggle_app_state(apps: &[App], index: usize, enable: bool) { - if let Some(app) = apps.get(index) { - let id = app.id_system().expect("Unable to obtain id"); - Command::new("systemctl") - .arg("--user") - .arg(if enable { "enable" } else { "disable" }) - .arg(format!("{}.service", id)) - .status() - .expect("Failed to toggle app state"); - } -} - -pub fn start_app(apps: &[App], index: usize) { - if let Some(app) = apps.get(index) { - let id = app.id_system().expect("Unable to obtain id"); - Command::new("systemctl") - .arg("--user") - .arg("start") - .arg(format!("{}.service", id)) - .status() - .expect("Failed to start app"); - } -} - -pub fn stop_app(apps: &[App], index: usize) { - if let Some(app) = apps.get(index) { - let id = app.id_system().expect("Unable to obtain id"); - Command::new("systemctl") - .arg("--user") - .arg("stop") - .arg(format!("{}.service", id)) - .status() - .expect("Failed to stop app"); - } -} - -pub fn restart_app(apps: &[App], index: usize) { - if let Some(app) = apps.get(index) { - let id = app.id_system().expect("Unable to obtain id"); - Command::new("systemctl") - .arg("--user") - .arg("restart") - .arg(format!("{}.service", id)) - .status() - .expect("Failed to restart app"); - } -} diff --git a/src/main.rs b/src/main.rs index 6894305..841ddc3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,26 @@ -mod app_manager; mod server_config; -use actix_web::{get, post, web, App, HttpServer, Responder, Result}; +mod service; +use actix_web::{ + get, + web::{self}, + App, HttpServer, Responder, Result, +}; +use serde_json::json; use sysinfo::{Disks, Networks, System}; -use crate::server_config::ServerStats; +use crate::server_config::{validate_master_key, ServerStats}; + +#[derive(serde::Deserialize)] +struct Auth { + auth: String, +} #[get("/")] -async fn sys_info() -> Result { +async fn sys_info(query: web::Query) -> Result { + if !validate_master_key(&query.auth) { + return Err(actix_web::error::ErrorUnauthorized("Invalid master key")); + } + let mut sys = System::new_all(); let mut disks: Disks = Disks::new_with_refreshed_list(); let mut networks: Networks = Networks::new_with_refreshed_list(); @@ -48,17 +62,124 @@ async fn sys_info() -> Result { } #[get("/apps")] -async fn get_apps() -> Result { - let apps = app_manager::init(); - Ok(web::Json(apps)) +async fn get_apps(query: web::Query) -> Result { + if !validate_master_key(&query.auth) { + return Err(actix_web::error::ErrorUnauthorized("Invalid master key")); + } + + let systemctl = systemctl::SystemCtl::default(); + match systemctl.list_units(None, None, None) { + Ok(units) => Ok(web::Json( + units + .iter() + .map(|v| match systemctl.status(v) { + Ok(unit) => serde_json::to_value(service::parse_service(unit)).unwrap(), + Err(_) => json!({ "name": v, "status": "undefined" }), + }) + .collect::>(), + )), + Err(e) => Err(actix_web::error::ErrorInternalServerError(format!( + "Failed to list units: {}", + e + ))), + } } -#[get("/app/{app_index}")] -async fn get_app(id: web::Path) -> Result { - let apps = app_manager::init(); - match apps.get(*id) { - Some(app) => Ok(web::Json(app.clone())), - None => Err(actix_web::error::ErrorNotFound("App not found")), +#[get("/apps/{app}")] +async fn get_app(id: web::Path, query: web::Query) -> Result { + if !validate_master_key(&query.auth) { + return Err(actix_web::error::ErrorUnauthorized("Invalid master key")); + } + + let systemctl = systemctl::SystemCtl::default(); + match systemctl.status(&*id) { + Ok(unit) => Ok(web::Json( + serde_json::to_value(service::parse_service(unit)).unwrap(), + )), + Err(e) => Err(actix_web::error::ErrorInternalServerError(format!( + "Failed to get unit: {}", + e + ))), + } +} + +#[get("/enable/{app}")] +async fn enable_app(id: web::Path, query: web::Query) -> Result { + if !validate_master_key(&query.auth) { + return Err(actix_web::error::ErrorUnauthorized("Invalid master key")); + } + + let systemctl = systemctl::SystemCtl::default(); + match systemctl.enable(&*id) { + Ok(status) => Ok(web::Json(json!({ "success": status.success() }))), + Err(e) => Err(actix_web::error::ErrorInternalServerError(format!( + "Failed to enable unit: {}", + e + ))), + } +} + +#[get("/disable/{app}")] +async fn disable_app(id: web::Path, query: web::Query) -> Result { + if !validate_master_key(&query.auth) { + return Err(actix_web::error::ErrorUnauthorized("Invalid master key")); + } + + let systemctl = systemctl::SystemCtl::default(); + match systemctl.disable(&*id) { + Ok(status) => Ok(web::Json(json!({ "success": status.success() }))), + Err(e) => Err(actix_web::error::ErrorInternalServerError(format!( + "Failed to disable unit: {}", + e + ))), + } +} + +#[get("/start/{app}")] +async fn start_app(id: web::Path, query: web::Query) -> Result { + if !validate_master_key(&query.auth) { + return Err(actix_web::error::ErrorUnauthorized("Invalid master key")); + } + + let systemctl = systemctl::SystemCtl::default(); + match systemctl.start(&*id) { + Ok(status) => Ok(web::Json(json!({ "success": status.success() }))), + Err(e) => Err(actix_web::error::ErrorInternalServerError(format!( + "Failed to start unit: {}", + e + ))), + } +} + +#[get("/stop/{app}")] +async fn stop_app(id: web::Path, query: web::Query) -> Result { + if !validate_master_key(&query.auth) { + return Err(actix_web::error::ErrorUnauthorized("Invalid master key")); + } + + let systemctl = systemctl::SystemCtl::default(); + match systemctl.stop(&*id) { + Ok(status) => Ok(web::Json(json!({ "success": status.success() }))), + Err(e) => Err(actix_web::error::ErrorInternalServerError(format!( + "Failed to stop unit: {}", + e + ))), + } +} + +#[get("/restart/{app}")] +async fn restart_app(id: web::Path, query: web::Query) -> Result { + if !validate_master_key(&query.auth) { + return Err(actix_web::error::ErrorUnauthorized("Invalid master key")); + } + + let systemctl = systemctl::SystemCtl::default(); + match systemctl.restart(&*id) { + Ok(status) => Ok(web::Json(json!({ "success": status.success() }))), + Err(e) => Err(actix_web::error::ErrorInternalServerError(format!( + "Failed to restart unit: {}", + e + ))), } } @@ -69,6 +190,11 @@ async fn main() -> std::io::Result<()> { .service(sys_info) .service(get_apps) .service(get_app) + .service(enable_app) + .service(disable_app) + .service(start_app) + .service(stop_app) + .service(restart_app) }) .bind(("127.0.0.1", 8080))? .run() diff --git a/src/server_config.rs b/src/server_config.rs index 8407272..09bac19 100644 --- a/src/server_config.rs +++ b/src/server_config.rs @@ -22,3 +22,8 @@ pub struct ServerStats { pub disk: u8, pub network: u64, } + +pub fn validate_master_key(master_key: &str) -> bool { + let config = ServerConfig::new(); + master_key == config.master_key +} diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 0000000..a24ea4b --- /dev/null +++ b/src/service.rs @@ -0,0 +1,65 @@ +use lazy_static::lazy_static; +use regex::Regex; +use serde::Serialize; + +#[derive(Debug, Serialize)] +pub struct Service { + pub name: Option, + pub description: Option, + pub loaded_status: Option, + pub unit_file_path: Option, + pub enabled: Option, + pub preset: Option, + pub active: Option, + pub active_state: Option, + pub trigger: Option, + pub triggers: Option, + pub docs: Option, + // Other fields can be added as needed +} + +lazy_static! { + static ref SERVICE_RE: Regex = Regex::new( + r#"(?x) + ^●\s(?P\S+)\s-\s(?P.+?)\n + \s+Loaded:\s(?P\S+)\s\((?P[^;]+)(?:;\s(?P\S+))?(?:;\s+preset:\s(?P\S+))?\)\n + \s+Active:\s(?P\S+)\s\((?P[^)]+)\).*\n + (?:\s+Trigger:\s(?P.+)\n)? + (?:\s+Triggers:\s●\s(?P[^\n]+)\n)? + (?:\s+Docs:\s(?P(?:.+\n(?:\s{7,}.+\n)*))?)? + "# + ).unwrap(); +} + +pub fn parse_service(input: String) -> Service { + if let Some(cap) = SERVICE_RE.captures(&input) { + Service { + name: cap.name("name").map(|m| m.as_str().to_string()), + description: cap.name("description").map(|m| m.as_str().to_string()), + loaded_status: cap.name("loaded_status").map(|m| m.as_str().to_string()), + unit_file_path: cap.name("unit_file_path").map(|m| m.as_str().to_string()), + enabled: cap.name("enabled").map(|m| m.as_str().to_string()), + preset: cap.name("preset").map(|m| m.as_str().to_string()), + active: cap.name("active").map(|m| m.as_str().to_string()), + active_state: cap.name("active_state").map(|m| m.as_str().to_string()), + trigger: cap.name("trigger").map(|m| m.as_str().to_string()), + triggers: cap.name("triggers").map(|m| m.as_str().to_string()), + docs: cap.name("docs").map(|m| m.as_str().trim().to_string()), + } + } else { + // Return an empty Service or handle error as needed + Service { + name: None, + description: None, + loaded_status: None, + unit_file_path: None, + enabled: None, + preset: None, + active: None, + active_state: None, + trigger: None, + triggers: None, + docs: None, + } + } +} diff --git a/test.js b/test.js deleted file mode 100644 index fc88507..0000000 --- a/test.js +++ /dev/null @@ -1,48 +0,0 @@ -const ws = new WebSocket("ws://localhost:5273"); - -/* -pub enum Method { - Install(App), - SetEnabled(String, bool), - Edit(String, App), - Uninstall(String), - Toggle(String, bool), - Start(String), - Stop(String), - Restart(String), -} -*/ - -ws.onmessage = (msg) => console.log("Received:", msg.data); -ws.onopen = () => { - ws.send("my-key"); - - // ws.send( - // JSON.stringify({ - // Install: { - // repo: "git@github.com:osui-rs/osui.git", - // branch: "master", - // install_command: "", - // run_command: "cargo run", - // } - // }) - // ); - - // ws.send( - // JSON.stringify({ - // SetEnabled: ["osui-rs/osui", true] - // }) - // ); - - ws.send( - JSON.stringify({ - Start: 0 - }) - ); - - // ws.send( - // JSON.stringify({ - // Uninstall: "git@github.com:osui-rs/osui.git" - // }) - // ); -};