diff --git a/peach-lib/Cargo.toml b/peach-lib/Cargo.toml index 53c8e92..4d4fe38 100644 --- a/peach-lib/Cargo.toml +++ b/peach-lib/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" [dependencies] chrono = "0.4.19" +dirs = "4.0" fslock="0.1.6" jsonrpc-client-core = "0.5" jsonrpc-client-http = "0.5" @@ -16,4 +17,5 @@ regex = "1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.8" +toml = "0.5.8" sha3 = "0.10.0" diff --git a/peach-lib/src/error.rs b/peach-lib/src/error.rs index e309195..3e7dff4 100644 --- a/peach-lib/src/error.rs +++ b/peach-lib/src/error.rs @@ -7,6 +7,9 @@ use std::{io, str, string}; /// This type represents all possible errors that can occur when interacting with the PeachCloud library. #[derive(Debug)] pub enum PeachError { + /// Represents a failure to determine the path of the user's home directory. + HomeDir, + /// Represents all other cases of `std::io::Error`. Io(io::Error), @@ -67,6 +70,9 @@ pub enum PeachError { /// Represents a failure to serialize or deserialize JSON. SerdeJson(serde_json::error::Error), + /// Represents a failure to deserialize TOML. + Toml(toml::de::Error), + /// Represents a failure to serialize or deserialize YAML. SerdeYaml(serde_yaml::Error), @@ -87,7 +93,7 @@ pub enum PeachError { Write { /// The underlying source of the error. source: io::Error, - /// The file path for the write attemp. + /// The file path for the write attempt. path: String, }, } @@ -95,6 +101,7 @@ pub enum PeachError { impl std::error::Error for PeachError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match *self { + PeachError::HomeDir => None, PeachError::Io(_) => None, PeachError::JsonRpcClientCore(_) => None, PeachError::JsonRpcCore(_) => None, @@ -111,6 +118,7 @@ impl std::error::Error for PeachError { PeachError::SerdeJson(_) => None, PeachError::SerdeYaml(_) => None, PeachError::SsbAdminIdNotFound { .. } => None, + PeachError::Toml(_) => None, PeachError::Utf8ToStr(_) => None, PeachError::Utf8ToString(_) => None, PeachError::Write { ref source, .. } => Some(source), @@ -121,6 +129,12 @@ impl std::error::Error for PeachError { impl std::fmt::Display for PeachError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { + PeachError::HomeDir => { + write!( + f, + "Unable to determine the path of the user's home directory" + ) + } PeachError::Io(ref err) => err.fmt(f), PeachError::JsonRpcClientCore(ref err) => err.fmt(f), PeachError::JsonRpcCore(ref err) => { @@ -158,6 +172,7 @@ impl std::fmt::Display for PeachError { PeachError::SsbAdminIdNotFound { ref id } => { write!(f, "Config error: SSB admin ID `{}` not found", id) } + PeachError::Toml(ref err) => err.fmt(f), PeachError::Utf8ToStr(ref err) => err.fmt(f), PeachError::Utf8ToString(ref err) => err.fmt(f), PeachError::Write { ref path, .. } => { @@ -209,6 +224,12 @@ impl From for PeachError { } } +impl From for PeachError { + fn from(err: toml::de::Error) -> PeachError { + PeachError::Toml(err) + } +} + impl From for PeachError { fn from(err: str::Utf8Error) -> PeachError { PeachError::Utf8ToStr(err) diff --git a/peach-lib/src/lib.rs b/peach-lib/src/lib.rs index 1144fc7..cdbe8fa 100644 --- a/peach-lib/src/lib.rs +++ b/peach-lib/src/lib.rs @@ -4,7 +4,7 @@ pub mod error; pub mod network_client; pub mod oled_client; pub mod password_utils; -pub mod sbot_client; +pub mod sbot; pub mod stats_client; // re-export error types diff --git a/peach-lib/src/password_utils.rs b/peach-lib/src/password_utils.rs index 9b427a2..fa28904 100644 --- a/peach-lib/src/password_utils.rs +++ b/peach-lib/src/password_utils.rs @@ -1,7 +1,7 @@ use nanorand::{Rng, WyRand}; use sha3::{Digest, Sha3_256}; -use crate::{config_manager, error::PeachError, sbot_client}; +use crate::{config_manager, error::PeachError}; /// Returns Ok(()) if the supplied password is correct, /// and returns Err if the supplied password is incorrect. @@ -102,7 +102,8 @@ using this link: http://peach.local/reset_password", // finally send the message to the admins let peach_config = config_manager::load_peach_config()?; for ssb_admin_id in peach_config.ssb_admin_ids { - sbot_client::private_message(&msg, &ssb_admin_id)?; + // TODO: replace with golgi + //sbot_client::private_message(&msg, &ssb_admin_id)?; } Ok(()) } diff --git a/peach-lib/src/sbot.rs b/peach-lib/src/sbot.rs new file mode 100644 index 0000000..2a2e5d5 --- /dev/null +++ b/peach-lib/src/sbot.rs @@ -0,0 +1,152 @@ +//! Data types and associated methods for monitoring and configuring go-sbot. + +use std::{fs, process::Command, str}; + +use serde::{Deserialize, Serialize}; +use toml; + +use crate::error::PeachError; + +/// go-sbot process status. +#[derive(Debug, Serialize, Deserialize)] +pub struct SbotStatus { + /// Current process state. + pub state: Option, + /// Current process boot state. + pub boot_state: Option, + /// Current process memory usage in bytes. + pub memory: Option, + /// Uptime for the process (if state is `active`). + pub uptime: Option, + /// Downtime for the process (if state is `inactive`). + pub downtime: Option, +} + +impl SbotStatus { + /// Default builder for `SbotStatus`. + fn default() -> Self { + Self { + state: None, + boot_state: None, + memory: None, + uptime: None, + downtime: None, + } + } + + /// Retrieve statistics for the go-sbot systemd process by querying `systemctl`. + pub fn read() -> Result { + let mut status = SbotStatus::default(); + + let info_output = Command::new("/usr/bin/systemctl") + .arg("--user") + .arg("show") + .arg("go-sbot.service") + .arg("--no-page") + .output()?; + + let service_info = std::str::from_utf8(&info_output.stdout)?; + + for line in service_info.lines() { + if line.starts_with("ActiveState=") { + if let Some(state) = line.strip_prefix("ActiveState=") { + status.state = Some(state.to_string()) + } + } else if line.starts_with("MemoryCurrent=") { + if let Some(memory) = line.strip_prefix("MemoryCurrent=") { + status.memory = memory.parse().ok() + } + } + } + + let status_output = Command::new("/usr/bin/systemctl") + .arg("--user") + .arg("status") + .arg("go-sbot.service") + .output()?; + + let service_status = str::from_utf8(&status_output.stdout)?; + //.map_err(PeachError::Utf8ToStr)?; + + for line in service_status.lines() { + // example of the output line we're looking for: + // `Loaded: loaded (/home/glyph/.config/systemd/user/go-sbot.service; enabled; vendor + // preset: enabled)` + if line.contains("Loaded:") { + let before_boot_state = line.find(';'); + let after_boot_state = line.rfind(';'); + if let (Some(start), Some(end)) = (before_boot_state, after_boot_state) { + // extract the enabled / disabled from the `Loaded: ...` line + // using the index of the first ';' + 2 and the last ';' + status.boot_state = Some(line[start + 2..end].to_string()); + } + // example of the output line we're looking for here: + // `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago` + } else if line.contains("Active:") { + let before_time = line.find(';'); + let after_time = line.find(" ago"); + if let (Some(start), Some(end)) = (before_time, after_time) { + // extract the uptime / downtime from the `Active: ...` line + // using the index of ';' + 2 and the index of " ago" + let time = Some(&line[start + 2..end]); + // if service is active then the `time` reading is uptime + if status.state == Some("active".to_string()) { + status.uptime = time.map(|t| t.to_string()) + // if service is inactive then the `time` reading is downtime + } else if status.state == Some("inactive".to_string()) { + status.downtime = time.map(|t| t.to_string()) + } + } + } + } + + Ok(status) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SbotConfig { + // TODO: maybe define as a Path type? + /// Directory path for the log and indexes. + repo: Option, + /// Directory path for writing debug output. + debugdir: Option, + /// Secret-handshake app-key (aka. network key). + shscap: Option, + /// HMAC hash used to sign messages. + hmac: Option, + /// Replication hops (1: friends, 2: friends of friends). + hops: Option, + /// Address to listen on. + lis: Option, + /// Address to listen on for WebSocket connections. + wslis: Option, + /// Address to for metrics and pprof HTTP server. + debuglis: Option, + /// Enable sending local UDP broadcasts. + localadv: Option, + /// Enable listening for UDP broadcasts and connecting. + localdiscov: Option, + /// Enable syncing by using epidemic-broadcast-trees (EBT). + #[serde(rename = "enable-ebt")] + enable_ebt: Option, + /// Bypass graph auth and fetch remote's feed (useful for pubs that are restoring their data + /// from peer; user beware - caveats about). + promisc: Option, + /// Disable the UNIX socket RPC interface. + nounixsock: Option, +} + +impl SbotConfig { + pub fn read() -> Result { + // determine path of user's home directory + let mut config_path = dirs::home_dir().ok_or(PeachError::HomeDir)?; + config_path.push(".ssb-go/config.toml"); + + let config_contents = fs::read_to_string(config_path)?; + + let config: SbotConfig = toml::from_str(&config_contents)?; + + Ok(config) + } +} diff --git a/peach-lib/src/sbot_client.rs b/peach-lib/src/sbot_client.rs deleted file mode 100644 index bb6cf36..0000000 --- a/peach-lib/src/sbot_client.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Interfaces for monitoring and configuring go-sbot using sbotcli. - -use std::process::Command; - -use serde::{Deserialize, Serialize}; - -use crate::error::PeachError; - -pub fn is_sbot_online() -> Result { - let output = Command::new("/usr/bin/systemctl") - .arg("status") - .arg("peach-go-sbot") - .output()?; - let status = output.status; - // returns true if the service had an exist status of 0 (is running) - let is_running = status.success(); - Ok(is_running) -} - -/// currently go-sbotcli determines where the working directory is -/// using the home directory of th user that invokes it -/// this could be changed to be supplied as CLI arg -/// but for now all sbotcli commands must first become peach-go-sbot before running -/// the sudoers file is configured to allow this to happen without a password -pub fn sbotcli_command() -> Command { - let mut command = Command::new("sudo"); - command - .arg("-u") - .arg("peach-go-sbot") - .arg("/usr/bin/sbotcli"); - command -} - -pub fn post(msg: &str) -> Result<(), PeachError> { - let mut command = sbotcli_command(); - let output = command.arg("publish").arg("post").arg(msg).output()?; - if output.status.success() { - Ok(()) - } else { - let stderr = std::str::from_utf8(&output.stderr)?; - Err(PeachError::SbotCli { - msg: format!("Error making ssb post: {}", stderr), - }) - } -} - -#[derive(Serialize, Deserialize)] -struct WhoAmIValue { - id: String, -} - -pub fn whoami() -> Result { - let mut command = sbotcli_command(); - let output = command.arg("call").arg("whoami").output()?; - let text_output = std::str::from_utf8(&output.stdout)?; - let value: WhoAmIValue = serde_json::from_str(text_output)?; - let id = value.id; - Ok(id) -} - -pub fn create_invite(uses: i32) -> Result { - let mut command = sbotcli_command(); - let output = command - .arg("invite") - .arg("create") - .arg("--uses") - .arg(uses.to_string()) - .output()?; - let text_output = std::str::from_utf8(&output.stdout)?; - let output = text_output.replace('\n', ""); - Ok(output) -} - -pub fn update_pub_name(new_name: &str) -> Result<(), PeachError> { - let pub_ssb_id = whoami()?; - let mut command = sbotcli_command(); - let output = command - .arg("publish") - .arg("about") - .arg("--name") - .arg(new_name) - .arg(pub_ssb_id) - .output()?; - if output.status.success() { - Ok(()) - } else { - let stderr = std::str::from_utf8(&output.stderr)?; - Err(PeachError::SbotCli { - msg: format!("Error updating pub name: {}", stderr), - }) - } -} - -pub fn private_message(msg: &str, recipient: &str) -> Result<(), PeachError> { - let mut command = sbotcli_command(); - let output = command - .arg("publish") - .arg("post") - .arg("--recps") - .arg(recipient) - .arg(msg) - .output()?; - if output.status.success() { - Ok(()) - } else { - let stderr = std::str::from_utf8(&output.stderr)?; - Err(PeachError::SbotCli { - msg: format!("Error sending ssb private message: {}", stderr), - }) - } -}