From 6ceb016cdc4d8a482c7075b77dfa72b15129aa2f Mon Sep 17 00:00:00 2001 From: mhfowler Date: Thu, 28 Oct 2021 10:01:31 +0200 Subject: [PATCH] Working on routes refactor --- peach-web/src/common.rs | 105 --- peach-web/src/context.rs | 890 ------------------ peach-web/src/device.rs | 28 - peach-web/src/forms.rs | 47 - peach-web/src/json_api.rs | 539 ----------- peach-web/src/lib.rs | 19 +- peach-web/src/routes.rs | 744 --------------- peach-web/src/routes/authentication.rs | 307 ++++++ peach-web/src/routes/device.rs | 300 ++++++ peach-web/src/routes/helpers.rs | 69 ++ peach-web/src/routes/index.rs | 79 ++ peach-web/src/routes/mod.rs | 7 + peach-web/src/routes/ping.rs | 98 ++ peach-web/src/routes/scuttlebutt.rs | 124 +++ peach-web/src/routes/settings/admin.rs | 224 +++++ peach-web/src/routes/settings/dns.rs | 176 ++++ peach-web/src/routes/settings/mod.rs | 3 + peach-web/src/routes/settings/network.rs | 1080 ++++++++++++++++++++++ peach-web/src/utils.rs | 29 + peach-web/src/{ => utils}/monitor.rs | 0 20 files changed, 2507 insertions(+), 2361 deletions(-) delete mode 100644 peach-web/src/common.rs delete mode 100644 peach-web/src/context.rs delete mode 100644 peach-web/src/device.rs delete mode 100644 peach-web/src/forms.rs delete mode 100644 peach-web/src/json_api.rs delete mode 100644 peach-web/src/routes.rs create mode 100644 peach-web/src/routes/authentication.rs create mode 100644 peach-web/src/routes/device.rs create mode 100644 peach-web/src/routes/helpers.rs create mode 100644 peach-web/src/routes/index.rs create mode 100644 peach-web/src/routes/mod.rs create mode 100644 peach-web/src/routes/ping.rs create mode 100644 peach-web/src/routes/scuttlebutt.rs create mode 100644 peach-web/src/routes/settings/admin.rs create mode 100644 peach-web/src/routes/settings/dns.rs create mode 100644 peach-web/src/routes/settings/mod.rs create mode 100644 peach-web/src/routes/settings/network.rs create mode 100644 peach-web/src/utils.rs rename peach-web/src/{ => utils}/monitor.rs (100%) diff --git a/peach-web/src/common.rs b/peach-web/src/common.rs deleted file mode 100644 index b608500..0000000 --- a/peach-web/src/common.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! This module contains core api functions shared by json_api.rs and by routes.rs -//! -//! These functions return Results which are then handled by the json api or the html routes -//! and turned into a rocket response appropriately. -use log::info; - -use crate::error::PeachWebError; -use crate::forms::{AddAdminForm, DnsForm, PasswordForm, ResetPasswordForm}; -use peach_lib::config_manager; -use peach_lib::dyndns_client; -use peach_lib::dyndns_client::{check_is_new_dyndns_domain, get_full_dynamic_domain}; -use peach_lib::error::PeachError; -use peach_lib::jsonrpc_client_core::{Error, ErrorKind}; -use peach_lib::jsonrpc_core::types::error::ErrorCode; -use peach_lib::password_utils; - -pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> { - // first save local configurations - config_manager::set_external_domain(&dns_form.external_domain)?; - config_manager::set_dyndns_enabled_value(dns_form.enable_dyndns)?; - // if dynamic dns is enabled and this is a new domain name, then register it - if dns_form.enable_dyndns { - let full_dynamic_domain = get_full_dynamic_domain(&dns_form.dynamic_domain); - // check if this is a new domain or if its already registered - let is_new_domain = check_is_new_dyndns_domain(&full_dynamic_domain); - if is_new_domain { - match dyndns_client::register_domain(&full_dynamic_domain) { - Ok(_) => { - info!("Registered new dyndns domain"); - // successful update - Ok(()) - } - Err(err) => { - info!("Failed to register dyndns domain: {:?}", err); - // json response for failed update - let msg: String = match err { - PeachError::JsonRpcClientCore { source } => { - match source { - Error(ErrorKind::JsonRpcError(err), _state) => match err.code { - ErrorCode::ServerError(-32030) => { - format!("Error registering domain: {} was previously registered", full_dynamic_domain) - } - _ => { - format!("Failed to register dyndns domain {:?}", err) - } - }, - _ => { - format!("Failed to register dyndns domain: {:?}", source) - } - } - } - _ => "Failed to register dyndns domain".to_string(), - }; - Err(PeachWebError::FailedToRegisterDynDomain { msg }) - } - } - } - // if the domain is already registered, then dont re-register, and just return success - else { - Ok(()) - } - } else { - Ok(()) - } -} - -/// this function is for use by a user who is already logged in to change their password -pub fn save_password_form(password_form: PasswordForm) -> Result<(), PeachWebError> { - info!( - "change password!: {} {} {}", - password_form.old_password, password_form.new_password1, password_form.new_password2 - ); - password_utils::verify_password(&password_form.old_password)?; - // if the previous line did not throw an error, then the old password is correct - password_utils::validate_new_passwords( - &password_form.new_password1, - &password_form.new_password2, - )?; - // if the previous line did not throw an error, then the new password is valid - password_utils::set_new_password(&password_form.new_password1)?; - Ok(()) -} - -/// this function is publicly exposed for users who have forgotten their password -pub fn save_reset_password_form(password_form: ResetPasswordForm) -> Result<(), PeachWebError> { - info!( - "reset password!: {} {} {}", - password_form.temporary_password, password_form.new_password1, password_form.new_password2 - ); - password_utils::verify_temporary_password(&password_form.temporary_password)?; - // if the previous line did not throw an error, then the secret_link is correct - password_utils::validate_new_passwords( - &password_form.new_password1, - &password_form.new_password2, - )?; - // if the previous line did not throw an error, then the new password is valid - password_utils::set_new_password(&password_form.new_password1)?; - Ok(()) -} - -pub fn save_add_admin_form(admin_form: AddAdminForm) -> Result<(), PeachWebError> { - let _result = config_manager::add_ssb_admin_id(&admin_form.ssb_id)?; - // if the previous line didn't throw an error then it was a success - Ok(()) -} diff --git a/peach-web/src/context.rs b/peach-web/src/context.rs deleted file mode 100644 index 6bbeae5..0000000 --- a/peach-web/src/context.rs +++ /dev/null @@ -1,890 +0,0 @@ -//! Build context objects for inclusion in Tera templates. -//! -//! Each context object is represented by a struct which implements a build -//! method. Context objects provide the means by which application and device -//! state are made available for rendering in the templates. - -// Context object struct names: -// -// DeviceContext -// ErrorContext -// FlashContext -// HelpContext -// HomeContext -// LoginContext -// MessageContext -// NetworkContext -// NetworkAddContext -// NetworkAlertContext -// NetworkDetailContext -// NetworkListContext -// PeerContext -// ProfileContext -// ShutdownContext - -use std::collections::HashMap; - -use serde::Serialize; - -use peach_lib::config_manager::load_peach_config; -use peach_lib::dyndns_client; -use peach_lib::dyndns_client::{get_dyndns_subdomain, is_dns_updater_online}; -use peach_lib::network_client; -use peach_lib::network_client::{AccessPoint, Networks, Scan}; -use peach_lib::oled_client; -use peach_lib::sbot_client; -use peach_lib::stats_client; -use peach_lib::stats_client::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat, Traffic}; - -use crate::monitor; -use crate::monitor::{Alert, Data, Threshold}; - -// used in /device for system statistics -#[derive(Debug, Serialize)] -pub struct DeviceContext { - pub back: Option, - pub cpu_stat_percent: Option, - pub disk_stats: Vec, - pub flash_name: Option, - pub flash_msg: Option, - pub load_average: Option, - pub mem_stats: Option, - pub network_ping: String, - pub oled_ping: String, - pub stats_ping: String, - pub dyndns_enabled: bool, - pub dyndns_is_online: bool, - pub config_is_valid: bool, - pub sbot_is_online: bool, - pub title: Option, - pub uptime: Option, -} - -impl DeviceContext { - pub fn build() -> DeviceContext { - // convert result to Option, discard any error - let cpu_stat_percent = stats_client::cpu_stats_percent().ok(); - let load_average = stats_client::load_average().ok(); - let mem_stats = stats_client::mem_stats().ok(); - let network_ping = match network_client::ping() { - Ok(_) => "ONLINE".to_string(), - Err(_) => "OFFLINE".to_string(), - }; - let oled_ping = match oled_client::ping() { - Ok(_) => "ONLINE".to_string(), - Err(_) => "OFFLINE".to_string(), - }; - let stats_ping = match stats_client::ping() { - Ok(_) => "ONLINE".to_string(), - Err(_) => "OFFLINE".to_string(), - }; - let uptime = match stats_client::uptime() { - Ok(mins) => mins, - Err(_) => "Unavailable".to_string(), - }; - - // serialize disk usage data into Vec - let disk_usage_stats = match stats_client::disk_usage() { - Ok(disks) => { - let partitions: Vec = serde_json::from_str(disks.as_str()) - .expect("Failed to deserialize disk_usage response"); - partitions - } - Err(_) => Vec::new(), - }; - - let mut disk_stats = Vec::new(); - // select only the partition we're interested in: /dev/mmcblk0p2 ("/") - for disk in disk_usage_stats { - if disk.mountpoint == "/" { - disk_stats.push(disk); - } - } - - // parse the uptime string to a signed integer (for math) - let uptime_parsed = uptime.parse::().ok(); - - // dyndns_is_online & config_is_valid - let dyndns_enabled: bool; - let dyndns_is_online: bool; - let config_is_valid: bool; - let load_peach_config_result = load_peach_config(); - match load_peach_config_result { - Ok(peach_config) => { - dyndns_enabled = peach_config.dyn_enabled; - config_is_valid = true; - if dyndns_enabled { - let is_dyndns_online_result = dyndns_client::is_dns_updater_online(); - match is_dyndns_online_result { - Ok(is_online) => { - dyndns_is_online = is_online; - } - Err(_err) => { - dyndns_is_online = false; - } - } - } else { - dyndns_is_online = false; - } - } - Err(_err) => { - dyndns_enabled = false; - dyndns_is_online = false; - config_is_valid = false; - } - } - - // test if go-sbot is running - let sbot_is_online: bool; - let sbot_is_online_result = sbot_client::is_sbot_online(); - match sbot_is_online_result { - Ok(val) => { - sbot_is_online = val; - } - Err(_err) => { - sbot_is_online = false; - } - } - - DeviceContext { - back: None, - cpu_stat_percent, - disk_stats, - flash_name: None, - flash_msg: None, - load_average, - mem_stats, - network_ping, - oled_ping, - stats_ping, - dyndns_enabled, - dyndns_is_online, - config_is_valid, - sbot_is_online, - title: None, - uptime: uptime_parsed, - } - } -} - -#[derive(Debug, Serialize)] -pub struct ErrorContext { - pub back: Option, - pub flash_name: Option, - pub flash_msg: Option, - pub title: Option, -} - -impl ErrorContext { - pub fn build() -> ErrorContext { - ErrorContext { - back: None, - flash_name: None, - flash_msg: None, - title: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct FlashContext { - pub flash_name: Option, - pub flash_msg: Option, -} - -#[derive(Debug, Serialize)] -pub struct HelpContext { - pub back: Option, - pub flash_name: Option, - pub flash_msg: Option, - pub title: Option, -} - -impl HelpContext { - pub fn build() -> HelpContext { - HelpContext { - back: None, - flash_name: None, - flash_msg: None, - title: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct HomeContext { - pub flash_name: Option, - pub flash_msg: Option, - pub title: Option, -} - -impl HomeContext { - pub fn build() -> HomeContext { - HomeContext { - flash_name: None, - flash_msg: None, - title: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct LoginContext { - pub back: Option, - pub flash_name: Option, - pub flash_msg: Option, - pub title: Option, -} - -impl LoginContext { - pub fn build() -> LoginContext { - LoginContext { - back: None, - flash_name: None, - flash_msg: None, - title: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct MessageContext { - pub back: Option, - pub flash_name: Option, - pub flash_msg: Option, - pub title: Option, -} - -impl MessageContext { - pub fn build() -> MessageContext { - MessageContext { - back: None, - flash_name: None, - flash_msg: None, - title: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct ConfigureDNSContext { - pub external_domain: String, - pub dyndns_subdomain: String, - pub enable_dyndns: bool, - pub is_dyndns_online: bool, - pub back: Option, - pub title: Option, - pub flash_name: Option, - pub flash_msg: Option, -} - -impl ConfigureDNSContext { - pub fn build() -> ConfigureDNSContext { - let peach_config = load_peach_config().unwrap(); - let dyndns_fulldomain = peach_config.dyn_domain; - let is_dyndns_online = is_dns_updater_online().unwrap(); - let dyndns_subdomain = - get_dyndns_subdomain(&dyndns_fulldomain).unwrap_or(dyndns_fulldomain); - ConfigureDNSContext { - external_domain: peach_config.external_domain, - dyndns_subdomain, - enable_dyndns: peach_config.dyn_enabled, - is_dyndns_online, - back: None, - title: None, - flash_name: None, - flash_msg: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct ChangePasswordContext { - pub back: Option, - pub title: Option, - pub flash_name: Option, - pub flash_msg: Option, -} - -impl ChangePasswordContext { - pub fn build() -> ChangePasswordContext { - ChangePasswordContext { - back: None, - title: None, - flash_name: None, - flash_msg: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct ConfigureAdminContext { - pub ssb_admin_ids: Vec, - pub back: Option, - pub title: Option, - pub flash_name: Option, - pub flash_msg: Option, -} - -impl ConfigureAdminContext { - pub fn build() -> ConfigureAdminContext { - let peach_config = load_peach_config().unwrap(); - let ssb_admin_ids = peach_config.ssb_admin_ids; - ConfigureAdminContext { - ssb_admin_ids, - back: None, - title: None, - flash_name: None, - flash_msg: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct AddAdminContext { - pub back: Option, - pub title: Option, - pub flash_name: Option, - pub flash_msg: Option, -} - -impl AddAdminContext { - pub fn build() -> AddAdminContext { - AddAdminContext { - back: None, - title: None, - flash_name: None, - flash_msg: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct ResetPasswordContext { - pub back: Option, - pub title: Option, - pub flash_name: Option, - pub flash_msg: Option, -} - -impl ResetPasswordContext { - pub fn build() -> ResetPasswordContext { - ResetPasswordContext { - back: None, - title: None, - flash_name: None, - flash_msg: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct SendPasswordResetContext { - pub back: Option, - pub title: Option, - pub flash_name: Option, - pub flash_msg: Option, -} - -impl SendPasswordResetContext { - pub fn build() -> SendPasswordResetContext { - SendPasswordResetContext { - back: None, - title: None, - flash_name: None, - flash_msg: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct NetworkContext { - pub ap_ip: String, - pub ap_ssid: String, - pub ap_state: String, - pub ap_traffic: Option, - pub wlan_ip: String, - pub wlan_rssi: Option, - pub wlan_scan: Option>, - pub wlan_ssid: String, - pub wlan_state: String, - pub wlan_status: String, - pub wlan_traffic: Option, - pub flash_name: Option, - pub flash_msg: Option, - // allows for passing in the ssid of a chosen access point - // this is used in the network_detail template - pub selected: Option, - // page title for header in navbar - pub title: Option, - // url for back-arrow link - pub back: Option, -} - -impl NetworkContext { - pub fn build() -> NetworkContext { - let ap_ip = match network_client::ip("ap0") { - Ok(ip) => ip, - Err(_) => "x.x.x.x".to_string(), - }; - let ap_ssid = match network_client::ssid("ap0") { - Ok(ssid) => ssid, - Err(_) => "Not currently activated".to_string(), - }; - let ap_state = match network_client::state("ap0") { - Ok(state) => state, - Err(_) => "Interface unavailable".to_string(), - }; - let ap_traffic = match network_client::traffic("ap0") { - Ok(traffic) => { - let mut t = traffic; - // modify traffic values & assign measurement unit - // based on received and transmitted values - // if received > 999 MB, convert it to GB - if t.received > 1_047_527_424 { - t.received /= 1_073_741_824; - t.rx_unit = Some("GB".to_string()); - } else if t.received > 0 { - // otherwise, convert it to MB - t.received = (t.received / 1024) / 1024; - t.rx_unit = Some("MB".to_string()); - } else { - t.received = 0; - t.rx_unit = Some("MB".to_string()); - } - - if t.transmitted > 1_047_527_424 { - t.transmitted /= 1_073_741_824; - t.tx_unit = Some("GB".to_string()); - } else if t.transmitted > 0 { - t.transmitted = (t.transmitted / 1024) / 1024; - t.tx_unit = Some("MB".to_string()); - } else { - t.transmitted = 0; - t.tx_unit = Some("MB".to_string()); - } - Some(t) - } - Err(_) => None, - }; - let wlan_ip = match network_client::ip("wlan0") { - Ok(ip) => ip, - Err(_) => "x.x.x.x".to_string(), - }; - let wlan_rssi = match network_client::rssi_percent("wlan0") { - Ok(rssi) => Some(rssi), - Err(_) => None, - }; - let wlan_scan = match network_client::available_networks("wlan0") { - Ok(networks) => { - let scan: Vec = serde_json::from_str(networks.as_str()) - .expect("Failed to deserialize scan_networks response"); - Some(scan) - } - Err(_) => None, - }; - let wlan_ssid = match network_client::ssid("wlan0") { - Ok(ssid) => ssid, - Err(_) => "Not connected".to_string(), - }; - let wlan_state = match network_client::state("wlan0") { - Ok(state) => state, - Err(_) => "Interface unavailable".to_string(), - }; - let wlan_status = match network_client::status("wlan0") { - Ok(status) => status, - Err(_) => "Interface unavailable".to_string(), - }; - let wlan_traffic = match network_client::traffic("wlan0") { - Ok(traffic) => { - let mut t = traffic; - // modify traffic values & assign measurement unit - // based on received and transmitted values - // if received > 999 MB, convert it to GB - if t.received > 1_047_527_424 { - t.received /= 1_073_741_824; - t.rx_unit = Some("GB".to_string()); - } else if t.received > 0 { - // otherwise, convert it to MB - t.received = (t.received / 1024) / 1024; - t.rx_unit = Some("MB".to_string()); - } else { - t.received = 0; - t.rx_unit = Some("MB".to_string()); - } - - if t.transmitted > 1_047_527_424 { - t.transmitted /= 1_073_741_824; - t.tx_unit = Some("GB".to_string()); - } else if t.transmitted > 0 { - t.transmitted = (t.transmitted / 1024) / 1024; - t.tx_unit = Some("MB".to_string()); - } else { - t.transmitted = 0; - t.tx_unit = Some("MB".to_string()); - } - Some(t) - } - Err(_) => None, - }; - - NetworkContext { - ap_ip, - ap_ssid, - ap_state, - ap_traffic, - wlan_ip, - wlan_rssi, - wlan_scan, - wlan_ssid, - wlan_state, - wlan_status, - wlan_traffic, - flash_name: None, - flash_msg: None, - selected: None, - title: None, - back: None, - } - } -} - -// used in /network/wifi/add? -#[derive(Debug, Serialize)] -pub struct NetworkAddContext { - pub back: Option, - pub flash_name: Option, - pub flash_msg: Option, - pub selected: Option, - pub title: Option, -} - -impl NetworkAddContext { - pub fn build() -> NetworkAddContext { - NetworkAddContext { - back: None, - flash_name: None, - flash_msg: None, - selected: None, - title: None, - } - } -} - -// used in /network/wifi/alert for traffic alerts -#[derive(Debug, Serialize)] -pub struct NetworkAlertContext { - pub alert: Alert, - pub back: Option, - pub data_total: Data, // combined stored and current wifi traffic in bytes - pub flash_name: Option, - pub flash_msg: Option, - pub threshold: Threshold, - pub title: Option, - pub traffic: Traffic, // current wifi traffic in bytes (since boot) -} - -impl NetworkAlertContext { - pub fn build() -> NetworkAlertContext { - let alert = monitor::get_alerts().unwrap(); - // stored wifi data values as bytes - let stored_traffic = monitor::get_data().unwrap(); - let threshold = monitor::get_thresholds().unwrap(); - // current wifi traffic values as bytes - let traffic = match network_client::traffic("wlan0") { - Ok(t) => t, - Err(_) => Traffic { - received: 0, - transmitted: 0, - rx_unit: None, - tx_unit: None, - }, - }; - - let current_traffic = traffic.received + traffic.transmitted; - let total = stored_traffic.total + current_traffic; - let data_total = Data { total }; - - NetworkAlertContext { - alert, - back: None, - data_total, - flash_name: None, - flash_msg: None, - threshold, - title: None, - traffic, - } - } -} - -#[derive(Debug, Serialize)] -pub struct NetworkDetailContext { - pub back: Option, - pub flash_name: Option, - pub flash_msg: Option, - pub saved_aps: Vec, - pub selected: Option, - pub title: Option, - pub wlan_ip: String, - pub wlan_networks: HashMap, - pub wlan_rssi: Option, - pub wlan_ssid: String, - pub wlan_state: String, - pub wlan_status: String, - pub wlan_traffic: Option, -} - -impl NetworkDetailContext { - pub fn build() -> NetworkDetailContext { - let wlan_ip = match network_client::ip("wlan0") { - Ok(ip) => ip, - Err(_) => "x.x.x.x".to_string(), - }; - // list of networks saved in wpa_supplicant.conf - let wlan_list = match network_client::saved_networks() { - Ok(ssids) => { - let networks: Vec = serde_json::from_str(ssids.as_str()) - .expect("Failed to deserialize scan_list response"); - networks - } - Err(_) => Vec::new(), - }; - // list of networks saved in wpa_supplicant.conf - // HACK: we're running the same function twice (wlan_list) - // see if we can implement clone for Vec instead - let saved_aps = match network_client::saved_networks() { - Ok(ssids) => { - let networks: Vec = serde_json::from_str(ssids.as_str()) - .expect("Failed to deserialize scan_list response"); - networks - } - Err(_) => Vec::new(), - }; - let wlan_rssi = match network_client::rssi_percent("wlan0") { - Ok(rssi) => Some(rssi), - Err(_) => None, - }; - // list of networks currently in range (online & accessible) - let wlan_scan = match network_client::available_networks("wlan0") { - Ok(networks) => { - let scan: Vec = serde_json::from_str(networks.as_str()) - .expect("Failed to deserialize scan_networks response"); - scan - } - Err(_) => Vec::new(), - }; - let wlan_ssid = match network_client::ssid("wlan0") { - Ok(ssid) => ssid, - Err(_) => "Not connected".to_string(), - }; - let wlan_state = match network_client::state("wlan0") { - Ok(state) => state, - Err(_) => "Interface unavailable".to_string(), - }; - let wlan_status = match network_client::status("wlan0") { - Ok(status) => status, - Err(_) => "Interface unavailable".to_string(), - }; - let wlan_traffic = match network_client::traffic("wlan0") { - Ok(traffic) => { - let mut t = traffic; - // modify traffic values & assign measurement unit - // based on received and transmitted values - // if received > 999 MB, convert it to GB - if t.received > 1_047_527_424 { - t.received /= 1_073_741_824; - t.rx_unit = Some("GB".to_string()); - } else if t.received > 0 { - // otherwise, convert it to MB - t.received = (t.received / 1024) / 1024; - t.rx_unit = Some("MB".to_string()); - } else { - t.received = 0; - t.rx_unit = Some("MB".to_string()); - } - - if t.transmitted > 1_047_527_424 { - t.transmitted /= 1_073_741_824; - t.tx_unit = Some("GB".to_string()); - } else if t.transmitted > 0 { - t.transmitted = (t.transmitted / 1024) / 1024; - t.tx_unit = Some("MB".to_string()); - } else { - t.transmitted = 0; - t.tx_unit = Some("MB".to_string()); - } - Some(t) - } - Err(_) => None, - }; - // create a hashmap to combine wlan_list & wlan_scan without repetition - let mut wlan_networks = HashMap::new(); - for ap in wlan_scan { - let ssid = ap.ssid.clone(); - let rssi = ap.signal_level.clone(); - // parse the string to a signed integer (for math) - let rssi_parsed = rssi.parse::().unwrap(); - // perform rssi (dBm) to quality (%) conversion - let quality_percent = 2 * (rssi_parsed + 100); - let ap_detail = AccessPoint { - detail: Some(ap), - state: "Available".to_string(), - signal: Some(quality_percent), - }; - wlan_networks.insert(ssid, ap_detail); - } - for network in wlan_list { - // avoid repetition by checking that ssid is not already in list - if !wlan_networks.contains_key(&network.ssid) { - let ssid = network.ssid.clone(); - let net_detail = AccessPoint { - detail: None, - state: "Not in range".to_string(), - signal: None, - }; - wlan_networks.insert(ssid, net_detail); - } - } - - NetworkDetailContext { - back: None, - flash_name: None, - flash_msg: None, - saved_aps, - selected: None, - title: None, - wlan_ip, - wlan_networks, - wlan_rssi, - wlan_ssid, - wlan_state, - wlan_status, - wlan_traffic, - } - } -} - -#[derive(Debug, Serialize)] -pub struct NetworkListContext { - pub ap_state: String, - pub back: Option, - pub flash_name: Option, - pub flash_msg: Option, - pub title: Option, - pub wlan_networks: HashMap, - pub wlan_ssid: String, -} - -impl NetworkListContext { - pub fn build() -> NetworkListContext { - // list of networks saved in the wpa_supplicant.conf - let wlan_list = match network_client::saved_networks() { - Ok(ssids) => { - let networks: Vec = serde_json::from_str(ssids.as_str()) - .expect("Failed to deserialize scan_list response"); - networks - } - Err(_) => Vec::new(), - }; - - // list of networks currently in range (online & accessible) - let wlan_scan = match network_client::available_networks("wlan0") { - Ok(networks) => { - let scan: Vec = serde_json::from_str(networks.as_str()) - .expect("Failed to deserialize scan_networks response"); - scan - } - Err(_) => Vec::new(), - }; - - let wlan_ssid = match network_client::ssid("wlan0") { - Ok(ssid) => ssid, - Err(_) => "Not connected".to_string(), - }; - - // create a hashmap to combine wlan_list & wlan_scan without repetition - let mut wlan_networks = HashMap::new(); - for ap in wlan_scan { - wlan_networks.insert(ap.ssid, "Available".to_string()); - } - for network in wlan_list { - // insert ssid (with state) only if it doesn't already exist - wlan_networks - .entry(network.ssid) - .or_insert_with(|| "Not in range".to_string()); - } - - let ap_state = match network_client::state("ap0") { - Ok(state) => state, - Err(_) => "Interface unavailable".to_string(), - }; - - NetworkListContext { - ap_state, - back: None, - flash_msg: None, - flash_name: None, - title: None, - wlan_networks, - wlan_ssid, - } - } -} - -#[derive(Debug, Serialize)] -pub struct PeerContext { - pub back: Option, - pub flash_name: Option, - pub flash_msg: Option, - pub title: Option, -} - -impl PeerContext { - pub fn build() -> PeerContext { - PeerContext { - back: None, - flash_name: None, - flash_msg: None, - title: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct ProfileContext { - pub back: Option, - pub flash_name: Option, - pub flash_msg: Option, - pub title: Option, -} - -impl ProfileContext { - pub fn build() -> ProfileContext { - ProfileContext { - back: None, - flash_name: None, - flash_msg: None, - title: None, - } - } -} - -#[derive(Debug, Serialize)] -pub struct ShutdownContext { - pub back: Option, - pub flash_name: Option, - pub flash_msg: Option, - pub title: Option, -} - -impl ShutdownContext { - pub fn build() -> ShutdownContext { - ShutdownContext { - back: None, - flash_name: None, - flash_msg: None, - title: None, - } - } -} diff --git a/peach-web/src/device.rs b/peach-web/src/device.rs deleted file mode 100644 index 5b715d9..0000000 --- a/peach-web/src/device.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! System calls for modifying the state of the PeachCloud device. - -use std::io; -use std::process::{Command, Output}; - -use log::info; - -/// Executes a system command to reboot the device immediately. -pub fn reboot() -> io::Result { - info!("Rebooting the device"); - // ideally, we'd like to reboot after 5 seconds to allow time for JSON - // response but this is not possible with the `shutdown` command alone. - // TODO: send "rebooting..." message to `peach-oled` for display - Command::new("sudo") - .arg("shutdown") - .arg("-r") - .arg("now") - .output() -} - -/// Executes a system command to shutdown the device immediately. -pub fn shutdown() -> io::Result { - info!("Shutting down the device"); - // ideally, we'd like to reboot after 5 seconds to allow time for JSON - // response but this is not possible with the `shutdown` command alone. - // TODO: send "shutting down..." message to `peach-oled` for display - Command::new("sudo").arg("shutdown").arg("now").output() -} diff --git a/peach-web/src/forms.rs b/peach-web/src/forms.rs deleted file mode 100644 index 3983bb5..0000000 --- a/peach-web/src/forms.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Provides data structures which are used to parse forms from post requests. -//! -use rocket::request::FromForm; -use rocket::UriDisplayQuery; -use serde::Deserialize; - -#[derive(Debug, Deserialize, FromForm)] -pub struct DnsForm { - pub external_domain: String, - pub enable_dyndns: bool, - pub dynamic_domain: String, -} - -#[derive(Debug, Deserialize, FromForm)] -pub struct PasswordForm { - pub old_password: String, - pub new_password1: String, - pub new_password2: String, -} - -#[derive(Debug, Deserialize, FromForm)] -pub struct ResetPasswordForm { - pub temporary_password: String, - pub new_password1: String, - pub new_password2: String, -} - -#[derive(Debug, Deserialize, FromForm, UriDisplayQuery)] -pub struct Ssid { - pub ssid: String, -} - -#[derive(Debug, Deserialize, FromForm)] -pub struct WiFi { - pub ssid: String, - pub pass: String, -} - -#[derive(Debug, Deserialize, FromForm)] -pub struct AddAdminForm { - pub ssb_id: String, -} - -#[derive(Debug, Deserialize, FromForm)] -pub struct DeleteAdminForm { - pub ssb_id: String, -} diff --git a/peach-web/src/json_api.rs b/peach-web/src/json_api.rs deleted file mode 100644 index 54da07a..0000000 --- a/peach-web/src/json_api.rs +++ /dev/null @@ -1,539 +0,0 @@ -//! JSON API routes for PeachCloud. -//! -//! This module contains handlers which allow retrieval and modification of -//! device state via JSON. -//! -//! API ROUTES -//! -//! | Method | URL | Description | -//! | ------ | -------------------------------- | ----------------------------- | -//! | POST | /api/v1/device/reboot | Reboot device | -//! | POST | /api/v1/device/shutdown | Shutdown device | -//! | POST | /api/v1/network/activate_ap | | -//! | POST | /api/v1/network/activate_client | | -//! | GET | /api/v1/network/ip | | -//! | GET | /api/v1/network/rssi | | -//! | GET | /api/v1/network/ssid | | -//! | GET | /api/v1/network/state | | -//! | GET | /api/v1/network/status | | -//! | GET | /api/v1/network/wifi | Retrieve available networks | -//! | POST | /api/v1/network/wifi | Add WiFi AP credentials | -//! | POST | /api/v1/network/wifi/connect | Connect to WiFi access point | -//! | POST | /api/v1/network/wifi/disconnect | Disconnect WiFi access point | -//! | POST | /api/v1/network/wifi/forget | Forget / remove network | -//! | POST | /api/v1/network/wifi/modify | Modify network password | -//! | POST | /api/v1/network/wifi/usage | Update alert thresholds | -//! | POST | /api/v1/network/wifi/usage/reset | Reset stored data usage total | -//! | GET | /api/v1/ping | | -//! | GET | /api/v1/ping/network | Ping `peach-network` | -//! | GET | /api/v1/ping/oled | Ping `peach-oled` | -//! | GET | /api/v1/ping/stats | Ping `peach-stats` | -//! | POST | /api/v1/dns/configure | Modify dns configurations | -//! | POST | /api/v1/settings/change_password | Change password (logged in) | -//! | POST | /public/api/v1/reset_password | Change password (public) | - -use log::{debug, warn}; -use rocket::{get, post}; -use rocket_contrib::json; -use rocket_contrib::json::{Json, JsonValue}; -use serde::Serialize; - -use peach_lib::dyndns_client::is_dns_updater_online; -use peach_lib::network_client; -use peach_lib::oled_client; -use peach_lib::stats_client; -use peach_lib::stats_client::Traffic; - -use crate::common::{save_dns_configuration, save_password_form, save_reset_password_form}; -use crate::device; -use crate::forms::{DnsForm, PasswordForm, ResetPasswordForm, Ssid, WiFi}; -use crate::monitor; -use crate::monitor::Threshold; - -#[derive(Serialize)] -pub struct JsonResponse { - pub status: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub msg: Option, -} - -// reboot the device -#[post("/api/v1/device/reboot")] -pub fn reboot_device() -> Json { - match device::reboot() { - Ok(_) => { - debug!("Going down for reboot..."); - let status = "success".to_string(); - let msg = "Going down for reboot.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(_) => { - warn!("Reboot failed"); - let status = "error".to_string(); - let msg = "Failed to reboot the device.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -// shutdown the device -#[post("/api/v1/device/shutdown")] -pub fn shutdown_device() -> Json { - match device::shutdown() { - Ok(_) => { - debug!("Going down for shutdown..."); - let status = "success".to_string(); - let msg = "Going down for shutdown.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(_) => { - warn!("Shutdown failed"); - let status = "error".to_string(); - let msg = "Failed to shutdown the device.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[post("/api/v1/network/activate_ap")] -pub fn activate_ap() -> Json { - // activate the wireless access point - debug!("Activating WiFi access point."); - match network_client::activate_ap() { - Ok(_) => { - let status = "success".to_string(); - Json(build_json_response(status, None, None)) - } - Err(_) => { - let status = "error".to_string(); - let msg = "Failed to activate WiFi access point.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[post("/api/v1/network/activate_client")] -pub fn activate_client() -> Json { - // activate the wireless client - debug!("Activating WiFi client mode."); - match network_client::activate_client() { - Ok(_) => { - let status = "success".to_string(); - Json(build_json_response(status, None, None)) - } - Err(_) => { - let status = "error".to_string(); - let msg = "Failed to activate WiFi client mode.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[get("/api/v1/network/ip")] -pub fn return_ip() -> Json { - // retrieve ip for wlan0 or set to x.x.x.x if not found - let wlan_ip = match network_client::ip("wlan0") { - Ok(ip) => ip, - Err(_) => "x.x.x.x".to_string(), - }; - // retrieve ip for ap0 or set to x.x.x.x if not found - let ap_ip = match network_client::ip("ap0") { - Ok(ip) => ip, - Err(_) => "x.x.x.x".to_string(), - }; - let data = json!({ - "wlan0": wlan_ip, - "ap0": ap_ip - }); - let status = "success".to_string(); - Json(build_json_response(status, Some(data), None)) -} - -#[get("/api/v1/network/rssi")] -pub fn return_rssi() -> Json { - // retrieve rssi for connected network - match network_client::rssi("wlan0") { - Ok(rssi) => { - let status = "success".to_string(); - let data = json!(rssi); - Json(build_json_response(status, Some(data), None)) - } - Err(_) => { - let status = "success".to_string(); - let msg = "Not currently connected to an access point.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[get("/api/v1/network/ssid")] -pub fn return_ssid() -> Json { - // retrieve ssid for connected network - match network_client::ssid("wlan0") { - Ok(network) => { - let status = "success".to_string(); - let data = json!(network); - Json(build_json_response(status, Some(data), None)) - } - Err(_) => { - let status = "success".to_string(); - let msg = "Not currently connected to an access point.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[get("/api/v1/network/state")] -pub fn return_state() -> Json { - // retrieve state of wlan0 or set to x.x.x.x if not found - let wlan_state = match network_client::state("wlan0") { - Ok(state) => state, - Err(_) => "unavailable".to_string(), - }; - // retrieve state for ap0 or set to x.x.x.x if not found - let ap_state = match network_client::state("ap0") { - Ok(state) => state, - Err(_) => "unavailable".to_string(), - }; - let data = json!({ - "wlan0": wlan_state, - "ap0": ap_state - }); - let status = "success".to_string(); - Json(build_json_response(status, Some(data), None)) -} - -#[get("/api/v1/network/status")] -pub fn return_status() -> Json { - // retrieve status info for wlan0 interface - match network_client::status("wlan0") { - Ok(network) => { - let status = "success".to_string(); - let data = json!(network); - Json(build_json_response(status, Some(data), None)) - } - Err(_) => { - let status = "success".to_string(); - let msg = "Not currently connected to an access point.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[get("/api/v1/network/wifi")] -pub fn scan_networks() -> Json { - // retrieve scan results for access-points within range of wlan0 - match network_client::available_networks("wlan0") { - Ok(networks) => { - let status = "success".to_string(); - let data = json!(networks); - Json(build_json_response(status, Some(data), None)) - } - Err(_) => { - let status = "success".to_string(); - let msg = "Unable to scan for networks. Interface may be deactivated.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[post("/api/v1/network/wifi", data = "")] -pub fn add_wifi(wifi: Json) -> Json { - // generate and write wifi config to wpa_supplicant - match network_client::add(&wifi.ssid, &wifi.pass) { - Ok(_) => { - debug!("Added WiFi credentials."); - // force reread of wpa_supplicant.conf file with new credentials - match network_client::reconfigure() { - Ok(_) => debug!("Successfully reconfigured wpa_supplicant."), - Err(_) => warn!("Failed to reconfigure wpa_supplicant."), - } - // json response for successful update - let status = "success".to_string(); - let msg = "WiFi credentials added.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(_) => { - debug!("Failed to add WiFi credentials."); - // json response for failed update - let status = "error".to_string(); - let msg = "Failed to add WiFi credentials.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[post("/api/v1/network/wifi/connect", data = "")] -pub fn connect_ap(ssid: Json) -> Json { - // retrieve the id for the given network ssid - match network_client::id("wlan0", &ssid.ssid) { - // attempt connection with the given network - Ok(id) => match network_client::connect(&id, "wlan0") { - Ok(_) => { - let status = "success".to_string(); - let msg = "Connected to chosen network.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(_) => { - let status = "error".to_string(); - let msg = "Failed to connect to chosen network.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - }, - Err(_) => { - let status = "error".to_string(); - let msg = "Failed to retrieve the network ID.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[post("/api/v1/network/wifi/disconnect", data = "")] -pub fn disconnect_ap(ssid: Json) -> Json { - // attempt to disable the current network for wlan0 interface - match network_client::disable("wlan0", &ssid.ssid) { - Ok(_) => { - let status = "success".to_string(); - let msg = "Disconnected from WiFi network.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(_) => { - let status = "error".to_string(); - let msg = "Failed to disconnect from WiFi network.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[post("/api/v1/network/wifi/forget", data = "")] -pub fn forget_ap(network: Json) -> Json { - let ssid = &network.ssid; - match network_client::forget("wlan0", ssid) { - Ok(_) => { - debug!("Removed WiFi credentials for chosen network."); - let status = "success".to_string(); - let msg = "WiFi network credentials removed.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(_) => { - warn!("Failed to remove WiFi credentials."); - let status = "error".to_string(); - let msg = "Failed to remove WiFi network credentials.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[post("/api/v1/network/wifi/modify", data = "")] -pub fn modify_password(wifi: Json) -> Json { - let ssid = &wifi.ssid; - let pass = &wifi.pass; - // we are using a helper function (`update`) to delete the old - // credentials and add the new ones. this is because the wpa_cli method - // for updating the password does not work. - match network_client::update("wlan0", ssid, pass) { - Ok(_) => { - debug!("WiFi password updated for chosen network."); - let status = "success".to_string(); - let msg = "WiFi password updated.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(_) => { - warn!("Failed to update WiFi password."); - let status = "error".to_string(); - let msg = "Failed to update WiFi password.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[post("/api/v1/network/wifi/usage", data = "")] -pub fn update_wifi_alerts(thresholds: Json) -> Json { - match monitor::update_store(thresholds.into_inner()) { - Ok(_) => { - debug!("WiFi data usage thresholds updated."); - let status = "success".to_string(); - let msg = "Updated alert threshold and flags.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(_) => { - warn!("Failed to update WiFi data usage thresholds."); - let status = "error".to_string(); - let msg = "Failed to update WiFi data usage thresholds.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[post("/api/v1/network/wifi/usage/reset")] -pub fn reset_data_total() -> Json { - match monitor::reset_data() { - Ok(_) => { - debug!("Reset network data usage total."); - let traffic = match network_client::traffic("wlan0") { - Ok(t) => t, - Err(_) => Traffic { - received: 0, - transmitted: 0, - rx_unit: None, - tx_unit: None, - }, - }; - // current wifi traffic values as bytes - let current_traffic = traffic.received + traffic.transmitted; - let data = json!(current_traffic); - let status = "success".to_string(); - let msg = "Reset network data usage total.".to_string(); - Json(build_json_response(status, Some(data), Some(msg))) - } - Err(_) => { - warn!("Failed to reset network data usage total."); - let status = "error".to_string(); - let msg = "Failed to reset network data usage total.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -// status route: useful for checking connectivity from web client -#[get("/api/v1/ping")] -pub fn ping_pong() -> Json { - // ping pong - let status = "success".to_string(); - let msg = "pong!".to_string(); - Json(build_json_response(status, None, Some(msg))) -} - -// test route: useful for ad hoc testing -#[get("/api/v1/test")] -pub fn test_route() -> Json { - let val = is_dns_updater_online().unwrap(); - let status = "success".to_string(); - let msg = val.to_string(); - Json(build_json_response(status, None, Some(msg))) -} - -// status route: check availability of `peach-network` microservice -#[get("/api/v1/ping/network")] -pub fn ping_network() -> Json { - match network_client::ping() { - Ok(_) => { - debug!("peach-network responded successfully"); - let status = "success".to_string(); - let msg = "peach-network is available.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(_) => { - warn!("peach-network failed to respond"); - let status = "error".to_string(); - let msg = "peach-network is unavailable.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -// status route: check availability of `peach-oled` microservice -#[get("/api/v1/ping/oled")] -pub fn ping_oled() -> Json { - match oled_client::ping() { - Ok(_) => { - debug!("peach-oled responded successfully"); - let status = "success".to_string(); - let msg = "peach-oled is available.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(_) => { - warn!("peach-oled failed to respond"); - let status = "error".to_string(); - let msg = "peach-oled is unavailable.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -// status route: check availability of `peach-stats` microservice -#[get("/api/v1/ping/stats")] -pub fn ping_stats() -> Json { - match stats_client::ping() { - Ok(_) => { - debug!("peach-stats responded successfully"); - let status = "success".to_string(); - let msg = "peach-stats is available.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(_) => { - warn!("peach-stats failed to respond"); - let status = "error".to_string(); - let msg = "peach-stats is unavailable.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[post("/api/v1/dns/configure", data = "")] -pub fn save_dns_configuration_endpoint(dns_form: Json) -> Json { - let result = save_dns_configuration(dns_form.into_inner()); - match result { - Ok(_) => { - let status = "success".to_string(); - let msg = "New dynamic dns configuration is now enabled".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(err) => { - let status = "error".to_string(); - let msg = format!("{}", err); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -#[post("/api/v1/settings/change_password", data = "")] -pub fn save_password_form_endpoint(password_form: Json) -> Json { - let result = save_password_form(password_form.into_inner()); - match result { - Ok(_) => { - let status = "success".to_string(); - let msg = "Your password was successfully changed".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(err) => { - let status = "error".to_string(); - let msg = format!("{}", err); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -/// this reset password route is used by a user who is not logged in -/// and is specifically for users who have forgotten their password -/// all routes under /public/* are excluded from nginx basic auth via the nginx config -#[post("/public/api/v1/reset_password", data = "")] -pub fn reset_password_form_endpoint( - reset_password_form: Json, -) -> Json { - let result = save_reset_password_form(reset_password_form.into_inner()); - match result { - Ok(_) => { - let status = "success".to_string(); - let msg = "New password is now saved. Return home to login.".to_string(); - Json(build_json_response(status, None, Some(msg))) - } - Err(err) => { - let status = "error".to_string(); - let msg = format!("{}", err); - Json(build_json_response(status, None, Some(msg))) - } - } -} - -// HELPER FUNCTIONS - -pub fn build_json_response( - status: String, - data: Option, - msg: Option, -) -> JsonResponse { - JsonResponse { status, data, msg } -} diff --git a/peach-web/src/lib.rs b/peach-web/src/lib.rs index a9ec234..47b6501 100644 --- a/peach-web/src/lib.rs +++ b/peach-web/src/lib.rs @@ -28,14 +28,9 @@ // to replace code with the same code that is already there (possibly a bug) #![allow(clippy::nonstandard_macro_braces)] -pub mod common; -pub mod context; -pub mod device; pub mod error; -pub mod forms; -pub mod json_api; -pub mod monitor; pub mod routes; +pub mod utils; #[cfg(test)] mod tests; mod ws; @@ -47,8 +42,16 @@ use log::{debug, error, info}; use rocket::{catchers, routes}; use rocket_contrib::templates::Template; -use crate::json_api::*; -use crate::routes::*; +use crate::routes::authentication::*; +use crate::routes::device::*; +use crate::routes::helpers::*; +use crate::routes::index::*; +use crate::routes::ping::*; +use crate::routes::scuttlebutt::*; +use crate::routes::device::*; +use crate::routes::settings::admin::*; +use crate::routes::settings::dns::*; +use crate::routes::settings::network::*; use crate::ws::*; pub type BoxError = Box; diff --git a/peach-web/src/routes.rs b/peach-web/src/routes.rs deleted file mode 100644 index 53feb98..0000000 --- a/peach-web/src/routes.rs +++ /dev/null @@ -1,744 +0,0 @@ -//! Route handlers for PeachCloud web routes. -//! -//! This module contains handlers which serve templates and static assests, -//! generate flash messages, catch errors and handle redirects for PeachCloud. -//! -//! WEB ROUTES -//! -//! | Method | URL | Description | -//! | ------ | --------------------------- | --------------------------------- | -//! | GET | / | Home | -//! | GET | /device | Device statistics | -//! | GET | /device/reboot | Reboot device | -//! | GET | /device/shutdown | Shutdown device | -//! | GET | /help | Help and usage guidelines | -//! | GET | /login | Login form | -//! | POST | /login | Login form submission | -//! | POST | /logout | Logout authenticated user | -//! | GET | /network | Network overview | -//! | GET | /network/ap/activate | Activate WiFi access point mode | -//! | GET | /network/wifi | List of networks | -//! | GET | /network/wifi? | Details of single network | -//! | GET | /network/wifi/activate | Activate WiFi client mode | -//! | GET | /network/wifi/add | Add WiFi form | -//! | POST | /network/wifi/add | WiFi form submission | -//! | GET | /network/wifi/add? | Add WiFi form (SSID populated) | -//! | POST | /network/wifi/connect | Connect to WiFi access point | -//! | POST | /network/wifi/disconnect | Disconnect from WiFi access point | -//! | POST | /network/wifi/forget | Remove WiFi | -//! | GET | /network/wifi/modify? | Modify WiFi password form | -//! | POST | /network/wifi/modify | Modify network password | -//! | GET | /network/wifi/usage | WiFi data usage form | -//! | POST | /network/wifi/usage | WiFi data usage form submission | -//! | GET | /network/wifi/usage/reset | Reset stored data usage total | -//! | GET | /messages | Private Scuttlebutt messages | -//! | GET | /peers | Scuttlebutt peers overview | -//! | GET | /profile | Scuttlebutt user profile | -//! | GET | /shutdown | Shutdown menu | -//! | GET | /network/dns | View DNS configurations | -//! | POST | /network/dns | Modify DNS configurations | -//! | GET | /settings/change_password | View password settings form | -//! | POST | /settings/change_password | Change admin password | -//! | GET | /reset_password | Change password using temp pass | -//! | POST | /reset_password | Rhange password using temp pass | -//! | GET | /send_password_reset | Send new password reset link | -//! | POST | /send_password_reset | Send new password reset link | - -use std::path::{Path, PathBuf}; - -use log::{debug, info, warn}; -use percent_encoding::percent_decode; -use rocket::http::RawStr; -use rocket::request::{FlashMessage, Form}; -use rocket::response::{Flash, NamedFile, Redirect}; -use rocket::{catch, get, post, uri}; -use rocket_contrib::templates::Template; - -use peach_lib::config_manager; -use peach_lib::network_client; -use peach_lib::password_utils; - -use crate::common::{ - save_add_admin_form, save_dns_configuration, save_password_form, save_reset_password_form, -}; -use crate::context::{ - AddAdminContext, ChangePasswordContext, ConfigureAdminContext, ConfigureDNSContext, - DeviceContext, ErrorContext, HelpContext, HomeContext, LoginContext, MessageContext, - NetworkAddContext, NetworkAlertContext, NetworkContext, NetworkDetailContext, - NetworkListContext, PeerContext, ProfileContext, ResetPasswordContext, - SendPasswordResetContext, ShutdownContext, -}; -use crate::device; -use crate::forms::{ - AddAdminForm, DeleteAdminForm, DnsForm, PasswordForm, ResetPasswordForm, Ssid, WiFi, -}; -use crate::monitor; -use crate::monitor::Threshold; - -#[get("/")] -pub fn index() -> Template { - let context = HomeContext { - flash_name: None, - flash_msg: None, - title: None, - }; - Template::render("index", &context) -} - -#[get("/device")] -pub fn device_stats(flash: Option) -> Template { - // assign context through context_builder call - let mut context = DeviceContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Device Status".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - // template_dir is set in Rocket.toml - Template::render("device", &context) -} - -#[get("/device/reboot")] -pub fn reboot_cmd() -> Flash { - match device::reboot() { - Ok(_) => Flash::success(Redirect::to("/shutdown"), "Rebooting the device"), - Err(_) => Flash::error(Redirect::to("/shutdown"), "Failed to reboot the device"), - } -} - -#[get("/device/shutdown")] -pub fn shutdown_cmd() -> Flash { - match device::shutdown() { - Ok(_) => Flash::success(Redirect::to("/shutdown"), "Shutting down the device"), - Err(_) => Flash::error(Redirect::to("/shutdown"), "Failed to shutdown the device"), - } -} - -#[get("/help")] -pub fn help(flash: Option) -> Template { - let mut context = HelpContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Help".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - Template::render("help", &context) -} - -#[get("/login")] -pub fn login(flash: Option) -> Template { - let mut context = LoginContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Login".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - Template::render("login", &context) -} - -#[post("/logout")] -pub fn logout() -> Flash { - // logout authenticated user - debug!("Attempting deauthentication of user."); - /* - match logout_user() { - Ok(_) => Flash::success(Redirect::to("/"), "Logout success"), - Err(_) => Flash::error( - Redirect::to("/"), - "Failed to logout", - ), - } - */ - Flash::success(Redirect::to("/"), "Logged out") -} - -#[get("/network")] -pub fn network_home(flash: Option) -> Template { - // assign context through context_builder call - let mut context = NetworkContext::build(); - // set back button (nav) url - context.back = Some("/".to_string()); - // set page title - context.title = Some("Network Configuration".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - // template_dir is set in Rocket.toml - Template::render("network_card", &context) -} - -#[get("/network/ap/activate")] -pub fn deploy_ap() -> Flash { - // activate the wireless access point - debug!("Activating WiFi access point."); - match network_client::activate_ap() { - Ok(_) => Flash::success(Redirect::to("/network"), "Activated WiFi access point"), - Err(_) => Flash::error( - Redirect::to("/network"), - "Failed to activate WiFi access point", - ), - } -} - -#[get("/network/wifi")] -pub fn wifi_list(flash: Option) -> Template { - // assign context through context_builder call - let mut context = NetworkListContext::build(); - context.back = Some("/network".to_string()); - context.title = Some("WiFi Networks".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - // template_dir is set in Rocket.toml - Template::render("network_list", &context) -} - -#[get("/network/wifi?")] -pub fn network_detail(ssid: &RawStr, flash: Option) -> Template { - // assign context through context_builder call - let mut context = NetworkDetailContext::build(); - context.back = Some("/network/wifi".to_string()); - context.title = Some("WiFi Network".to_string()); - // decode ssid from url - let decoded_ssid = percent_decode(ssid.as_bytes()).decode_utf8().unwrap(); - context.selected = Some(decoded_ssid.to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - // template_dir is set in Rocket.toml - Template::render("network_detail", &context) -} - -#[get("/network/wifi/activate")] -pub fn deploy_client() -> Flash { - // activate the wireless client - debug!("Activating WiFi client mode."); - match network_client::activate_client() { - Ok(_) => Flash::success(Redirect::to("/network"), "Activated WiFi client"), - Err(_) => Flash::error(Redirect::to("/network"), "Failed to activate WiFi client"), - } -} - -#[get("/network/wifi/add")] -pub fn network_add_wifi(flash: Option) -> Template { - let mut context = NetworkContext::build(); - // set back icon link to network route - context.back = Some("/network".to_string()); - context.title = Some("Add WiFi Network".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - // template_dir is set in Rocket.toml - Template::render("network_add", &context) -} - -#[get("/network/wifi/add?")] -pub fn network_add_ssid(ssid: &RawStr, flash: Option) -> Template { - // decode ssid from url - let decoded_ssid = percent_decode(ssid.as_bytes()).decode_utf8().unwrap(); - let mut context = NetworkAddContext::build(); - context.back = Some("/network/wifi".to_string()); - context.selected = Some(decoded_ssid.to_string()); - context.title = Some("Add WiFi Network".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - // template_dir is set in Rocket.toml - Template::render("network_add", &context) -} - -#[post("/network/wifi/add", data = "")] -pub fn add_credentials(wifi: Form) -> Template { - // check if the credentials already exist for this access point - // note: this is nicer but it's an unstable feature: - // if check_saved_aps(&wifi.ssid).contains(true) - // use unwrap_or instead, set value to false if err is returned - let creds_exist = network_client::saved_ap(&wifi.ssid).unwrap_or(false); - if creds_exist { - let mut context = NetworkAddContext::build(); - context.back = Some("/network".to_string()); - context.flash_name = Some("error".to_string()); - context.flash_msg = - Some("Network credentials already exist for this access point".to_string()); - context.title = Some("Add WiFi Network".to_string()); - // return early from handler with "creds already exist" message - return Template::render("network_add", &context); - }; - - // if credentials not found, generate and write wifi config to wpa_supplicant - match network_client::add(&wifi.ssid, &wifi.pass) { - Ok(_) => { - debug!("Added WiFi credentials."); - // force reread of wpa_supplicant.conf file with new credentials - match network_client::reconfigure() { - Ok(_) => debug!("Successfully reconfigured wpa_supplicant"), - Err(_) => warn!("Failed to reconfigure wpa_supplicant"), - } - let mut context = NetworkAddContext::build(); - context.back = Some("/network".to_string()); - context.flash_name = Some("success".to_string()); - context.flash_msg = Some("Added WiFi credentials".to_string()); - context.title = Some("Add WiFi Network".to_string()); - Template::render("network_add", &context) - } - Err(_) => { - debug!("Failed to add WiFi credentials."); - let mut context = NetworkAddContext::build(); - context.back = Some("/network".to_string()); - context.flash_name = Some("error".to_string()); - context.flash_msg = Some("Failed to add WiFi credentials".to_string()); - context.title = Some("Add WiFi Network".to_string()); - Template::render("network_add", &context) - } - } -} - -#[get("/network/wifi/usage")] -pub fn wifi_usage(flash: Option) -> Template { - let mut context = NetworkAlertContext::build(); - // set back icon link to network route - context.back = Some("/network".to_string()); - context.title = Some("Network Data Usage".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - // template_dir is set in Rocket.toml - Template::render("network_usage", &context) -} - -#[post("/network/wifi/usage", data = "")] -pub fn wifi_usage_alerts(thresholds: Form) -> Flash { - match monitor::update_store(thresholds.into_inner()) { - Ok(_) => { - debug!("WiFi data usage thresholds updated."); - Flash::success( - Redirect::to("/network/wifi/usage"), - "Updated alert thresholds and flags", - ) - } - Err(_) => { - warn!("Failed to update WiFi data usage thresholds."); - Flash::error( - Redirect::to("/network/wifi/usage"), - "Failed to update alert thresholds and flags", - ) - } - } -} - -#[get("/network/dns")] -pub fn configure_dns(flash: Option) -> Template { - let mut context = ConfigureDNSContext::build(); - // set back icon link to network route - context.back = Some("/network".to_string()); - context.title = Some("Configure DNS".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - Template::render("configure_dns", &context) -} - -#[post("/network/dns", data = "")] -pub fn configure_dns_post(dns: Form) -> Template { - let result = save_dns_configuration(dns.into_inner()); - match result { - Ok(_) => { - let mut context = ConfigureDNSContext::build(); - // set back icon link to network route - context.back = Some("/network".to_string()); - context.title = Some("Configure DNS".to_string()); - context.flash_name = Some("success".to_string()); - context.flash_msg = Some("New dynamic dns configuration is now enabled".to_string()); - Template::render("configure_dns", &context) - } - Err(err) => { - let mut context = ConfigureDNSContext::build(); - // set back icon link to network route - context.back = Some("/network".to_string()); - context.title = Some("Configure DNS".to_string()); - context.flash_name = Some("error".to_string()); - context.flash_msg = Some(format!("Failed to save dns configurations: {}", err)); - Template::render("configure_dns", &context) - } - } -} - -/// this change password route is used by a user who is already logged in -#[get("/settings/change_password")] -pub fn change_password(flash: Option) -> Template { - let mut context = ChangePasswordContext::build(); - // set back icon link to network route - context.back = Some("/network".to_string()); - context.title = Some("Change Password".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - Template::render("password/change_password", &context) -} - -/// this change password route is used by a user who is already logged in -#[post("/settings/change_password", data = "")] -pub fn change_password_post(password_form: Form) -> Template { - let result = save_password_form(password_form.into_inner()); - match result { - Ok(_) => { - let mut context = ChangePasswordContext::build(); - // set back icon link to network route - context.back = Some("/network".to_string()); - context.title = Some("Change Password".to_string()); - context.flash_name = Some("success".to_string()); - context.flash_msg = Some("New password is now saved".to_string()); - // template_dir is set in Rocket.toml - Template::render("password/change_password", &context) - } - Err(err) => { - let mut context = ChangePasswordContext::build(); - // set back icon link to network route - context.back = Some("/network".to_string()); - context.title = Some("Configure DNS".to_string()); - context.flash_name = Some("error".to_string()); - context.flash_msg = Some(format!("Failed to save new password: {}", err)); - Template::render("password/change_password", &context) - } - } -} - -/// this reset password route is used by a user who is not logged in -/// and is specifically for users who have forgotten their password -/// all routes under /public/* are excluded from nginx basic auth via the nginx config -#[get("/reset_password")] -pub fn reset_password(flash: Option) -> Template { - let mut context = ResetPasswordContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Reset Password".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - Template::render("password/reset_password", &context) -} - -/// this reset password route is used by a user who is not logged in -/// and is specifically for users who have forgotten their password -/// and is excluded from nginx basic auth via the nginx config -#[post("/reset_password", data = "")] -pub fn reset_password_post(reset_password_form: Form) -> Template { - let result = save_reset_password_form(reset_password_form.into_inner()); - match result { - Ok(_) => { - let mut context = ChangePasswordContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Reset Password".to_string()); - context.flash_name = Some("success".to_string()); - let flash_msg = "New password is now saved. Return home to login".to_string(); - context.flash_msg = Some(flash_msg); - Template::render("password/reset_password", &context) - } - Err(err) => { - let mut context = ChangePasswordContext::build(); - // set back icon link to network route - context.back = Some("/".to_string()); - context.title = Some("Reset Password".to_string()); - context.flash_name = Some("error".to_string()); - context.flash_msg = Some(format!("Failed to reset password: {}", err)); - Template::render("password/reset_password", &context) - } - } -} - -/// this route is used by a user who is not logged in to send a new password reset link -#[get("/send_password_reset")] -pub fn send_password_reset_page(flash: Option) -> Template { - let mut context = SendPasswordResetContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Send Password Reset".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - Template::render("password/send_password_reset", &context) -} - -/// this send_password_reset route is used by a user who is not logged in -/// and is specifically for users who have forgotten their password -#[post("/send_password_reset")] -pub fn send_password_reset_post() -> Template { - info!("++ send password reset post"); - let result = password_utils::send_password_reset(); - match result { - Ok(_) => { - let mut context = ChangePasswordContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Send Password Reset".to_string()); - context.flash_name = Some("success".to_string()); - let flash_msg = - "A password reset link has been sent to the admin of this device".to_string(); - context.flash_msg = Some(flash_msg); - Template::render("password/send_password_reset", &context) - } - Err(err) => { - let mut context = ChangePasswordContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Send Password Reset".to_string()); - context.flash_name = Some("error".to_string()); - context.flash_msg = Some(format!("Failed to send password reset link: {}", err)); - Template::render("password/send_password_reset", &context) - } - } -} - -/// this is a route for viewing and deleting currently configured admin -#[get("/settings/configure_admin")] -pub fn configure_admin(flash: Option) -> Template { - let mut context = ConfigureAdminContext::build(); - // set back icon link to network route - context.back = Some("/network".to_string()); - context.title = Some("Configure Admin".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - Template::render("admin/configure_admin", &context) -} - -#[get("/settings/admin/add")] -pub fn add_admin(flash: Option) -> Template { - let mut context = AddAdminContext::build(); - context.back = Some("/settings/configure_admin".to_string()); - context.title = Some("Add Admin".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - // template_dir is set in Rocket.toml - Template::render("admin/add_admin", &context) -} - -#[post("/settings/admin/add", data = "")] -pub fn add_admin_post(add_admin_form: Form) -> Flash { - let result = save_add_admin_form(add_admin_form.into_inner()); - let url = uri!(configure_admin); - match result { - Ok(_) => Flash::success(Redirect::to(url), "Successfully added new admin"), - Err(_) => Flash::error(Redirect::to(url), "Failed to add new admin"), - } -} - -#[post("/settings/admin/delete", data = "")] -pub fn delete_admin_post(delete_admin_form: Form) -> Flash { - let result = config_manager::delete_ssb_admin_id(&delete_admin_form.ssb_id); - let url = uri!(configure_admin); - match result { - Ok(_) => Flash::success(Redirect::to(url), "Successfully removed admin id"), - Err(_) => Flash::error(Redirect::to(url), "Failed to remove admin id"), - } -} - -#[get("/network/wifi/usage/reset")] -pub fn wifi_usage_reset() -> Flash { - let url = uri!(wifi_usage); - match monitor::reset_data() { - Ok(_) => Flash::success(Redirect::to(url), "Reset stored network traffic total"), - Err(_) => Flash::error( - Redirect::to(url), - "Failed to reset stored network traffic total", - ), - } -} - -#[post("/network/wifi/connect", data = "")] -pub fn connect_wifi(network: Form) -> Flash { - let ssid = &network.ssid; - let url = uri!(network_detail: ssid); - match network_client::id("wlan0", ssid) { - Ok(id) => match network_client::connect(&id, "wlan0") { - Ok(_) => Flash::success(Redirect::to(url), "Connected to chosen network"), - Err(_) => Flash::error(Redirect::to(url), "Failed to connect to chosen network"), - }, - Err(_) => Flash::error(Redirect::to(url), "Failed to retrieve the network ID"), - } -} - -#[post("/network/wifi/disconnect", data = "")] -pub fn disconnect_wifi(network: Form) -> Flash { - let ssid = &network.ssid; - let url = uri!(network_home); - match network_client::disable("wlan0", ssid) { - Ok(_) => Flash::success(Redirect::to(url), "Disconnected from WiFi network"), - Err(_) => Flash::error(Redirect::to(url), "Failed to disconnect from WiFi network"), - } -} - -#[post("/network/wifi/forget", data = "")] -pub fn forget_wifi(network: Form) -> Flash { - let ssid = &network.ssid; - let url = uri!(network_home); - match network_client::forget("wlan0", ssid) { - Ok(_) => Flash::success(Redirect::to(url), "WiFi credentials removed"), - Err(_) => Flash::error( - Redirect::to(url), - "Failed to remove WiFi credentials".to_string(), - ), - } -} - -#[get("/network/wifi/modify?")] -pub fn wifi_password(ssid: &RawStr, flash: Option) -> Template { - // decode ssid from url - let decoded_ssid = percent_decode(ssid.as_bytes()).decode_utf8().unwrap(); - let mut context = NetworkAddContext { - back: Some("/network/wifi".to_string()), - flash_name: None, - flash_msg: None, - selected: Some(decoded_ssid.to_string()), - title: Some("Update WiFi Password".to_string()), - }; - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - // template_dir is set in Rocket.toml - Template::render("network_modify", &context) -} - -#[post("/network/wifi/modify", data = "")] -pub fn wifi_set_password(wifi: Form) -> Flash { - let ssid = &wifi.ssid; - let pass = &wifi.pass; - let url = uri!(network_detail: ssid); - match network_client::update("wlan0", ssid, pass) { - Ok(_) => Flash::success(Redirect::to(url), "WiFi password updated".to_string()), - Err(_) => Flash::error( - Redirect::to(url), - "Failed to update WiFi password".to_string(), - ), - } -} - -#[get("/messages")] -pub fn messages(flash: Option) -> Template { - let mut context = MessageContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Private Messages".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - Template::render("messages", &context) -} - -#[get("/peers")] -pub fn peers(flash: Option) -> Template { - let mut context = PeerContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Scuttlebutt Peers".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - Template::render("peers", &context) -} - -#[get("/profile")] -pub fn profile(flash: Option) -> Template { - let mut context = ProfileContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Profile".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - Template::render("profile", &context) -} - -#[get("/shutdown")] -pub fn shutdown_menu(flash: Option) -> Template { - let mut context = ShutdownContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Shutdown Device".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.name().to_string()); - context.flash_msg = Some(flash.msg().to_string()); - }; - Template::render("shutdown", &context) -} - -#[get("/", rank = 2)] -pub fn files(file: PathBuf) -> Option { - NamedFile::open(Path::new("static/").join(file)).ok() -} - -#[catch(404)] -pub fn not_found() -> Template { - debug!("404 Page Not Found"); - let mut context = ErrorContext::build(); - context.back = Some("/".to_string()); - context.title = Some("404: Page Not Found".to_string()); - context.flash_name = Some("error".to_string()); - context.flash_msg = Some("No resource found for given URL".to_string()); - - Template::render("not_found", context) -} - -#[catch(500)] -pub fn internal_error() -> Template { - debug!("500 Internal Server Error"); - let mut context = ErrorContext::build(); - context.back = Some("/".to_string()); - context.title = Some("500: Internal Server Error".to_string()); - context.flash_name = Some("error".to_string()); - context.flash_msg = Some("Internal server error".to_string()); - - Template::render("internal_error", context) -} diff --git a/peach-web/src/routes/authentication.rs b/peach-web/src/routes/authentication.rs new file mode 100644 index 0000000..c035928 --- /dev/null +++ b/peach-web/src/routes/authentication.rs @@ -0,0 +1,307 @@ +use serde::{Serialize, Deserialize}; +use rocket_contrib::json; +use rocket_contrib::json::{Json, JsonValue}; +use log::{debug, info, warn}; +use percent_encoding::percent_decode; +use rocket::http::RawStr; +use rocket::request::{FlashMessage, Form}; +use rocket::response::{Flash, NamedFile, Redirect}; +use rocket::{catch, get, post, uri}; +use rocket::request::FromForm; +use rocket::UriDisplayQuery; +use rocket_contrib::templates::Template; + +use crate::utils::{build_json_response, JsonResponse}; +use crate::error::PeachWebError; + +use peach_lib::password_utils; + + +#[derive(Debug, Deserialize, FromForm)] +pub struct PasswordForm { + pub old_password: String, + pub new_password1: String, + pub new_password2: String, +} + +/// this function is for use by a user who is already logged in to change their password +pub fn save_password_form(password_form: PasswordForm) -> Result<(), PeachWebError> { + info!( + "change password!: {} {} {}", + password_form.old_password, password_form.new_password1, password_form.new_password2 + ); + password_utils::verify_password(&password_form.old_password)?; + // if the previous line did not throw an error, then the old password is correct + password_utils::validate_new_passwords( + &password_form.new_password1, + &password_form.new_password2, + )?; + // if the previous line did not throw an error, then the new password is valid + password_utils::set_new_password(&password_form.new_password1)?; + Ok(()) +} + +/// # helpers and routes for /login +///////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct LoginContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl LoginContext { + pub fn build() -> LoginContext { + LoginContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[get("/login")] +pub fn login(flash: Option) -> Template { + let mut context = LoginContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Login".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("login", &context) +} + +#[post("/logout")] +pub fn logout() -> Flash { + // logout authenticated user + debug!("Attempting deauthentication of user."); + /* + match logout_user() { + Ok(_) => Flash::success(Redirect::to("/"), "Logout success"), + Err(_) => Flash::error( + Redirect::to("/"), + "Failed to logout", + ), + } + */ + Flash::success(Redirect::to("/"), "Logged out") +} + +/// # helpers and routes for password reset +////////////////////////////////////////// + +#[derive(Debug, Deserialize, FromForm)] +pub struct ResetPasswordForm { + pub temporary_password: String, + pub new_password1: String, + pub new_password2: String, +} + +#[derive(Debug, Serialize)] +pub struct ResetPasswordContext { + pub back: Option, + pub title: Option, + pub flash_name: Option, + pub flash_msg: Option, +} + +impl ResetPasswordContext { + pub fn build() -> ResetPasswordContext { + ResetPasswordContext { + back: None, + title: None, + flash_name: None, + flash_msg: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct ChangePasswordContext { + pub back: Option, + pub title: Option, + pub flash_name: Option, + pub flash_msg: Option, +} + +impl ChangePasswordContext { + pub fn build() -> ChangePasswordContext { + ChangePasswordContext { + back: None, + title: None, + flash_name: None, + flash_msg: None, + } + } +} + +/// this function is publicly exposed for users who have forgotten their password +pub fn save_reset_password_form(password_form: ResetPasswordForm) -> Result<(), PeachWebError> { + info!( + "reset password!: {} {} {}", + password_form.temporary_password, password_form.new_password1, password_form.new_password2 + ); + password_utils::verify_temporary_password(&password_form.temporary_password)?; + // if the previous line did not throw an error, then the secret_link is correct + password_utils::validate_new_passwords( + &password_form.new_password1, + &password_form.new_password2, + )?; + // if the previous line did not throw an error, then the new password is valid + password_utils::set_new_password(&password_form.new_password1)?; + Ok(()) +} + +/// this reset password route is used by a user who is not logged in +/// and is specifically for users who have forgotten their password +/// all routes under /public/* are excluded from nginx basic auth via the nginx config +#[get("/reset_password")] +pub fn reset_password(flash: Option) -> Template { + let mut context = ResetPasswordContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Reset Password".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("password/reset_password", &context) +} + +/// this reset password route is used by a user who is not logged in +/// and is specifically for users who have forgotten their password +/// and is excluded from nginx basic auth via the nginx config +#[post("/reset_password", data = "")] +pub fn reset_password_post(reset_password_form: Form) -> Template { + let result = save_reset_password_form(reset_password_form.into_inner()); + match result { + Ok(_) => { + let mut context = ChangePasswordContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Reset Password".to_string()); + context.flash_name = Some("success".to_string()); + let flash_msg = "New password is now saved. Return home to login".to_string(); + context.flash_msg = Some(flash_msg); + Template::render("password/reset_password", &context) + } + Err(err) => { + let mut context = ChangePasswordContext::build(); + // set back icon link to network route + context.back = Some("/".to_string()); + context.title = Some("Reset Password".to_string()); + context.flash_name = Some("error".to_string()); + context.flash_msg = Some(format!("Failed to reset password: {}", err)); + Template::render("password/reset_password", &context) + } + } +} + +/// this reset password route is used by a user who is not logged in +/// and is specifically for users who have forgotten their password +/// all routes under /public/* are excluded from nginx basic auth via the nginx config +#[post("/public/api/v1/reset_password", data = "")] +pub fn reset_password_form_endpoint( + reset_password_form: Json, +) -> Json { + let result = save_reset_password_form(reset_password_form.into_inner()); + match result { + Ok(_) => { + let status = "success".to_string(); + let msg = "New password is now saved. Return home to login.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(err) => { + let status = "error".to_string(); + let msg = format!("{}", err); + Json(build_json_response(status, None, Some(msg))) + } + } +} + + +#[derive(Debug, Serialize)] +pub struct SendPasswordResetContext { + pub back: Option, + pub title: Option, + pub flash_name: Option, + pub flash_msg: Option, +} + +impl SendPasswordResetContext { + pub fn build() -> SendPasswordResetContext { + SendPasswordResetContext { + back: None, + title: None, + flash_name: None, + flash_msg: None, + } + } +} + + +/// this route is used by a user who is not logged in to send a new password reset link +#[get("/send_password_reset")] +pub fn send_password_reset_page(flash: Option) -> Template { + let mut context = SendPasswordResetContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Send Password Reset".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("password/send_password_reset", &context) +} + +/// this send_password_reset route is used by a user who is not logged in +/// and is specifically for users who have forgotten their password +#[post("/send_password_reset")] +pub fn send_password_reset_post() -> Template { + info!("++ send password reset post"); + let result = password_utils::send_password_reset(); + match result { + Ok(_) => { + let mut context = ChangePasswordContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Send Password Reset".to_string()); + context.flash_name = Some("success".to_string()); + let flash_msg = + "A password reset link has been sent to the admin of this device".to_string(); + context.flash_msg = Some(flash_msg); + Template::render("password/send_password_reset", &context) + } + Err(err) => { + let mut context = ChangePasswordContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Send Password Reset".to_string()); + context.flash_name = Some("error".to_string()); + context.flash_msg = Some(format!("Failed to send password reset link: {}", err)); + Template::render("password/send_password_reset", &context) + } + } +} + +#[post("/api/v1/settings/change_password", data = "")] +pub fn save_password_form_endpoint(password_form: Json) -> Json { + let result = save_password_form(password_form.into_inner()); + match result { + Ok(_) => { + let status = "success".to_string(); + let msg = "Your password was successfully changed".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(err) => { + let status = "error".to_string(); + let msg = format!("{}", err); + Json(build_json_response(status, None, Some(msg))) + } + } +} \ No newline at end of file diff --git a/peach-web/src/routes/device.rs b/peach-web/src/routes/device.rs new file mode 100644 index 0000000..a4e1bec --- /dev/null +++ b/peach-web/src/routes/device.rs @@ -0,0 +1,300 @@ +use std::collections::HashMap; + +use serde::Serialize; +use rocket_contrib::json; +use rocket_contrib::json::{Json, JsonValue}; +use log::{debug, info, warn}; +use percent_encoding::percent_decode; +use rocket::http::RawStr; +use rocket::request::{FlashMessage, Form}; +use rocket::response::{Flash, NamedFile, Redirect}; +use rocket::{catch, get, post, uri}; +use rocket::request::FromForm; +use rocket::UriDisplayQuery; +use rocket_contrib::templates::Template; + +use crate::utils::{build_json_response, JsonResponse}; + +use peach_lib::config_manager::load_peach_config; +use peach_lib::dyndns_client; +use peach_lib::dyndns_client::{get_dyndns_subdomain, is_dns_updater_online}; +use peach_lib::network_client; +use peach_lib::network_client::{AccessPoint, Networks, Scan}; +use peach_lib::oled_client; +use peach_lib::sbot_client; +use peach_lib::stats_client; +use peach_lib::stats_client::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat, Traffic}; + +use crate::utils::monitor; +use crate::utils::monitor::{Alert, Data, Threshold}; + +use std::io; +use std::process::{Command, Output}; + + +/// # helpers and routes for /device +///////////////////////////////// + +// used in /device for system statistics +#[derive(Debug, Serialize)] +pub struct DeviceContext { + pub back: Option, + pub cpu_stat_percent: Option, + pub disk_stats: Vec, + pub flash_name: Option, + pub flash_msg: Option, + pub load_average: Option, + pub mem_stats: Option, + pub network_ping: String, + pub oled_ping: String, + pub stats_ping: String, + pub dyndns_enabled: bool, + pub dyndns_is_online: bool, + pub config_is_valid: bool, + pub sbot_is_online: bool, + pub title: Option, + pub uptime: Option, +} + +impl DeviceContext { + pub fn build() -> DeviceContext { + // convert result to Option, discard any error + let cpu_stat_percent = stats_client::cpu_stats_percent().ok(); + let load_average = stats_client::load_average().ok(); + let mem_stats = stats_client::mem_stats().ok(); + let network_ping = match network_client::ping() { + Ok(_) => "ONLINE".to_string(), + Err(_) => "OFFLINE".to_string(), + }; + let oled_ping = match oled_client::ping() { + Ok(_) => "ONLINE".to_string(), + Err(_) => "OFFLINE".to_string(), + }; + let stats_ping = match stats_client::ping() { + Ok(_) => "ONLINE".to_string(), + Err(_) => "OFFLINE".to_string(), + }; + let uptime = match stats_client::uptime() { + Ok(mins) => mins, + Err(_) => "Unavailable".to_string(), + }; + + // serialize disk usage data into Vec + let disk_usage_stats = match stats_client::disk_usage() { + Ok(disks) => { + let partitions: Vec = serde_json::from_str(disks.as_str()) + .expect("Failed to deserialize disk_usage response"); + partitions + } + Err(_) => Vec::new(), + }; + + let mut disk_stats = Vec::new(); + // select only the partition we're interested in: /dev/mmcblk0p2 ("/") + for disk in disk_usage_stats { + if disk.mountpoint == "/" { + disk_stats.push(disk); + } + } + + // parse the uptime string to a signed integer (for math) + let uptime_parsed = uptime.parse::().ok(); + + // dyndns_is_online & config_is_valid + let dyndns_enabled: bool; + let dyndns_is_online: bool; + let config_is_valid: bool; + let load_peach_config_result = load_peach_config(); + match load_peach_config_result { + Ok(peach_config) => { + dyndns_enabled = peach_config.dyn_enabled; + config_is_valid = true; + if dyndns_enabled { + let is_dyndns_online_result = dyndns_client::is_dns_updater_online(); + match is_dyndns_online_result { + Ok(is_online) => { + dyndns_is_online = is_online; + } + Err(_err) => { + dyndns_is_online = false; + } + } + } else { + dyndns_is_online = false; + } + } + Err(_err) => { + dyndns_enabled = false; + dyndns_is_online = false; + config_is_valid = false; + } + } + + // test if go-sbot is running + let sbot_is_online: bool; + let sbot_is_online_result = sbot_client::is_sbot_online(); + match sbot_is_online_result { + Ok(val) => { + sbot_is_online = val; + } + Err(_err) => { + sbot_is_online = false; + } + } + + DeviceContext { + back: None, + cpu_stat_percent, + disk_stats, + flash_name: None, + flash_msg: None, + load_average, + mem_stats, + network_ping, + oled_ping, + stats_ping, + dyndns_enabled, + dyndns_is_online, + config_is_valid, + sbot_is_online, + title: None, + uptime: uptime_parsed, + } + } +} + +#[get("/device")] +pub fn device_stats(flash: Option) -> Template { + // assign context through context_builder call + let mut context = DeviceContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Device Status".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("device", &context) +} + +/// # helpers and routes for /device/reboot +///////////////////////////////// + +/// Executes a system command to reboot the device immediately. +pub fn reboot() -> io::Result { + info!("Rebooting the device"); + // ideally, we'd like to reboot after 5 seconds to allow time for JSON + // response but this is not possible with the `shutdown` command alone. + // TODO: send "rebooting..." message to `peach-oled` for display + Command::new("sudo") + .arg("shutdown") + .arg("-r") + .arg("now") + .output() +} + +#[get("/device/reboot")] +pub fn reboot_cmd() -> Flash { + match reboot() { + Ok(_) => Flash::success(Redirect::to("/shutdown"), "Rebooting the device"), + Err(_) => Flash::error(Redirect::to("/shutdown"), "Failed to reboot the device"), + } +} + +// reboot the device +#[post("/api/v1/device/reboot")] +pub fn reboot_device() -> Json { + match reboot() { + Ok(_) => { + debug!("Going down for reboot..."); + let status = "success".to_string(); + let msg = "Going down for reboot.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("Reboot failed"); + let status = "error".to_string(); + let msg = "Failed to reboot the device.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + + +/// # helpers and routes for /device/shutdown +///////////////////////////////// + +/// Executes a system command to shutdown the device immediately. +pub fn shutdown() -> io::Result { +info ! ("Shutting down the device"); +// ideally, we'd like to reboot after 5 seconds to allow time for JSON +// response but this is not possible with the `shutdown` command alone. +// TODO: send "shutting down..." message to `peach-oled` for display +Command::new("sudo").arg("shutdown").arg("now").output() +} + +#[get("/device/shutdown")] +pub fn shutdown_cmd() -> Flash { + match shutdown() { + Ok(_) => Flash::success(Redirect::to("/shutdown"), "Shutting down the device"), + Err(_) => Flash::error(Redirect::to("/shutdown"), "Failed to shutdown the device"), + } +} + +// shutdown the device +#[post("/api/v1/device/shutdown")] +pub fn shutdown_device() -> Json { + match shutdown() { + Ok(_) => { + debug!("Going down for shutdown..."); + let status = "success".to_string(); + let msg = "Going down for shutdown.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("Shutdown failed"); + let status = "error".to_string(); + let msg = "Failed to shutdown the device.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + + +/// # helpers and routes for /shutdown +///////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct ShutdownContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl ShutdownContext { + pub fn build() -> ShutdownContext { + ShutdownContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[get("/shutdown")] +pub fn shutdown_menu(flash: Option) -> Template { + let mut context = ShutdownContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Shutdown Device".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("shutdown", &context) +} \ No newline at end of file diff --git a/peach-web/src/routes/helpers.rs b/peach-web/src/routes/helpers.rs new file mode 100644 index 0000000..e62f4b0 --- /dev/null +++ b/peach-web/src/routes/helpers.rs @@ -0,0 +1,69 @@ +use rocket_contrib::json::{Json, JsonValue}; +use std::path::{Path, PathBuf}; +use log::{debug, info, warn}; +use percent_encoding::percent_decode; +use rocket::http::RawStr; +use rocket::request::{FlashMessage, Form}; +use rocket::response::{Flash, NamedFile, Redirect}; +use rocket::{catch, get, post, uri}; +use rocket::request::FromForm; +use rocket::UriDisplayQuery; +use rocket_contrib::templates::Template; +use serde::Serialize; + +use crate::utils::{build_json_response, JsonResponse}; + +#[get("/", rank = 2)] +pub fn files(file: PathBuf) -> Option { + NamedFile::open(Path::new("static/").join(file)).ok() +} + + +/// # helpers and routes for /404 +///////////////////////////////// +#[derive(Debug, Serialize)] +pub struct ErrorContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl ErrorContext { + pub fn build() -> ErrorContext { + ErrorContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[catch(404)] +pub fn not_found() -> Template { + debug!("404 Page Not Found"); + let mut context = ErrorContext::build(); + context.back = Some("/".to_string()); + context.title = Some("404: Page Not Found".to_string()); + context.flash_name = Some("error".to_string()); + context.flash_msg = Some("No resource found for given URL".to_string()); + + Template::render("not_found", context) +} + +/// # helpers and routes for 500 +///////////////////////////////// + +#[catch(500)] +pub fn internal_error() -> Template { + debug!("500 Internal Server Error"); + let mut context = ErrorContext::build(); + context.back = Some("/".to_string()); + context.title = Some("500: Internal Server Error".to_string()); + context.flash_name = Some("error".to_string()); + context.flash_msg = Some("Internal server error".to_string()); + + Template::render("internal_error", context) +} + diff --git a/peach-web/src/routes/index.rs b/peach-web/src/routes/index.rs new file mode 100644 index 0000000..ef8a2c2 --- /dev/null +++ b/peach-web/src/routes/index.rs @@ -0,0 +1,79 @@ +use std::path::{Path, PathBuf}; + +use log::{debug, info, warn}; +use percent_encoding::percent_decode; +use rocket::http::RawStr; +use rocket::request::{FlashMessage, Form}; +use rocket::response::{Flash, NamedFile, Redirect}; +use rocket::{catch, get, post, uri}; +use rocket_contrib::templates::Template; +use serde::Serialize; + + +/// # helpers and routes for / (home page) +///////////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct HomeContext { + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl HomeContext { + pub fn build() -> HomeContext { + HomeContext { + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[get("/")] +pub fn index() -> Template { + let context = HomeContext { + flash_name: None, + flash_msg: None, + title: None, + }; + Template::render("index", &context) +} + +/// # helpers and routes for /help +///////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct HelpContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl HelpContext { + pub fn build() -> HelpContext { + HelpContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[get("/help")] +pub fn help(flash: Option) -> Template { + let mut context = HelpContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Help".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("help", &context) +} + + diff --git a/peach-web/src/routes/mod.rs b/peach-web/src/routes/mod.rs new file mode 100644 index 0000000..87368cc --- /dev/null +++ b/peach-web/src/routes/mod.rs @@ -0,0 +1,7 @@ +pub mod authentication; +pub mod device; +pub mod helpers; +pub mod index; +pub mod ping; +pub mod scuttlebutt; +pub mod settings; \ No newline at end of file diff --git a/peach-web/src/routes/ping.rs b/peach-web/src/routes/ping.rs new file mode 100644 index 0000000..f93d00e --- /dev/null +++ b/peach-web/src/routes/ping.rs @@ -0,0 +1,98 @@ +//! Helper routes for pinging services to check that they are active + +use rocket_contrib::json::{Json, JsonValue}; +use log::{debug, info, warn}; +use percent_encoding::percent_decode; +use rocket::http::RawStr; +use rocket::request::{FlashMessage, Form}; +use rocket::response::{Flash, NamedFile, Redirect}; +use rocket::{catch, get, post, uri}; +use rocket::request::FromForm; +use rocket::UriDisplayQuery; +use rocket_contrib::templates::Template; +use serde::Serialize; + +use crate::utils::{build_json_response, JsonResponse}; + +use peach_lib::dyndns_client::{get_dyndns_subdomain, is_dns_updater_online}; +use peach_lib::network_client; +use peach_lib::network_client::{AccessPoint, Networks, Scan}; +use peach_lib::oled_client; +use peach_lib::sbot_client; +use peach_lib::stats_client; +use peach_lib::stats_client::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat, Traffic}; + +// status route: useful for checking connectivity from web client +#[get("/api/v1/ping")] +pub fn ping_pong() -> Json { + // ping pong + let status = "success".to_string(); + let msg = "pong!".to_string(); + Json(build_json_response(status, None, Some(msg))) +} + +// test route: useful for ad hoc testing +#[get("/api/v1/test")] +pub fn test_route() -> Json { + let val = is_dns_updater_online().unwrap(); + let status = "success".to_string(); + let msg = val.to_string(); + Json(build_json_response(status, None, Some(msg))) +} + +// status route: check availability of `peach-network` microservice +#[get("/api/v1/ping/network")] +pub fn ping_network() -> Json { + match network_client::ping() { + Ok(_) => { + debug!("peach-network responded successfully"); + let status = "success".to_string(); + let msg = "peach-network is available.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("peach-network failed to respond"); + let status = "error".to_string(); + let msg = "peach-network is unavailable.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +// status route: check availability of `peach-oled` microservice +#[get("/api/v1/ping/oled")] +pub fn ping_oled() -> Json { + match oled_client::ping() { + Ok(_) => { + debug!("peach-oled responded successfully"); + let status = "success".to_string(); + let msg = "peach-oled is available.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("peach-oled failed to respond"); + let status = "error".to_string(); + let msg = "peach-oled is unavailable.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +// status route: check availability of `peach-stats` microservice +#[get("/api/v1/ping/stats")] +pub fn ping_stats() -> Json { + match stats_client::ping() { + Ok(_) => { + debug!("peach-stats responded successfully"); + let status = "success".to_string(); + let msg = "peach-stats is available.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("peach-stats failed to respond"); + let status = "error".to_string(); + let msg = "peach-stats is unavailable.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} \ No newline at end of file diff --git a/peach-web/src/routes/scuttlebutt.rs b/peach-web/src/routes/scuttlebutt.rs new file mode 100644 index 0000000..60a75f9 --- /dev/null +++ b/peach-web/src/routes/scuttlebutt.rs @@ -0,0 +1,124 @@ +//! Routes for scuttlebutt related functionality. + +/// # helpers and routes for /messages +///////////////////////////////// + +use rocket_contrib::json::{Json, JsonValue}; +use log::{debug, info, warn}; +use percent_encoding::percent_decode; +use rocket::http::RawStr; +use rocket::request::{FlashMessage, Form}; +use rocket::response::{Flash, NamedFile, Redirect}; +use rocket::{catch, get, post, uri}; +use rocket::request::FromForm; +use rocket::UriDisplayQuery; +use rocket_contrib::templates::Template; +use serde::Serialize; + +use crate::utils::{build_json_response, JsonResponse}; + +#[derive(Debug, Serialize)] +pub struct MessageContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl MessageContext { + pub fn build() -> MessageContext { + MessageContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[get("/messages")] +pub fn messages(flash: Option) -> Template { + let mut context = MessageContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Private Messages".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("messages", &context) +} + +/// # helpers and routes for /peers +///////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct PeerContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl PeerContext { + pub fn build() -> PeerContext { + PeerContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[get("/peers")] +pub fn peers(flash: Option) -> Template { + let mut context = PeerContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Scuttlebutt Peers".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("peers", &context) +} + + +/// # helpers and routes for /profile +///////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct ProfileContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, +} + +impl ProfileContext { + pub fn build() -> ProfileContext { + ProfileContext { + back: None, + flash_name: None, + flash_msg: None, + title: None, + } + } +} + +#[get("/profile")] +pub fn profile(flash: Option) -> Template { + let mut context = ProfileContext::build(); + context.back = Some("/".to_string()); + context.title = Some("Profile".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("profile", &context) +} \ No newline at end of file diff --git a/peach-web/src/routes/settings/admin.rs b/peach-web/src/routes/settings/admin.rs new file mode 100644 index 0000000..d3b7710 --- /dev/null +++ b/peach-web/src/routes/settings/admin.rs @@ -0,0 +1,224 @@ +use rocket_contrib::json::{Json, JsonValue}; +use log::{debug, info, warn}; +use percent_encoding::percent_decode; +use rocket::http::RawStr; +use rocket::request::{FlashMessage, Form}; +use rocket::response::{Flash, NamedFile, Redirect}; +use rocket::{catch, get, post, uri}; +use rocket::request::FromForm; +use rocket::UriDisplayQuery; +use rocket_contrib::templates::Template; +use serde::{Serialize, Deserialize}; + +use crate::error::PeachWebError; + +use peach_lib::config_manager; +use peach_lib::config_manager::{load_peach_config}; +use peach_lib::password_utils; + + + +#[derive(Debug, Deserialize, FromForm)] +pub struct AddAdminForm { + pub ssb_id: String, +} +#[derive(Debug, Deserialize, FromForm)] +pub struct DeleteAdminForm { + pub ssb_id: String, +} + + +/// # helpers and routes for /settings/change_password +///////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct ChangePasswordContext { + pub back: Option, + pub title: Option, + pub flash_name: Option, + pub flash_msg: Option, +} +impl ChangePasswordContext { + pub fn build() -> ChangePasswordContext { + ChangePasswordContext { + back: None, + title: None, + flash_name: None, + flash_msg: None, + } + } +} + +#[derive(Debug, Deserialize, FromForm)] +pub struct PasswordForm { + pub old_password: String, + pub new_password1: String, + pub new_password2: String, +} + +/// this function is for use by a user who is already logged in to change their password +pub fn save_password_form(password_form: PasswordForm) -> Result<(), PeachWebError> { + info!( + "change password!: {} {} {}", + password_form.old_password, password_form.new_password1, password_form.new_password2 + ); + password_utils::verify_password(&password_form.old_password)?; + // if the previous line did not throw an error, then the old password is correct + password_utils::validate_new_passwords( + &password_form.new_password1, + &password_form.new_password2, + )?; + // if the previous line did not throw an error, then the new password is valid + password_utils::set_new_password(&password_form.new_password1)?; + Ok(()) +} + +/// this change password route is used by a user who is already logged in +#[get("/settings/change_password")] +pub fn change_password(flash: Option) -> Template { + let mut context = ChangePasswordContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Change Password".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("password/change_password", &context) +} + +/// this change password route is used by a user who is already logged in +#[post("/settings/change_password", data = "")] +pub fn change_password_post(password_form: Form) -> Template { + let result = save_password_form(password_form.into_inner()); + match result { + Ok(_) => { + let mut context = ChangePasswordContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Change Password".to_string()); + context.flash_name = Some("success".to_string()); + context.flash_msg = Some("New password is now saved".to_string()); + // template_dir is set in Rocket.toml + Template::render("password/change_password", &context) + } + Err(err) => { + let mut context = ChangePasswordContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Configure DNS".to_string()); + context.flash_name = Some("error".to_string()); + context.flash_msg = Some(format!("Failed to save new password: {}", err)); + Template::render("password/change_password", &context) + } + } +} + + + +pub fn save_add_admin_form(admin_form: AddAdminForm) -> Result<(), PeachWebError> { + let _result = config_manager::add_ssb_admin_id(&admin_form.ssb_id)?; + // if the previous line didn't throw an error then it was a success + Ok(()) +} + +/// # helpers and routes for /settings/configure_admin +///////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct ConfigureAdminContext { + pub ssb_admin_ids: Vec, + pub back: Option, + pub title: Option, + pub flash_name: Option, + pub flash_msg: Option, +} + +impl ConfigureAdminContext { + pub fn build() -> ConfigureAdminContext { + let peach_config = load_peach_config().unwrap(); + let ssb_admin_ids = peach_config.ssb_admin_ids; + ConfigureAdminContext { + ssb_admin_ids, + back: None, + title: None, + flash_name: None, + flash_msg: None, + } + } +} + +/// this is a route for viewing and deleting currently configured admin +#[get("/settings/configure_admin")] +pub fn configure_admin(flash: Option) -> Template { + let mut context = ConfigureAdminContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Configure Admin".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("admin/configure_admin", &context) +} + +/// # helpers and routes for /settings/admin/add +///////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct AddAdminContext { + pub back: Option, + pub title: Option, + pub flash_name: Option, + pub flash_msg: Option, +} + +impl AddAdminContext { + pub fn build() -> AddAdminContext { + AddAdminContext { + back: None, + title: None, + flash_name: None, + flash_msg: None, + } + } +} + +#[get("/settings/admin/add")] +pub fn add_admin(flash: Option) -> Template { + let mut context = AddAdminContext::build(); + context.back = Some("/settings/configure_admin".to_string()); + context.title = Some("Add Admin".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("admin/add_admin", &context) +} + +#[post("/settings/admin/add", data = "")] +pub fn add_admin_post(add_admin_form: Form) -> Flash { + let result = save_add_admin_form(add_admin_form.into_inner()); + let url = uri!(configure_admin); + match result { + Ok(_) => Flash::success(Redirect::to(url), "Successfully added new admin"), + Err(_) => Flash::error(Redirect::to(url), "Failed to add new admin"), + } +} + +#[post("/settings/admin/delete", data = "")] +pub fn delete_admin_post(delete_admin_form: Form) -> Flash { + let result = config_manager::delete_ssb_admin_id(&delete_admin_form.ssb_id); + let url = uri!(configure_admin); + match result { + Ok(_) => Flash::success(Redirect::to(url), "Successfully removed admin id"), + Err(_) => Flash::error(Redirect::to(url), "Failed to remove admin id"), + } +} diff --git a/peach-web/src/routes/settings/dns.rs b/peach-web/src/routes/settings/dns.rs new file mode 100644 index 0000000..23f9bcf --- /dev/null +++ b/peach-web/src/routes/settings/dns.rs @@ -0,0 +1,176 @@ +use rocket_contrib::json; +use rocket_contrib::json::{Json, JsonValue}; +use log::{debug, info, warn}; +use percent_encoding::percent_decode; +use rocket::http::RawStr; +use rocket::request::{FlashMessage, Form}; +use rocket::response::{Flash, NamedFile, Redirect}; +use rocket::{catch, get, post, uri}; +use rocket::request::FromForm; +use rocket::UriDisplayQuery; +use rocket_contrib::templates::Template; +use serde::{Serialize, Deserialize}; + +use crate::error::PeachWebError; +use crate::utils::{build_json_response, JsonResponse}; + +use peach_lib::config_manager; +use peach_lib::config_manager::{load_peach_config}; +use peach_lib::dyndns_client; +use peach_lib::dyndns_client::{check_is_new_dyndns_domain, get_full_dynamic_domain}; +use peach_lib::dyndns_client::{get_dyndns_subdomain, is_dns_updater_online}; +use peach_lib::error::PeachError; +use peach_lib::jsonrpc_client_core::{Error, ErrorKind}; +use peach_lib::jsonrpc_core::types::error::ErrorCode; +use peach_lib::password_utils; + +#[derive(Debug, Deserialize, FromForm)] +pub struct DnsForm { + pub external_domain: String, + pub enable_dyndns: bool, + pub dynamic_domain: String, +} + +pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> { + // first save local configurations + config_manager::set_external_domain(&dns_form.external_domain)?; + config_manager::set_dyndns_enabled_value(dns_form.enable_dyndns)?; + // if dynamic dns is enabled and this is a new domain name, then register it + if dns_form.enable_dyndns { + let full_dynamic_domain = get_full_dynamic_domain(&dns_form.dynamic_domain); + // check if this is a new domain or if its already registered + let is_new_domain = check_is_new_dyndns_domain(&full_dynamic_domain); + if is_new_domain { + match dyndns_client::register_domain(&full_dynamic_domain) { + Ok(_) => { + info!("Registered new dyndns domain"); + // successful update + Ok(()) + } + Err(err) => { + info!("Failed to register dyndns domain: {:?}", err); + // json response for failed update + let msg: String = match err { + PeachError::JsonRpcClientCore { source } => { + match source { + Error(ErrorKind::JsonRpcError(err), _state) => match err.code { + ErrorCode::ServerError(-32030) => { + format!("Error registering domain: {} was previously registered", full_dynamic_domain) + } + _ => { + format!("Failed to register dyndns domain {:?}", err) + } + }, + _ => { + format!("Failed to register dyndns domain: {:?}", source) + } + } + } + _ => "Failed to register dyndns domain".to_string(), + }; + Err(PeachWebError::FailedToRegisterDynDomain { msg }) + } + } + } + // if the domain is already registered, then dont re-register, and just return success + else { + Ok(()) + } + } else { + Ok(()) + } +} + +/// # helpers and routes for /network/dns +///////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct ConfigureDNSContext { + pub external_domain: String, + pub dyndns_subdomain: String, + pub enable_dyndns: bool, + pub is_dyndns_online: bool, + pub back: Option, + pub title: Option, + pub flash_name: Option, + pub flash_msg: Option, +} + +impl ConfigureDNSContext { + pub fn build() -> ConfigureDNSContext { + let peach_config = load_peach_config().unwrap(); + let dyndns_fulldomain = peach_config.dyn_domain; + let is_dyndns_online = is_dns_updater_online().unwrap(); + let dyndns_subdomain = + get_dyndns_subdomain(&dyndns_fulldomain).unwrap_or(dyndns_fulldomain); + ConfigureDNSContext { + external_domain: peach_config.external_domain, + dyndns_subdomain, + enable_dyndns: peach_config.dyn_enabled, + is_dyndns_online, + back: None, + title: None, + flash_name: None, + flash_msg: None, + } + } +} + +#[get("/network/dns")] +pub fn configure_dns(flash: Option) -> Template { + let mut context = ConfigureDNSContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Configure DNS".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + Template::render("configure_dns", &context) +} + +#[post("/network/dns", data = "")] +pub fn configure_dns_post(dns: Form) -> Template { + let result = save_dns_configuration(dns.into_inner()); + match result { + Ok(_) => { + let mut context = ConfigureDNSContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Configure DNS".to_string()); + context.flash_name = Some("success".to_string()); + context.flash_msg = Some("New dynamic dns configuration is now enabled".to_string()); + Template::render("configure_dns", &context) + } + Err(err) => { + let mut context = ConfigureDNSContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Configure DNS".to_string()); + context.flash_name = Some("error".to_string()); + context.flash_msg = Some(format!("Failed to save dns configurations: {}", err)); + Template::render("configure_dns", &context) + } + } +} + +#[post("/api/v1/dns/configure", data = "")] +pub fn save_dns_configuration_endpoint(dns_form: Json) -> Json { + let result = save_dns_configuration(dns_form.into_inner()); + match result { + Ok(_) => { + let status = "success".to_string(); + let msg = "New dynamic dns configuration is now enabled".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(err) => { + let status = "error".to_string(); + let msg = format!("{}", err); + Json(build_json_response(status, None, Some(msg))) + } + } +} + + diff --git a/peach-web/src/routes/settings/mod.rs b/peach-web/src/routes/settings/mod.rs new file mode 100644 index 0000000..8fee4d7 --- /dev/null +++ b/peach-web/src/routes/settings/mod.rs @@ -0,0 +1,3 @@ +pub mod admin; +pub mod dns; +pub mod network; \ No newline at end of file diff --git a/peach-web/src/routes/settings/network.rs b/peach-web/src/routes/settings/network.rs new file mode 100644 index 0000000..bbff192 --- /dev/null +++ b/peach-web/src/routes/settings/network.rs @@ -0,0 +1,1080 @@ +use std::collections::HashMap; + +use rocket_contrib::json; +use rocket_contrib::json::{Json, JsonValue}; +use log::{debug, info, warn}; +use percent_encoding::percent_decode; +use rocket::http::RawStr; +use rocket::request::{FlashMessage, Form}; +use rocket::response::{Flash, NamedFile, Redirect}; +use rocket::{catch, get, post, uri}; +use rocket::request::FromForm; +use rocket::UriDisplayQuery; +use rocket_contrib::templates::Template; +use serde::{Serialize, Deserialize}; + +use crate::utils::{build_json_response, JsonResponse}; +use crate::utils::monitor; +use crate::utils::monitor::{Threshold, Data, Alert}; +use peach_lib::config_manager::load_peach_config; + +use peach_lib::network_client; +use peach_lib::network_client::{AccessPoint, Networks, Scan}; +use peach_lib::oled_client; +use peach_lib::sbot_client; +use peach_lib::stats_client; +use peach_lib::stats_client::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat, Traffic}; + +/// # structs used by network routes +//////////////////////////////////// + +#[derive(Debug, Deserialize, FromForm, UriDisplayQuery)] +pub struct Ssid { + pub ssid: String, +} + +#[derive(Debug, Deserialize, FromForm)] +pub struct WiFi { + pub ssid: String, + pub pass: String, +} + + +/// # helpers and routes for /network/wifi/usage/reset +///////////////////////////////// + +#[get("/network/wifi/usage/reset")] +pub fn wifi_usage_reset() -> Flash { + let url = uri!(wifi_usage); + match monitor::reset_data() { + Ok(_) => Flash::success(Redirect::to(url), "Reset stored network traffic total"), + Err(_) => Flash::error( + Redirect::to(url), + "Failed to reset stored network traffic total", + ), + } +} + +#[post("/network/wifi/connect", data = "")] +pub fn connect_wifi(network: Form) -> Flash { + let ssid = &network.ssid; + let url = uri!(network_detail: ssid); + match network_client::id("wlan0", ssid) { + Ok(id) => match network_client::connect(&id, "wlan0") { + Ok(_) => Flash::success(Redirect::to(url), "Connected to chosen network"), + Err(_) => Flash::error(Redirect::to(url), "Failed to connect to chosen network"), + }, + Err(_) => Flash::error(Redirect::to(url), "Failed to retrieve the network ID"), + } +} + +#[post("/network/wifi/disconnect", data = "")] +pub fn disconnect_wifi(network: Form) -> Flash { + let ssid = &network.ssid; + let url = uri!(network_home); + match network_client::disable("wlan0", ssid) { + Ok(_) => Flash::success(Redirect::to(url), "Disconnected from WiFi network"), + Err(_) => Flash::error(Redirect::to(url), "Failed to disconnect from WiFi network"), + } +} + +#[post("/network/wifi/forget", data = "")] +pub fn forget_wifi(network: Form) -> Flash { + let ssid = &network.ssid; + let url = uri!(network_home); + match network_client::forget("wlan0", ssid) { + Ok(_) => Flash::success(Redirect::to(url), "WiFi credentials removed"), + Err(_) => Flash::error( + Redirect::to(url), + "Failed to remove WiFi credentials".to_string(), + ), + } +} + +#[get("/network/wifi/modify?")] +pub fn wifi_password(ssid: &RawStr, flash: Option) -> Template { + // decode ssid from url + let decoded_ssid = percent_decode(ssid.as_bytes()).decode_utf8().unwrap(); + let mut context = NetworkAddContext { + back: Some("/network/wifi".to_string()), + flash_name: None, + flash_msg: None, + selected: Some(decoded_ssid.to_string()), + title: Some("Update WiFi Password".to_string()), + }; + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_modify", &context) +} + +#[post("/network/wifi/modify", data = "")] +pub fn wifi_set_password(wifi: Form) -> Flash { + let ssid = &wifi.ssid; + let pass = &wifi.pass; + let url = uri!(network_detail: ssid); + match network_client::update("wlan0", ssid, pass) { + Ok(_) => Flash::success(Redirect::to(url), "WiFi password updated".to_string()), + Err(_) => Flash::error( + Redirect::to(url), + "Failed to update WiFi password".to_string(), + ), + } +} + +/// # helpers and routes for /network +///////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct NetworkContext { + pub ap_ip: String, + pub ap_ssid: String, + pub ap_state: String, + pub ap_traffic: Option, + pub wlan_ip: String, + pub wlan_rssi: Option, + pub wlan_scan: Option>, + pub wlan_ssid: String, + pub wlan_state: String, + pub wlan_status: String, + pub wlan_traffic: Option, + pub flash_name: Option, + pub flash_msg: Option, + // allows for passing in the ssid of a chosen access point + // this is used in the network_detail template + pub selected: Option, + // page title for header in navbar + pub title: Option, + // url for back-arrow link + pub back: Option, +} + +impl NetworkContext { + pub fn build() -> NetworkContext { + let ap_ip = match network_client::ip("ap0") { + Ok(ip) => ip, + Err(_) => "x.x.x.x".to_string(), + }; + let ap_ssid = match network_client::ssid("ap0") { + Ok(ssid) => ssid, + Err(_) => "Not currently activated".to_string(), + }; + let ap_state = match network_client::state("ap0") { + Ok(state) => state, + Err(_) => "Interface unavailable".to_string(), + }; + let ap_traffic = match network_client::traffic("ap0") { + Ok(traffic) => { + let mut t = traffic; + // modify traffic values & assign measurement unit + // based on received and transmitted values + // if received > 999 MB, convert it to GB + if t.received > 1_047_527_424 { + t.received /= 1_073_741_824; + t.rx_unit = Some("GB".to_string()); + } else if t.received > 0 { + // otherwise, convert it to MB + t.received = (t.received / 1024) / 1024; + t.rx_unit = Some("MB".to_string()); + } else { + t.received = 0; + t.rx_unit = Some("MB".to_string()); + } + + if t.transmitted > 1_047_527_424 { + t.transmitted /= 1_073_741_824; + t.tx_unit = Some("GB".to_string()); + } else if t.transmitted > 0 { + t.transmitted = (t.transmitted / 1024) / 1024; + t.tx_unit = Some("MB".to_string()); + } else { + t.transmitted = 0; + t.tx_unit = Some("MB".to_string()); + } + Some(t) + } + Err(_) => None, + }; + let wlan_ip = match network_client::ip("wlan0") { + Ok(ip) => ip, + Err(_) => "x.x.x.x".to_string(), + }; + let wlan_rssi = match network_client::rssi_percent("wlan0") { + Ok(rssi) => Some(rssi), + Err(_) => None, + }; + let wlan_scan = match network_client::available_networks("wlan0") { + Ok(networks) => { + let scan: Vec = serde_json::from_str(networks.as_str()) + .expect("Failed to deserialize scan_networks response"); + Some(scan) + } + Err(_) => None, + }; + let wlan_ssid = match network_client::ssid("wlan0") { + Ok(ssid) => ssid, + Err(_) => "Not connected".to_string(), + }; + let wlan_state = match network_client::state("wlan0") { + Ok(state) => state, + Err(_) => "Interface unavailable".to_string(), + }; + let wlan_status = match network_client::status("wlan0") { + Ok(status) => status, + Err(_) => "Interface unavailable".to_string(), + }; + let wlan_traffic = match network_client::traffic("wlan0") { + Ok(traffic) => { + let mut t = traffic; + // modify traffic values & assign measurement unit + // based on received and transmitted values + // if received > 999 MB, convert it to GB + if t.received > 1_047_527_424 { + t.received /= 1_073_741_824; + t.rx_unit = Some("GB".to_string()); + } else if t.received > 0 { + // otherwise, convert it to MB + t.received = (t.received / 1024) / 1024; + t.rx_unit = Some("MB".to_string()); + } else { + t.received = 0; + t.rx_unit = Some("MB".to_string()); + } + + if t.transmitted > 1_047_527_424 { + t.transmitted /= 1_073_741_824; + t.tx_unit = Some("GB".to_string()); + } else if t.transmitted > 0 { + t.transmitted = (t.transmitted / 1024) / 1024; + t.tx_unit = Some("MB".to_string()); + } else { + t.transmitted = 0; + t.tx_unit = Some("MB".to_string()); + } + Some(t) + } + Err(_) => None, + }; + + NetworkContext { + ap_ip, + ap_ssid, + ap_state, + ap_traffic, + wlan_ip, + wlan_rssi, + wlan_scan, + wlan_ssid, + wlan_state, + wlan_status, + wlan_traffic, + flash_name: None, + flash_msg: None, + selected: None, + title: None, + back: None, + } + } +} + +#[get("/network")] +pub fn network_home(flash: Option) -> Template { + // assign context through context_builder call + let mut context = NetworkContext::build(); + // set back button (nav) url + context.back = Some("/".to_string()); + // set page title + context.title = Some("Network Configuration".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_card", &context) +} + +/// # helpers and routes for /network/ap/activate +///////////////////////////////// + +#[get("/network/ap/activate")] +pub fn deploy_ap() -> Flash { + // activate the wireless access point + debug!("Activating WiFi access point."); + match network_client::activate_ap() { + Ok(_) => Flash::success(Redirect::to("/network"), "Activated WiFi access point"), + Err(_) => Flash::error( + Redirect::to("/network"), + "Failed to activate WiFi access point", + ), + } +} + +/// # helpers and routes for /network/wifi +///////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct NetworkListContext { + pub ap_state: String, + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, + pub wlan_networks: HashMap, + pub wlan_ssid: String, +} + +impl NetworkListContext { + pub fn build() -> NetworkListContext { + // list of networks saved in the wpa_supplicant.conf + let wlan_list = match network_client::saved_networks() { + Ok(ssids) => { + let networks: Vec = serde_json::from_str(ssids.as_str()) + .expect("Failed to deserialize scan_list response"); + networks + } + Err(_) => Vec::new(), + }; + + // list of networks currently in range (online & accessible) + let wlan_scan = match network_client::available_networks("wlan0") { + Ok(networks) => { + let scan: Vec = serde_json::from_str(networks.as_str()) + .expect("Failed to deserialize scan_networks response"); + scan + } + Err(_) => Vec::new(), + }; + + let wlan_ssid = match network_client::ssid("wlan0") { + Ok(ssid) => ssid, + Err(_) => "Not connected".to_string(), + }; + + // create a hashmap to combine wlan_list & wlan_scan without repetition + let mut wlan_networks = HashMap::new(); + for ap in wlan_scan { + wlan_networks.insert(ap.ssid, "Available".to_string()); + } + for network in wlan_list { + // insert ssid (with state) only if it doesn't already exist + wlan_networks + .entry(network.ssid) + .or_insert_with(|| "Not in range".to_string()); + } + + let ap_state = match network_client::state("ap0") { + Ok(state) => state, + Err(_) => "Interface unavailable".to_string(), + }; + + NetworkListContext { + ap_state, + back: None, + flash_msg: None, + flash_name: None, + title: None, + wlan_networks, + wlan_ssid, + } + } +} + +#[get("/network/wifi")] +pub fn wifi_list(flash: Option) -> Template { + // assign context through context_builder call + let mut context = NetworkListContext::build(); + context.back = Some("/network".to_string()); + context.title = Some("WiFi Networks".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_list", &context) +} + + +/// # helpers and routes for /network/wifi +///////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct NetworkDetailContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub saved_aps: Vec, + pub selected: Option, + pub title: Option, + pub wlan_ip: String, + pub wlan_networks: HashMap, + pub wlan_rssi: Option, + pub wlan_ssid: String, + pub wlan_state: String, + pub wlan_status: String, + pub wlan_traffic: Option, +} + +impl NetworkDetailContext { + pub fn build() -> NetworkDetailContext { + let wlan_ip = match network_client::ip("wlan0") { + Ok(ip) => ip, + Err(_) => "x.x.x.x".to_string(), + }; + // list of networks saved in wpa_supplicant.conf + let wlan_list = match network_client::saved_networks() { + Ok(ssids) => { + let networks: Vec = serde_json::from_str(ssids.as_str()) + .expect("Failed to deserialize scan_list response"); + networks + } + Err(_) => Vec::new(), + }; + // list of networks saved in wpa_supplicant.conf + // HACK: we're running the same function twice (wlan_list) + // see if we can implement clone for Vec instead + let saved_aps = match network_client::saved_networks() { + Ok(ssids) => { + let networks: Vec = serde_json::from_str(ssids.as_str()) + .expect("Failed to deserialize scan_list response"); + networks + } + Err(_) => Vec::new(), + }; + let wlan_rssi = match network_client::rssi_percent("wlan0") { + Ok(rssi) => Some(rssi), + Err(_) => None, + }; + // list of networks currently in range (online & accessible) + let wlan_scan = match network_client::available_networks("wlan0") { + Ok(networks) => { + let scan: Vec = serde_json::from_str(networks.as_str()) + .expect("Failed to deserialize scan_networks response"); + scan + } + Err(_) => Vec::new(), + }; + let wlan_ssid = match network_client::ssid("wlan0") { + Ok(ssid) => ssid, + Err(_) => "Not connected".to_string(), + }; + let wlan_state = match network_client::state("wlan0") { + Ok(state) => state, + Err(_) => "Interface unavailable".to_string(), + }; + let wlan_status = match network_client::status("wlan0") { + Ok(status) => status, + Err(_) => "Interface unavailable".to_string(), + }; + let wlan_traffic = match network_client::traffic("wlan0") { + Ok(traffic) => { + let mut t = traffic; + // modify traffic values & assign measurement unit + // based on received and transmitted values + // if received > 999 MB, convert it to GB + if t.received > 1_047_527_424 { + t.received /= 1_073_741_824; + t.rx_unit = Some("GB".to_string()); + } else if t.received > 0 { + // otherwise, convert it to MB + t.received = (t.received / 1024) / 1024; + t.rx_unit = Some("MB".to_string()); + } else { + t.received = 0; + t.rx_unit = Some("MB".to_string()); + } + + if t.transmitted > 1_047_527_424 { + t.transmitted /= 1_073_741_824; + t.tx_unit = Some("GB".to_string()); + } else if t.transmitted > 0 { + t.transmitted = (t.transmitted / 1024) / 1024; + t.tx_unit = Some("MB".to_string()); + } else { + t.transmitted = 0; + t.tx_unit = Some("MB".to_string()); + } + Some(t) + } + Err(_) => None, + }; + // create a hashmap to combine wlan_list & wlan_scan without repetition + let mut wlan_networks = HashMap::new(); + for ap in wlan_scan { + let ssid = ap.ssid.clone(); + let rssi = ap.signal_level.clone(); + // parse the string to a signed integer (for math) + let rssi_parsed = rssi.parse::().unwrap(); + // perform rssi (dBm) to quality (%) conversion + let quality_percent = 2 * (rssi_parsed + 100); + let ap_detail = AccessPoint { + detail: Some(ap), + state: "Available".to_string(), + signal: Some(quality_percent), + }; + wlan_networks.insert(ssid, ap_detail); + } + for network in wlan_list { + // avoid repetition by checking that ssid is not already in list + if !wlan_networks.contains_key(&network.ssid) { + let ssid = network.ssid.clone(); + let net_detail = AccessPoint { + detail: None, + state: "Not in range".to_string(), + signal: None, + }; + wlan_networks.insert(ssid, net_detail); + } + } + + NetworkDetailContext { + back: None, + flash_name: None, + flash_msg: None, + saved_aps, + selected: None, + title: None, + wlan_ip, + wlan_networks, + wlan_rssi, + wlan_ssid, + wlan_state, + wlan_status, + wlan_traffic, + } + } +} + +#[get("/network/wifi?")] +pub fn network_detail(ssid: &RawStr, flash: Option) -> Template { + // assign context through context_builder call + let mut context = NetworkDetailContext::build(); + context.back = Some("/network/wifi".to_string()); + context.title = Some("WiFi Network".to_string()); + // decode ssid from url + let decoded_ssid = percent_decode(ssid.as_bytes()).decode_utf8().unwrap(); + context.selected = Some(decoded_ssid.to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_detail", &context) +} + + +/// # helpers and routes for /network/wifi/activate +///////////////////////////////// + +#[get("/network/wifi/activate")] +pub fn deploy_client() -> Flash { + // activate the wireless client + debug!("Activating WiFi client mode."); + match network_client::activate_client() { + Ok(_) => Flash::success(Redirect::to("/network"), "Activated WiFi client"), + Err(_) => Flash::error(Redirect::to("/network"), "Failed to activate WiFi client"), + } +} + +#[get("/network/wifi/add")] +pub fn network_add_wifi(flash: Option) -> Template { + let mut context = NetworkContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Add WiFi Network".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_add", &context) +} + + +// used in /network/wifi/add? +#[derive(Debug, Serialize)] +pub struct NetworkAddContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub selected: Option, + pub title: Option, +} + +impl NetworkAddContext { + pub fn build() -> NetworkAddContext { + NetworkAddContext { + back: None, + flash_name: None, + flash_msg: None, + selected: None, + title: None, + } + } +} + +#[get("/network/wifi/add?")] +pub fn network_add_ssid(ssid: &RawStr, flash: Option) -> Template { + // decode ssid from url + let decoded_ssid = percent_decode(ssid.as_bytes()).decode_utf8().unwrap(); + let mut context = NetworkAddContext::build(); + context.back = Some("/network/wifi".to_string()); + context.selected = Some(decoded_ssid.to_string()); + context.title = Some("Add WiFi Network".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_add", &context) +} + +#[post("/network/wifi/add", data = "")] +pub fn add_credentials(wifi: Form) -> Template { + // check if the credentials already exist for this access point + // note: this is nicer but it's an unstable feature: + // if check_saved_aps(&wifi.ssid).contains(true) + // use unwrap_or instead, set value to false if err is returned + let creds_exist = network_client::saved_ap(&wifi.ssid).unwrap_or(false); + if creds_exist { + let mut context = NetworkAddContext::build(); + context.back = Some("/network".to_string()); + context.flash_name = Some("error".to_string()); + context.flash_msg = + Some("Network credentials already exist for this access point".to_string()); + context.title = Some("Add WiFi Network".to_string()); + // return early from handler with "creds already exist" message + return Template::render("network_add", &context); + }; + + // if credentials not found, generate and write wifi config to wpa_supplicant + match network_client::add(&wifi.ssid, &wifi.pass) { + Ok(_) => { + debug!("Added WiFi credentials."); + // force reread of wpa_supplicant.conf file with new credentials + match network_client::reconfigure() { + Ok(_) => debug!("Successfully reconfigured wpa_supplicant"), + Err(_) => warn!("Failed to reconfigure wpa_supplicant"), + } + let mut context = NetworkAddContext::build(); + context.back = Some("/network".to_string()); + context.flash_name = Some("success".to_string()); + context.flash_msg = Some("Added WiFi credentials".to_string()); + context.title = Some("Add WiFi Network".to_string()); + Template::render("network_add", &context) + } + Err(_) => { + debug!("Failed to add WiFi credentials."); + let mut context = NetworkAddContext::build(); + context.back = Some("/network".to_string()); + context.flash_name = Some("error".to_string()); + context.flash_msg = Some("Failed to add WiFi credentials".to_string()); + context.title = Some("Add WiFi Network".to_string()); + Template::render("network_add", &context) + } + } +} + + +/// # helpers and routes for /network/wifi/usage +///////////////////////////////// + +#[derive(Debug, Serialize)] +pub struct NetworkAlertContext { + pub alert: Alert, + pub back: Option, + pub data_total: Data, // combined stored and current wifi traffic in bytes + pub flash_name: Option, + pub flash_msg: Option, + pub threshold: Threshold, + pub title: Option, + pub traffic: Traffic, // current wifi traffic in bytes (since boot) +} + +impl NetworkAlertContext { + pub fn build() -> NetworkAlertContext { + let alert = monitor::get_alerts().unwrap(); + // stored wifi data values as bytes + let stored_traffic = monitor::get_data().unwrap(); + let threshold = monitor::get_thresholds().unwrap(); + // current wifi traffic values as bytes + let traffic = match network_client::traffic("wlan0") { + Ok(t) => t, + Err(_) => Traffic { + received: 0, + transmitted: 0, + rx_unit: None, + tx_unit: None, + }, + }; + + let current_traffic = traffic.received + traffic.transmitted; + let total = stored_traffic.total + current_traffic; + let data_total = Data { total }; + + NetworkAlertContext { + alert, + back: None, + data_total, + flash_name: None, + flash_msg: None, + threshold, + title: None, + traffic, + } + } +} + +#[get("/network/wifi/usage")] +pub fn wifi_usage(flash: Option) -> Template { + let mut context = NetworkAlertContext::build(); + // set back icon link to network route + context.back = Some("/network".to_string()); + context.title = Some("Network Data Usage".to_string()); + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.name().to_string()); + context.flash_msg = Some(flash.msg().to_string()); + }; + // template_dir is set in Rocket.toml + Template::render("network_usage", &context) +} + +#[post("/network/wifi/usage", data = "")] +pub fn wifi_usage_alerts(thresholds: Form) -> Flash { + match monitor::update_store(thresholds.into_inner()) { + Ok(_) => { + debug!("WiFi data usage thresholds updated."); + Flash::success( + Redirect::to("/network/wifi/usage"), + "Updated alert thresholds and flags", + ) + } + Err(_) => { + warn!("Failed to update WiFi data usage thresholds."); + Flash::error( + Redirect::to("/network/wifi/usage"), + "Failed to update alert thresholds and flags", + ) + } + } +} + + +/// # helpers and routes for /network/activate_ap +////////////////////////////////// + +#[post("/api/v1/network/activate_ap")] +pub fn activate_ap() -> Json { + // activate the wireless access point + debug!("Activating WiFi access point."); + match network_client::activate_ap() { + Ok(_) => { + let status = "success".to_string(); + Json(build_json_response(status, None, None)) + } + Err(_) => { + let status = "error".to_string(); + let msg = "Failed to activate WiFi access point.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/activate_client")] +pub fn activate_client() -> Json { + // activate the wireless client + debug!("Activating WiFi client mode."); + match network_client::activate_client() { + Ok(_) => { + let status = "success".to_string(); + Json(build_json_response(status, None, None)) + } + Err(_) => { + let status = "error".to_string(); + let msg = "Failed to activate WiFi client mode.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[get("/api/v1/network/ip")] +pub fn return_ip() -> Json { + // retrieve ip for wlan0 or set to x.x.x.x if not found + let wlan_ip = match network_client::ip("wlan0") { + Ok(ip) => ip, + Err(_) => "x.x.x.x".to_string(), + }; + // retrieve ip for ap0 or set to x.x.x.x if not found + let ap_ip = match network_client::ip("ap0") { + Ok(ip) => ip, + Err(_) => "x.x.x.x".to_string(), + }; + let data = json!({ + "wlan0": wlan_ip, + "ap0": ap_ip + }); + let status = "success".to_string(); + Json(build_json_response(status, Some(data), None)) +} + +#[get("/api/v1/network/rssi")] +pub fn return_rssi() -> Json { + // retrieve rssi for connected network + match network_client::rssi("wlan0") { + Ok(rssi) => { + let status = "success".to_string(); + let data = json!(rssi); + Json(build_json_response(status, Some(data), None)) + } + Err(_) => { + let status = "success".to_string(); + let msg = "Not currently connected to an access point.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[get("/api/v1/network/ssid")] +pub fn return_ssid() -> Json { + // retrieve ssid for connected network + match network_client::ssid("wlan0") { + Ok(network) => { + let status = "success".to_string(); + let data = json!(network); + Json(build_json_response(status, Some(data), None)) + } + Err(_) => { + let status = "success".to_string(); + let msg = "Not currently connected to an access point.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[get("/api/v1/network/state")] +pub fn return_state() -> Json { + // retrieve state of wlan0 or set to x.x.x.x if not found + let wlan_state = match network_client::state("wlan0") { + Ok(state) => state, + Err(_) => "unavailable".to_string(), + }; + // retrieve state for ap0 or set to x.x.x.x if not found + let ap_state = match network_client::state("ap0") { + Ok(state) => state, + Err(_) => "unavailable".to_string(), + }; + let data = json!({ + "wlan0": wlan_state, + "ap0": ap_state + }); + let status = "success".to_string(); + Json(build_json_response(status, Some(data), None)) +} + +#[get("/api/v1/network/status")] +pub fn return_status() -> Json { + // retrieve status info for wlan0 interface + match network_client::status("wlan0") { + Ok(network) => { + let status = "success".to_string(); + let data = json!(network); + Json(build_json_response(status, Some(data), None)) + } + Err(_) => { + let status = "success".to_string(); + let msg = "Not currently connected to an access point.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[get("/api/v1/network/wifi")] +pub fn scan_networks() -> Json { + // retrieve scan results for access-points within range of wlan0 + match network_client::available_networks("wlan0") { + Ok(networks) => { + let status = "success".to_string(); + let data = json!(networks); + Json(build_json_response(status, Some(data), None)) + } + Err(_) => { + let status = "success".to_string(); + let msg = "Unable to scan for networks. Interface may be deactivated.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi", data = "")] +pub fn add_wifi(wifi: Json) -> Json { + // generate and write wifi config to wpa_supplicant + match network_client::add(&wifi.ssid, &wifi.pass) { + Ok(_) => { + debug!("Added WiFi credentials."); + // force reread of wpa_supplicant.conf file with new credentials + match network_client::reconfigure() { + Ok(_) => debug!("Successfully reconfigured wpa_supplicant."), + Err(_) => warn!("Failed to reconfigure wpa_supplicant."), + } + // json response for successful update + let status = "success".to_string(); + let msg = "WiFi credentials added.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + debug!("Failed to add WiFi credentials."); + // json response for failed update + let status = "error".to_string(); + let msg = "Failed to add WiFi credentials.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi/connect", data = "")] +pub fn connect_ap(ssid: Json) -> Json { + // retrieve the id for the given network ssid + match network_client::id("wlan0", &ssid.ssid) { + // attempt connection with the given network + Ok(id) => match network_client::connect(&id, "wlan0") { + Ok(_) => { + let status = "success".to_string(); + let msg = "Connected to chosen network.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + let status = "error".to_string(); + let msg = "Failed to connect to chosen network.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + }, + Err(_) => { + let status = "error".to_string(); + let msg = "Failed to retrieve the network ID.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi/disconnect", data = "")] +pub fn disconnect_ap(ssid: Json) -> Json { + // attempt to disable the current network for wlan0 interface + match network_client::disable("wlan0", &ssid.ssid) { + Ok(_) => { + let status = "success".to_string(); + let msg = "Disconnected from WiFi network.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + let status = "error".to_string(); + let msg = "Failed to disconnect from WiFi network.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi/forget", data = "")] +pub fn forget_ap(network: Json) -> Json { + let ssid = &network.ssid; + match network_client::forget("wlan0", ssid) { + Ok(_) => { + debug!("Removed WiFi credentials for chosen network."); + let status = "success".to_string(); + let msg = "WiFi network credentials removed.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("Failed to remove WiFi credentials."); + let status = "error".to_string(); + let msg = "Failed to remove WiFi network credentials.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi/modify", data = "")] +pub fn modify_password(wifi: Json) -> Json { + let ssid = &wifi.ssid; + let pass = &wifi.pass; + // we are using a helper function (`update`) to delete the old + // credentials and add the new ones. this is because the wpa_cli method + // for updating the password does not work. + match network_client::update("wlan0", ssid, pass) { + Ok(_) => { + debug!("WiFi password updated for chosen network."); + let status = "success".to_string(); + let msg = "WiFi password updated.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("Failed to update WiFi password."); + let status = "error".to_string(); + let msg = "Failed to update WiFi password.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi/usage", data = "")] +pub fn update_wifi_alerts(thresholds: Json) -> Json { + match monitor::update_store(thresholds.into_inner()) { + Ok(_) => { + debug!("WiFi data usage thresholds updated."); + let status = "success".to_string(); + let msg = "Updated alert threshold and flags.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + Err(_) => { + warn!("Failed to update WiFi data usage thresholds."); + let status = "error".to_string(); + let msg = "Failed to update WiFi data usage thresholds.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + +#[post("/api/v1/network/wifi/usage/reset")] +pub fn reset_data_total() -> Json { + match monitor::reset_data() { + Ok(_) => { + debug!("Reset network data usage total."); + let traffic = match network_client::traffic("wlan0") { + Ok(t) => t, + Err(_) => Traffic { + received: 0, + transmitted: 0, + rx_unit: None, + tx_unit: None, + }, + }; + // current wifi traffic values as bytes + let current_traffic = traffic.received + traffic.transmitted; + let data = json!(current_traffic); + let status = "success".to_string(); + let msg = "Reset network data usage total.".to_string(); + Json(build_json_response(status, Some(data), Some(msg))) + } + Err(_) => { + warn!("Failed to reset network data usage total."); + let status = "error".to_string(); + let msg = "Failed to reset network data usage total.".to_string(); + Json(build_json_response(status, None, Some(msg))) + } + } +} + diff --git a/peach-web/src/utils.rs b/peach-web/src/utils.rs new file mode 100644 index 0000000..7b6b69f --- /dev/null +++ b/peach-web/src/utils.rs @@ -0,0 +1,29 @@ +pub mod monitor; + +use rocket_contrib::json::{Json, JsonValue}; +use serde::Serialize; + +// HELPER FUNCTIONS +#[derive(Serialize)] +pub struct JsonResponse { + pub status: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub msg: Option, +} + +pub fn build_json_response( + status: String, + data: Option, + msg: Option, +) -> JsonResponse { + JsonResponse { status, data, msg } +} + + +#[derive(Debug, Serialize)] +pub struct FlashContext { + pub flash_name: Option, + pub flash_msg: Option, +} diff --git a/peach-web/src/monitor.rs b/peach-web/src/utils/monitor.rs similarity index 100% rename from peach-web/src/monitor.rs rename to peach-web/src/utils/monitor.rs