use std::{ io, process::{Command, Output}, }; use log::{info, warn}; use peach_lib::sbot::{SbotConfig, SbotStatus}; use rocket::{ form::{Form, FromForm}, get, post, request::FlashMessage, response::{Flash, Redirect}, }; use rocket_dyn_templates::{tera::Context, Template}; use crate::routes::authentication::Authenticated; use crate::utils; #[derive(Debug, FromForm)] pub struct SbotConfigForm { /// Directory path for the log and indexes. repo: String, /// Directory path for writing debug output. debugdir: String, /// Secret-handshake app-key (aka. network key). shscap: String, /// HMAC hash used to sign messages. hmac: String, /// Replication hops (1: friends, 2: friends of friends). hops: u8, /// IP address to listen on. lis_ip: String, /// Port to listen on. lis_port: String, /// Address to listen on for WebSocket connections. wslis: String, /// Address to for metrics and pprof HTTP server. debuglis: String, /// Enable sending local UDP broadcasts. localadv: bool, /// Enable listening for UDP broadcasts and connecting. localdiscov: bool, /// Enable syncing by using epidemic-broadcast-trees (EBT). enable_ebt: bool, /// Bypass graph auth and fetch remote's feed (useful for pubs that are restoring their data /// from peer; user beware - caveats about). promisc: bool, /// Disable the UNIX socket RPC interface. nounixsock: bool, /// Run the go-sbot on system start-up (systemd service enabled). startup: bool, /// Attempt to repair the filesystem before starting. repair: bool, } // HELPERS AND ROUTES FOR /settings/scuttlebutt /// Scuttlebutt settings menu. #[get("/")] pub fn ssb_settings_menu(flash: Option, _auth: Authenticated) -> Template { // retrieve current ui theme let theme = utils::get_theme(); // retrieve go-sbot systemd process status let sbot_status = SbotStatus::read().ok(); let mut context = Context::new(); context.insert("theme", &theme); context.insert("sbot_status", &sbot_status); context.insert("back", &Some("/settings".to_string())); context.insert("title", &Some("Scuttlebutt Settings".to_string())); if let Some(flash) = flash { context.insert("flash_name", &Some(flash.kind().to_string())); context.insert("flash_msg", &Some(flash.message().to_string())); }; Template::render("settings/scuttlebutt/menu", &context.into_json()) } /// Sbot configuration page (includes form for updating configuration parameters). #[get("/configure")] pub fn configure_sbot(flash: Option, _auth: Authenticated) -> Template { // retrieve current ui theme let theme = utils::get_theme(); // retrieve go-sbot systemd process status let sbot_status = SbotStatus::read().ok(); let run_on_startup = sbot_status.map(|status| status.boot_state); // retrieve sbot config parameters let sbot_config = SbotConfig::read().ok(); let mut context = Context::new(); context.insert("theme", &theme); context.insert("back", &Some("/settings/scuttlebutt".to_string())); context.insert("title", &Some("Sbot Configuration".to_string())); context.insert("sbot_config", &sbot_config); context.insert("run_on_startup", &Some(run_on_startup)); if let Some(flash) = flash { context.insert("flash_name", &Some(flash.kind().to_string())); context.insert("flash_msg", &Some(flash.message().to_string())); }; Template::render("settings/scuttlebutt/configure_sbot", &context.into_json()) } // TODO: consider using `Contextual` here to collect all submitted form // fields to re-render forms with submitted values on error /// Receive the sbot configuration form data and save it to file. #[post("/configure?", data = "")] pub fn configure_sbot_post( restart: bool, config: Form, _auth: Authenticated, ) -> Flash { // call `into_inner()` to take ownership of the `config` data let owned_config = config.into_inner(); // concat the ip and port for listen address let lis = format!("{}:{}", owned_config.lis_ip, owned_config.lis_port); // instantiate `SbotConfig` from form data let config = SbotConfig { lis, hops: owned_config.hops, repo: owned_config.repo, debugdir: owned_config.debugdir, shscap: owned_config.shscap, localadv: owned_config.localadv, localdiscov: owned_config.localdiscov, hmac: owned_config.hmac, wslis: owned_config.wslis, debuglis: owned_config.debuglis, enable_ebt: owned_config.enable_ebt, promisc: owned_config.promisc, nounixsock: owned_config.nounixsock, repair: owned_config.repair, }; match owned_config.startup { true => { info!("Enabling go-sbot.service"); if let Err(e) = systemctl_sbot_cmd("enable") { warn!("Failed to enable go-sbot.service: {}", e) } } false => { info!("Disabling go-sbot.service"); if let Err(e) = systemctl_sbot_cmd("disable") { warn!("Failed to disable go-sbot.service: {}", e) } } }; // write config to file match SbotConfig::write(config) { Ok(_) => { // if `restart` query parameter is `true`, attempt sbot process (re)start if restart { restart_sbot_process( // redirect url "/settings/scuttlebutt/configure", // success flash msg "Updated configuration and restarted the sbot process", // first failed flash msg "Updated configuration but failed to start the sbot process", // second failed flash msg "Updated configuration but failed to stop the sbot process", ) } else { Flash::success( Redirect::to("/settings/scuttlebutt/configure"), "Updated configuration", ) } } Err(e) => Flash::error( Redirect::to("/settings/scuttlebutt/configure"), format!("Failed to update configuration: {}", e), ), } } /// Set default configuration parameters for the go-sbot and save them to file. #[get("/configure/default")] pub fn configure_sbot_default(_auth: Authenticated) -> Flash { let default_config = SbotConfig::default(); // write default config to file match SbotConfig::write(default_config) { Ok(_) => Flash::success( Redirect::to("/settings/scuttlebutt/configure"), "Restored default configuration", ), Err(e) => Flash::error( Redirect::to("/settings/scuttlebutt/configure"), format!("Failed to restore default configuration: {}", e), ), } } /// Attempt to start the go-sbot.service process. /// Redirect to the Scuttlebutt settings menu and communicate the outcome of /// the attempt via a flash message. #[get("/start")] pub fn start_sbot(_auth: Authenticated) -> Flash { info!("Starting go-sbot.service"); match systemctl_sbot_cmd("start") { Ok(_) => Flash::success( Redirect::to("/settings/scuttlebutt"), "Sbot process has been started", ), Err(_) => Flash::error( Redirect::to("/settings/scuttlebutt"), "Failed to start the sbot process", ), } } /// Attempt to stop the go-sbot.service process. /// Redirect to the Scuttlebutt settings menu and communicate the outcome of /// the attempt via a flash message. #[get("/stop")] pub fn stop_sbot(_auth: Authenticated) -> Flash { info!("Stopping go-sbot.service"); match systemctl_sbot_cmd("stop") { Ok(_) => Flash::success( Redirect::to("/settings/scuttlebutt"), "Sbot process has been stopped", ), Err(_) => Flash::error( Redirect::to("/settings/scuttlebutt"), "Failed to stop the sbot process", ), } } /// Attempt to restart the go-sbot.service process. /// Redirect to the Scuttlebutt settings menu and communicate the outcome of /// the attempt via a flash message. #[get("/restart")] pub fn restart_sbot(_auth: Authenticated) -> Flash { restart_sbot_process( "/settings/scuttlebutt", "Sbot process has been restarted", "Failed to start the sbot process", "Failed to stop the sbot process", ) } // HELPER FUNCTIONS /// Executes a systemctl command for the go-sbot.service process. pub fn systemctl_sbot_cmd(cmd: &str) -> io::Result { Command::new("systemctl") .arg("--user") .arg(cmd) .arg("go-sbot.service") .output() } /// Executes a systemctl stop command followed by start command. /// Returns a redirect with a flash message stating the output of the restart attempt. fn restart_sbot_process( redirect_url: &str, success_msg: &str, start_failed_msg: &str, stop_failed_msg: &str, ) -> Flash { let url = redirect_url.to_string(); info!("Restarting go-sbot.service"); match systemctl_sbot_cmd("stop") { // if stop was successful, try to start the process Ok(_) => match systemctl_sbot_cmd("start") { Ok(_) => Flash::success(Redirect::to(url), success_msg), Err(e) => Flash::error(Redirect::to(url), format!("{}: {}", start_failed_msg, e)), }, Err(e) => Flash::error(Redirect::to(url), format!("{}: {}", stop_failed_msg, e)), } }