Merge pull request 'Refactor routes into functional groupings' (#12) from routes-refactor into main

Reviewed-on: #12
This commit is contained in:
notplants 2021-11-03 10:24:27 +00:00
commit 1aa5821adf
26 changed files with 2656 additions and 5675 deletions

539
Cargo.lock generated

File diff suppressed because it is too large Load Diff

1460
peach-buttons/Cargo.lock generated

File diff suppressed because it is too large Load Diff

1603
peach-oled/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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(())
}

View File

@ -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<String>,
pub cpu_stat_percent: Option<CpuStatPercentages>,
pub disk_stats: Vec<DiskUsage>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub load_average: Option<LoadAverage>,
pub mem_stats: Option<MemStat>,
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<String>,
pub uptime: Option<i32>,
}
impl DeviceContext {
pub fn build() -> DeviceContext {
// convert result to Option<CpuStatPercentages>, 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<DiskUsage>
let disk_usage_stats = match stats_client::disk_usage() {
Ok(disks) => {
let partitions: Vec<DiskUsage> = 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::<i32>().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<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
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<String>,
pub flash_msg: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct HelpContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
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<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl HomeContext {
pub fn build() -> HomeContext {
HomeContext {
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[derive(Debug, Serialize)]
pub struct LoginContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
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<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
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<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
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<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
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<String>,
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
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<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
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<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
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<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
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<Traffic>,
pub wlan_ip: String,
pub wlan_rssi: Option<String>,
pub wlan_scan: Option<Vec<Scan>>,
pub wlan_ssid: String,
pub wlan_state: String,
pub wlan_status: String,
pub wlan_traffic: Option<Traffic>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
// allows for passing in the ssid of a chosen access point
// this is used in the network_detail template
pub selected: Option<String>,
// page title for header in navbar
pub title: Option<String>,
// url for back-arrow link
pub back: Option<String>,
}
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<Scan> = 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?<ssid>
#[derive(Debug, Serialize)]
pub struct NetworkAddContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub selected: Option<String>,
pub title: Option<String>,
}
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<String>,
pub data_total: Data, // combined stored and current wifi traffic in bytes
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub threshold: Threshold,
pub title: Option<String>,
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<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub saved_aps: Vec<Networks>,
pub selected: Option<String>,
pub title: Option<String>,
pub wlan_ip: String,
pub wlan_networks: HashMap<String, AccessPoint>,
pub wlan_rssi: Option<String>,
pub wlan_ssid: String,
pub wlan_state: String,
pub wlan_status: String,
pub wlan_traffic: Option<Traffic>,
}
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<Networks> = 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<Networks> instead
let saved_aps = match network_client::saved_networks() {
Ok(ssids) => {
let networks: Vec<Networks> = 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<Scan> = 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::<i32>().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<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
pub wlan_networks: HashMap<String, String>,
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<Networks> = 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<Networks> = 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<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
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<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
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<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl ShutdownContext {
pub fn build() -> ShutdownContext {
ShutdownContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}

View File

@ -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<Output> {
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<Output> {
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()
}

View File

@ -1,4 +1,4 @@
//!! different types of PeachWebError
//! Custom error type representing all possible error variants for peach-web.
use peach_lib::error::PeachError;
use peach_lib::{serde_json, serde_yaml};

View File

@ -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,
}

View File

@ -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<JsonValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg: Option<String>,
}
// reboot the device
#[post("/api/v1/device/reboot")]
pub fn reboot_device() -> Json<JsonResponse> {
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<JsonResponse> {
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<JsonResponse> {
// 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<JsonResponse> {
// 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<JsonResponse> {
// 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<JsonResponse> {
// 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<JsonResponse> {
// 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<JsonResponse> {
// 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<JsonResponse> {
// 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<JsonResponse> {
// 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 = "<wifi>")]
pub fn add_wifi(wifi: Json<WiFi>) -> Json<JsonResponse> {
// 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 = "<ssid>")]
pub fn connect_ap(ssid: Json<Ssid>) -> Json<JsonResponse> {
// 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 = "<ssid>")]
pub fn disconnect_ap(ssid: Json<Ssid>) -> Json<JsonResponse> {
// 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 = "<network>")]
pub fn forget_ap(network: Json<Ssid>) -> Json<JsonResponse> {
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 = "<wifi>")]
pub fn modify_password(wifi: Json<WiFi>) -> Json<JsonResponse> {
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 = "<thresholds>")]
pub fn update_wifi_alerts(thresholds: Json<Threshold>) -> Json<JsonResponse> {
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<JsonResponse> {
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<JsonResponse> {
// 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<JsonResponse> {
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<JsonResponse> {
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<JsonResponse> {
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<JsonResponse> {
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 = "<dns_form>")]
pub fn save_dns_configuration_endpoint(dns_form: Json<DnsForm>) -> Json<JsonResponse> {
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 = "<password_form>")]
pub fn save_password_form_endpoint(password_form: Json<PasswordForm>) -> Json<JsonResponse> {
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 = "<reset_password_form>")]
pub fn reset_password_form_endpoint(
reset_password_form: Json<ResetPasswordForm>,
) -> Json<JsonResponse> {
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<JsonValue>,
msg: Option<String>,
) -> JsonResponse {
JsonResponse { status, data, msg }
}

View File

@ -24,20 +24,12 @@
//! of the template to be rendered.
#![feature(proc_macro_hygiene, decl_macro)]
// this is to ignore a clippy warning that suggests
// 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;
#[cfg(test)]
mod tests;
pub mod utils;
mod ws;
use std::{env, thread};
@ -47,8 +39,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::settings::admin::*;
use crate::routes::settings::dns::*;
use crate::routes::settings::network::*;
use crate::ws::*;
pub type BoxError = Box<dyn std::error::Error>;

View File

@ -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?<ssid> | 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?<ssid> | 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?<ssid> | 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<FlashMessage>) -> 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<Redirect> {
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<Redirect> {
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<FlashMessage>) -> 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<FlashMessage>) -> 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<Redirect> {
// 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<FlashMessage>) -> 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<Redirect> {
// 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<FlashMessage>) -> 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?<ssid>")]
pub fn network_detail(ssid: &RawStr, flash: Option<FlashMessage>) -> 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<Redirect> {
// 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<FlashMessage>) -> 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?<ssid>")]
pub fn network_add_ssid(ssid: &RawStr, flash: Option<FlashMessage>) -> 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 = "<wifi>")]
pub fn add_credentials(wifi: Form<WiFi>) -> 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<FlashMessage>) -> 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 = "<thresholds>")]
pub fn wifi_usage_alerts(thresholds: Form<Threshold>) -> Flash<Redirect> {
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<FlashMessage>) -> 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 = "<dns>")]
pub fn configure_dns_post(dns: Form<DnsForm>) -> 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<FlashMessage>) -> 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 = "<password_form>")]
pub fn change_password_post(password_form: Form<PasswordForm>) -> 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<FlashMessage>) -> 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 = "<reset_password_form>")]
pub fn reset_password_post(reset_password_form: Form<ResetPasswordForm>) -> 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<FlashMessage>) -> 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<FlashMessage>) -> 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<FlashMessage>) -> 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 = "<add_admin_form>")]
pub fn add_admin_post(add_admin_form: Form<AddAdminForm>) -> Flash<Redirect> {
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 = "<delete_admin_form>")]
pub fn delete_admin_post(delete_admin_form: Form<DeleteAdminForm>) -> Flash<Redirect> {
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<Redirect> {
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 = "<network>")]
pub fn connect_wifi(network: Form<Ssid>) -> Flash<Redirect> {
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 = "<network>")]
pub fn disconnect_wifi(network: Form<Ssid>) -> Flash<Redirect> {
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 = "<network>")]
pub fn forget_wifi(network: Form<Ssid>) -> Flash<Redirect> {
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?<ssid>")]
pub fn wifi_password(ssid: &RawStr, flash: Option<FlashMessage>) -> 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 = "<wifi>")]
pub fn wifi_set_password(wifi: Form<WiFi>) -> Flash<Redirect> {
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<FlashMessage>) -> 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<FlashMessage>) -> 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<FlashMessage>) -> 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<FlashMessage>) -> 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("/<file..>", rank = 2)]
pub fn files(file: PathBuf) -> Option<NamedFile> {
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)
}

View File

@ -0,0 +1,347 @@
use log::{debug, info};
use rocket::request::{FlashMessage, Form, FromForm};
use rocket::response::{Flash, Redirect};
use rocket::{get, post};
use rocket_contrib::{json::Json, templates::Template};
use serde::{Deserialize, Serialize};
use peach_lib::password_utils;
use crate::error::PeachWebError;
use crate::utils::{build_json_response, JsonResponse};
// HELPERS AND ROUTES FOR /login
#[derive(Debug, Serialize)]
pub struct LoginContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl LoginContext {
pub fn build() -> LoginContext {
LoginContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[get("/login")]
pub fn login(flash: Option<FlashMessage>) -> 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)
}
// HELPERS AND ROUTES FOR /logout
#[post("/logout")]
pub fn logout() -> Flash<Redirect> {
// 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 /reset_password
#[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<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
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<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl ChangePasswordContext {
pub fn build() -> ChangePasswordContext {
ChangePasswordContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
/// Verify, validate and save the submitted password. 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(())
}
/// Password reset request handler. This 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<FlashMessage>) -> 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)
}
/// Password reset form request handler. This route is used by a user who is not logged in
/// and is specifically for users who have forgotten their password.
/// This route is excluded from nginx basic auth via the nginx config.
#[post("/reset_password", data = "<reset_password_form>")]
pub fn reset_password_post(reset_password_form: Form<ResetPasswordForm>) -> 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)
}
}
}
/// JSON password reset form request handler. This 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 = "<reset_password_form>")]
pub fn reset_password_form_endpoint(
reset_password_form: Json<ResetPasswordForm>,
) -> Json<JsonResponse> {
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)))
}
}
}
// HELPERS AND ROUTES FOR /send_password_reset
#[derive(Debug, Serialize)]
pub struct SendPasswordResetContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl SendPasswordResetContext {
pub fn build() -> SendPasswordResetContext {
SendPasswordResetContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
/// Password reset request handler. 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<FlashMessage>) -> 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)
}
/// Send password reset request handler. This route is used by a user who is not logged in
/// and is specifically for users who have forgotten their password. A successful request results
/// in a Scuttlebutt private message being sent to the account of the device admin.
#[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)
}
}
}
// HELPERS AND ROUTES FOR /settings/change_password
#[derive(Debug, Deserialize, FromForm)]
pub struct PasswordForm {
pub old_password: String,
pub new_password1: String,
pub new_password2: String,
}
/// Password save form request handler. 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(())
}
/// Change password request handler. This is used by a user who is already logged in.
#[get("/settings/change_password")]
pub fn change_password(flash: Option<FlashMessage>) -> 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)
}
/// Change password form request handler. This route is used by a user who is already logged in.
#[post("/settings/change_password", data = "<password_form>")]
pub fn change_password_post(password_form: Form<PasswordForm>) -> 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)
}
}
}
/// JSON change password form request handler.
#[post("/api/v1/settings/change_password", data = "<password_form>")]
pub fn save_password_form_endpoint(password_form: Json<PasswordForm>) -> Json<JsonResponse> {
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)))
}
}
}

View File

@ -0,0 +1,279 @@
use log::{debug, info, warn};
use rocket::{
get, post,
request::FlashMessage,
response::{Flash, Redirect},
};
use rocket_contrib::{json::Json, templates::Template};
use serde::Serialize;
use std::{
io,
process::{Command, Output},
};
use peach_lib::config_manager::load_peach_config;
use peach_lib::stats_client::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat};
use peach_lib::{dyndns_client, network_client, oled_client, sbot_client, stats_client};
use crate::utils::{build_json_response, JsonResponse};
// HELPERS AND ROUTES FOR /device
/// System statistics data.
#[derive(Debug, Serialize)]
pub struct DeviceContext {
pub back: Option<String>,
pub cpu_stat_percent: Option<CpuStatPercentages>,
pub disk_stats: Vec<DiskUsage>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub load_average: Option<LoadAverage>,
pub mem_stats: Option<MemStat>,
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<String>,
pub uptime: Option<i32>,
}
impl DeviceContext {
pub fn build() -> DeviceContext {
// convert result to Option<CpuStatPercentages>, 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<DiskUsage>
let disk_usage_stats = match stats_client::disk_usage() {
Ok(disks) => {
let partitions: Vec<DiskUsage> = 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::<i32>().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<FlashMessage>) -> 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<Output> {
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<Redirect> {
match reboot() {
Ok(_) => Flash::success(Redirect::to("/shutdown"), "Rebooting the device"),
Err(_) => Flash::error(Redirect::to("/shutdown"), "Failed to reboot the device"),
}
}
/// JSON request handler for device reboot.
#[post("/api/v1/device/reboot")]
pub fn reboot_device() -> Json<JsonResponse> {
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<Output> {
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<Redirect> {
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<JsonResponse> {
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<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
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<FlashMessage>) -> 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)
}

View File

@ -0,0 +1,57 @@
use log::debug;
use rocket::{catch, get, response::NamedFile};
use rocket_contrib::templates::Template;
use serde::Serialize;
use std::path::{Path, PathBuf};
#[get("/<file..>", rank = 2)]
pub fn files(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new("static/").join(file)).ok()
}
// HELPERS AND ROUTES FOR 404 ERROR
#[derive(Debug, Serialize)]
pub struct ErrorContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
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 ERROR
#[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)
}

View File

@ -0,0 +1,67 @@
use rocket::{get, request::FlashMessage};
use rocket_contrib::templates::Template;
use serde::Serialize;
// HELPERS AND ROUTES FOR / (HOME PAGE)
#[derive(Debug, Serialize)]
pub struct HomeContext {
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
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<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl HelpContext {
pub fn build() -> HelpContext {
HelpContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[get("/help")]
pub fn help(flash: Option<FlashMessage>) -> 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)
}

View File

@ -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;

View File

@ -0,0 +1,86 @@
//! Helper routes for pinging services to check that they are active
use log::{debug, warn};
use rocket::get;
use rocket_contrib::json::Json;
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 crate::utils::{build_json_response, JsonResponse};
/// Status route: useful for checking connectivity from web client.
#[get("/api/v1/ping")]
pub fn ping_pong() -> Json<JsonResponse> {
// 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<JsonResponse> {
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<JsonResponse> {
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<JsonResponse> {
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<JsonResponse> {
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)))
}
}
}

View File

@ -0,0 +1,110 @@
//! Routes for ScuttleButt related functionality.
use rocket::{get, request::FlashMessage};
use rocket_contrib::templates::Template;
use serde::Serialize;
// HELPERS AND ROUTES FOR /messages
#[derive(Debug, Serialize)]
pub struct MessageContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl MessageContext {
pub fn build() -> MessageContext {
MessageContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[get("/messages")]
pub fn messages(flash: Option<FlashMessage>) -> 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<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl PeerContext {
pub fn build() -> PeerContext {
PeerContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[get("/peers")]
pub fn peers(flash: Option<FlashMessage>) -> 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<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
}
impl ProfileContext {
pub fn build() -> ProfileContext {
ProfileContext {
back: None,
flash_name: None,
flash_msg: None,
title: None,
}
}
}
#[get("/profile")]
pub fn profile(flash: Option<FlashMessage>) -> 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)
}

View File

@ -0,0 +1,128 @@
use rocket::{
get, post,
request::{FlashMessage, Form, FromForm},
response::{Flash, Redirect},
uri,
};
use rocket_contrib::templates::Template;
use serde::{Deserialize, Serialize};
use peach_lib::config_manager;
use peach_lib::config_manager::load_peach_config;
use crate::error::PeachWebError;
// HELPERS AND ROUTES FOR /settings/configure_admin
#[derive(Debug, Serialize)]
pub struct ConfigureAdminContext {
pub ssb_admin_ids: Vec<String>,
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
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,
}
}
}
/// View and delete currently configured admin.
#[get("/settings/configure_admin")]
pub fn configure_admin(flash: Option<FlashMessage>) -> 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, Deserialize, FromForm)]
pub struct AddAdminForm {
pub ssb_id: String,
}
#[derive(Debug, Serialize)]
pub struct AddAdminContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl AddAdminContext {
pub fn build() -> AddAdminContext {
AddAdminContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
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(())
}
#[get("/settings/admin/add")]
pub fn add_admin(flash: Option<FlashMessage>) -> 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 = "<add_admin_form>")]
pub fn add_admin_post(add_admin_form: Form<AddAdminForm>) -> Flash<Redirect> {
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"),
}
}
// HELPERS AND ROUTES FOR /settings/admin/delete
#[derive(Debug, Deserialize, FromForm)]
pub struct DeleteAdminForm {
pub ssb_id: String,
}
#[post("/settings/admin/delete", data = "<delete_admin_form>")]
pub fn delete_admin_post(delete_admin_form: Form<DeleteAdminForm>) -> Flash<Redirect> {
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"),
}
}

View File

@ -0,0 +1,167 @@
use log::info;
use rocket::{
get, post,
request::{FlashMessage, Form, FromForm},
};
use rocket_contrib::{json::Json, templates::Template};
use serde::{Deserialize, Serialize};
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_dyndns_subdomain, get_full_dynamic_domain,
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 crate::error::PeachWebError;
use crate::utils::{build_json_response, JsonResponse};
#[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(())
}
}
#[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<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
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<FlashMessage>) -> 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 = "<dns>")]
pub fn configure_dns_post(dns: Form<DnsForm>) -> 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 = "<dns_form>")]
pub fn save_dns_configuration_endpoint(dns_form: Json<DnsForm>) -> Json<JsonResponse> {
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)))
}
}
}

View File

@ -0,0 +1,3 @@
pub mod admin;
pub mod dns;
pub mod network;

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,9 @@ use rocket::http::{ContentType, Status};
use rocket::local::Client;
use rocket_contrib::json;
use crate::utils::build_json_response;
use super::rocket;
use crate::json_api::build_json_response;
// helper function to test correct retrieval and content of a file
fn test_query_file<T>(path: &str, file: T, status: Status)

29
peach-web/src/utils.rs Normal file
View File

@ -0,0 +1,29 @@
pub mod monitor;
use rocket_contrib::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<JsonValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub msg: Option<String>,
}
pub fn build_json_response(
status: String,
data: Option<JsonValue>,
msg: Option<String>,
) -> JsonResponse {
JsonResponse { status, data, msg }
}
#[derive(Debug, Serialize)]
pub struct FlashContext {
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}

View File

@ -9,12 +9,12 @@ use websocket::sync::Server;
use websocket::{Message, OwnedMessage};
pub fn websocket_server(address: String) -> io::Result<()> {
// Start listening for WebSocket connections
// start listening for WebSocket connections
let ws_server = Server::bind(address)?;
info!("Listening for WebSocket connections.");
for connection in ws_server.filter_map(Result::ok) {
// Spawn a new thread for each connection.
// spawn a new thread for each connection
thread::spawn(move || {
if !connection
.protocols()