7 Commits

Author SHA1 Message Date
4d08323d77 bump minor version and remove anyhow dep 2022-01-03 14:06:22 +02:00
df91968762 add network api doc comments 2022-01-03 14:05:23 +02:00
9255abb078 add remaining network rpc methods 2022-01-03 14:04:47 +02:00
1ad956c0c7 add additional network rpc structs 2022-01-03 14:04:33 +02:00
39c15d0fe5 Merge branch 'update_network_args' into network_rpc_server
Merge peach-network argument changes which impact the jsonrpc-server
code.
2022-01-03 12:03:38 +02:00
fd12e97bc4 begin adding set methods for network rpc 2022-01-03 11:15:34 +02:00
318fa9768a add get methods for network rpcs 2022-01-03 10:46:38 +02:00
99 changed files with 3921 additions and 2041 deletions

3
.gitignore vendored
View File

@ -1,5 +1,2 @@
.idea
target
*peachdeploy.sh
*vpsdeploy.sh
*bindeploy.sh

707
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,5 +11,6 @@ members = [
"peach-monitor",
"peach-stats",
"peach-jsonrpc-server",
"peach-probe",
"peach-dyndns-updater"
]

View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,6 +1,6 @@
[package]
name = "peach-config"
version = "0.1.17"
version = "0.1.10"
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
edition = "2018"
description = "Command line tool for installing, updating and configuring PeachCloud"
@ -35,5 +35,3 @@ structopt = "0.3.13"
clap = "2.33.3"
log = "0.4"
lazy_static = "1.4.0"
peach-lib = { path = "../peach-lib" }
rpassword = "5.0"

View File

@ -8,7 +8,7 @@ dtparam=i2c_arm=on
# Apply device tree overlay to enable pull-up resistors for buttons
device_tree_overlay=overlays/mygpio.dtbo
kernel=vmlinuz-4.19.0-18-arm64
kernel=vmlinuz-4.19.0-17-arm64
# For details on the initramfs directive, see
# https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=10532
initramfs initrd.img-4.19.0-18-arm64
initramfs initrd.img-4.19.0-17-arm64

View File

@ -1,35 +0,0 @@
use crate::error::PeachConfigError;
use crate::ChangePasswordOpts;
use peach_lib::password_utils::set_new_password;
/// Utility function to set the admin password for peach-web from the command-line.
pub fn set_peach_web_password(opts: ChangePasswordOpts) -> Result<(), PeachConfigError> {
match opts.password {
// read password from CLI arg
Some(password) => {
set_new_password(&password)
.map_err(|err| PeachConfigError::ChangePasswordError { source: err })?;
println!(
"Your new password has been set for peach-web. You can login through the \
web interface with username admin."
);
Ok(())
}
// read password from tty
None => {
let pass1 = rpassword::read_password_from_tty(Some("New password: "))?;
let pass2 = rpassword::read_password_from_tty(Some("Confirm password: "))?;
if pass1 != pass2 {
Err(PeachConfigError::InvalidPassword)
} else {
set_new_password(&pass1)
.map_err(|err| PeachConfigError::ChangePasswordError { source: err })?;
println!(
"Your new password has been set for peach-web. You can login through the \
web interface with username admin."
);
Ok(())
}
}
}
}

View File

@ -3,12 +3,15 @@
pub const CONF: &str = "/var/lib/peachcloud/conf";
// List of package names which are installed via apt-get
pub const SERVICES: [&str; 8] = [
pub const SERVICES: [&str; 11] = [
"peach-oled",
"peach-network",
"peach-stats",
"peach-web",
"peach-probe",
"peach-menu",
"peach-buttons",
"peach-oled",
"peach-monitor",
"peach-probe",
"peach-dyndns-updater",
"peach-go-sbot",
"peach-config",

View File

@ -1,5 +1,4 @@
#![allow(clippy::nonstandard_macro_braces)]
use peach_lib::error::PeachError;
pub use snafu::ResultExt;
use snafu::Snafu;
@ -31,10 +30,6 @@ pub enum PeachConfigError {
},
#[snafu(display("Error serializing json: {}", source))]
SerdeError { source: serde_json::Error },
#[snafu(display("Error changing password: {}", source))]
ChangePasswordError { source: PeachError },
#[snafu(display("Entered passwords did not match. Please try again."))]
InvalidPassword,
}
impl From<std::io::Error> for PeachConfigError {

View File

@ -1,8 +1,6 @@
mod change_password;
mod constants;
mod error;
mod generate_manifest;
mod set_permissions;
mod setup_networking;
mod setup_peach;
mod setup_peach_deb;
@ -14,6 +12,10 @@ use log::error;
use serde::{Deserialize, Serialize};
use structopt::StructOpt;
use crate::generate_manifest::generate_manifest;
use crate::setup_peach::setup_peach;
use crate::update::update;
#[derive(StructOpt, Debug)]
#[structopt(
name = "peach-config",
@ -42,14 +44,6 @@ enum PeachConfig {
/// Updates all PeachCloud microservices
#[structopt(name = "update")]
Update(UpdateOpts),
/// Changes the password for the peach-web interface
#[structopt(name = "changepassword")]
ChangePassword(ChangePasswordOpts),
/// Updates file permissions on PeachCloud device
#[structopt(name = "permissions")]
SetPermissions,
}
#[derive(StructOpt, Debug)]
@ -82,14 +76,6 @@ pub struct UpdateOpts {
list: bool,
}
#[derive(StructOpt, Debug)]
pub struct ChangePasswordOpts {
/// Optional argument to specify password as CLI argument
/// if not specified, this command asks for user input for the passwords
#[structopt(short, long)]
password: Option<String>,
}
arg_enum! {
/// enum options for real-time clock choices
#[derive(Debug)]
@ -113,48 +99,28 @@ fn main() {
if let Some(subcommand) = opt.commands {
match subcommand {
PeachConfig::Setup(cfg) => {
match setup_peach::setup_peach(cfg.no_input, cfg.default_locale, cfg.i2c, cfg.rtc) {
match setup_peach(cfg.no_input, cfg.default_locale, cfg.i2c, cfg.rtc) {
Ok(_) => {}
Err(err) => {
error!("peach-config encountered an error: {}", err)
}
}
}
PeachConfig::Manifest => match generate_manifest::generate_manifest() {
PeachConfig::Manifest => match generate_manifest() {
Ok(_) => {}
Err(err) => {
error!(
"peach-config encountered an error generating manifest: {}",
"peach-config countered an error generating manifest: {}",
err
)
}
},
PeachConfig::Update(opts) => match update::update(opts) {
PeachConfig::Update(opts) => match update(opts) {
Ok(_) => {}
Err(err) => {
error!("peach-config encountered an error during update: {}", err)
}
},
PeachConfig::ChangePassword(opts) => {
match change_password::set_peach_web_password(opts) {
Ok(_) => {}
Err(err) => {
error!(
"peach-config encountered an error during password update: {}",
err
)
}
}
}
PeachConfig::SetPermissions => match set_permissions::set_permissions() {
Ok(_) => {}
Err(err) => {
error!(
"peach-config ecountered an error updating file permissions: {}",
err
)
}
},
}
}
}

View File

@ -1,21 +0,0 @@
use crate::error::PeachConfigError;
use crate::utils::cmd;
/// All configs are stored in this folder, and should be read/writeable by peach group
/// so they can be read and written by all PeachCloud services.
pub const CONFIGS_DIR: &str = "/var/lib/peachcloud";
pub const PEACH_WEB_DIR: &str = "/usr/share/peach-web";
/// Utility function to set correct file permissions on the PeachCloud device.
/// Accidentally changing file permissions is a fairly common thing to happen,
/// so this is a useful CLI function for quickly correcting anything that may be out of order.
pub fn set_permissions() -> Result<(), PeachConfigError> {
println!("[ UPDATING FILE PERMISSIONS ON PEACHCLOUD DEVICE ]");
cmd(&["chmod", "-R", "u+rwX,g+rwX", CONFIGS_DIR])?;
cmd(&["chown", "-R", "peach", CONFIGS_DIR])?;
cmd(&["chgrp", "-R", "peach", CONFIGS_DIR])?;
cmd(&["chmod", "-R", "u+rwX,g+rwX", PEACH_WEB_DIR])?;
cmd(&["chown", "-R", "peach-web:peach", PEACH_WEB_DIR])?;
println!("[ PERMISSIONS SUCCESSFULLY UPDATED ]");
Ok(())
}

View File

@ -68,7 +68,6 @@ pub fn setup_peach(
"libssl-dev",
"nginx",
"wget",
"dnsutils",
"-y",
])?;

View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,6 +1,6 @@
[package]
name = "peach-dyndns-updater"
version = "0.1.8"
version = "0.1.6"
authors = ["Max Fowler <mfowler@commoninternet.net>"]
edition = "2018"
description = "Sytemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate."

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
# exit when any command fails
set -e
KEYFILE=/Users/notplants/.ssh/id_rsa
SERVICE=peach-dyndns-updater
# deploy
rsync -avzh --exclude target --exclude .idea --exclude .git -e "ssh -i $KEYFILE" . rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/$SERVICE/
rsync -avzh --exclude target --exclude .idea --exclude .git -e "ssh -i $KEYFILE" ~/computer/projects/peachcloud/peach-workspace/peach-lib/ rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/peach-lib/
echo "++ cross compiling on vps"
BIN_PATH=$(ssh -i $KEYFILE rust@167.99.136.83 'cd /srv/peachcloud/automation/peach-workspace/peach-dyndns-updater; /home/rust/.cargo/bin/cargo clean -p peach-lib; /home/rust/.cargo/bin/cargo build --release --target=aarch64-unknown-linux-gnu')
echo "++ copying ${BIN_PATH} to local"
rm -f target/$SERVICE
scp -i $KEYFILE rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/target/aarch64-unknown-linux-gnu/release/peach-dyndns-updater ../target/vps-bin-$SERVICE
#echo "++ cross compiling"
BINFILE="../target/vps-bin-$SERVICE"
echo $BINFILE
echo "++ build successful"
echo "++ copying to pi"
ssh -t -i $KEYFILE peach@peach.link 'mkdir -p /srv/dev/bins'
scp -i $KEYFILE $BINFILE peach@peach.link:/srv/dev/bins/$SERVICE

View File

@ -1,5 +1,6 @@
use log::info;
use peach_lib::dyndns_client::dyndns_update_ip;
use log::{info};
fn main() {
// initalize the logger
@ -8,4 +9,4 @@ fn main() {
info!("Running peach-dyndns-updater");
let result = dyndns_update_ip();
info!("result: {:?}", result);
}
}

View File

@ -1,7 +1,7 @@
[package]
name = "peach-jsonrpc-server"
authors = ["Andrew Reid <glyph@mycelial.technology>"]
version = "0.1.0"
version = "0.2.0"
edition = "2021"
description = "JSON-RPC over HTTP for the PeachCloud system. Provides a JSON-RPC wrapper around the stats, network and oled libraries."
homepage = "https://opencollective.com/peachcloud"
@ -18,8 +18,10 @@ env_logger = "0.9"
jsonrpc-core = "18"
jsonrpc-http-server = "18"
log = "0.4"
peach-network = { path = "../peach-network", features = ["serde_support"] }
peach-stats = { path = "../peach-stats", features = ["serde_support"] }
serde_json = "1.0.74"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dev-dependencies]
jsonrpc-test = "18"

View File

@ -1,16 +1,18 @@
use std::fmt;
use jsonrpc_core::{Error as JsonRpcError, ErrorCode};
use serde_json::error::Error as SerdeJsonError;
use peach_network::NetworkError;
use peach_stats::StatsError;
use serde_json::Error as SerdeError;
/// Custom error type encapsulating all possible errors for a JSON-RPC server
/// and associated methods.
#[derive(Debug)]
pub enum JsonRpcServerError {
/// Failed to serialize a string from a data structure.
Serde(SerdeJsonError),
pub enum ServerError {
/// An error returned from the `peach-network` library.
Network(NetworkError),
/// Failed to serialize a data structure.
Serialize(SerdeError),
/// An error returned from the `peach-stats` library.
Stats(StatsError),
/// An expected JSON-RPC method parameter was not provided.
@ -19,40 +21,48 @@ pub enum JsonRpcServerError {
ParseParameter(JsonRpcError),
}
impl fmt::Display for JsonRpcServerError {
impl fmt::Display for ServerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
JsonRpcServerError::ParseParameter(ref source) => {
ServerError::ParseParameter(ref source) => {
write!(f, "Failed to parse parameter: {}", source)
}
JsonRpcServerError::MissingParameter(ref source) => {
ServerError::MissingParameter(ref source) => {
write!(f, "Missing expected parameter: {}", source)
}
JsonRpcServerError::Serde(ref source) => {
ServerError::Network(ref source) => {
write!(f, "{}", source)
}
JsonRpcServerError::Stats(ref source) => {
ServerError::Serialize(ref source) => {
write!(f, "Serde serialization failure: {}", source)
}
ServerError::Stats(ref source) => {
write!(f, "{}", source)
}
}
}
}
impl From<JsonRpcServerError> for JsonRpcError {
fn from(err: JsonRpcServerError) -> Self {
impl From<ServerError> for JsonRpcError {
fn from(err: ServerError) -> Self {
match &err {
JsonRpcServerError::Serde(source) => JsonRpcError {
code: ErrorCode::ServerError(-32002),
message: format!("{}", source),
data: None,
},
JsonRpcServerError::Stats(source) => JsonRpcError {
ServerError::Network(source) => JsonRpcError {
code: ErrorCode::ServerError(-32001),
message: format!("{}", source),
data: None,
},
JsonRpcServerError::MissingParameter(source) => source.clone(),
JsonRpcServerError::ParseParameter(source) => source.clone(),
ServerError::Stats(source) => JsonRpcError {
code: ErrorCode::ServerError(-32003),
message: format!("{}", source),
data: None,
},
ServerError::Serialize(source) => JsonRpcError {
code: ErrorCode::ServerError(-32000),
message: format!("{}", source),
data: None,
},
ServerError::MissingParameter(source) => source.clone(),
ServerError::ParseParameter(source) => source.clone(),
}
}
}

View File

@ -5,71 +5,332 @@
use std::env;
use std::result::Result;
use jsonrpc_core::{IoHandler, Value};
use jsonrpc_core::{Error as RpcCoreError, IoHandler, Params, Value};
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
use log::info;
use peach_network::network;
use peach_stats::stats;
mod error;
use crate::error::JsonRpcServerError;
mod params;
use crate::error::ServerError;
use crate::params::{Iface, IfaceId, IfaceIdPass, IfaceSsid, WiFi, WlanAndAp};
/// Create JSON-RPC I/O handler, add RPC methods and launch HTTP server.
pub fn run() -> Result<(), JsonRpcServerError> {
pub fn run() -> Result<(), ServerError> {
info!("Starting up.");
info!("Creating JSON-RPC I/O handler.");
let mut io = IoHandler::default();
io.add_sync_method("ping", |_| Ok(Value::String("success".to_string())));
io.add_method("ping", |_| async {
Ok(Value::String("success".to_string()))
});
// TODO: add blocks of methods according to provided flags
/* PEACH-NETWORK RPC METHODS */
// get - all network rpc methods for querying state
io.add_method("available_networks", |params: Params| async move {
let parsed: Result<Iface, RpcCoreError> = params.parse();
match parsed {
Ok(i) => {
match network::available_networks(&i.iface).map_err(ServerError::Network)? {
Some(list) => {
let json_list =
serde_json::to_string(&list).map_err(ServerError::Serialize)?;
Ok(Value::String(json_list))
}
// return `Null` if no networks were found
None => Ok(Value::Null),
}
}
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("id", |params: Params| async move {
let parsed: Result<IfaceSsid, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::id(&i.iface, &i.ssid).map_err(ServerError::Network)? {
Some(id) => Ok(Value::String(id)),
None => Ok(Value::Null),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("ip", |params: Params| async move {
let parsed: Result<Iface, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::ip(&i.iface).map_err(ServerError::Network)? {
Some(ip) => Ok(Value::String(ip)),
None => Ok(Value::Null),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("rssi", |params: Params| async move {
let parsed: Result<Iface, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::rssi(&i.iface).map_err(ServerError::Network)? {
Some(rssi) => Ok(Value::String(rssi)),
None => Ok(Value::Null),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("rssi_percent", |params: Params| async move {
let parsed: Result<Iface, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::rssi_percent(&i.iface).map_err(ServerError::Network)? {
Some(rssi) => Ok(Value::String(rssi)),
None => Ok(Value::Null),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("saved_networks", |_| async {
let list = network::saved_networks().map_err(ServerError::Network)?;
match list {
Some(list) => {
let json_list = serde_json::to_string(&list).map_err(ServerError::Serialize)?;
Ok(Value::String(json_list))
}
None => Ok(Value::Null),
}
});
io.add_method("ssid", |params: Params| async move {
let parsed: Result<Iface, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::ssid(&i.iface).map_err(ServerError::Network)? {
Some(ip) => Ok(Value::String(ip)),
None => Ok(Value::Null),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("state", |params: Params| async move {
let parsed: Result<Iface, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::state(&i.iface).map_err(ServerError::Network)? {
Some(state) => Ok(Value::String(state)),
None => Ok(Value::Null),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("status", |params: Params| async move {
let parsed: Result<Iface, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::status(&i.iface).map_err(ServerError::Network)? {
Some(status) => {
let json_status =
serde_json::to_string(&status).map_err(ServerError::Serialize)?;
Ok(Value::String(json_status))
}
None => Ok(Value::Null),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("traffic", |params: Params| async move {
let parsed: Result<Iface, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::traffic(&i.iface).map_err(ServerError::Network)? {
Some(traffic) => {
let json_traffic =
serde_json::to_string(&traffic).map_err(ServerError::Serialize)?;
Ok(Value::String(json_traffic))
}
None => Ok(Value::Null),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
// set - all network rpc methods for modifying state
io.add_method("add", |params: Params| async move {
let parsed: Result<WiFi, RpcCoreError> = params.parse();
match parsed {
Ok(w) => match network::add(&w.iface, &w.ssid, &w.pass) {
Ok(_) => Ok(Value::String("success".to_string())),
Err(e) => Err(RpcCoreError::from(ServerError::Network(e))),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("check_iface", |params: Params| async move {
let parsed: Result<WlanAndAp, RpcCoreError> = params.parse();
match parsed {
Ok(w) => match network::check_iface(&w.wlan_iface, &w.ap_iface) {
Ok(_) => Ok(Value::String("success".to_string())),
Err(e) => Err(RpcCoreError::from(ServerError::Network(e))),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("connect", |params: Params| async move {
let parsed: Result<IfaceId, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::connect(&i.id, &i.iface) {
Ok(_) => Ok(Value::String("success".to_string())),
Err(e) => Err(RpcCoreError::from(ServerError::Network(e))),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("delete", |params: Params| async move {
let parsed: Result<IfaceId, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::delete(&i.id, &i.iface) {
Ok(_) => Ok(Value::String("success".to_string())),
Err(e) => Err(RpcCoreError::from(ServerError::Network(e))),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("disable", |params: Params| async move {
let parsed: Result<IfaceId, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::disable(&i.id, &i.iface) {
Ok(_) => Ok(Value::String("success".to_string())),
Err(e) => Err(RpcCoreError::from(ServerError::Network(e))),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("disconnect", |params: Params| async move {
let parsed: Result<Iface, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::disconnect(&i.iface) {
Ok(_) => Ok(Value::String("success".to_string())),
Err(e) => Err(RpcCoreError::from(ServerError::Network(e))),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("modify", |params: Params| async move {
let parsed: Result<IfaceIdPass, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::modify(&i.iface, &i.id, &i.pass) {
Ok(_) => Ok(Value::String("success".to_string())),
Err(e) => Err(RpcCoreError::from(ServerError::Network(e))),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("reassociate", |params: Params| async move {
let parsed: Result<Iface, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::reassociate(&i.iface) {
Ok(_) => Ok(Value::String("success".to_string())),
Err(e) => Err(RpcCoreError::from(ServerError::Network(e))),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("reconfigure", |_| async {
match network::reconfigure() {
Ok(_) => Ok(Value::String("success".to_string())),
Err(e) => Err(RpcCoreError::from(ServerError::Network(e))),
}
});
io.add_method("reconnect", |params: Params| async move {
let parsed: Result<Iface, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::reconnect(&i.iface) {
Ok(_) => Ok(Value::String("success".to_string())),
Err(e) => Err(RpcCoreError::from(ServerError::Network(e))),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
io.add_method("save", |_| async {
match network::save() {
Ok(_) => Ok(Value::String("success".to_string())),
Err(e) => Err(RpcCoreError::from(ServerError::Network(e))),
}
});
io.add_method("start_iface_service", |params: Params| async move {
let parsed: Result<Iface, RpcCoreError> = params.parse();
match parsed {
Ok(i) => match network::start_iface_service(&i.iface) {
Ok(_) => Ok(Value::String("success".to_string())),
Err(e) => Err(RpcCoreError::from(ServerError::Network(e))),
},
Err(e) => Err(RpcCoreError::from(ServerError::MissingParameter(e))),
}
});
/* PEACH-STATS RPC METHODS */
io.add_sync_method("cpu_stats", move |_| {
io.add_method("cpu_stats", |_| async {
info!("Fetching CPU statistics.");
let cpu = stats::cpu_stats().map_err(JsonRpcServerError::Stats)?;
let json_cpu = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?;
let cpu = stats::cpu_stats().map_err(ServerError::Stats)?;
let json_cpu = serde_json::to_string(&cpu).map_err(ServerError::Serialize)?;
Ok(Value::String(json_cpu))
});
io.add_sync_method("cpu_stats_percent", move |_| {
io.add_method("cpu_stats_percent", |_| async {
info!("Fetching CPU statistics as percentages.");
let cpu = stats::cpu_stats_percent().map_err(JsonRpcServerError::Stats)?;
let json_cpu = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?;
let cpu = stats::cpu_stats_percent().map_err(ServerError::Stats)?;
let json_cpu = serde_json::to_string(&cpu).map_err(ServerError::Serialize)?;
Ok(Value::String(json_cpu))
});
io.add_sync_method("disk_usage", move |_| {
io.add_method("disk_usage", |_| async {
info!("Fetching disk usage statistics.");
let disks = stats::disk_usage().map_err(JsonRpcServerError::Stats)?;
let json_disks = serde_json::to_string(&disks).map_err(JsonRpcServerError::Serde)?;
let disks = stats::disk_usage().map_err(ServerError::Stats)?;
let json_disks = serde_json::to_string(&disks).map_err(ServerError::Serialize)?;
Ok(Value::String(json_disks))
});
io.add_sync_method("load_average", move |_| {
io.add_method("load_average", |_| async {
info!("Fetching system load average statistics.");
let avg = stats::load_average().map_err(JsonRpcServerError::Stats)?;
let json_avg = serde_json::to_string(&avg).map_err(JsonRpcServerError::Serde)?;
let avg = stats::load_average().map_err(ServerError::Stats)?;
let json_avg = serde_json::to_string(&avg).map_err(ServerError::Serialize)?;
Ok(Value::String(json_avg))
});
io.add_sync_method("mem_stats", move |_| {
io.add_method("mem_stats", |_| async {
info!("Fetching current memory statistics.");
let mem = stats::mem_stats().map_err(JsonRpcServerError::Stats)?;
let json_mem = serde_json::to_string(&mem).map_err(JsonRpcServerError::Serde)?;
let mem = stats::mem_stats().map_err(ServerError::Stats)?;
let json_mem = serde_json::to_string(&mem).map_err(ServerError::Serialize)?;
Ok(Value::String(json_mem))
});
io.add_sync_method("uptime", move |_| {
io.add_method("uptime", |_| async {
info!("Fetching system uptime.");
let uptime = stats::uptime().map_err(JsonRpcServerError::Stats)?;
let json_uptime = serde_json::to_string(&uptime).map_err(JsonRpcServerError::Serde)?;
let uptime = stats::uptime().map_err(ServerError::Stats)?;
let json_uptime = serde_json::to_string(&uptime).map_err(ServerError::Serialize)?;
Ok(Value::String(json_uptime))
});
@ -105,7 +366,7 @@ mod tests {
fn rpc_success() {
let rpc = {
let mut io = IoHandler::new();
io.add_sync_method("rpc_success_response", |_| {
io.add_method("rpc_success_response", |_| async {
Ok(Value::String("success".into()))
});
test_rpc::Rpc::from(io)
@ -118,13 +379,13 @@ mod tests {
fn rpc_parse_error() {
let rpc = {
let mut io = IoHandler::new();
io.add_sync_method("rpc_parse_error", |_| {
io.add_method("rpc_parse_error", |_| async {
let e = JsonRpcError {
code: ErrorCode::ParseError,
message: String::from("Parse error"),
data: None,
};
Err(JsonRpcError::from(JsonRpcServerError::MissingParameter(e)))
Err(JsonRpcError::from(ServerError::MissingParameter(e)))
});
test_rpc::Rpc::from(io)
};

View File

@ -10,12 +10,51 @@
//!
//! | Method | Description | Returns |
//! | --- | --- | --- |
//! | `ping` | Microservice status | `success` if running |
//!
//! ### Network
//!
//! Methods for **retrieving data**:
//!
//! | Method | Description | Returns |
//! | --- | --- | --- |
//! | `available_networks` | `iface` | List SSID, flags (security), frequency and signal level for all networks in range of given interface |
//! | `id` | `iface`, `ssid` | Return ID of given SSID |
//! | `ip` | `iface` | Return IP of given network interface |
//! | `rssi` | `iface` | Return average signal strength (dBm) for given interface |
//! | `rssi_percent` | `iface` | Return average signal strength (%) for given interface |
//! | `saved_networks` | | List all networks saved in wpasupplicant config |
//! | `ssid` | `iface` | Return SSID of currently-connected network for given interface |
//! | `state` | `iface` | Return state of given interface |
//! | `status` | `iface` | Return status parameters for given interface |
//! | `traffic` | `iface` | Return network traffic for given interface |
//!
//! Methods for **modifying state**:
//!
//! | Method | Parameters | Description |
//! | --- | --- | --- |
//! | `add` | `iface`, `ssid`, `pass` | Add WiFi credentials to `wpa_supplicant-<iface>.conf` |
//! | `check_iface` | `wlan_iface`, `ap_iface` | Activate WiFi access point on <ap_iface> if <wlan_iface> is active without a connection |
//! | `connect` | `id`, `iface` | Disable other networks and attempt connection with AP represented by given id |
//! | `delete` | `id`, `iface` | Remove WiFi credentials for given network id and interface |
//! | `disable` | `id`, `iface` | Disable connection with AP represented by given id |
//! | `disconnect` | `iface` | Disconnect given interface |
//! | `modify` | `id`, `iface`, `pass` | Set a new password for given network id and interface |
//! | `reassociate` | `iface` | Reassociate with current AP for given interface |
//! | `reconfigure` | | Force wpa_supplicant to re-read its configuration file |
//! | `reconnect` | `iface` | Disconnect and reconnect given interface |
//! | `save` | | Save configuration changes to `wpa_supplicant-wlan0.conf` |
//! | `start_iface_server` | `iface` | Start the `systemd` service for the given interface |
//!
//! ### System Statistics
//!
//! | Method | Description | Returns |
//! | --- | --- | --- |
//! | `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` |
//! | `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` |
//! | `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` |
//! | `load_average` | Load average statistics | `one`, `five`, `fifteen` |
//! | `mem_stats` | Memory statistics | `total`, `free`, `used` |
//! | `ping` | Microservice status | `success` if running |
//! | `uptime` | System uptime | `secs` |
use std::process;

View File

@ -0,0 +1,54 @@
//! Data structures for parsing JSON-RPC method parameters.
use serde::Deserialize;
// why do we have multiple structs when we could rather combine them by using
// `Option<String>` for some fields? simply because it's easier to handle the
// parsed parameters in our json-rpc server methods. we don't have to check
// if a given field is `Some` or `None` before passing it into the relevant
// function.
/// Network interface name.
#[derive(Debug, Deserialize)]
pub struct Iface {
pub iface: String,
}
/// Network interface name and network identifier.
#[derive(Debug, Deserialize)]
pub struct IfaceId {
pub iface: String,
pub id: String,
}
/// Network interface name, network identifier and password.
#[derive(Debug, Deserialize)]
pub struct IfaceIdPass {
pub iface: String,
pub id: String,
pub pass: String,
}
/// Network interface name and network SSID.
#[derive(Debug, Deserialize)]
pub struct IfaceSsid {
pub iface: String,
pub ssid: String,
}
/// Wireless interface (for which the WiFi credentials will be added), SSID
/// and password for a wireless access point.
#[derive(Debug, Deserialize)]
pub struct WiFi {
pub iface: String,
pub ssid: String,
pub pass: String,
}
/// Wireles network interface and local Access Point network interface (the
/// interface on which we deloy or own AP).
#[derive(Debug, Deserialize)]
pub struct WlanAndAp {
pub wlan_iface: String,
pub ap_iface: String,
}

View File

@ -1,6 +1,6 @@
[package]
name = "peach-lib"
version = "1.3.2"
version = "1.3.1"
authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2018"
@ -13,7 +13,7 @@ jsonrpc-core = "8.0.1"
log = "0.4"
nanorand = "0.6.1"
regex = "1"
rust-crypto = "0.2.36"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.8"
sha3 = "0.10.0"

View File

@ -17,10 +17,6 @@ pub const YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
// lock file (used to avoid race conditions during config reading & writing)
pub const LOCK_FILE_PATH: &str = "/var/lib/peachcloud/config.lock";
// default values
pub const DEFAULT_DYN_SERVER_ADDRESS: &str = "http://dynserver.dyn.peachcloud.org";
pub const DEFAULT_DYN_NAMESERVER: &str = "ns.peachcloud.org";
// we make use of Serde default values in order to make PeachCloud
// robust and keep running even with a not fully complete config.yml
// main type which represents all peachcloud configurations
@ -33,10 +29,6 @@ pub struct PeachConfig {
#[serde(default)]
pub dyn_dns_server_address: String,
#[serde(default)]
pub dyn_use_custom_server: bool,
#[serde(default)]
pub dyn_nameserver: String,
#[serde(default)]
pub dyn_tsig_key_path: String,
#[serde(default)] // default is false
pub dyn_enabled: bool,
@ -71,19 +63,20 @@ fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachErro
pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
let peach_config_exists = std::path::Path::new(YAML_PATH).exists();
let peach_config: PeachConfig = if !peach_config_exists {
PeachConfig {
let peach_config: PeachConfig;
// if this is the first time loading peach_config, we can create a default here
if !peach_config_exists {
peach_config = PeachConfig {
external_domain: "".to_string(),
dyn_domain: "".to_string(),
dyn_dns_server_address: DEFAULT_DYN_SERVER_ADDRESS.to_string(),
dyn_use_custom_server: false,
dyn_nameserver: DEFAULT_DYN_NAMESERVER.to_string(),
dyn_dns_server_address: "".to_string(),
dyn_tsig_key_path: "".to_string(),
dyn_enabled: false,
ssb_admin_ids: Vec::new(),
admin_password_hash: "".to_string(),
temporary_password_hash: "".to_string(),
}
};
}
// otherwise we load peach config from disk
else {
@ -91,8 +84,8 @@ pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
source,
path: YAML_PATH.to_string(),
})?;
serde_yaml::from_str(&contents)?
};
peach_config = serde_yaml::from_str(&contents)?;
}
Ok(peach_config)
}
@ -129,18 +122,6 @@ pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> {
}
}
pub fn get_dyndns_server_address() -> Result<String, PeachError> {
let peach_config = load_peach_config()?;
// if the user is using a custom dyn server then load the address from the config
if peach_config.dyn_use_custom_server {
Ok(peach_config.dyn_dns_server_address)
}
// otherwise hardcode the address
else {
Ok(DEFAULT_DYN_SERVER_ADDRESS.to_string())
}
}
pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config()?;
peach_config.dyn_enabled = enabled_value;

View File

@ -9,8 +9,13 @@
//!
//! The domain for dyndns updates is stored in /var/lib/peachcloud/config.yml
//! The tsig key for authenticating the updates is stored in /var/lib/peachcloud/peach-dyndns/tsig.key
use std::ffi::OsStr;
use std::{fs, fs::OpenOptions, io::Write, process::Command, str::FromStr};
use std::{
fs,
fs::OpenOptions,
io::Write,
process::{Command, Stdio},
str::FromStr,
};
use chrono::prelude::*;
use jsonrpc_client_core::{expand_params, jsonrpc_client};
@ -18,10 +23,13 @@ use jsonrpc_client_http::HttpTransport;
use log::{debug, info};
use regex::Regex;
use crate::config_manager::get_dyndns_server_address;
use crate::{config_manager, error::PeachError};
use crate::{
config_manager::{load_peach_config, set_peach_dyndns_config},
error::PeachError,
};
/// constants for dyndns configuration
pub const PEACH_DYNDNS_URL: &str = "http://dynserver.dyn.peachcloud.org";
pub const TSIG_KEY_PATH: &str = "/var/lib/peachcloud/peach-dyndns/tsig.key";
pub const PEACH_DYNDNS_CONFIG_PATH: &str = "/var/lib/peachcloud/peach-dyndns";
pub const DYNDNS_LOG_PATH: &str = "/var/lib/peachcloud/peach-dyndns/latest_result.log";
@ -54,10 +62,9 @@ pub fn save_dyndns_key(key: &str) -> Result<(), PeachError> {
pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for dyndns client.");
let transport = HttpTransport::new().standalone()?;
let http_server = get_dyndns_server_address()?;
info!("Using dyndns http server address: {:?}", http_server);
debug!("Creating HTTP transport handle on {}.", &http_server);
let transport_handle = transport.handle(&http_server)?;
let http_server = PEACH_DYNDNS_URL;
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(http_server)?;
info!("Creating client for peach-dyndns service.");
let mut client = PeachDynDnsClient::new(transport_handle);
@ -66,8 +73,7 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError>
// save new TSIG key
save_dyndns_key(&key)?;
// save new configuration values
let set_config_result =
config_manager::set_peach_dyndns_config(domain, &http_server, TSIG_KEY_PATH, true);
let set_config_result = set_peach_dyndns_config(domain, PEACH_DYNDNS_URL, TSIG_KEY_PATH, true);
match set_config_result {
Ok(_) => {
let response = "success".to_string();
@ -81,9 +87,9 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError>
pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError> {
debug!("Creating HTTP transport for dyndns client.");
let transport = HttpTransport::new().standalone()?;
let http_server = get_dyndns_server_address()?;
debug!("Creating HTTP transport handle on {}.", &http_server);
let transport_handle = transport.handle(&http_server)?;
let http_server = PEACH_DYNDNS_URL;
debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(http_server)?;
info!("Creating client for peach_network service.");
let mut client = PeachDynDnsClient::new(transport_handle);
@ -107,31 +113,31 @@ fn get_public_ip_address() -> Result<String, PeachError> {
/// Reads dyndns configurations from config.yml
/// and then uses nsupdate to update the IP address for the configured domain
pub fn dyndns_update_ip() -> Result<bool, PeachError> {
let peach_config = config_manager::load_peach_config()?;
info!("Running dyndns_update_ip");
let peach_config = load_peach_config()?;
info!(
"Using config:
dyn_tsig_key_path: {:?}
dyn_domain: {:?}
dyn_dns_server_address: {:?}
dyn_enabled: {:?}
dyn_nameserver: {:?}
",
peach_config.dyn_tsig_key_path,
peach_config.dyn_domain,
peach_config.dyn_dns_server_address,
peach_config.dyn_enabled,
peach_config.dyn_nameserver,
);
if !peach_config.dyn_enabled {
info!("dyndns is not enabled, not updating");
Ok(false)
} else {
// call nsupdate passing appropriate configs
let mut nsupdate_command = Command::new("/usr/bin/nsupdate");
nsupdate_command
let mut nsupdate_command = Command::new("/usr/bin/nsupdate")
.arg("-k")
.arg(&peach_config.dyn_tsig_key_path)
.arg("-v");
.arg("-v")
.stdin(Stdio::piped())
.spawn()?;
// pass nsupdate commands via stdin
let public_ip_address = get_public_ip_address()?;
info!("found public ip address: {}", public_ip_address);
@ -142,20 +148,20 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
update delete {DOMAIN} A
update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS}
send",
NAMESERVER = peach_config.dyn_nameserver,
NAMESERVER = "ns.peachcloud.org",
ZONE = peach_config.dyn_domain,
DOMAIN = peach_config.dyn_domain,
PUBLIC_IP_ADDRESS = public_ip_address,
);
info!("ns_commands: {:?}", ns_commands);
info!("creating nsupdate temp file");
let temp_file_path = "/var/lib/peachcloud/nsupdate.sh";
// write ns_commands to temp_file
fs::write(temp_file_path, ns_commands)?;
nsupdate_command.arg(temp_file_path);
let nsupdate_output = nsupdate_command.output()?;
let args: Vec<&OsStr> = nsupdate_command.get_args().collect();
info!("nsupdate command: {:?}", args);
let mut nsupdate_stdin = nsupdate_command.stdin.take().ok_or(PeachError::NsUpdate {
msg: "unable to capture stdin handle for `nsupdate` command".to_string(),
})?;
write!(nsupdate_stdin, "{}", ns_commands).map_err(|source| PeachError::Write {
source,
path: peach_config.dyn_tsig_key_path.to_string(),
})?;
let nsupdate_output = nsupdate_command.wait_with_output()?;
info!("nsupdate output: {:?}", nsupdate_output);
// We only return a successful result if nsupdate was successful
if nsupdate_output.status.success() {
info!("nsupdate succeeded, returning ok");
@ -198,7 +204,7 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
})?;
// replace newline if found
// TODO: maybe we can use `.trim()` instead
let contents = contents.replace('\n', "");
let contents = contents.replace("\n", "");
// TODO: consider adding additional context?
let time_ran_dt = DateTime::parse_from_rfc3339(&contents).map_err(|source| {
PeachError::ParseDateTime {
@ -217,15 +223,20 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
/// and has successfully run recently (in the last six minutes)
pub fn is_dns_updater_online() -> Result<bool, PeachError> {
// first check if it is enabled in peach-config
let peach_config = config_manager::load_peach_config()?;
let peach_config = load_peach_config()?;
let is_enabled = peach_config.dyn_enabled;
// then check if it has successfully run within the last 6 minutes (60*6 seconds)
let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?;
let ran_recently: bool = match num_seconds_since_successful_update {
Some(seconds) => seconds < (60 * 6),
let ran_recently: bool;
match num_seconds_since_successful_update {
Some(seconds) => {
ran_recently = seconds < (60 * 6);
}
// if the value is None, then the last time it ran successfully is unknown
None => false,
};
None => {
ran_recently = false;
}
}
// debug log
info!("is_dyndns_enabled: {:?}", is_enabled);
info!("dyndns_ran_recently: {:?}", ran_recently);
@ -247,10 +258,11 @@ pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option<String> {
}
// helper function which checks if a dyndns domain is new
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> Result<bool, PeachError> {
let peach_config = config_manager::load_peach_config()?;
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> bool {
// TODO: return `Result<bool, PeachError>` and replace `unwrap` with `?` operator
let peach_config = load_peach_config().unwrap();
let previous_dyndns_domain = peach_config.dyn_domain;
Ok(dyndns_full_domain != previous_dyndns_domain)
dyndns_full_domain != previous_dyndns_domain
}
jsonrpc_client!(pub struct PeachDynDnsClient {

View File

@ -1,5 +1,5 @@
use crypto::{digest::Digest, sha3::Sha3};
use nanorand::{Rng, WyRand};
use sha3::{Digest, Sha3_256};
use crate::{config_manager, error::PeachError, sbot_client};
@ -7,7 +7,7 @@ use crate::{config_manager, error::PeachError, sbot_client};
/// and returns Err if the supplied password is incorrect.
pub fn verify_password(password: &str) -> Result<(), PeachError> {
let real_admin_password_hash = config_manager::get_admin_password_hash()?;
let password_hash = hash_password(password);
let password_hash = hash_password(&password.to_string());
if real_admin_password_hash == password_hash {
Ok(())
} else {
@ -29,7 +29,7 @@ pub fn validate_new_passwords(new_password1: &str, new_password2: &str) -> Resul
/// Sets a new password for the admin user
pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
let new_password_hash = hash_password(new_password);
let new_password_hash = hash_password(&new_password.to_string());
config_manager::set_admin_password_hash(&new_password_hash)?;
Ok(())
@ -37,19 +37,15 @@ pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
/// Creates a hash from a password string
pub fn hash_password(password: &str) -> String {
let mut hasher = Sha3_256::new();
// write input message
hasher.update(password);
// read hash digest
let result = hasher.finalize();
// convert `u8` to `String`
result[0].to_string()
let mut hasher = Sha3::sha3_256();
hasher.input_str(password);
hasher.result_str()
}
/// Sets a new temporary password for the admin user
/// which can be used to reset the permanent password
pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> {
let new_password_hash = hash_password(new_password);
let new_password_hash = hash_password(&new_password.to_string());
config_manager::set_temporary_password_hash(&new_password_hash)?;
Ok(())
@ -59,7 +55,7 @@ pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError>
/// and returns Err if the supplied temp_password is incorrect
pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> {
let temporary_admin_password_hash = config_manager::get_temporary_password_hash()?;
let password_hash = hash_password(password);
let password_hash = hash_password(&password.to_string());
if temporary_admin_password_hash == password_hash {
Ok(())
} else {

View File

@ -67,7 +67,7 @@ pub fn create_invite(uses: i32) -> Result<String, PeachError> {
.arg(uses.to_string())
.output()?;
let text_output = std::str::from_utf8(&output.stdout)?;
let output = text_output.replace('\n', "");
let output = text_output.replace("\n", "");
Ok(output)
}

4
peach-menu/.cargo/config Normal file
View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,6 +1,6 @@
[package]
name = "peach-network"
version = "0.4.2"
version = "0.4.1"
authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2021"
description = "Query and configure network interfaces."

View File

@ -1,6 +1,6 @@
# peach-network
![Generic badge](https://img.shields.io/badge/version-0.4.2-<COLOR>.svg)
![Generic badge](https://img.shields.io/badge/version-0.4.0-<COLOR>.svg)
Network interface state query and modification library.

View File

@ -148,7 +148,7 @@ pub enum NetworkError {
/// Failed to retrieve connection state of wlan0 interface.
WlanOperstate(IoError),
/// Failed to save wpa_supplicant configuration changes to file.
Save(IoError),
Save,
/// Failed to connect to network.
Connect {
/// ID.
@ -197,7 +197,7 @@ impl std::error::Error for NetworkError {
NetworkError::Delete { .. } => None,
NetworkError::WlanState(ref source) => Some(source),
NetworkError::WlanOperstate(ref source) => Some(source),
NetworkError::Save(ref source) => Some(source),
NetworkError::Save => None,
NetworkError::Connect { .. } => None,
NetworkError::StartInterface { ref source, .. } => Some(source),
NetworkError::WpaCtrl(ref source) => Some(source),
@ -326,11 +326,7 @@ impl std::fmt::Display for NetworkError {
NetworkError::WlanOperstate(_) => {
write!(f, "Failed to retrieve connection state of wlan0 interface")
}
NetworkError::Save(ref source) => write!(
f,
"Failed to save configuration changes to file: {}",
source
),
NetworkError::Save => write!(f, "Failed to save configuration changes to file"),
NetworkError::Connect { ref id, ref iface } => {
write!(
f,

View File

@ -138,7 +138,7 @@ pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError
// we only want to return the auth / crypto flags
if flags_vec[0] != "[ESS]" {
// parse auth / crypto flag and assign it to protocol
protocol.push_str(flags_vec[0].replace('[', "").replace(']', "").as_str());
protocol.push_str(flags_vec[0].replace("[", "").replace("]", "").as_str());
}
let ssid = v[4].to_string();
let response = Scan {
@ -513,14 +513,16 @@ pub fn add(wlan_iface: &str, ssid: &str, pass: &str) -> Result<(), NetworkError>
// append wpa_passphrase output to wpa_supplicant-<wlan_iface>.conf if successful
if output.status.success() {
// open file in append mode
let mut file = OpenOptions::new()
.append(true)
.open(wlan_config)
// TODO: create the file if it doesn't exist
.map_err(NetworkError::Save)?;
file.write(&wpa_details).map_err(NetworkError::Save)?;
let file = OpenOptions::new().append(true).open(wlan_config);
let _file = match file {
// if file exists & open succeeds, write wifi configuration
Ok(mut f) => f.write(&wpa_details),
// TODO: handle this better: create file if not found
// & seed with 'ctrl_interace' & 'update_config' settings
// config file could also be copied from peach/config fs location
Err(e) => panic!("Failed to write to file: {}", e),
};
Ok(())
} else {
let err_msg = String::from_utf8_lossy(&output.stdout);
@ -640,38 +642,6 @@ pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
Ok(())
}
/// Forget credentials for the given network SSID and interface.
/// Look up the network identified for the given SSID, delete the credentials
/// and then save.
///
/// # Arguments
///
/// * `iface` - A string slice holding the name of a wireless network interface
/// * `ssid` - A string slice holding the SSID for a wireless access point
///
/// If the credentials are successfully deleted and saved, an `Ok` `Result`
/// type is returned. In the event of an error, a `NetworkError` is returned
/// in the `Result`.
pub fn forget(iface: &str, ssid: &str) -> Result<(), NetworkError> {
// get the id of the network
let id_opt = id(iface, ssid)?;
let id = id_opt.ok_or(NetworkError::Id {
ssid: ssid.to_string(),
iface: iface.to_string(),
})?;
// delete the old credentials
// TODO: i've switched these back to the "correct" order
// WEIRD BUG: the parameters below are technically in the wrong order:
// it should be id first and then iface, but somehow they get twisted.
// i don't understand computers.
//delete(&iface, &id)?;
delete(&id, iface)?;
// save the updates to wpa_supplicant.conf
save()?;
Ok(())
}
/// Modify password for a given network identifier and interface.
///
/// # Arguments
@ -738,7 +708,7 @@ pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
/// Save configuration updates to the `wpa_supplicant` configuration file.
///
/// If wireless network configuration updates are successfully saved to the
/// If wireless network configuration updates are successfully save to the
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
/// event of an error, a `NetworkError` is returned in the `Result`.
pub fn save() -> Result<(), NetworkError> {
@ -746,18 +716,3 @@ pub fn save() -> Result<(), NetworkError> {
wpa.request("SAVE_CONFIG")?;
Ok(())
}
/// Update password for an access point and save configuration updates to the
/// `wpa_supplicant` configuration file.
///
/// If wireless network configuration updates are successfully saved to the
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
/// event of an error, a `NetworkError` is returned in the `Result`.
pub fn update(iface: &str, ssid: &str, pass: &str) -> Result<(), NetworkError> {
// delete the old credentials and save the changes
forget(iface, ssid)?;
// add the new credentials
add(iface, ssid, pass)?;
reconfigure()?;
Ok(())
}

4
peach-oled/.cargo/config Normal file
View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

4
peach-web/.cargo/config Normal file
View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -1,6 +1,6 @@
[package]
name = "peach-web"
version = "0.5.0"
version = "0.4.12"
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
edition = "2018"
description = "peach-web is a web application which provides a web interface for monitoring and interacting with the PeachCloud device. This allows administration of the single-board computer (ie. Raspberry Pi) running PeachCloud, as well as the ssb-server and related plugins."
@ -21,12 +21,12 @@ maintainer-scripts="debian"
systemd-units = { unit-name = "peach-web" }
assets = [
["target/release/peach-web", "/usr/bin/", "755"],
["Rocket.toml", "/usr/share/peach-web/Rocket.toml", "644"],
["templates/**/*", "/usr/share/peach-web/templates/", "644"],
["static/*", "/usr/share/peach-web/static/", "644"],
["static/css/*", "/usr/share/peach-web/static/css/", "644"],
["static/icons/*", "/usr/share/peach-web/static/icons/", "644"],
["static/images/*", "/usr/share/peach-web/static/images/", "644"],
["static/js/*", "/usr/share/peach-web/static/js/", "644"],
["README.md", "/usr/share/doc/peach-web/README", "644"],
]
@ -38,12 +38,14 @@ maintenance = { status = "actively-developed" }
env_logger = "0.8"
log = "0.4"
nest = "1.0.0"
openssl = { version = "0.10", features = ["vendored"] }
peach-lib = { path = "../peach-lib" }
peach-network = { path = "../peach-network", features = ["serde_support"] }
peach-stats = { path = "../peach-stats", features = ["serde_support"] }
percent-encoding = "2.1.0"
regex = "1"
rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
snafu = "0.6"
tera = { version = "1.12.1", features = ["builtins"] }
xdg = "2.2.0"

View File

@ -1,14 +1,14 @@
# peach-web
[![Build Status](https://travis-ci.com/peachcloud/peach-web.svg?branch=master)](https://travis-ci.com/peachcloud/peach-web) ![Generic badge](https://img.shields.io/badge/version-0.5.0-<COLOR>.svg)
[![Build Status](https://travis-ci.com/peachcloud/peach-web.svg?branch=master)](https://travis-ci.com/peachcloud/peach-web) ![Generic badge](https://img.shields.io/badge/version-0.4.12-<COLOR>.svg)
## Web Interface for PeachCloud
**peach-web** provides a web interface for the PeachCloud device.
**peach-web** provides a web interface for the PeachCloud device. It serves static assets and exposes a JSON API for programmatic interactions.
Initial development is focused on administration of the device itself, beginning with networking functionality, with SSB-related administration to be integrated at a later stage.
The peach-web stack currently consists of [Rocket](https://rocket.rs/) (Rust web framework), [Tera](http://tera.netlify.com/) (Rust template engine), HTML and CSS.
The peach-web stack currently consists of [Rocket](https://rocket.rs/) (Rust web framework), [Tera](http://tera.netlify.com/) (Rust template engine), HTML, CSS and JavaScript.
_Note: This is a work-in-progress._
@ -25,7 +25,7 @@ Move into the repo and compile:
Run the tests:
`ROCKET_DISABLE_AUTH=true ROCKET_STANDALONE_MODE=false cargo test`
`cargo test`
Move back to the `peach-workspace` directory:
@ -35,25 +35,21 @@ Run the binary:
`./target/release/peach-web`
_Note: Networking functionality requires peach-network microservice to be running._
### Environment
**Deployment Profile**
**Deployment Mode**
The web application deployment profile can be configured with the `ROCKET_ENV` environment variable:
The web application deployment mode is configured with the `ROCKET_ENV` environment variable:
`export ROCKET_ENV=stage`
Default configuration parameters are defined in `Rocket.toml`. This file defines a set of default parameters, some of which are overwritten when running in `debug` mode (ie. `cargo run` or `cargo build`) or `release` mode (ie. `cargo run --release` or `cargo build --release`).
Read the [Rocket Environment Configurations docs](https://rocket.rs/v0.5-rc/guide/configuration/#environment-variables) for further information.
**Configuration Mode**
The web application can be run with a minimal set of routes and functionality (PeachPub - a simple sbot manager) or with the full-suite of capabilities, including network management and access to device statistics (PeachCloud). The mode is enabled by default (as defined in `Rocket.toml`) but can be overwritten using the `ROCKET_STANDALONE_MODE` environment variable: `true` or `false`. If the variable is unset or the value is incorrectly set, the application defaults to standalone mode.
Other deployment modes are `dev` and `prod`. Read the [Rocket Environment Configurations docs](https://rocket.rs/v0.5-rc/guide/configuration/#environment-variables) for further information.
**Authentication**
Authentication is disabled in `debug` mode and enabled by default when running the application in `release` mode. It can be disabled by setting the `ROCKET_DISABLE_AUTH` environment variable to `true`:
Authentication is disabled in `development` mode and enabled by default when running the application in `production` mode. It can be disabled by setting the `ROCKET_DISABLE_AUTH` environment variable to `true`:
`export ROCKET_DISABLE_AUTH=true`
@ -99,21 +95,7 @@ Remove configuration files (not removed with `apt-get remove`):
### Design
`peach-web` is built on the Rocket webserver and Tera templating engine. It presents a web interface for interacting with the device. HTML is rendered server-side. Request handlers call `peach-` libraries and serve HTML and assets. Each Tera template is passed a context object. In the case of Rust, this object is a `struct` and must implement `Serialize`. The fields of the context object are available in the context of the template to be rendered.
### Configuration
Configuration variables are stored in /var/lib/peachcloud/config.yml.
Peach-web also updates this file when changes are made to configurations via
the web interface. peach-web has no database, so all configurations are stored in this file.
#### Dynamic DNS Configuration
Most users will want to use the default PeachCloud dynamic dns server.
If the config dyn_use_custom_server=false, then default values will be used.
If the config dyn_use_custom_server=true, then a value must also be set for dyn_dns_server_address (e.g. "http://peachdynserver.commoninternet.net").
This value is the URL of the instance of peach-dyndns-server that requests will be sent to for domain registration.
Using a custom value can here can be useful for testing.
`peach-web` is built on the Rocket webserver and Tera templating engine. It presents a web interface for interacting with the device. HTML is rendered server-side. Request handlers call JSON-RPC microservices and serve HTML and assets. A JSON API is exposed for remote calls and dynamic client-side content updates (via plain JavaScript following unobstructive design principles). Each Tera template is passed a context object. In the case of Rust, this object is a `struct` and must implement `Serialize`. The fields of the context object are available in the context of the template to be rendered.
### Licensing

View File

@ -1,11 +1,7 @@
[default]
secret_key = "VYVUDivXvu8g6llxeJd9F92pMfocml5xl/Jjv5Sk4yw="
disable_auth = false
standalone_mode = true
[debug]
[development]
template_dir = "templates/"
disable_auth = true
[release]
[production]
template_dir = "templates/"
disable_auth = false

View File

@ -5,18 +5,54 @@ set -e
adduser --quiet --system peach-web
usermod -g peach peach-web
# create secret passwords folder if it doesn't already exist
mkdir -p /var/lib/peachcloud/passwords
chown -R peach-web:peach /var/lib/peachcloud/passwords
chmod -R u+rwX,go+rX,go-w /var/lib/peachcloud/passwords
# create nginx config
cat <<EOF > /etc/nginx/sites-enabled/default
server {
listen 80 default_server;
server_name peach.local www.peach.local;
# nginx authentication
auth_basic "If you have forgotten your password visit: http://peach.local/send_password_reset/";
auth_basic_user_file /var/lib/peachcloud/passwords/htpasswd;
# remove trailing slash if found
rewrite ^/(.*)/$ /$1 permanent;
location / {
proxy_pass http://127.0.0.1:3000;
}
# public routes
location /send_password_reset {
auth_basic off;
proxy_pass http://127.0.0.1:3000;
}
location /reset_password {
auth_basic off;
proxy_pass http://127.0.0.1:3000;
}
location /public/ {
auth_basic off;
proxy_pass http://127.0.0.1:3000;
}
location /js/ {
auth_basic off;
proxy_pass http://127.0.0.1:3000;
}
location /css/ {
auth_basic off;
proxy_pass http://127.0.0.1:3000;
}
location /icons/ {
auth_basic off;
proxy_pass http://127.0.0.1:3000;
}
}
EOF

View File

@ -1,36 +0,0 @@
use peach_lib::{config_manager, dyndns_client};
use rocket::serde::Serialize;
#[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 {
// TODO: replace `unwrap` with resilient error handling
let peach_config = config_manager::load_peach_config().unwrap();
let dyndns_fulldomain = peach_config.dyn_domain;
let is_dyndns_online = dyndns_client::is_dns_updater_online().unwrap();
let dyndns_subdomain =
dyndns_client::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,
}
}
}

View File

@ -1,2 +0,0 @@
pub mod dns;
pub mod network;

View File

@ -1,398 +0,0 @@
//! Data retrieval for the purpose of serving routes and hydrating
//! network-related HTML templates.
use std::collections::HashMap;
use rocket::{
form::FromForm,
serde::{Deserialize, Serialize},
UriDisplayQuery,
};
use peach_network::{
network,
network::{Scan, Status, Traffic},
};
use crate::{
utils::{
monitor,
monitor::{Alert, Data, Threshold},
},
AP_IFACE, WLAN_IFACE,
};
#[derive(Debug, Serialize)]
pub struct AccessPoint {
pub detail: Option<Scan>,
pub signal: Option<i32>,
pub state: String,
}
pub fn ap_state() -> String {
match network::state(&*AP_IFACE) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_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,
}
fn convert_traffic(traffic: Traffic) -> Option<IfaceTraffic> {
// modify traffic values & assign measurement unit
// based on received and transmitted values
let (rx, rx_unit) = if traffic.received > 1_047_527_424 {
// convert to GB
(traffic.received / 1_073_741_824, "GB".to_string())
} else if traffic.received > 0 {
// otherwise, convert it to MB
((traffic.received / 1024) / 1024, "MB".to_string())
} else {
(0, "MB".to_string())
};
let (tx, tx_unit) = if traffic.transmitted > 1_047_527_424 {
// convert to GB
(traffic.transmitted / 1_073_741_824, "GB".to_string())
} else if traffic.transmitted > 0 {
((traffic.transmitted / 1024) / 1024, "MB".to_string())
} else {
(0, "MB".to_string())
};
Some(IfaceTraffic {
rx,
rx_unit,
tx,
tx_unit,
})
}
#[derive(Debug, Serialize)]
pub struct IfaceTraffic {
pub rx: u64,
pub rx_unit: String,
pub tx: u64,
pub tx_unit: String,
}
#[derive(Debug, Serialize)]
pub struct NetworkAlertContext {
pub alert: Alert,
pub back: Option<String>,
pub data_total: Option<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: Option<IfaceTraffic>, // 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();
let (traffic, data_total) = match network::traffic(&*WLAN_IFACE) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(t)) => {
let current_traffic = t.received + t.transmitted;
let traffic = convert_traffic(t);
let total = stored_traffic.total + current_traffic;
let data_total = Data { total };
(traffic, Some(data_total))
}
_ => (None, None),
};
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 selected: Option<String>,
pub title: Option<String>,
pub saved_aps: Vec<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: Option<Status>,
pub wlan_traffic: Option<IfaceTraffic>,
}
impl NetworkDetailContext {
pub fn build() -> NetworkDetailContext {
let wlan_ip = match network::ip(&*WLAN_IFACE) {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
// list of networks saved in wpa_supplicant.conf
let wlan_list = match network::saved_networks() {
Ok(Some(ssids)) => ssids,
_ => Vec::new(),
};
// list of networks saved in wpa_supplicant.conf
let saved_aps = wlan_list.clone();
let wlan_rssi = match network::rssi_percent(&*WLAN_IFACE) {
Ok(rssi) => rssi,
Err(_) => None,
};
// list of networks currently in range (online & accessible)
let wlan_scan = match network::available_networks(&*WLAN_IFACE) {
Ok(Some(networks)) => networks,
_ => Vec::new(),
};
let wlan_ssid = match network::ssid(&*WLAN_IFACE) {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
let wlan_state = match network::state(&*WLAN_IFACE) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let wlan_status = match network::status(&*WLAN_IFACE) {
Ok(status) => status,
// interface unavailable
_ => None,
};
let wlan_traffic = match network::traffic(&*WLAN_IFACE) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => 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) {
let ssid = network.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,
selected: None,
title: None,
saved_aps,
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 wpa_supplicant.conf
let wlan_list = match network::saved_networks() {
Ok(Some(ssids)) => ssids,
_ => Vec::new(),
};
// list of networks currently in range (online & accessible)
let wlan_scan = match network::available_networks(&*WLAN_IFACE) {
Ok(Some(networks)) => networks,
_ => Vec::new(),
};
let wlan_ssid = match network::ssid(&*WLAN_IFACE) {
Ok(Some(ssid)) => ssid,
_ => "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)
.or_insert_with(|| "Not in range".to_string());
}
let ap_state = match network::state(&*AP_IFACE) {
Ok(Some(state)) => state,
_ => "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 NetworkStatusContext {
pub ap_ip: String,
pub ap_ssid: String,
pub ap_state: String,
pub ap_traffic: Option<IfaceTraffic>,
pub wlan_ip: String,
pub wlan_rssi: Option<String>,
pub wlan_ssid: String,
pub wlan_state: String,
pub wlan_status: Option<Status>,
pub wlan_traffic: Option<IfaceTraffic>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
// passing in the ssid of a chosen access point
pub selected: Option<String>,
pub title: Option<String>,
pub back: Option<String>,
}
impl NetworkStatusContext {
pub fn build() -> Self {
let ap_ip = match network::ip(&*AP_IFACE) {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
let ap_ssid = match network::ssid(&*AP_IFACE) {
Ok(Some(ssid)) => ssid,
_ => "Not currently activated".to_string(),
};
let ap_state = match network::state(&*AP_IFACE) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let ap_traffic = match network::traffic(&*AP_IFACE) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => None,
};
let wlan_ip = match network::ip(&*WLAN_IFACE) {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
let wlan_rssi = match network::rssi_percent(&*WLAN_IFACE) {
Ok(rssi) => rssi,
_ => None,
};
let wlan_ssid = match network::ssid(&*WLAN_IFACE) {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
let wlan_state = match network::state(&*WLAN_IFACE) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let wlan_status = match network::status(&*WLAN_IFACE) {
Ok(status) => status,
_ => None,
};
let wlan_traffic = match network::traffic(&*WLAN_IFACE) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => None,
};
NetworkStatusContext {
ap_ip,
ap_ssid,
ap_state,
ap_traffic,
wlan_ip,
wlan_rssi,
wlan_ssid,
wlan_state,
wlan_status,
wlan_traffic,
flash_name: None,
flash_msg: None,
selected: None,
title: None,
back: None,
}
}
}

View File

@ -2,57 +2,35 @@
use peach_lib::error::PeachError;
use peach_lib::{serde_json, serde_yaml};
use serde_json::error::Error as JsonError;
use serde_yaml::Error as YamlError;
use snafu::Snafu;
/// Custom error type encapsulating all possible errors for the web application.
#[derive(Debug)]
#[derive(Debug, Snafu)]
pub enum PeachWebError {
Json(JsonError),
Yaml(YamlError),
FailedToRegisterDynDomain(String),
PeachLib { source: PeachError, msg: String },
#[snafu(display("Error loading serde json"))]
Serde { source: serde_json::error::Error },
#[snafu(display("Error loading peach-config yaml"))]
YamlError { source: serde_yaml::Error },
#[snafu(display("{}", msg))]
FailedToRegisterDynDomain { msg: String },
#[snafu(display("{}: {}", source, msg))]
PeachLibError { source: PeachError, msg: String },
}
impl std::error::Error for PeachWebError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
PeachWebError::Json(ref source) => Some(source),
PeachWebError::Yaml(ref source) => Some(source),
PeachWebError::FailedToRegisterDynDomain(_) => None,
PeachWebError::PeachLib { ref source, .. } => Some(source),
}
impl From<serde_json::error::Error> for PeachWebError {
fn from(err: serde_json::error::Error) -> PeachWebError {
PeachWebError::Serde { source: err }
}
}
impl std::fmt::Display for PeachWebError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
PeachWebError::Json(ref source) => write!(f, "Serde JSON error: {}", source),
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
write!(f, "DYN DNS error: {}", msg)
}
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
}
}
}
impl From<JsonError> for PeachWebError {
fn from(err: JsonError) -> PeachWebError {
PeachWebError::Json(err)
}
}
impl From<YamlError> for PeachWebError {
fn from(err: YamlError) -> PeachWebError {
PeachWebError::Yaml(err)
impl From<serde_yaml::Error> for PeachWebError {
fn from(err: serde_yaml::Error) -> PeachWebError {
PeachWebError::YamlError { source: err }
}
}
impl From<PeachError> for PeachWebError {
fn from(err: PeachError) -> PeachWebError {
PeachWebError::PeachLib {
PeachWebError::PeachLibError {
source: err,
msg: "".to_string(),
}

View File

@ -24,59 +24,145 @@
#![feature(proc_macro_hygiene, decl_macro)]
mod context;
pub mod error;
mod router;
pub mod routes;
#[cfg(test)]
mod tests;
pub mod utils;
use log::{error, info};
use std::process;
use log::{debug, error, info};
use rocket::{fairing::AdHoc, serde::Deserialize, Build, Rocket};
use rocket::{catchers, fs::FileServer, routes, Build, Rocket};
use rocket_dyn_templates::Template;
use crate::routes::authentication::*;
use crate::routes::catchers::*;
use crate::routes::index::*;
use crate::routes::scuttlebutt::*;
use crate::routes::status::device::*;
use crate::routes::status::network::*;
use crate::routes::status::ping::*;
use crate::routes::settings::admin::*;
use crate::routes::settings::dns::*;
use crate::routes::settings::menu::*;
use crate::routes::settings::network::*;
use crate::routes::settings::scuttlebutt::*;
pub type BoxError = Box<dyn std::error::Error>;
/// Application configuration parameters.
/// These values are extracted from Rocket's default configuration provider:
/// `Config::figment()`. As such, the values are drawn from `Rocket.toml` or
/// the TOML file path in the `ROCKET_CONFIG` environment variable. The TOML
/// file parameters are automatically overruled by any `ROCKET_` variables
/// which might be set.
#[derive(Debug, Deserialize)]
pub struct RocketConfig {
disable_auth: bool,
standalone_mode: bool,
}
static WLAN_IFACE: &str = "wlan0";
static AP_IFACE: &str = "ap0";
pub fn init_rocket() -> Rocket<Build> {
info!("Initializing Rocket");
// build a basic rocket instance
let rocket = rocket::build();
// return the default provider figment used by `rocket::build()`
let figment = rocket.figment();
// deserialize configuration parameters into our `RocketConfig` struct (defined above)
// since we're in the intialisation phase, panic if the extraction fails
let config: RocketConfig = figment.extract().expect("configuration extraction failed");
debug!("{:?}", config);
info!("Mounting Rocket routes");
let mounted_rocket = if config.standalone_mode {
router::mount_peachpub_routes(rocket)
} else {
router::mount_peachcloud_routes(rocket)
};
info!("Attaching application configuration to managed state");
mounted_rocket.attach(AdHoc::config::<RocketConfig>())
/// Create rocket instance & mount all routes.
fn init_rocket() -> Rocket<Build> {
rocket::build()
// GENERAL HTML ROUTES
.mount(
"/",
routes![
help,
home,
login,
login_post,
logout,
reboot_cmd,
shutdown_cmd,
power_menu,
settings_menu,
],
)
// STATUS HTML ROUTES
.mount("/status", routes![device_status, network_status])
// ADMIN SETTINGS HTML ROUTES
.mount(
"/settings/admin",
routes![
admin_menu,
configure_admin,
add_admin,
add_admin_post,
delete_admin_post,
change_password,
change_password_post,
reset_password,
reset_password_post,
forgot_password_page,
send_password_reset_post,
],
)
// NETWORK SETTINGS HTML ROUTES
.mount(
"/settings/network",
routes![
add_credentials,
connect_wifi,
configure_dns,
configure_dns_post,
disconnect_wifi,
deploy_ap,
deploy_client,
forget_wifi,
network_home,
add_ssid,
add_wifi,
network_detail,
wifi_list,
wifi_password,
wifi_set_password,
wifi_usage,
wifi_usage_alerts,
wifi_usage_reset,
],
)
// SCUTTLEBUTT SETTINGS HTML ROUTES
.mount("/settings/scuttlebutt", routes![ssb_settings_menu])
// SCUTTLEBUTT SOCIAL HTML ROUTES
.mount(
"/scuttlebutt",
routes![
peers, friends, follows, followers, blocks, profile, private, follow, unfollow,
block, publish,
],
)
// GENERAL JSON API ROUTES
.mount(
"/api/v1",
routes![ping_pong, ping_network, ping_oled, ping_stats,],
)
// ADMIN JSON API ROUTES
.mount(
"/api/v1/admin",
routes![
save_password_form_endpoint,
reset_password_form_endpoint,
reboot_device,
shutdown_device,
],
)
// NETWORK JSON API ROUTES
.mount(
"/api/v1/network",
routes![
activate_ap,
activate_client,
add_wifi_credentials,
connect_ap,
disconnect_ap,
forget_ap,
modify_password,
reset_data_total,
return_ip,
return_rssi,
return_ssid,
return_state,
return_status,
scan_networks,
update_wifi_alerts,
save_dns_configuration_endpoint,
],
)
.mount("/", FileServer::from("static"))
.register("/", catchers![not_found, internal_error, forbidden])
.attach(Template::fairing())
}
/// Launch the peach-web rocket server.
@ -86,6 +172,7 @@ async fn main() {
env_logger::init();
// initialize rocket
info!("Initializing Rocket");
let rocket = init_rocket();
// launch rocket

View File

@ -1,95 +0,0 @@
use rocket::{catchers, fs::FileServer, routes, Build, Rocket};
use rocket_dyn_templates::Template;
use crate::routes::{
authentication::*,
catchers::*,
index::*,
scuttlebutt::*,
settings::{admin::*, dns::*, menu::*, network::*, scuttlebutt::*},
status::{device::*, network::*, scuttlebutt::*},
};
/// Create a Rocket instance and mount PeachPub routes, fileserver and
/// catchers. This gives us everything we need to run PeachPub and excludes
/// settings and status routes related to networking and the device (memory,
/// hard disk, CPU etc.).
pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
rocket
.mount(
"/",
routes![
help,
home,
login,
login_post,
logout,
reboot_cmd,
shutdown_cmd,
power_menu,
settings_menu,
],
)
.mount(
"/settings/admin",
routes![
admin_menu,
configure_admin,
add_admin,
add_admin_post,
delete_admin_post,
change_password,
change_password_post,
reset_password,
reset_password_post,
forgot_password_page,
send_password_reset_post,
],
)
.mount(
"/settings/scuttlebutt",
routes![ssb_settings_menu, configure_sbot],
)
.mount(
"/scuttlebutt",
routes![
peers, friends, follows, followers, blocks, profile, private, follow, unfollow,
block, publish,
],
)
.mount("/status", routes![scuttlebutt_status])
.mount("/", FileServer::from("static"))
.register("/", catchers![not_found, internal_error, forbidden])
.attach(Template::fairing())
}
/// Create a Rocket instance with PeachPub routes, fileserver and catchers by
/// calling `mount_peachpub_routes()` and then mount all additional routes
/// required to run a complete PeachCloud build.
pub fn mount_peachcloud_routes(rocket: Rocket<Build>) -> Rocket<Build> {
mount_peachpub_routes(rocket)
.mount(
"/settings/network",
routes![
add_credentials,
connect_wifi,
configure_dns,
configure_dns_post,
disconnect_wifi,
deploy_ap,
deploy_client,
forget_wifi,
network_home,
add_ssid,
add_wifi,
network_detail,
wifi_list,
wifi_password,
wifi_set_password,
wifi_usage,
wifi_usage_alerts,
wifi_usage_reset,
],
)
.mount("/status", routes![device_status, network_status])
}

View File

@ -1,21 +1,20 @@
use log::info;
use rocket::{
form::{Form, FromForm},
get,
http::{Cookie, CookieJar, Status},
post,
request::{self, FlashMessage, FromRequest, Request},
response::{Flash, Redirect},
serde::Deserialize,
use rocket::form::{Form, FromForm};
use rocket::http::{Cookie, CookieJar, Status};
use rocket::request::{self, FlashMessage, FromRequest, Request};
use rocket::response::{Flash, Redirect};
use rocket::serde::{
json::{Json, Value},
Deserialize, Serialize,
};
use rocket_dyn_templates::{tera::Context, Template};
use rocket::{get, post, Config};
use rocket_dyn_templates::Template;
use peach_lib::{error::PeachError, password_utils};
use peach_lib::error::PeachError;
use peach_lib::password_utils;
use crate::error::PeachWebError;
use crate::utils::TemplateOrRedirect;
//use crate::DisableAuth;
use crate::RocketConfig;
use crate::utils::{build_json_response, TemplateOrRedirect};
// HELPERS AND STRUCTS FOR AUTHENTICATION WITH COOKIES
@ -43,14 +42,14 @@ impl<'r> FromRequest<'r> for Authenticated {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
// retrieve auth state from managed state (returns `Option<bool>`).
// this value is read from the Rocket.toml config file on start-up
let authentication_is_disabled: bool = *req
.rocket()
.state::<RocketConfig>()
.map(|config| (&config.disable_auth))
.unwrap_or(&false);
// check for `disable_auth` config value; set to `false` if unset
// can be set via the `ROCKET_DISABLE_AUTH` environment variable
// - env var, if set, takes precedence over value defined in `Rocket.toml`
let authentication_is_disabled: bool = match Config::figment().find_value("disable_auth") {
// deserialize the boolean value; set to `false` if an error is encountered
Ok(value) => value.deserialize().unwrap_or(false),
Err(_) => false,
};
if authentication_is_disabled {
let auth = Authenticated {};
request::Outcome::Success(auth)
@ -70,19 +69,37 @@ impl<'r> FromRequest<'r> for Authenticated {
// 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 = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Login".to_string()));
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 {
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("login", &context.into_json())
Template::render("login", &context)
}
#[derive(Debug, Deserialize, FromForm)]
@ -102,7 +119,8 @@ pub fn verify_login_form(login_form: LoginForm) -> Result<(), PeachError> {
#[post("/login", data = "<login_form>")]
pub fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) -> TemplateOrRedirect {
match verify_login_form(login_form.into_inner()) {
let result = verify_login_form(login_form.into_inner());
match result {
Ok(_) => {
// if successful login, add a cookie indicating the user is authenticated
// and redirect to home page
@ -110,18 +128,17 @@ pub fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) -> Templ
// is just admin (this is arbitrary).
// If we had multiple users, we could put the user_id here.
cookies.add_private(Cookie::new(AUTH_COOKIE_KEY, ADMIN_USERNAME));
TemplateOrRedirect::Redirect(Redirect::to("/"))
}
Err(_) => {
// if unsuccessful login, render /login page again
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Login".to_string()));
context.insert("flash_name", &("error".to_string()));
context.insert("flash_msg", &("Invalid password".to_string()));
TemplateOrRedirect::Template(Template::render("login", &context.into_json()))
let mut context = LoginContext::build();
context.back = Some("/".to_string());
context.title = Some("Login".to_string());
context.flash_name = Some("error".to_string());
let flash_msg = "Invalid password".to_string();
context.flash_msg = Some(flash_msg);
TemplateOrRedirect::Template(Template::render("login", &context))
}
}
}
@ -145,6 +162,44 @@ pub struct ResetPasswordForm {
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!(
@ -166,63 +221,100 @@ pub fn save_reset_password_form(password_form: ResetPasswordForm) -> Result<(),
/// and is specifically for users who have forgotten their password.
#[get("/reset_password")]
pub fn reset_password(flash: Option<FlashMessage>) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Reset Password".to_string()));
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 {
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/admin/reset_password", &context.into_json())
Template::render("settings/admin/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.
#[post("/reset_password", data = "<reset_password_form>")]
pub fn reset_password_post(reset_password_form: Form<ResetPasswordForm>) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Reset Password".to_string()));
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("settings/admin/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("settings/admin/reset_password", &context)
}
}
}
let (flash_name, flash_msg) = match save_reset_password_form(reset_password_form.into_inner()) {
Ok(_) => (
"success".to_string(),
"New password is now saved. Return home to login".to_string(),
),
Err(err) => (
"error".to_string(),
format!("Failed to reset password: {}", err),
),
};
context.insert("flash_name", &Some(flash_name));
context.insert("flash_msg", &Some(flash_msg));
Template::render("settings/admin/reset_password", &context.into_json())
/// 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.
#[post("/reset_password", data = "<reset_password_form>")]
pub fn reset_password_form_endpoint(reset_password_form: Json<ResetPasswordForm>) -> Value {
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();
build_json_response(status, None, Some(msg))
}
Err(err) => {
let status = "error".to_string();
let msg = format!("{}", err);
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,
}
}
}
/// Page for users who have forgotten their password.
/// This route is used by a user who is not logged in
/// to initiate the sending of a new password reset.
#[get("/forgot_password")]
pub fn forgot_password_page(flash: Option<FlashMessage>) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Send Password Reset".to_string()));
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.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/admin/forgot_password", &context.into_json())
Template::render("settings/admin/forgot_password", &context)
}
/// Send password reset request handler. This route is used by a user who is not logged in
@ -231,25 +323,27 @@ pub fn forgot_password_page(flash: Option<FlashMessage>) -> Template {
#[post("/send_password_reset")]
pub fn send_password_reset_post() -> Template {
info!("++ send password reset post");
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Send Password Reset".to_string()));
let (flash_name, flash_msg) = match password_utils::send_password_reset() {
Ok(_) => (
"success".to_string(),
"A password reset link has been sent to the admin of this device".to_string(),
),
Err(err) => (
"error".to_string(),
format!("Failed to send password reset link: {}", err),
),
};
context.insert("flash_name", &Some(flash_name));
context.insert("flash_msg", &Some(flash_msg));
Template::render("settings/admin/forgot_password", &context.into_json())
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("settings/admin/forgot_password", &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("settings/admin/forgot_password", &context)
}
}
}
// HELPERS AND ROUTES FOR /settings/change_password
@ -281,40 +375,63 @@ pub fn save_password_form(password_form: PasswordForm) -> Result<(), PeachWebErr
/// Change password request handler. This is used by a user who is already logged in.
#[get("/change_password")]
pub fn change_password(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/admin".to_string()));
context.insert("title", &Some("Change Password".to_string()));
let mut context = ChangePasswordContext::build();
// set back icon link to network route
context.back = Some("/settings/admin".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.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/admin/change_password", &context.into_json())
Template::render("settings/admin/change_password", &context)
}
/// Change password form request handler. This route is used by a user who is already logged in.
#[post("/change_password", data = "<password_form>")]
pub fn change_password_post(password_form: Form<PasswordForm>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/admin".to_string()));
context.insert("title", &Some("Change Password".to_string()));
let (flash_name, flash_msg) = match save_password_form(password_form.into_inner()) {
Ok(_) => (
"success".to_string(),
"New password is now saved".to_string(),
),
Err(err) => (
"error".to_string(),
format!("Failed to save new password: {}", err),
),
};
context.insert("flash_name", &Some(flash_name));
context.insert("flash_msg", &Some(flash_msg));
Template::render("settings/admin/change_password", &context.into_json())
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("/settings/admin".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("settings/admin/change_password", &context)
}
Err(err) => {
let mut context = ChangePasswordContext::build();
// set back icon link to network route
context.back = Some("/settings/admin".to_string());
context.title = Some("Change Password".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg = Some(format!("Failed to save new password: {}", err));
Template::render("settings/admin/change_password", &context)
}
}
}
/// JSON change password form request handler.
#[post("/change_password", data = "<password_form>")]
pub fn save_password_form_endpoint(
password_form: Json<PasswordForm>,
_auth: Authenticated,
) -> Value {
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();
build_json_response(status, None, Some(msg))
}
Err(err) => {
let status = "error".to_string();
let msg = format!("{}", err);
build_json_response(status, None, Some(msg))
}
}
}

View File

@ -1,37 +1,69 @@
use rocket::{get, request::FlashMessage, State};
use rocket_dyn_templates::{tera::Context, Template};
use rocket::{get, request::FlashMessage};
use rocket_dyn_templates::Template;
use serde::Serialize;
use crate::routes::authentication::Authenticated;
use crate::RocketConfig;
// 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 home(_auth: Authenticated, config: &State<RocketConfig>) -> Template {
let mut context = Context::new();
context.insert("flash_name", &None::<()>);
context.insert("flash_msg", &None::<()>);
context.insert("title", &None::<()>);
// pass in mode from managed state so we can define appropriate urls in template
context.insert("standalone_mode", &config.standalone_mode);
Template::render("home", &context.into_json())
pub fn home(_auth: Authenticated) -> Template {
let context = HomeContext {
flash_name: None,
flash_msg: None,
title: None,
};
Template::render("home", &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 = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Help".to_string()));
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 {
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("help", &context.into_json())
Template::render("help", &context)
}

View File

@ -1,69 +1,95 @@
use rocket::serde::{Deserialize, Serialize};
use rocket::{
form::{Form, FromForm},
get, post,
request::FlashMessage,
response::{Flash, Redirect},
serde::Deserialize,
uri,
};
use rocket_dyn_templates::{tera::Context, Template};
use rocket_dyn_templates::Template;
use peach_lib::config_manager;
use peach_lib::config_manager::load_peach_config;
use crate::error::PeachWebError;
use crate::routes::authentication::Authenticated;
// HELPERS AND ROUTES FOR /settings/admin
#[derive(Debug, Serialize)]
pub struct AdminMenuContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl AdminMenuContext {
pub fn build() -> AdminMenuContext {
AdminMenuContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
/// Administrator settings menu.
#[get("/")]
pub fn admin_menu(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings".to_string()));
context.insert("title", &Some("Administrator Settings".to_string()));
let mut context = AdminMenuContext::build();
// set back icon link to settings route
context.back = Some("/settings".to_string());
context.title = Some("Administrator Settings".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/admin/menu", &context.into_json())
Template::render("settings/admin/menu", &context)
}
// HELPERS AND ROUTES FOR /settings/admin/configure
#[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("/configure")]
pub fn configure_admin(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/admin".to_string()));
context.insert("title", &Some("Configure Admin".to_string()));
let mut context = ConfigureAdminContext::build();
// set back icon link to settings route
context.back = Some("/settings/admin".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 {
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
// load the peach configuration vector
match config_manager::load_peach_config() {
Ok(config) => {
// retrieve the vector of ssb admin ids
let ssb_admin_ids = config.ssb_admin_ids;
context.insert("ssb_admin_ids", &ssb_admin_ids);
}
// if load fails, overwrite the flash_name and flash_msg
Err(e) => {
context.insert("flash_name", &Some("error".to_string()));
context.insert(
"flash_msg",
&Some(format!("Failed to load Peach config: {}", e)),
);
}
}
Template::render("settings/admin/configure_admin", &context.into_json())
Template::render("settings/admin/configure_admin", &context)
}
// HELPERS AND ROUTES FOR /settings/admin/add
@ -73,6 +99,25 @@ 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
@ -81,24 +126,23 @@ pub fn save_add_admin_form(admin_form: AddAdminForm) -> Result<(), PeachWebError
#[get("/add")]
pub fn add_admin(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/admin/configure".to_string()));
context.insert("title", &Some("Add Admin".to_string()));
let mut context = AddAdminContext::build();
context.back = Some("/settings/admin/configure".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 {
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
// template_dir is set in Rocket.toml
Template::render("settings/admin/add_admin", &context.into_json())
Template::render("settings/admin/add_admin", &context)
}
#[post("/add", data = "<add_admin_form>")]
pub fn add_admin_post(add_admin_form: Form<AddAdminForm>, _auth: Authenticated) -> Flash<Redirect> {
let result = save_add_admin_form(add_admin_form.into_inner());
let url = uri!("/settings/admin/configure");
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"),

View File

@ -3,20 +3,27 @@ use rocket::{
form::{Form, FromForm},
get, post,
request::FlashMessage,
serde::Deserialize,
serde::{
json::{Json, Value},
Deserialize, Serialize,
},
};
use rocket_dyn_templates::Template;
use peach_lib::{
config_manager, dyndns_client,
error::PeachError,
jsonrpc_client_core::{Error, ErrorKind},
jsonrpc_core::types::error::ErrorCode,
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::{
context::dns::ConfigureDNSContext, error::PeachWebError, routes::authentication::Authenticated,
};
use crate::error::PeachWebError;
use crate::routes::authentication::Authenticated;
use crate::utils::build_json_response;
#[derive(Debug, Deserialize, FromForm)]
pub struct DnsForm {
@ -29,12 +36,11 @@ 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 = dyndns_client::get_full_dynamic_domain(&dns_form.dynamic_domain);
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 = dyndns_client::check_is_new_dyndns_domain(&full_dynamic_domain)?;
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(_) => {
@ -46,22 +52,24 @@ pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> {
info!("Failed to register dyndns domain: {:?}", err);
// json response for failed update
let msg: String = match err {
PeachError::JsonRpcClientCore(Error(
ErrorKind::JsonRpcError(err),
_state,
)) => {
if let ErrorCode::ServerError(-32030) = err.code {
format!(
"Error registering domain: {} was previously registered",
full_dynamic_domain
)
} else {
"Failed to register dyndns domain".to_string()
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))
Err(PeachWebError::FailedToRegisterDynDomain { msg })
}
}
}
@ -74,44 +82,91 @@ pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> {
}
}
#[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("/dns")]
pub fn configure_dns(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = ConfigureDNSContext::build();
// set back icon link to network route
context.back = Some("/settings/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.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/network/configure_dns", &context)
}
#[post("/dns", data = "<dns>")]
pub fn configure_dns_post(dns: Form<DnsForm>, _auth: Authenticated) -> Template {
let result = save_dns_configuration(dns.into_inner());
let mut context = ConfigureDNSContext::build();
// set back icon link to network route
context.back = Some("/settings/network".to_string());
context.title = Some("Configure DNS".to_string());
match result {
Ok(_) => {
let mut context = ConfigureDNSContext::build();
// set back icon link to network route
context.back = Some("/settings/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("settings/network/configure_dns", &context)
}
Err(err) => {
let mut context = ConfigureDNSContext::build();
// set back icon link to network route
context.back = Some("/settings/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("settings/network/configure_dns", &context)
}
}
}
#[post("/dns/configure", data = "<dns_form>")]
pub fn save_dns_configuration_endpoint(dns_form: Json<DnsForm>, _auth: Authenticated) -> Value {
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();
build_json_response(status, None, Some(msg))
}
Err(err) => {
let status = "error".to_string();
let msg = format!("{}", err);
build_json_response(status, None, Some(msg))
}
}
Template::render("settings/network/configure_dns", &context)
}

View File

@ -1,22 +1,41 @@
use rocket::{get, request::FlashMessage};
use rocket_dyn_templates::{tera::Context, Template};
use rocket::{get, request::FlashMessage, serde::Serialize};
use rocket_dyn_templates::Template;
use crate::routes::authentication::Authenticated;
// HELPERS AND ROUTES FOR /settings
#[derive(Debug, Serialize)]
pub struct SettingsMenuContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl SettingsMenuContext {
pub fn build() -> SettingsMenuContext {
SettingsMenuContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
/// View and delete currently configured admin.
#[get("/settings")]
pub fn settings_menu(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/".to_string()));
context.insert("title", &Some("Settings".to_string()));
let mut context = SettingsMenuContext::build();
// set back icon link to network route
context.back = Some("/".to_string());
context.title = Some("Settings".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/menu", &context.into_json())
Template::render("settings/menu", &context)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +1,41 @@
use rocket::{get, request::FlashMessage};
use rocket_dyn_templates::{tera::Context, Template};
use rocket::{get, request::FlashMessage, serde::Serialize};
use rocket_dyn_templates::Template;
use crate::routes::authentication::Authenticated;
// HELPERS AND ROUTES FOR /settings/scuttlebutt
#[derive(Debug, Serialize)]
pub struct ScuttlebuttSettingsContext {
pub back: Option<String>,
pub title: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
impl ScuttlebuttSettingsContext {
pub fn build() -> ScuttlebuttSettingsContext {
ScuttlebuttSettingsContext {
back: None,
title: None,
flash_name: None,
flash_msg: None,
}
}
}
/// Scuttlebutt settings menu.
#[get("/")]
pub fn ssb_settings_menu(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings".to_string()));
context.insert("title", &Some("Scuttlebutt Settings".to_string()));
let mut context = ScuttlebuttSettingsContext::build();
// set back icon link to network route
context.back = Some("/settings".to_string());
context.title = Some("Scuttlebutt Settings".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/scuttlebutt/menu", &context.into_json())
}
/// Sbot configuration page (includes form for updating configuration parameters).
#[get("/configure")]
pub fn configure_sbot(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/scuttlebutt".to_string()));
context.insert("title", &Some("Sbot Configuration".to_string()));
if let Some(flash) = flash {
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("settings/scuttlebutt/configure_sbot", &context.into_json())
Template::render("settings/scuttlebutt", &context)
}

View File

@ -1,9 +1,10 @@
use log::info;
use log::{debug, info, warn};
use rocket::{
get,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
};
use rocket_dyn_templates::Template;
use serde::Serialize;
use std::{
@ -11,15 +12,13 @@ use std::{
process::{Command, Output},
};
use peach_lib::{
config_manager::load_peach_config, dyndns_client, network_client, oled_client, sbot_client,
};
use peach_stats::{
stats,
stats::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat},
};
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::routes::authentication::Authenticated;
use crate::utils::build_json_response;
use rocket::serde::json::Value;
// HELPERS AND ROUTES FOR /status
@ -35,6 +34,7 @@ pub struct StatusContext {
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,
@ -46,12 +46,9 @@ pub struct StatusContext {
impl StatusContext {
pub fn build() -> StatusContext {
// convert result to Option<CpuStatPercentages>, discard any error
let cpu_stat_percent = stats::cpu_stats_percent().ok();
let load_average = stats::load_average().ok();
let mem_stats = stats::mem_stats().ok();
// TODO: add `wpa_supplicant_status` to peach_network to replace this ping call
// instead of: "is the network json-rpc server running?", we want to ask:
// "is the wpa_supplicant systemd service functioning correctly?"
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(),
@ -60,21 +57,22 @@ impl StatusContext {
Ok(_) => "ONLINE".to_string(),
Err(_) => "OFFLINE".to_string(),
};
let uptime = match stats::uptime() {
Ok(secs) => {
let uptime_mins = secs / 60;
uptime_mins.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(),
};
// parse the uptime string to a signed integer (for math)
let uptime_parsed = uptime.parse::<i32>().ok();
// serialize disk usage data into Vec<DiskUsage>
let disk_usage_stats = match stats::disk_usage() {
Ok(disks) => disks,
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(),
};
@ -86,6 +84,9 @@ impl StatusContext {
}
}
// 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;
@ -117,11 +118,16 @@ impl StatusContext {
}
// test if go-sbot is running
let sbot_is_online: bool;
let sbot_is_online_result = sbot_client::is_sbot_online();
let sbot_is_online: bool = match sbot_is_online_result {
Ok(val) => val,
Err(_err) => false,
};
match sbot_is_online_result {
Ok(val) => {
sbot_is_online = val;
}
Err(_err) => {
sbot_is_online = false;
}
}
StatusContext {
back: None,
@ -133,6 +139,7 @@ impl StatusContext {
mem_stats,
network_ping,
oled_ping,
stats_ping,
dyndns_enabled,
dyndns_is_online,
config_is_valid,
@ -182,6 +189,25 @@ pub fn reboot_cmd(_auth: Authenticated) -> Flash<Redirect> {
}
}
/// JSON request handler for device reboot.
#[post("/api/v1/admin/reboot")]
pub fn reboot_device(_auth: Authenticated) -> Value {
match reboot() {
Ok(_) => {
debug!("Going down for reboot...");
let status = "success".to_string();
let msg = "Going down for reboot.".to_string();
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();
build_json_response(status, None, Some(msg))
}
}
}
// HELPERS AND ROUTES FOR /power/shutdown
/// Executes a system command to shutdown the device immediately.
@ -201,6 +227,25 @@ pub fn shutdown_cmd(_auth: Authenticated) -> Flash<Redirect> {
}
}
// shutdown the device
#[post("/power/shutdown")]
pub fn shutdown_device(_auth: Authenticated) -> Value {
match shutdown() {
Ok(_) => {
debug!("Going down for shutdown...");
let status = "success".to_string();
let msg = "Going down for shutdown.".to_string();
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();
build_json_response(status, None, Some(msg))
}
}
}
// HELPERS AND ROUTES FOR /power
#[derive(Debug, Serialize)]

View File

@ -1,3 +1,3 @@
pub mod device;
pub mod network;
pub mod scuttlebutt;
pub mod ping;

View File

@ -1,21 +1,164 @@
use rocket::{get, request::FlashMessage};
use rocket_dyn_templates::Template;
use serde::Serialize;
use peach_lib::network_client;
use peach_lib::stats_client::Traffic;
use crate::context::network::NetworkStatusContext;
use crate::routes::authentication::Authenticated;
// HELPERS AND ROUTES FOR /status/network
#[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_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>,
// 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_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_ssid,
wlan_state,
wlan_status,
wlan_traffic,
flash_name: None,
flash_msg: None,
title: None,
back: None,
}
}
}
#[get("/network")]
pub fn network_status(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = NetworkStatusContext::build();
// assign context through context_builder call
let mut context = NetworkContext::build();
context.back = Some("/status".to_string());
context.title = Some("Network 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.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
// template_dir is set in Rocket.toml
Template::render("status/network", &context)
}

View File

@ -0,0 +1,78 @@
//! Helper routes for pinging services to check that they are active
use log::{debug, warn};
use rocket::get;
use rocket::serde::json::Value;
use peach_lib::network_client;
use peach_lib::oled_client;
use peach_lib::stats_client;
use crate::routes::authentication::Authenticated;
use crate::utils::build_json_response;
/// Status route: useful for checking connectivity from web client.
#[get("/ping")]
pub fn ping_pong(_auth: Authenticated) -> Value {
//pub fn ping_pong() -> Value {
// ping pong
let status = "success".to_string();
let msg = "pong!".to_string();
build_json_response(status, None, Some(msg))
}
/// Status route: check availability of `peach-network` microservice.
#[get("/ping/network")]
pub fn ping_network(_auth: Authenticated) -> Value {
match network_client::ping() {
Ok(_) => {
debug!("peach-network responded successfully");
let status = "success".to_string();
let msg = "peach-network is available.".to_string();
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();
build_json_response(status, None, Some(msg))
}
}
}
/// Status route: check availability of `peach-oled` microservice.
#[get("/ping/oled")]
pub fn ping_oled(_auth: Authenticated) -> Value {
match oled_client::ping() {
Ok(_) => {
debug!("peach-oled responded successfully");
let status = "success".to_string();
let msg = "peach-oled is available.".to_string();
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();
build_json_response(status, None, Some(msg))
}
}
}
/// Status route: check availability of `peach-stats` microservice.
#[get("/ping/stats")]
pub fn ping_stats(_auth: Authenticated) -> Value {
match stats_client::ping() {
Ok(_) => {
debug!("peach-stats responded successfully");
let status = "success".to_string();
let msg = "peach-stats is available.".to_string();
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();
build_json_response(status, None, Some(msg))
}
}
}

View File

@ -1,16 +0,0 @@
use rocket::get;
use rocket_dyn_templates::{tera::Context, Template};
use crate::routes::authentication::Authenticated;
// HELPERS AND ROUTES FOR /status/scuttlebutt
#[get("/scuttlebutt")]
pub fn scuttlebutt_status(_auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("flash_name", &None::<()>);
context.insert("flash_msg", &None::<()>);
context.insert("title", &Some("Scuttlebutt Status"));
Template::render("status/scuttlebutt", &context.into_json())
}

View File

@ -3,8 +3,11 @@ use std::io::Read;
use rocket::http::{ContentType, Status};
use rocket::local::blocking::Client;
use rocket::serde::json::{json, Value};
use rocket::{Build, Config, Rocket};
use crate::utils::build_json_response;
use super::init_rocket;
// define authentication mode
@ -325,13 +328,6 @@ fn network_settings_menu_html() {
assert!(body.contains("Network Configuration"));
}
/*
NOTE: these tests are commented-out for the moment, due to the fact that they
invoke system commands (resulting in a `sudo` password request during
test execution). see if we can find a way to test the results without
triggering the `systemctl` call.
#[test]
fn deploy_ap() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
@ -341,16 +337,6 @@ fn deploy_ap() {
assert_eq!(response.content_type(), None);
}
#[test]
fn deploy_client() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/network/wifi/activate").dispatch();
// check for 303 status (redirect)
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.content_type(), None);
}
*/
#[test]
fn dns_settings_html() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
@ -387,6 +373,15 @@ fn ap_details_html() {
//assert!(body.contains("Network not found"));
}
#[test]
fn deploy_client() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/network/wifi/activate").dispatch();
// check for 303 status (redirect)
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.content_type(), None);
}
#[test]
fn add_ap_html() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
@ -514,6 +509,179 @@ fn network_status_html() {
assert!(body.contains("DOWNLOAD"));
assert!(body.contains("UPLOAD"));
}
// JSON API ROUTES
#[test]
fn activate_ap() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/activate_ap")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
}
#[test]
fn activate_client() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/activate_client")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
}
#[test]
fn return_ip() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/ip")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("wlan0"));
assert!(body.contains("ap0"));
}
#[test]
fn return_rssi() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/rssi")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Not currently connected to an access point."));
}
#[test]
fn return_ssid() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/ssid")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Not currently connected to an access point."));
}
#[test]
fn return_state() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/state")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("ap0"));
assert!(body.contains("wlan0"));
assert!(body.contains("unavailable"));
}
#[test]
fn return_status() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/status")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Not currently connected to an access point."));
}
#[test]
fn scan_networks() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/wifi")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Unable to scan for networks. Interface may be deactivated."));
}
#[test]
fn add_wifi() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/wifi")
.header(ContentType::JSON)
.body(r#"{ "ssid": "Home", "pass": "Password" }"#)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Failed to add WiFi credentials."));
}
#[test]
fn remove_wifi() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/wifi/forget")
.header(ContentType::JSON)
.body(r#"{ "ssid": "Home" }"#)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Failed to remove WiFi network credentials."));
}
#[test]
fn new_password() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/wifi/modify")
.header(ContentType::JSON)
.body(r#"{ "ssid": "Home", "pass": "Password" }"#)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("Failed to update WiFi password."));
}
#[test]
fn ping_pong() {
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/ping")
.header(ContentType::JSON)
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::JSON));
let body = response.into_string().unwrap();
assert!(body.contains("pong!"));
}
// HELPER FUNCTION TESTS
#[test]
fn test_build_json_response() {
let status = "success".to_string();
let data = json!("WiFi credentials added.".to_string());
let j: Value = build_json_response(status, Some(data), None);
assert_eq!(j["status"], "success");
assert_eq!(j["data"], "WiFi credentials added.");
assert_eq!(j["msg"], json!(null));
}
// FILE TESTS
#[test]

View File

@ -3,16 +3,26 @@ pub mod monitor;
use rocket_dyn_templates::Template;
use rocket::response::{Redirect, Responder};
use rocket::serde::Serialize;
use rocket::serde::json::{Value, json};
use rocket::serde::{Serialize};
// HELPER FUNCTIONS
pub fn build_json_response(
status: String,
data: Option<Value>,
msg: Option<String>,
) -> Value {
json!({ "status": status, "data": data, "msg": msg })
}
#[derive(Debug, Serialize)]
pub struct FlashContext {
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
}
/// A helper enum which allows routes to either return a Template or a Redirect
/// from: https://github.com/SergioBenitez/Rocket/issues/253#issuecomment-532356066
#[allow(clippy::large_enum_variant)]
@ -20,4 +30,4 @@ pub struct FlashContext {
pub enum TemplateOrRedirect {
Template(Template),
Redirect(Redirect),
}
}

View File

@ -3,7 +3,7 @@
use std::convert::TryInto;
use nest::{Error, Store, Value};
use rocket::form::FromForm;
use rocket::form::{FromForm};
use rocket::serde::{Deserialize, Serialize};
use serde_json::json;

View File

@ -684,7 +684,6 @@ html {
font-family: var(--sans-serif);
font-size: var(--font-size-7);
display: block;
margin-bottom: 2px;
}
.label-medium {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,57 @@
/*
behavioural layer for the `change_password.html.tera` template
- intercept button click for save (form submission of passwords)
- perform json api call
- update the dom
methods:
PEACH_AUTH.changePassword();
*/
var PEACH_AUTH = {};
// catch click of 'Save' button and make POST request
PEACH_AUTH.changePassword = function() {
document.addEventListener('DOMContentLoaded', function() {
document.body.addEventListener('submit', function(e) {
// prevent redirect on button press (default behavior)
e.preventDefault();
// capture form data
var formElement = document.querySelector("form");
// create form data object from the changePassword form element
var formData = new FormData(formElement);
var object = {};
// assign values from form
formData.forEach(function(value, key){
object[key] = value;
});
// perform json serialization
console.log(object);
var jsonData = JSON.stringify(object);
// write in-progress status message to ui
PEACH.flashMsg("info", "Saving new password.");
// send change_password POST request
fetch("/api/v1/admin/change_password", {
method: "post",
headers: {
'Content-Type': 'application/json',
},
body: jsonData
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
})
}, false);
});
}
var changePassInstance = PEACH_AUTH;
changePassInstance.changePassword();

View File

@ -0,0 +1,46 @@
/*
*
* Common javascript functions shared by multiple pages:
* - flashMsg
* - logout
*
*/
var PEACH = {};
// display a message by appending a paragraph element
PEACH.flashMsg = function(status, msg) {
// set the class of the element according to status
var elementClass;
if (status === "success") {
elementClass = "capsule center-text flash-message font-success";
} else if (status === "info") {
elementClass = "capsule center-text flash-message font-info";
} else {
elementClass = "capsule center-text flash-message font-failure";
};
var flashElement = document.getElementById("flashMsg");
// if flashElement exists, update the class & text
if (flashElement) {
flashElement.className = elementClass;
flashElement.innerText = msg;
// if flashElement does not exist, create it, set id, class, text & append
} else {
// create new div for flash message
var flashDiv = document.createElement("DIV");
// set div attributes
flashDiv.id = "flashMsg";
flashDiv.className = elementClass;
// add json response message to flash message div
var flashMsg = document.createTextNode(msg);
flashDiv.appendChild(flashMsg);
// insert the flash message div below the button div
var buttonDiv = document.getElementById("buttonDiv");
// flashDiv will be added to the end since buttonDiv is the last
// child within the parent element (card-container div)
buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling);
}
}
var commonInstance = PEACH;

View File

@ -0,0 +1,63 @@
/*
behavioural layer for the `configure_dns.html.tera` template,
corresponding to the web route `/settings/network/dns`
- intercept button click for save (form submission of dns settings)
- perform json api call
- update the dom
*/
var PEACH_DNS = {};
// catch click of 'Add' button and make POST request
PEACH_DNS.configureDns = function() {
document.addEventListener('DOMContentLoaded', function() {
document.body.addEventListener('submit', function(e) {
// prevent redirect on button press (default behavior)
e.preventDefault();
// capture form data
var formElement = document.querySelector("form");
// create form data object from the configureDNS form element
var formData = new FormData(formElement);
var object = {};
// set checkbox to false (the value is only passed to formData if it is "on")
object["enable_dyndns"] = false;
// assign values from form
formData.forEach(function(value, key){
// convert checkbox to bool
if (key === "enable_dyndns") {
value = (value === "on");
}
object[key] = value;
});
// perform json serialization
console.log(object);
var jsonData = JSON.stringify(object);
// write in-progress status message to ui
PEACH.flashMsg("info", "Saving new DNS configurations");
// send add_wifi POST request
fetch("/api/v1/network/dns/configure", {
method: "post",
headers: {
'Content-Type': 'application/json',
},
body: jsonData
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
let statusIndicator = document.getElementById("dyndns-status-indicator");
// only remove the "dyndns-status-indicator" element if it exists
if (statusIndicator != null ) statusIndicator.remove();
})
}, false);
});
}
var configureDnsInstance = PEACH_DNS;
configureDnsInstance.configureDns();

View File

@ -0,0 +1,57 @@
/*
behavioural layer for the `network_add.html.tera` template,
corresponding to the web route `/network/wifi/add`
- intercept button click for add (form submission of credentials)
- perform json api call
- update the dom
methods:
PEACH_NETWORK.add();
*/
var PEACH_NETWORK = {};
// catch click of 'Add' button and make POST request
PEACH_NETWORK.add = function() {
document.addEventListener('DOMContentLoaded', function() {
document.body.addEventListener('submit', function(e) {
// prevent redirect on button press (default behavior)
e.preventDefault();
// capture form data
var formElement = document.querySelector("form");
// create form data object from the wifiCreds form element
var formData = new FormData(formElement);
var object = {};
// assign ssid and pass from form
formData.forEach(function(value, key){
object[key] = value;
});
// perform json serialization
var jsonData = JSON.stringify(object);
// write in-progress status message to ui
PEACH.flashMsg("info", "Adding WiFi credentials...");
// send add_wifi POST request
fetch("/api/v1/network/wifi", {
method: "post",
headers: {
'Content-Type': 'application/json',
},
body: jsonData
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
})
}, false);
});
}
var addInstance = PEACH_NETWORK;
addInstance.add();

View File

@ -0,0 +1,133 @@
/*
behavioural layer for the `network_card.html.tera` template,
corresponding to the web route `/settings/network`
- intercept form submissions
- perform json api calls
- update the dom
methods:
PEACH_NETWORK.activateAp();
PEACH_NETWORK.activateClient();
PEACH_NETWORK.apMode();
PEACH_NETWORK.clientMode();
*/
var PEACH_NETWORK = {};
// catch click of 'Deploy Access Point' and make POST request
PEACH_NETWORK.activateAp = function() {
document.addEventListener('DOMContentLoaded', function() {
var deployAP = document.getElementById('deployAccessPoint');
if (deployAP) {
deployAP.addEventListener('click', function(e) {
// prevent form submission (default behavior)
e.preventDefault();
// write in-progress status message to ui
PEACH.flashMsg("info", "Deploying access point...");
// send activate_ap POST request
fetch("/api/v1/network/activate_ap", {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
console.log(jsonData.msg);
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
// if ap activation is successful, update the ui
if (jsonData.status === "success") {
PEACH_NETWORK.apMode();
}
})
}, false);
}
});
}
// catch click of 'Enable WiFi' and make POST request
PEACH_NETWORK.activateClient = function() {
document.addEventListener('DOMContentLoaded', function() {
var enableWifi = document.getElementById('connectWifi');
if (enableWifi) {
enableWifi.addEventListener('click', function(e) {
// prevent form submission (default behavior)
e.preventDefault();
// write in-progress status message to ui
PEACH.flashMsg("info", "Enabling WiFi client...");
// send activate_ap POST request
fetch("/api/v1/network/activate_client", {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
console.log(jsonData.msg);
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
// if client activation is successful, update the ui
if (jsonData.status === "success") {
PEACH_NETWORK.clientMode();
}
})
}, false);
}
});
}
// replace 'Deploy Access Point' button with 'Enable WiFi' button
PEACH_NETWORK.apMode = function() {
// create Enable WiFi button and add it to button div
var wifiButton = document.createElement("A");
wifiButton.className = "button center";
wifiButton.href = "/settings/network/wifi/activate";
wifiButton.id = "connectWifi";
var label = "Enable WiFi";
var buttonText = document.createTextNode(label);
wifiButton.appendChild(buttonText);
// append the new button to the buttons div
let buttons = document.getElementById("buttons");
buttons.appendChild(wifiButton);
// remove the old 'Deploy Access Point' button
let apButton = document.getElementById("deployAccessPoint");
apButton.remove();
}
// replace 'Enable WiFi' button with 'Deploy Access Point' button
PEACH_NETWORK.clientMode = function() {
// create Deploy Access Point button and add it to button div
var apButton = document.createElement("A");
apButton.className = "button center";
apButton.href = "/settings/network/ap/activate";
apButton.id = "deployAccessPoint";
var label = "Deploy Access Point";
var buttonText = document.createTextNode(label);
apButton.appendChild(buttonText);
// append the new button to the buttons div
let buttons = document.getElementById("buttons");
buttons.appendChild(apButton);
// remove the old 'Enable Wifi' button
let wifiButton = document.getElementById("connectWifi");
wifiButton.remove();
}
var networkInstance = PEACH_NETWORK;
networkInstance.activateAp();
networkInstance.activateClient();

View File

@ -0,0 +1,131 @@
/*
behavioural layer for the `network_detail.html.tera` template,
corresponding to the web route `/settings/network/wifi?<ssid>`
- intercept button clicks for connect, disconnect and forget
- perform json api call
- update the dom
methods:
PEACH_NETWORK.connect();
PEACH_NETWORK.disconnect();
PEACH_NETWORK.forget();
*/
var PEACH_NETWORK = {};
// catch click of 'Connect' button (form) and make POST request
PEACH_NETWORK.connect = function() {
document.addEventListener('DOMContentLoaded', function() {
var connectWifi = document.getElementById('connectWifi');
if (connectWifi) {
connectWifi.addEventListener('click', function(e) {
// prevent form submission (default behavior)
e.preventDefault();
// retrieve ssid value and append to form data object
var ssid = document.getElementById('connectSsid').value;
// create key:value pair
var ssidData = { ssid: ssid };
// perform json serialization
var jsonData = JSON.stringify(ssidData);
// write in-progress status message to ui
PEACH.flashMsg("info", "Connecting to access point...");
// send add_wifi POST request
fetch("/api/v1/network/wifi/connect", {
method: "post",
headers: {
'Content-Type': 'application/json',
},
body: jsonData
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
})
}, false);
};
});
}
// catch click of 'Disconnect' button and make POST request
PEACH_NETWORK.disconnect = function() {
document.addEventListener('DOMContentLoaded', function() {
var disconnectWifi = document.getElementById('disconnectWifi');
if (disconnectWifi) {
disconnectWifi.addEventListener('click', function(e) {
// prevent form submission (default behavior)
e.preventDefault();
// retrieve ssid value and append to form data object
var ssid = document.getElementById('disconnectSsid').value;
// create key:value pair
var ssidData = { ssid: ssid };
// perform json serialization
var jsonData = JSON.stringify(ssidData);
// write in-progress status message to ui
PEACH.flashMsg("info", "Disconnecting from access point...");
// send disconnect_wifi POST request
fetch("/api/v1/network/wifi/disconnect", {
method: "post",
headers: {
'Content-Type': 'application/json',
},
body: jsonData
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
})
}, false);
};
});
}
// catch click of 'Forget' button (form) and make POST request
PEACH_NETWORK.forget = function() {
document.addEventListener('DOMContentLoaded', function() {
var forgetWifi = document.getElementById('forgetWifi');
if (forgetWifi) {
forgetWifi.addEventListener('click', function(e) {
// prevent form submission (default behavior)
e.preventDefault();
// retrieve ssid value
var ssid = document.getElementById('forgetSsid').value;
// create key:value pair
var ssidData = { ssid: ssid };
// perform json serialization
var jsonData = JSON.stringify(ssidData);
// write in-progress status message to ui
PEACH.flashMsg("info", "Removing credentials for access point...");
// send forget_ap POST request
fetch("/api/v1/network/wifi/forget", {
method: "post",
headers: {
'Content-Type': 'application/json',
},
body: jsonData
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
})
}, false);
};
});
}
var detailInstance = PEACH_NETWORK;
detailInstance.connect();
detailInstance.disconnect();
detailInstance.forget();

View File

@ -0,0 +1,56 @@
/*
behavioural layer for the `network_modify.html.tera` template
- intercept button click for modify (form submission of credentials)
- perform json api call
- update the dom
methods:
PEACH_NETWORK.modify();
*/
var PEACH_NETWORK = {};
// catch click of 'Save' button and make POST request
PEACH_NETWORK.modify = function() {
document.addEventListener('DOMContentLoaded', function() {
document.body.addEventListener('submit', function(e) {
// prevent redirect on button press (default behavior)
e.preventDefault();
// capture form data
var formElement = document.querySelector("form");
// create form data object from the wifiModify form element
var formData = new FormData(formElement);
var object = {};
// assign ssid and pass from form
formData.forEach(function(value, key){
object[key] = value;
});
// perform json serialization
var jsonData = JSON.stringify(object);
// write in-progress status message to ui
PEACH.flashMsg("info", "Updating WiFi password...");
// send new_password POST request
fetch("/api/v1/network/wifi/modify", {
method: "post",
headers: {
'Content-Type': 'application/json',
},
body: jsonData
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
})
}, false);
});
}
var modifyInstance = PEACH_NETWORK;
modifyInstance.modify();

View File

@ -0,0 +1,139 @@
/*
behavioural layer for the `network_usage.html.tera` template,
corresponding to the web route `/settings/network/wifi/usage`
- intercept form submissions
- perform json api calls
- update the dom
methods:
PEACH_NETWORK.updateAlerts();
PEACH_NETWORK.resetUsage();
PEACH_NETWORK.toggleWarning();
PEACH_NETWORK.toggleCutoff();
*/
var PEACH_NETWORK = {};
// catch click of 'Update' and make POST request
PEACH_NETWORK.updateAlerts = function() {
document.addEventListener('DOMContentLoaded', function() {
document.body.addEventListener('submit', function(e) {
// prevent redirect on button press (default behavior)
e.preventDefault();
// capture form data
var formElement = document.querySelector("form");
let warn = formElement.elements.warn.value;
let cut = formElement.elements.cut.value;
let warn_flag = formElement.elements.warn_flag.checked;
let cut_flag = formElement.elements.cut_flag.checked;
// perform json serialization
var jsonData = JSON.stringify({
"warn": parseFloat(warn),
"cut": parseFloat(cut),
"warn_flag": warn_flag,
"cut_flag": cut_flag,
});
// send update_alerts POST request
fetch("/api/v1/network/wifi/usage", {
method: "post",
headers: {
'Content-Type': 'application/json',
},
body: jsonData
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
})
}, false);
});
}
// catch click of 'Reset' and make POST request
PEACH_NETWORK.resetUsage = function() {
document.addEventListener('DOMContentLoaded', function() {
var resetBtn = document.getElementById('resetTotal');
if (resetBtn) {
resetBtn.addEventListener('click', function(e) {
// prevent form submission (default behavior)
e.preventDefault();
// send reset_data_usage POST request
fetch("/api/v1/network/wifi/usage/reset", {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
console.log(jsonData.msg);
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
// if reset is successful, update the ui
if (jsonData.status === "success") {
console.log(jsonData.data);
PEACH_NETWORK.updateTotal(jsonData.data);
}
})
}, false);
}
});
}
// update data usage total in ui
PEACH_NETWORK.updateTotal = function(data) {
document.addEventListener('DOMContentLoaded', function() {
console.log(data);
let label = document.getElementById("dataTotal");
// take usage total as bytes, convert to MB and round to nearest integer
label.textContent = (data / 1024 / 1024).round();
});
};
// update ui for warning
PEACH_NETWORK.toggleWarning = function() {
document.addEventListener('DOMContentLoaded', function() {
let i = document.getElementById("warnIcon");
let warnCheck = document.getElementById("warnCheck");
warnCheck.addEventListener('click', function(e) {
console.log('Toggling warning icon state');
if (warnCheck.checked) {
i.className = "icon";
} else {
i.className = "icon icon-inactive";
}
});
});
};
// update ui for cutoff
PEACH_NETWORK.toggleCutoff = function() {
document.addEventListener('DOMContentLoaded', function() {
let i = document.getElementById("cutIcon");
let cutCheck = document.getElementById("cutCheck");
cutCheck.addEventListener('click', function(e) {
console.log('Toggling cutoff icon state');
if (cutCheck.checked) {
i.className = "icon";
} else {
i.className = "icon icon-inactive";
}
});
});
};
var usageInstance = PEACH_NETWORK;
usageInstance.resetUsage();
usageInstance.toggleWarning();
usageInstance.toggleCutoff();
usageInstance.updateAlerts();

View File

@ -0,0 +1,83 @@
/*
behavioural layer for the `power.html.tera` template,
corresponding to the web route `/power`
- intercept button clicks for reboot & shutdown
- perform json api calls
- update the dom
methods:
PEACH_DEVICE.reboot();
PEACH_DEVICE.shutdown();
*/
var PEACH_DEVICE = {};
// catch click of 'Reboot' button and make POST request
PEACH_DEVICE.reboot = function() {
document.addEventListener('DOMContentLoaded', function() {
var rebootDevice = document.getElementById('rebootBtn');
if (rebootDevice) {
rebootDevice.addEventListener('click', function(e) {
// prevent redirect on button press (default behavior)
e.preventDefault();
// write reboot flash message
PEACH.flashMsg("success", "Rebooting the device...");
// send reboot_device POST request
fetch("/api/v1/admin/reboot", {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
console.log(jsonData.msg);
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
})
}, false);
}
});
}
// catch click of 'Shutdown' button and make POST request
PEACH_DEVICE.shutdown = function() {
document.addEventListener('DOMContentLoaded', function() {
var shutdownDevice = document.getElementById('shutdownBtn');
if (shutdownDevice) {
shutdownDevice.addEventListener('click', function(e) {
// prevent form submission (default behavior)
e.preventDefault();
// write shutdown flash message
PEACH.flashMsg("success", "Shutting down the device...");
// send shutdown_device POST request
fetch("/api/v1/shutdown", {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
console.log(jsonData.msg);
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
})
}, false);
}
});
}
var deviceInstance = PEACH_DEVICE;
deviceInstance.reboot();
deviceInstance.shutdown();

View File

@ -0,0 +1,47 @@
/*
* behavioural layer for the `reset_password.html.tera` template,
*/
var PEACH_AUTH = {};
// catch click of 'Save' button and make POST request
PEACH_AUTH.resetPassword = function() {
document.addEventListener('DOMContentLoaded', function() {
document.body.addEventListener('submit', function(e) {
// prevent redirect on button press (default behavior)
e.preventDefault();
// capture form data
var formElement = document.querySelector("form");
// create form data object from the changePassword form element
var formData = new FormData(formElement);
var object = {};
// assign values from form
formData.forEach(function(value, key){
object[key] = value;
});
// perform json serialization
console.log(object);
var jsonData = JSON.stringify(object);
// write in-progress status message to ui
PEACH.flashMsg("info", "Saving new password.");
// send add_wifi POST request
fetch("/api/v1/admin/reset_password", {
method: "post",
headers: {
'Content-Type': 'application/json',
},
body: jsonData
})
.then( (response) => {
return response.json()
})
.then( (jsonData) => {
// write json response message to ui
PEACH.flashMsg(jsonData.status, jsonData.msg);
})
}, false);
});
}
var resetPassInstance = PEACH_AUTH;
resetPassInstance.resetPassword();

View File

@ -13,4 +13,5 @@
<body>
{% block nav %}{% endblock nav %}
</body>
<script type="text/javascript" src="/js/common.js"></script>
</html>

View File

@ -12,6 +12,10 @@
<!-- display error message -->
<div class="center-text flash-message font-failure" style="padding-left: 5px;">{{ flash_msg }}.</div>
{%- endif %}
<!-- share ux information with the user if JS is disabled -->
<noscript>
<p class="center-text flash-message">This website will be unresponsive while the device shuts down or reboots.</p>
</noscript>
</div>
</div>
{%- endblock card -%}

View File

@ -24,16 +24,13 @@
</div>
</a>
<!-- middle -->
<a class="middle">
<div class="circle circle-large"></div>
<a class="middle" href="/hello">
<div class="circle circle-large">
</div>
</a>
<!-- bottom-left -->
<!-- SYSTEM STATUS LINK AND ICON -->
{%- if standalone_mode == true -%}
<a class="bottom-left" href="/status/scuttlebutt" title="Status">
{% else -%}
<a class="bottom-left" href="/status" title="Status">
{%- endif -%}
<div class="circle circle-small">
<img class="icon-medium" src="/icons/heart-pulse.svg">
</div>

View File

@ -10,6 +10,9 @@
</div>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %}
</div>
</div>
<script type="text/javascript" src="/js/power_menu.js"></script>
{%- endblock card -%}

View File

@ -5,6 +5,8 @@
<div class="card-container">
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %}
</div>
</div>
{%- endblock card -%}

View File

@ -10,8 +10,13 @@
<a class="button button-secondary center" href="/settings/admin/configure" title="Cancel">Cancel</a>
</div>
</form>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %}
</div>
</div>
{%- endblock card -%}

View File

@ -3,7 +3,7 @@
<!-- CHANGE PASSWORD FORM -->
<div class="card center">
<div class="form-container">
<form id="changePassword" action="/settings/admin/change_password" method="post">
<form id="changePassword" action="/settings/change_password" method="post">
<!-- input for current password -->
<input id="currentPassword" class="center input" name="current_password" type="password" placeholder="Current password" title="Current password" autofocus>
<!-- input for new password -->
@ -17,6 +17,9 @@
</form>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %}
</div>
</div>
<script type="text/javascript" src="/js/change_password.js"></script>
{%- endblock card -%}

View File

@ -20,7 +20,11 @@
{% endif %}
<a class="button button-primary center full-width" style="margin-top: 25px;" href="/settings/admin/add" title="Add Admin">Add Admin</a>
</div>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %}
</div>
{%- endblock card -%}

View File

@ -14,5 +14,7 @@
</form>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %}
</div>
{%- endblock card -%}

View File

@ -35,8 +35,14 @@
<input id="changePasswordButton" class="button button-primary center" title="Add" type="submit" value="Save">
</div>
</form>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %}
</div>
</div>
<script type="text/javascript" src="/js/reset_password.js"></script>
{%- endblock card -%}

View File

@ -3,7 +3,7 @@
<!-- NETWORK ADD CREDENTIALS FORM -->
<div class="card center">
<div class="card-container">
<form id="wifiCreds" action="/settings/network/wifi/add" method="post">
<form id="wifiCreds" action="/network/wifi/add" method="post">
<!-- input for network ssid -->
<input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value="{%- if selected -%}{{ selected }}{%- endif -%}" autofocus>
<!-- input for network password -->
@ -15,6 +15,9 @@
</form>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %}
</div>
</div>
<script type="text/javascript" src="/js/network_add.js"></script>
{%- endblock card -%}

View File

@ -69,6 +69,7 @@
{% include "snippets/flash_message" %}
</div>
</div>
<script type="text/javascript" src="/js/network_detail.js"></script>
{%- endif -%}
{%- endfor -%}
{%- endif -%}

View File

@ -2,7 +2,9 @@
{%- block card %}
<!-- CONFIGURE DNS FORM -->
<div class="card center">
<div class="form-container">
{% if enable_dyndns %}
<!-- DYNDNS STATUS INDICATOR -->
<div id="dyndns-status-indicator" class="stack capsule{% if is_dyndns_online %} success-border{% else %} warning-border{% endif %}">
@ -15,6 +17,7 @@
</div>
</div>
{% endif %}
<form id="configureDNS" action="/settings/network/dns" method="post">
<div class="input-wrapper">
<!-- input for externaldomain -->
@ -22,6 +25,7 @@
<label class="label-small input-label font-gray" for="external_domain" style="padding-top: 0.25rem;">External Domain (optional)</label>
<input id="external_domain" class="form-input" style="margin-bottom: 0;"
name="external_domain" type="text" title="external domain" value="{{ external_domain }}"></label>
</div>
<div class="input-wrapper">
<div>
@ -37,13 +41,16 @@
<label id="cut" class="label-small input-label font-near-black">
<label class="label-small input-label font-gray" for="cut" style="padding-top: 0.25rem;">Dynamic DNS Domain</label>
<input id="dyndns_domain" class="alert-input" name="dynamic_domain" placeholder="" type="text" title="dyndns_domain" value="{{ dyndns_subdomain }}">.dyn.peachcloud.org</label>
</div>
</div>
<div id="buttonDiv">
<input id="configureDNSButton" class="button button-primary center" title="Add" type="submit" value="Save">
</div>
</form>
<!-- FLASH MESSAGE -->
<!-- FLASH MESSAGE -->
<!-- check for flash message and display accordingly -->
{% if flash_msg and flash_name == "success" %}
<!-- display success message -->
@ -55,6 +62,14 @@
<!-- display error message -->
<div class="capsule center-text flash-message font-failure">{{ flash_msg }}.</div>
{%- endif -%}
<!-- share ux information with the user if JS is disabled -->
<noscript>
<div class="capsule flash-message info-border">
<p class="center-text">This website may be temporarily unresponsive while settings are being saved.</p>
</div>
</noscript>
</div>
</div>
<script type="text/javascript" src="/js/configure_dns.js"></script>
{%- endblock card -%}

View File

@ -1,19 +1,10 @@
{%- extends "nav" -%}
{%- block card -%}
{# ASSIGN VARIABLES #}
{# ---------------- #}
{%- if data_total -%}
{% set data_usage_total = data_total.total / 1024 / 1024 | round -%}
{%- else -%}
{% set data_usage_total = "x" -%}
{% endif -%}
<!-- NETWORK DATA ALERTS VIEW -->
<form id="wifiAlerts" action="/settings/network/wifi/usage" class="card center" method="post">
<div class="stack capsule" style="margin-left: 2rem; margin-right: 2rem;">
<div class="flex-grid">
<label id="dataTotal" class="label-large" title="Data download total in MB">
{{ data_usage_total }}
</label>
<label id="dataTotal" class="label-large" title="Data download total in MB">{{ data_total.total / 1024 / 1024 | round }}</label>
<label class="label-small font-near-black">MB</label>
</div>
<label class="center-text label-small font-gray">USAGE TOTAL</label>
@ -52,4 +43,5 @@
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</form>
<script type="text/javascript" src="/js/network_usage.js"></script>
{%- endblock card %}

View File

@ -20,4 +20,5 @@
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</div>
<script type="text/javascript" src="/js/network_card.js"></script>
{%- endblock card -%}

View File

@ -17,4 +17,5 @@
{% include "snippets/flash_message" %}
</div>
</div>
<script type="text/javascript" src="/js/network_modify.js"></script>
{%- endblock card -%}

View File

@ -4,13 +4,15 @@
<div class="card center">
<!-- BUTTONS -->
<div id="settingsButtons">
<a id="configureSbot" class="button button-primary center" href="/settings/scuttlebutt/configure" title="Configure Sbot">Configure Sbot</a>
<a id="networkKey" class="button button-primary center" href="/settings/scuttlebutt/network_key" title="Set Network Key">Set Network Key</a>
<a id="replicationHops" class="button button-primary center" href="/settings/scuttlebutt/hops" title="Set Replication Hops">Set Replication Hops</a>
<a id="removeFeeds" class="button button-primary center" href="/settings/scuttlebutt/remove_feeds" title="Remove Blocked Feeds">Remove Blocked Feeds</a>
<a id="setDirectory" class="button button-primary center" href="/settings/scuttlebutt/set_directory" title="Set Database Directory">Set Database Directory</a>
<a id="checkFilesystem" class="button button-primary center" href="/settings/scuttlebutt/check_fs" title="Check Filesystem">Check Filesystem</a>
<a id="repairFilesystem" class="button button-primary center" href="/settings/scuttlebutt/repair" title="Repair Filesystem">Repair Filesystem</a>
<a id="disable" class="button button-primary center" href="/settings/scuttlebutt/disable" title="Disable Sbot">Disable Sbot</a>
<a id="enable" class="button button-primary center" href="/settings/scuttlebutt/enable" title="Enable Sbot">Enable Sbot</a>
<a id="restart" class="button button-primary center" href="/settings/scuttlebutt/restart" title="Restart Sbot">Restart Sbot</a>
<a id="checkFilesystem" class="button button-primary center" href="/settings/scuttlebutt/check_fs" title="Check Filesystem">Check Filesystem</a>
<a id="repairFilesystem" class="button button-primary center" href="/settings/scuttlebutt/repair" title="Repair Filesystem">Repair Filesystem</a>
<a id="removeFeeds" class="button button-primary center" href="/settings/scuttlebutt/remove_feeds" title="Remove Blocked Feeds">Remove Blocked Feeds</a>
</div>
</div>
{%- endblock card -%}

View File

@ -1,55 +0,0 @@
{%- extends "nav" -%}
{%- block card %}
<!-- SBOT CONFIGURATION FORM -->
<div class="card center">
<form>
<div class="center" style="display: flex; flex-direction: column; width: 80%;" title="Number of hops to replicate">
<label for="hops" class="label-small">HOPS</label>
<div id="hops" style="display: flex; justify-content: space-evenly;">
<input type="radio" id="hops_0" name="hops" value="0">
<label for="hops_0">0</label>
<input type="radio" id="hops_1" name="hops" value="1">
<label for="hops_1">1</label>
<input type="radio" id="hops_2" name="hops" value="2">
<label for="hops_2">2</label>
<input type="radio" id="hops_3" name="hops" value="3">
<label for="hops_3">3</label>
<input type="radio" id="hops_4" name="hops" value="4">
<label for="hops_4">4</label><br>
</div>
</div>
<br>
<div class="center" style="display: flex; justify-content: space-between; width: 80%;">
<div style="display: flex; flex-direction: column; width: 60%;" title="IP address on which the sbot runs">
<label for="ip" class="label-small">IP ADDRESS</label>
<input type="text" id="ip" name="ip">
</div>
<div style="display: flex; flex-direction: column; width: 20%;" title="Port on which the sbot runs">
<label for="port" class="label-small">PORT</label>
<input type="text" id="port" name="port">
</div>
</div>
<br>
<div class="center" style="display: flex; flex-direction: column; width: 80%;" title="Network key (aka 'caps key') to define the Scuttleverse in which the sbot operates in">
<label for="network_key" class="label-small">NETWORK KEY</label>
<input type="text" id="network_key" name="network_key">
</div>
<br>
<div class="center" style="display: flex; flex-direction: column; width: 80%;" title="Directory in which the sbot database is saved">
<label for="database_dir" class="label-small">DATABASE DIRECTORY</label>
<input type="text" id="database_dir" name="database_dir">
</div>
<br>
<div class="center" style="width: 80%;" title="Broadcast the IP and port of this sbot instance so that local peers can discovery it and attempt to connect">
<input type="checkbox" id="lan_broadcast" name="lan_broadcast">
<label for="lan_broadcast">Enable LAN Broadcasting</label><br>
<br>
<input type="checkbox" id="lan_discovery" name="lan_discovery" title="Listen for the presence of local peers and attempt to connect if found">
<label for="lan_discovery">Enable LAN Discovery</label><br>
</div>
<br>
<input class="button button-primary center" type="button" value="Save">
<input class="button button-warning center" type="button" value="Restore Defaults">
</form>
</div>
{%- endblock card -%}

View File

@ -0,0 +1,6 @@
<!-- share ux information with the user if JS is disabled -->
<noscript>
<div class="capsule flash-message info-border">
<p class="center-text">This website may be temporarily unresponsive while settings are being saved.</p>
</div>
</noscript>

View File

@ -25,13 +25,13 @@
{# Display microservice status for network, oled & stats #}
<!-- PEACH-NETWORK STATUS STACK -->
<a class="link" href="/status/network">
<div class="stack capsule{% if network_ping == "ONLINE" %} success-border{% else %} warning-border{% endif %}">
<img id="networkIcon" class="icon{% if network_ping == "OFFLINE" %} icon-inactive{% endif %} icon-medium" alt="Network" title="Network microservice status" src="/icons/wifi.svg">
<div class="stack" style="padding-top: 0.5rem;">
<label class="label-small font-near-black">Networking</label>
<label class="label-small font-near-black">{{ network_ping }}</label>
</div>
<div class="stack capsule{% if network_ping == "ONLINE" %} success-border{% else %} warning-border{% endif %}">
<img id="networkIcon" class="icon{% if network_ping == "OFFLINE" %} icon-inactive{% endif %} icon-medium" alt="Network" title="Network microservice status" src="/icons/wifi.svg">
<div class="stack" style="padding-top: 0.5rem;">
<label class="label-small font-near-black">Networking</label>
<label class="label-small font-near-black">{{ network_ping }}</label>
</div>
</div>
</a>
<!-- PEACH-OLED STATUS STACK -->
<div class="stack capsule{% if oled_ping == "ONLINE" %} success-border{% else %} warning-border{% endif %}">
@ -42,11 +42,11 @@
</div>
</div>
<!-- PEACH-STATS STATUS STACK -->
<div class="stack capsule success-border">
<img id="statsIcon" class="icon icon-medium" alt="Stats" title="System statistics microservice status" src="/icons/chart.svg">
<div class="stack capsule{% if stats_ping == "ONLINE" %} success-border{% else %} warning-border{% endif %}">
<img id="statsIcon" class="icon{% if stats_ping == "OFFLINE" %} icon-inactive{% endif %} icon-medium" alt="Stats" title="System statistics microservice status" src="/icons/chart.svg">
<div class="stack" style="padding-top: 0.5rem;">
<label class="label-small font-near-black">Statistics</label>
<label class="label-small font-near-black">AVAILABLE</label>
<label class="label-small font-near-black">{{ stats_ping }}</label>
</div>
</div>
{# Display status for dynsdns, config & sbot #}
@ -67,16 +67,14 @@
</div>
</div>
<!-- SBOT STATUS STACK -->
<a class="link" href="/status/scuttlebutt">
<div class="stack capsule{% if sbot_is_online %} success-border{% else %} warning-border{% endif %}">
<img id="networkIcon" class="icon{% if sbot_is_online != true %} icon-inactive{% endif %} icon-medium" alt="Sbot" title="Sbot status" src="/icons/hermies.svg">
<div class="stack" style="padding-top: 0.5rem;">
<label class="label-small font-near-black">Sbot</label>
<label class="label-small font-near-black">{% if sbot_is_online %} ONLINE {% else %} OFFLINE {% endif %} </label>
</div>
<div class="stack capsule{% if sbot_is_online %} success-border{% else %} warning-border{% endif %}">
<img id="networkIcon" class="icon{% if sbot_is_online != true %} icon-inactive{% endif %} icon-medium" alt="Sbot" title="Sbot status" src="/icons/hermies.svg">
<div class="stack" style="padding-top: 0.5rem;">
<label class="label-small font-near-black">Sbot</label>
<label class="label-small font-near-black">{% if sbot_is_online %} ONLINE {% else %} OFFLINE {% endif %} </label>
</div>
</div>
</a>
</div>
<div class="card-container">
{# Display CPU usage meter #}
{%- if cpu_stat_percent -%}

View File

@ -1,87 +0,0 @@
{%- extends "nav" -%}
{%- block card %}
<!-- SCUTTLEBUTT STATUS -->
<div class="card center">
<!-- NETWORK INFO BOX -->
<div class="capsule capsule-container success-border">
<!-- NETWORK STATUS GRID -->
<div class="two-grid" title="PeachCloud network mode and status">
<!-- top-right config icon -->
<a class="link two-grid-top-right" href="/settings/scuttlebutt" title="Configure Scuttlebutt settings">
<img id="configureNetworking" class="icon-small" src="/icons/cog.svg" alt="Configure">
</a>
<!-- left column -->
<!-- network mode icon with label -->
<div class="grid-column-1">
<img id="netModeIcon" class="center icon icon-active" src="/icons/hermies.svg" alt="WiFi router">
<label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" style="margin-top: 0.5rem;" title="Access Point Online">ACTIVE</label>
</div>
<!-- right column -->
<!-- network mode, ssid & ip with labels -->
<div class="grid-column-2">
<label class="label-small font-gray" for="netMode" title="Network Mode">VERSION</label>
<p id="netMode" class="card-text" title="Network Mode">1.1.0-alpha</p>
<label class="label-small font-gray" for="netSsid" title="Access Point SSID" style="margin-top: 0.5rem;">UPTIME</label>
<p id="netSsid" class="card-text" title="SSID">3 days & 7 hours</p>
</div>
</div>
<div id="middleSection" style="margin-top: 1rem;">
<div id="ssbSocialCounts" class="center" style="display: flex; justify-content: space-between; width: 90%;">
<div style="display: flex; align-items: last baseline;">
<label class="card-text" style="margin-right: 2px;">21</label>
<label class="label-small font-gray">Friends</label>
</div>
<div style="display: flex; align-items: last baseline;">
<label class="card-text" style="margin-right: 2px;">5</label>
<label class="label-small font-gray">Follows</label>
</div>
<div style="display: flex; align-items: last baseline;">
<label class="card-text" style="margin-right: 2px;">38</label>
<label class="label-small font-gray">Followers</label>
</div>
<div style="display: flex; align-items: last baseline;">
<label class="card-text" style="margin-right: 2px;">2</label>
<label class="label-small font-gray">Blocks</label>
</div>
</div>
</div>
<!--
<p class="card-text">Latest sequence number: </p>
<p class="card-text">Network key: </p>
<p>Process uptime: </p>
<p>Sbot version: </p>
<p>Replication hops: </p>
<p>Process status: </p>
<p>Blobstore size: </p>
<p>Last time you visited this page, latest sequence was x ... now it's y</p>
<p>Number of follows / followers / friends / blocks</p>
-->
<!-- THREE-ACROSS STACK -->
<div class="three-grid card-container" style="margin-top: 1rem;">
<div class="stack">
<img class="icon" title="Hops" src="/icons/orbits.png">
<div class="flex-grid" style="padding-top: 0.5rem;">
<label class="label-medium" style="padding-right: 3px;" title="Replication hops">2</label>
</div>
<label class="label-small font-gray">HOPS</label>
</div>
<div class="stack">
<img class="icon" title="Blobs" src="/icons/image-file.png">
<div class="flex-grid" style="padding-top: 0.5rem;">
<label class="label-medium" style="padding-right: 3px;" title="Blobstore size in MB">163</label>
<label class="label-small font-near-black">MB</label>
</div>
<label class="label-small font-gray">BLOBSTORE</label>
</div>
<div class="stack">
<img class="icon" title="Memory" src="/icons/ram.png">
<div class="flex-grid" style="padding-top: 0.5rem;">
<label class="label-medium" style="padding-right: 3px;" title="Memory usage of the go-sbot process in MB">47</label>
<label class="label-small font-near-black">MB</label>
</div>
<label class="label-small font-gray">MEMORY</label>
</div>
</div>
</div>
</div>
{%- endblock card -%}