99 Commits

Author SHA1 Message Date
a5415aad99 merge direct_call_net_stats branch 2022-01-12 11:35:55 +02:00
037e5c34b6 merge replace_rust_crypto branch 2022-01-12 11:21:18 +02:00
699f2b13c9 merge update_network_args branch 2022-01-12 11:19:24 +02:00
c3fbc5cd73 try operator for dyns dns domain check 2022-01-12 10:59:13 +02:00
4a27892ab6 idiomatic paths and result type for checking new dns address 2022-01-12 10:58:36 +02:00
4adf5547c9 formatting 2022-01-12 10:58:11 +02:00
bdfbd7057f Remove commented out code 2022-01-11 18:10:36 -05:00
171d051710 Fix clippy warmings 2022-01-11 18:06:51 -05:00
1ea0ea2ed1 Fix regression of peach-dyndns-updater 2022-01-11 18:03:07 -05:00
57ed0ab66a add fullstop to docs sentence 2022-01-06 11:56:45 +02:00
49ad74595c cleanup use paths and leave network_ping note 2022-01-06 11:56:23 +02:00
17d52c771f Merge branch 'main' into direct_call_net_stats
Merge crypto library update for peach-lib.
2022-01-04 18:34:21 +02:00
6792e4702d Merge pull request 'Replace outdated crypto crate' (#61) from replace_rust_crypto into main
Reviewed-on: #61
2022-01-04 16:33:24 +00:00
446927f587 replace outdated crypto crate 2022-01-04 15:23:41 +02:00
567b0bbc2a replace network rpc client calls with direct calls to peach_network 2022-01-04 14:55:17 +02:00
3ab3e65eb7 replace stats rpc client calls with direct calls to peach_stats 2022-01-04 14:06:57 +02:00
a0e80fcda7 add deps for network and stats 2022-01-04 14:06:35 +02:00
731bc1958b Merge pull request 'Update network args and remove structs' (#60) from update_network_args into main
Reviewed-on: #60
2022-01-04 08:38:23 +00:00
58f2ddde05 replace String with str and remove unnecessary structs 2022-01-03 11:55:37 +02:00
4b0b2626a4 update code examples 2022-01-03 11:55:20 +02:00
a05e67c22f bump patch version 2022-01-03 11:55:06 +02:00
c75608fb1a Merge branch 'main' of https://git.coopcloud.tech/PeachCloud/peach-workspace into main3 2021-12-22 12:19:56 -05:00
068d3430d7 Merge pull request 'Add permissions function peach-config' (#56) from permissions into main
Reviewed-on: #56
2021-12-22 17:18:24 +00:00
62793f401e Change imports and add permissions for peach-web dir 2021-12-22 10:04:15 -05:00
b8f394b901 Debugging dyndns 2021-12-22 09:59:20 -05:00
9324b3ec0b Merge pull request 'Copy Rocket.toml to /usr/share/peach-web' (#55) from copy-rocket-toml into main
Reviewed-on: #55
2021-12-22 14:53:21 +00:00
f43fbf19f5 Merge pull request 'Add changepassword function to peach-config' (#53) from change-password into main
Reviewed-on: #53
2021-12-22 14:51:27 +00:00
29cc40be48 Fix setup of nsupdate 2021-12-18 11:24:43 -05:00
570f6a679b Change permissions to u+rwX,g+rwX 2021-12-18 10:22:50 -05:00
399af51ccc Add permissions function peach-config 2021-12-18 10:00:40 -05:00
94bac00664 Fix typo in secret_key 2021-12-18 09:22:55 -05:00
c41dae8d04 Copy Rocket.toml to /usr/share/peach-web 2021-12-17 17:23:27 -05:00
e34df3b656 Remove configuration of http basic auth 2021-12-17 17:19:04 -05:00
3399a3c80f Add changepassword function to peach-config 2021-12-17 16:23:47 -05:00
1c26cb70fa Merge pull request 'Bump version number for peach-config' (#51) from version-number into main
Reviewed-on: #51
2021-12-17 17:38:14 +00:00
c79bd4b19f Bump version number for peach-config 2021-12-17 12:37:43 -05:00
7743511923 Merge pull request 'Update kernel version to 4.19.0-18-arm64' (#50) from kernel-version into main
Reviewed-on: #50
2021-12-17 17:15:36 +00:00
10833078fa Update kernel version to 4.19.0-18-arm64 2021-12-17 12:15:02 -05:00
244a2132fa Merge pull request 'Move cargo/.config to root of workspace' (#49) from workspace into main
Reviewed-on: #49
2021-12-17 15:59:14 +00:00
f737236abc Move cargo/.config to root of workspace 2021-12-16 12:55:14 -05:00
b5ce677a5b Merge pull request 'Removed hardcoded interfaces from peach-network' (#47) from iface_agnostic into main
Reviewed-on: #47
2021-12-14 20:42:40 +00:00
4d6dbd511e Merge pull request 'Remove jsonrpc from peach-network' (#46) from split_network_logic into main
Reviewed-on: #46
2021-12-14 18:55:16 +00:00
7fe4715014 bump version and switch to rust 2021 2021-12-14 16:13:27 +02:00
dd33fdd47d Merge pull request 'Create JSON-RPC server repo with stats' (#44) from jsonrpc_server into main
Reviewed-on: #44
2021-12-14 13:05:00 +00:00
1986d31461 feature flag docs and license 2021-12-13 11:00:16 +02:00
a824be53b9 update docs and remove unnecessary structs 2021-12-13 10:55:27 +02:00
287082381e remove json-rpc methods 2021-12-13 10:55:04 +02:00
9f40378fce remove json-rpc error code and improve docs 2021-12-13 10:54:51 +02:00
4f5eb3aa04 update dependencies and version 2021-12-13 10:54:38 +02:00
f4113f0632 remove debian files and main 2021-12-13 10:54:24 +02:00
8032b83c41 resolve merge conflicts 2021-12-13 08:26:25 +02:00
dfa1306b2d update lockfile 2021-12-13 08:20:03 +02:00
2f7c7aac8f bump version and fix wpactrl dep import 2021-12-13 08:19:55 +02:00
46b7c0fc2b update wpactrl tests 2021-12-13 08:19:25 +02:00
f62c8f0b51 merge stats changes 2021-12-13 07:25:30 +02:00
06e48deb3a Merge pull request 'Remove jsonrpc from peach-stats' (#43) from separate_stats_logic into main
Reviewed-on: #43
2021-12-13 05:20:58 +00:00
fb6d0317b6 bump version 2021-12-13 07:19:14 +02:00
cfbf052d27 fix example code 2021-12-13 07:19:05 +02:00
d240741958 update lockfile 2021-12-10 11:02:06 +02:00
33486b4e1d jsonrpc server with peach-stats methods 2021-12-10 11:01:51 +02:00
8c3fecb875 add docs about feature flags 2021-12-09 09:54:53 +02:00
0907fbc474 remove jsonrpc from peach-stats 2021-12-09 09:44:27 +02:00
b747ff6db2 Merge pull request 'Use tuple err source PeachError::JsonRpcClientCore' (#42) from fix_client_error into main
Reviewed-on: #42
2021-12-08 08:10:07 +00:00
220c7fd540 use tuple err source 2021-12-08 10:07:58 +02:00
ed7e172efb Merge pull request 'Fix manifest mess for peach-lib from nanorand PR' (#40) from nanorand_again into main
Reviewed-on: #40
2021-12-07 12:16:47 +00:00
bc0f2d595b fix pr mess for peach-lib 2021-12-07 14:14:51 +02:00
61b33d1613 trying to resolve conflict for nanorand pr 2021-12-07 14:08:21 +02:00
c3fa188400 testing refactored wpactrl 2021-12-07 10:04:58 +02:00
a1444cf478 Merge pull request 'Error refactor for peach-lib' (#38) from lib_error_refactor into main
Reviewed-on: #38
2021-12-07 08:00:51 +00:00
79c94e6af0 replace snafu with custom error type 2021-12-01 14:26:30 +02:00
cd8e5737c4 implement custom error type 2021-11-30 13:48:16 +02:00
2429ea8fdd replace rand with nanorand and bump version 2021-11-25 15:00:19 +02:00
c8d0a2ddf6 Merge pull request 'Add ENV VAR to allow disabling Rocket authentication' (#36) from disable_auth into main
Reviewed-on: #36
2021-11-25 10:10:54 +00:00
adc1a5bd77 update lockfile 2021-11-25 11:33:25 +02:00
d760f9f92c add init_rocket test wrapper 2021-11-25 11:33:09 +02:00
5c4ef4a529 update auth docs 2021-11-25 11:15:39 +02:00
35ff408365 use rocket config to (en|dis)able auth 2021-11-25 11:12:50 +02:00
c2b785f54b remove unnecessary websocket dependency 2021-11-25 11:12:05 +02:00
1dc740eeae Merge pull request 'Custom error implementation for peach-oled' (#34) from lean_refactor into main
Reviewed-on: #34
2021-11-24 09:16:19 +00:00
b3c6138e03 Merge pull request 'Fix broken JS and tweak styling (CSS & HTML)' (#33) from js_and_style_fixes into main
Reviewed-on: #33
2021-11-24 09:15:50 +00:00
b59e62f920 bump crate version 2021-11-23 12:30:34 +02:00
3325706dcb document disable auth flag 2021-11-23 12:30:24 +02:00
bc28a84ad4 disable auth for tests 2021-11-23 12:30:09 +02:00
bb34bdd653 add auth flag check 2021-11-23 12:29:58 +02:00
ac98bde760 lockfile update 2021-11-23 10:53:09 +02:00
361b159299 update version number 2021-11-23 10:52:40 +02:00
7344d6f4e0 minor variable name changes and updated error handling 2021-11-23 10:52:26 +02:00
8d18e712a1 implement custom error 2021-11-23 10:51:54 +02:00
116afe78fd remove snafu and bump deps 2021-11-23 10:51:42 +02:00
7e3c500b1e Merge branch 'main' into js_and_style_fixes 2021-11-22 15:57:44 +02:00
b59eb22082 fix function names and remove flashMsg code duplication 2021-11-22 15:31:39 +02:00
ee1da0599c remove status update logic 2021-11-22 15:30:45 +02:00
e5f9a9be83 remove duplicate flashMsg code 2021-11-22 15:30:18 +02:00
e54ff8829a remove duplicate flashMsg code 2021-11-22 15:29:40 +02:00
554997a5c0 fix context back and title 2021-11-18 13:49:10 +02:00
da51070ccd improve template styling consistency 2021-11-18 11:48:37 +02:00
925051a379 improve not_found template 2021-11-18 11:48:03 +02:00
380ee2683a fix js 2021-11-18 11:47:50 +02:00
bae3b7c2ce tweak styling 2021-11-18 11:47:33 +02:00
79 changed files with 2301 additions and 3812 deletions

3
.gitignore vendored
View File

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

1084
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,4 +0,0 @@
[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] [package]
name = "peach-config" name = "peach-config"
version = "0.1.10" version = "0.1.15"
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"] authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
edition = "2018" edition = "2018"
description = "Command line tool for installing, updating and configuring PeachCloud" description = "Command line tool for installing, updating and configuring PeachCloud"
@ -35,3 +35,5 @@ structopt = "0.3.13"
clap = "2.33.3" clap = "2.33.3"
log = "0.4" log = "0.4"
lazy_static = "1.4.0" 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 # Apply device tree overlay to enable pull-up resistors for buttons
device_tree_overlay=overlays/mygpio.dtbo device_tree_overlay=overlays/mygpio.dtbo
kernel=vmlinuz-4.19.0-17-arm64 kernel=vmlinuz-4.19.0-18-arm64
# For details on the initramfs directive, see # For details on the initramfs directive, see
# https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=10532 # https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=10532
initramfs initrd.img-4.19.0-17-arm64 initramfs initrd.img-4.19.0-18-arm64

View File

@ -0,0 +1,35 @@
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

@ -1,4 +1,5 @@
#![allow(clippy::nonstandard_macro_braces)] #![allow(clippy::nonstandard_macro_braces)]
use peach_lib::error::PeachError;
pub use snafu::ResultExt; pub use snafu::ResultExt;
use snafu::Snafu; use snafu::Snafu;
@ -30,6 +31,10 @@ pub enum PeachConfigError {
}, },
#[snafu(display("Error serializing json: {}", source))] #[snafu(display("Error serializing json: {}", source))]
SerdeError { source: serde_json::Error }, 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 { impl From<std::io::Error> for PeachConfigError {

View File

@ -1,6 +1,8 @@
mod change_password;
mod constants; mod constants;
mod error; mod error;
mod generate_manifest; mod generate_manifest;
mod set_permissions;
mod setup_networking; mod setup_networking;
mod setup_peach; mod setup_peach;
mod setup_peach_deb; mod setup_peach_deb;
@ -12,10 +14,6 @@ use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use structopt::StructOpt; use structopt::StructOpt;
use crate::generate_manifest::generate_manifest;
use crate::setup_peach::setup_peach;
use crate::update::update;
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
#[structopt( #[structopt(
name = "peach-config", name = "peach-config",
@ -44,6 +42,14 @@ enum PeachConfig {
/// Updates all PeachCloud microservices /// Updates all PeachCloud microservices
#[structopt(name = "update")] #[structopt(name = "update")]
Update(UpdateOpts), 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)] #[derive(StructOpt, Debug)]
@ -76,6 +82,14 @@ pub struct UpdateOpts {
list: bool, 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! { arg_enum! {
/// enum options for real-time clock choices /// enum options for real-time clock choices
#[derive(Debug)] #[derive(Debug)]
@ -99,28 +113,48 @@ fn main() {
if let Some(subcommand) = opt.commands { if let Some(subcommand) = opt.commands {
match subcommand { match subcommand {
PeachConfig::Setup(cfg) => { PeachConfig::Setup(cfg) => {
match setup_peach(cfg.no_input, cfg.default_locale, cfg.i2c, cfg.rtc) { match setup_peach::setup_peach(cfg.no_input, cfg.default_locale, cfg.i2c, cfg.rtc) {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("peach-config encountered an error: {}", err) error!("peach-config encountered an error: {}", err)
} }
} }
} }
PeachConfig::Manifest => match generate_manifest() { PeachConfig::Manifest => match generate_manifest::generate_manifest() {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!( error!(
"peach-config countered an error generating manifest: {}", "peach-config encountered an error generating manifest: {}",
err err
) )
} }
}, },
PeachConfig::Update(opts) => match update(opts) { PeachConfig::Update(opts) => match update::update(opts) {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {
error!("peach-config encountered an error during update: {}", 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

@ -0,0 +1,21 @@
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,6 +68,7 @@ pub fn setup_peach(
"libssl-dev", "libssl-dev",
"nginx", "nginx",
"wget", "wget",
"dnsutils",
"-y", "-y",
])?; ])?;

View File

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

View File

@ -0,0 +1,29 @@
#!/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,6 +1,5 @@
use log::info;
use peach_lib::dyndns_client::dyndns_update_ip; use peach_lib::dyndns_client::dyndns_update_ip;
use log::{info};
fn main() { fn main() {
// initalize the logger // initalize the logger

View File

@ -0,0 +1,25 @@
[package]
name = "peach-jsonrpc-server"
authors = ["Andrew Reid <glyph@mycelial.technology>"]
version = "0.1.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"
repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace"
readme = "README.md"
license = "AGPL-3.0-only"
publish = false
[badges]
maintenance = { status = "actively-developed" }
[dependencies]
env_logger = "0.9"
jsonrpc-core = "18"
jsonrpc-http-server = "18"
log = "0.4"
miniserde = "0.1.15"
peach-stats = { path = "../peach-stats", features = ["miniserde_support"] }
[dev-dependencies]
jsonrpc-test = "18"

View File

@ -0,0 +1,72 @@
# peach-jsonrpc-server
A JSON-RPC server for the PeachCloud system which exposes an API over HTTP.
Currently includes peach-stats capability (system statistics).
## JSON-RPC API
| 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` |
## Environment
The JSON-RPC HTTP server is currently hardcoded to run on "127.0.0.1:5110". Address and port configuration settings will later be exposed via CLI arguments and possibly an environment variable.
Logging is made available with `env_logger`:
`export RUST_LOG=info`
Other logging levels include `debug`, `warn` and `error`.
## Setup
Clone the peach-workspace repo:
`git clone https://git.coopcloud.tech/PeachCloud/peach-workspace`
Move into the repo peaach-jsonrpc-server directory and compile a release build:
`cd peach-jsonrpc-server`
`cargo build --release`
Run the binary:
`./peach-workspace/target/release/peach-jsonrpc-server`
## Debian Packaging
TODO.
## Example Usage
**Get CPU Statistics**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "cpu_stats", "id":1 }' 127.0.0.1:5110`
Server responds with:
`{"jsonrpc":"2.0","result":"{\"user\":4661083,\"system\":1240371,\"idle\":326838290,\"nice\":0}","id":1}`
**Get System Uptime**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "uptime", "id":1 }' 127.0.0.1:5110`
Server responds with:
`{"jsonrpc":"2.0","result":"{\"secs\":840968}","id":1}`
### Licensing
AGPL-3.0

View File

@ -0,0 +1,46 @@
use std::fmt;
use jsonrpc_core::{Error as JsonRpcError, ErrorCode};
use peach_stats::StatsError;
/// Custom error type encapsulating all possible errors for a JSON-RPC server
/// and associated methods.
#[derive(Debug)]
pub enum JsonRpcServerError {
/// An error returned from the `peach-stats` library.
Stats(StatsError),
/// An expected JSON-RPC method parameter was not provided.
MissingParameter(JsonRpcError),
/// Failed to parse a provided JSON-RPC method parameter.
ParseParameter(JsonRpcError),
}
impl fmt::Display for JsonRpcServerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
JsonRpcServerError::ParseParameter(ref source) => {
write!(f, "Failed to parse parameter: {}", source)
}
JsonRpcServerError::MissingParameter(ref source) => {
write!(f, "Missing expected parameter: {}", source)
}
JsonRpcServerError::Stats(ref source) => {
write!(f, "{}", source)
}
}
}
}
impl From<JsonRpcServerError> for JsonRpcError {
fn from(err: JsonRpcServerError) -> Self {
match &err {
JsonRpcServerError::Stats(source) => JsonRpcError {
code: ErrorCode::ServerError(-32001),
message: format!("{}", source),
data: None,
},
JsonRpcServerError::MissingParameter(source) => source.clone(),
JsonRpcServerError::ParseParameter(source) => source.clone(),
}
}
}

View File

@ -0,0 +1,141 @@
//! # peach-jsonrpc-server
//!
//! A JSON-RPC server which exposes an API over HTTP.
use std::env;
use std::result::Result;
use jsonrpc_core::{IoHandler, Value};
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
use log::info;
use miniserde::json;
use peach_stats::stats;
mod error;
use crate::error::JsonRpcServerError;
/// Create JSON-RPC I/O handler, add RPC methods and launch HTTP server.
pub fn run() -> Result<(), JsonRpcServerError> {
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())));
// TODO: add blocks of methods according to provided flags
/* PEACH-STATS RPC METHODS */
io.add_sync_method("cpu_stats", move |_| {
info!("Fetching CPU statistics.");
let cpu = stats::cpu_stats().map_err(JsonRpcServerError::Stats)?;
let json_cpu = json::to_string(&cpu);
Ok(Value::String(json_cpu))
});
io.add_sync_method("cpu_stats_percent", move |_| {
info!("Fetching CPU statistics as percentages.");
let cpu = stats::cpu_stats_percent().map_err(JsonRpcServerError::Stats)?;
let json_cpu = json::to_string(&cpu);
Ok(Value::String(json_cpu))
});
io.add_sync_method("disk_usage", move |_| {
info!("Fetching disk usage statistics.");
let disks = stats::disk_usage().map_err(JsonRpcServerError::Stats)?;
let json_disks = json::to_string(&disks);
Ok(Value::String(json_disks))
});
io.add_sync_method("load_average", move |_| {
info!("Fetching system load average statistics.");
let avg = stats::load_average().map_err(JsonRpcServerError::Stats)?;
let json_avg = json::to_string(&avg);
Ok(Value::String(json_avg))
});
io.add_sync_method("mem_stats", move |_| {
info!("Fetching current memory statistics.");
let mem = stats::mem_stats().map_err(JsonRpcServerError::Stats)?;
let json_mem = json::to_string(&mem);
Ok(Value::String(json_mem))
});
io.add_sync_method("uptime", move |_| {
info!("Fetching system uptime.");
let uptime = stats::uptime().map_err(JsonRpcServerError::Stats)?;
let json_uptime = json::to_string(&uptime);
Ok(Value::String(json_uptime))
});
let http_server =
env::var("PEACH_JSONRPC_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
info!("Starting JSON-RPC server on {}.", http_server);
let server = ServerBuilder::new(io)
.cors(DomainsValidation::AllowOnly(vec![
AccessControlAllowOrigin::Null,
]))
.start_http(
&http_server
.parse()
.expect("Invalid HTTP address and port combination"),
)
.expect("Unable to start RPC server");
server.wait();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use jsonrpc_core::{Error as JsonRpcError, ErrorCode};
use jsonrpc_test as test_rpc;
#[test]
fn rpc_success() {
let rpc = {
let mut io = IoHandler::new();
io.add_sync_method("rpc_success_response", |_| {
Ok(Value::String("success".into()))
});
test_rpc::Rpc::from(io)
};
assert_eq!(rpc.request("rpc_success_response", &()), r#""success""#);
}
#[test]
fn rpc_parse_error() {
let rpc = {
let mut io = IoHandler::new();
io.add_sync_method("rpc_parse_error", |_| {
let e = JsonRpcError {
code: ErrorCode::ParseError,
message: String::from("Parse error"),
data: None,
};
Err(JsonRpcError::from(JsonRpcServerError::MissingParameter(e)))
});
test_rpc::Rpc::from(io)
};
assert_eq!(
rpc.request("rpc_parse_error", &()),
r#"{
"code": -32700,
"message": "Parse error"
}"#
);
}
}

View File

@ -0,0 +1,34 @@
#![warn(missing_docs)]
//! # peach-jsonrpc-server
//!
//! A JSON-RPC server which exposes an over HTTP.
//!
//! Currently includes peach-stats capability (system statistics).
//!
//! ## API
//!
//! | 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;
use log::error;
fn main() {
// initalize the logger
env_logger::init();
// handle errors returned from `run`
if let Err(e) = peach_jsonrpc_server::run() {
error!("Application error: {}", e);
process::exit(1);
}
}

View File

@ -1,19 +1,19 @@
[package] [package]
name = "peach-lib" name = "peach-lib"
version = "1.3.0" version = "1.3.2"
authors = ["Andrew Reid <glyph@mycelial.technology>"] authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
log = "0.4" chrono = "0.4.19"
fslock="0.1.6"
jsonrpc-client-core = "0.5" jsonrpc-client-core = "0.5"
jsonrpc-client-http = "0.5" jsonrpc-client-http = "0.5"
jsonrpc-core = "8.0.1" jsonrpc-core = "8.0.1"
log = "0.4"
nanorand = "0.6.1"
regex = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
rust-crypto = "0.2.36"
serde_json = "1.0" serde_json = "1.0"
serde_yaml = "0.8" serde_yaml = "0.8"
regex = "1" sha3 = "0.10.0"
chrono = "0.4.19"
rand="0.8.4"
fslock="0.1.6"

View File

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

View File

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

View File

@ -1,7 +1,5 @@
use std::iter; use nanorand::{Rng, WyRand};
use sha3::{Digest, Sha3_256};
use crypto::{digest::Digest, sha3::Sha3};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use crate::{config_manager, error::PeachError, sbot_client}; use crate::{config_manager, error::PeachError, sbot_client};
@ -39,9 +37,13 @@ pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
/// Creates a hash from a password string /// Creates a hash from a password string
pub fn hash_password(password: &str) -> String { pub fn hash_password(password: &str) -> String {
let mut hasher = Sha3::sha3_256(); let mut hasher = Sha3_256::new();
hasher.input_str(password); // write input message
hasher.result_str() hasher.update(password);
// read hash digest
let result = hasher.finalize();
// convert `u8` to `String`
result[0].to_string()
} }
/// Sets a new temporary password for the admin user /// Sets a new temporary password for the admin user
@ -68,13 +70,10 @@ pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> {
/// Generates a temporary password and sends it via ssb dm /// Generates a temporary password and sends it via ssb dm
/// to the ssb id configured to be the admin of the peachcloud device /// to the ssb id configured to be the admin of the peachcloud device
pub fn send_password_reset() -> Result<(), PeachError> { pub fn send_password_reset() -> Result<(), PeachError> {
// first generate a new random password of ascii characters // initialise random number generator
let mut rng = thread_rng(); let mut rng = WyRand::new();
let temporary_password: String = iter::repeat(()) // generate a new password of random numbers
.map(|()| rng.sample(Alphanumeric)) let temporary_password = rng.generate::<u64>().to_string();
.map(char::from)
.take(10)
.collect();
// save this string as a new temporary password // save this string as a new temporary password
set_new_temporary_password(&temporary_password)?; set_new_temporary_password(&temporary_password)?;
let domain = config_manager::get_peachcloud_domain()?; let domain = config_manager::get_peachcloud_domain()?;

View File

@ -1,4 +0,0 @@
[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,4 +0,0 @@
[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,4 +0,0 @@
[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,44 +1,32 @@
[package] [package]
name = "peach-network" name = "peach-network"
version = "0.2.12" version = "0.4.1"
authors = ["Andrew Reid <gnomad@cryptolab.net>"] authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2018" edition = "2021"
description = "Query and configure network interfaces using JSON-RPC over HTTP." description = "Query and configure network interfaces."
homepage = "https://opencollective.com/peachcloud" homepage = "https://opencollective.com/peachcloud"
repository = "https://github.com/peachcloud/peach-network" repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-network"
readme = "README.md" readme = "README.md"
license = "AGPL-3.0-only" license = "LGPL-3.0-only"
publish = false publish = false
[package.metadata.deb]
depends = "$auto"
extended-description = """\
peach-network is a microservice to query and configure network interfaces \
using JSON-RPC over HTTP."""
maintainer-scripts="debian"
systemd-units = { unit-name = "peach-network" }
assets = [
["target/release/peach-network", "usr/bin/", "755"],
["README.md", "usr/share/doc/peach-network/README", "644"],
]
[badges] [badges]
travis-ci = { repository = "peachcloud/peach-network", branch = "master" }
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
env_logger = "0.6"
failure = "0.1"
get_if_addrs = "0.5.3" get_if_addrs = "0.5.3"
jsonrpc-core = "11" miniserde = { version = "0.1.15", optional = true }
jsonrpc-http-server = "11" probes = "0.4.1"
log = "0.4" serde = { version = "1.0.130", features = ["derive"], optional = true }
probes = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
snafu = "0.6"
regex = "1" regex = "1"
wpactrl = "0.3.1" # replace this with crate import once latest changes have been published
wpactrl = { git = "https://github.com/sauyon/wpa-ctrl-rs.git", branch = "master" }
[dev-dependencies] [features]
jsonrpc-test = "11" default = []
# Provide `Serialize` and `Deserialize` traits for library structs using `miniserde`
miniserde_support = ["miniserde"]
# Provide `Serialize` and `Deserialize` traits for library structs using `serde`
serde_support = ["serde"]

View File

@ -1,178 +1,46 @@
# peach-network # peach-network
[![Build Status](https://travis-ci.com/peachcloud/peach-network.svg?branch=master)](https://travis-ci.com/peachcloud/peach-network) ![Generic badge](https://img.shields.io/badge/version-0.2.12-<COLOR>.svg) ![Generic badge](https://img.shields.io/badge/version-0.4.0-<COLOR>.svg)
Networking microservice module for PeachCloud. Query and configure device interfaces using [JSON-RPC](https://www.jsonrpc.org/specification) over http. Network interface state query and modification library.
Interaction with wireless interfaces occurs primarily through the [wpactrl crate](https://docs.rs/wpactrl/0.3.1/wpactrl/) which provides "a pure-Rust lowlevel library for controlling wpasupplicant remotely". This approach is akin to using `wpa_cli` (a WPA command line client). Interaction with wireless interfaces occurs primarily through the [wpactrl crate](https://docs.rs/wpactrl/0.3.1/wpactrl/) which provides "a pure-Rust lowlevel library for controlling wpasupplicant remotely". This approach is akin to using `wpa_cli` (a WPA command line client).
_Note: This module is a work-in-progress._ ## API Documentation
### JSON-RPC API API documentation can be built and served with `cargo doc --no-deps --open`. The full set of available data structures and functions is listed in the `peach_network::network` module. A custom error type (`NetworkError`) is also publically exposed for library users; it encapsulates all possible error variants.
Methods for **retrieving data**: ## Example Usage
| Method | Parameters | Description | ```rust
| --- | --- | --- | use peach_network::{network, NetworkError};
| `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 |
| `ping` | | Respond with `success` if microservice is running |
| `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**: fn main() -> Result<(), NetworkError> {
let wlan_iface = "wlan0";
| Method | Parameters | Description | let wlan_ip = network::ip(wlan_iface)?;
| --- | --- | --- | let wlan_ssid = network::ssid(wlan_iface)?;
| `activate_ap` | | Activate WiFi access point (start `wpa_supplicant@ap0.service`) |
| `activate_client` | | Activate WiFi client connection (start `wpa_supplicant@wlan0.service`) |
| `add` | `ssid`, `pass` | Add WiFi credentials to `wpa_supplicant-wlan0.conf` |
| `check_iface` | | Activate WiFi access point if client mode 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`, `password` | 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` |
### API Documentation let ssid = "Home";
let pass = "SuperSecret";
API documentation can be built and served with `cargo doc --no-deps --open`. This set of documentation is intended for developers who wish to work on the project or better understand the API of the `src/network.rs` module. network::add(&wlan_iface, &ssid, &pass)?;
network::save()?;
### Environment Ok(())
}
```
The JSON-RPC HTTP server address and port can be configured with the `PEACH_NETWORK_SERVER` environment variable: ## Feature Flags
`export PEACH_NETWORK_SERVER=127.0.0.1:5000` Feature flags are used to offer `Serialize` and `Deserialize` implementations for all `struct` data types provided by this library. These traits are not provided by default. A choice of `miniserde` and `serde` is provided.
When not set, the value defaults to `127.0.0.1:5110`. Define the desired feature in the `Cargo.toml` manifest of your project:
Logging is made available with `env_logger`: ```toml
peach-network = { version = "0.3.0", features = ["miniserde_support"] }
```
`export RUST_LOG=info` ## License
Other logging levels include `debug`, `warn` and `error`. LGPL-3.0.
### Setup
Clone this repo:
`git clone https://github.com/peachcloud/peach-network.git`
Move into the repo and compile:
`cd peach-network`
`cargo build --release`
Run the binary (sudo needed to satisfy permission requirements):
`sudo ./target/release/peach-network`
### Debian Packaging
A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-network` to be easily bundled as a Debian package (`.deb`). The `cargo-deb` [crate](https://crates.io/crates/cargo-deb) can be used to achieve this.
Install `cargo-deb`:
`cargo install cargo-deb`
Move into the repo:
`cd peach-network`
Build the package:
`cargo deb`
The output will be written to `target/debian/peach-network_0.2.4_arm64.deb` (or similar).
Build the package (aarch64):
`cargo deb --target aarch64-unknown-linux-gnu`
Install the package as follows:
`sudo dpkg -i target/debian/peach-network_0.2.4_arm64.deb`
The service will be automatically enabled and started.
Uninstall the service:
`sudo apt-get remove peach-network`
Remove configuration files (not removed with `apt-get remove`):
`sudo apt-get purge peach-network`
### Example Usage
**Retrieve IP address for wlan0**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "ip", "params" : {"iface": "wlan0" }, "id":1 }' 127.0.0.1:5110`
Server responds with:
`{"jsonrpc":"2.0","result":"192.168.1.21","id":1}`
**Retrieve SSID of connected access point for wlan1**
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "ssid", "params" : {"iface": "wlan1" }, "id":1 }' 127.0.0.1:5110`
Server response when interface is connected:
`{"jsonrpc":"2.0","result":"Home","id":1}`
Server response when interface is not connected:
`{"jsonrpc":"2.0","error":{"code":-32003,"message":"Failed to retrieve SSID for wlan1. Interface may not be connected."},"id":1}`
**Retrieve list of SSIDs for all networks in range of wlan0**
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "available_networks", "params" : {"iface": "wlan0" }, "id":1 }' 127.0.0.1:5110`
Server response when interface is connected:
`{"jsonrpc":"2.0","result":"[{\"frequency\":\"2412\",\"signal_level\":\"-72\",\"ssid\":\"Home\",\"flags\":\"[WPA2-PSK-CCMP][ESS]\"},{\"frequency\":\"2472\",\"signal_level\":\"-56\",\"ssid\":\"podetium\",\"flags\":\"[WPA2-PSK-CCMP+TKIP][ESS]\"}]","id":1}`
Server response when interface is not connected:
`{"jsonrpc":"2.0","error":{"code":-32006,"message":"No networks found in range of wlan0"},"id":1}`
**Retrieve network traffic statistics for wlan1**
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "traffic", "params" : {"iface": "wlan1" }, "id":1 }' 127.0.0.1:5110`
Server response if interface exists:
`{"jsonrpc":"2.0","result":"{\"received\":26396361,\"transmitted\":22352530}","id":1}`
Server response when interface is not found:
`{"jsonrpc":"2.0","error":{"code":-32004,"message":"Failed to retrieve network traffic for wlan3. Interface may not be connected"},"id":1}`
**Retrieve status information for wlan0**
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "status", "params" : {"iface": "wlan0" }, "id":1 }' 127.0.0.1:5110`
Server response if interface exists:
`{"jsonrpc":"2.0","result":"{\"address\":\"b8:27:eb:9b:5d:5f\",\"bssid\":\"f4:8c:eb:cd:31:81\",\"freq\":\"2412\",\"group_cipher\":\"CCMP\",\"id\":\"0\",\"ip_address\":\"192.168.0.162\",\"key_mgmt\":\"WPA2-PSK\",\"mode\":\"station\",\"pairwise_cipher\":\"CCMP\",\"ssid\":\"Home\",\"wpa_state\":\"COMPLETED\"}","id":1}`
Server response when interface is not found:
`{"jsonrpc":"2.0","error":{"code":-32013,"message":"Failed to open control interface for wpasupplicant: No such file or directory (os error 2)"},"id":1}`
### Licensing
AGPL-3.0

View File

@ -1,13 +0,0 @@
[Unit]
Description=Query and configure network interfaces using JSON-RPC over HTTP.
[Service]
Type=simple
User=root
Group=netdev
Environment="RUST_LOG=error"
ExecStart=/usr/bin/peach-network
Restart=always
[Install]
WantedBy=multi-user.target

View File

@ -1,351 +1,363 @@
use std::{error, io, str}; //! Custom error type for `peach-network`.
use jsonrpc_core::{types::error::Error, ErrorCode}; use std::io;
use std::num::ParseIntError;
use io::Error as IoError;
use probes::ProbeError; use probes::ProbeError;
use serde_json::error::Error as SerdeError; use regex::Error as RegexError;
use snafu::Snafu; use wpactrl::WpaError;
pub type BoxError = Box<dyn error::Error>; /// Custom error type encapsulating all possible errors when querying
/// network interfaces and modifying their state.
#[derive(Debug, Snafu)] #[derive(Debug)]
#[snafu(visibility(pub(crate)))]
pub enum NetworkError { pub enum NetworkError {
#[snafu(display("{}", err_msg))] /// Failed to add network.
ActivateAp { err_msg: String }, Add {
/// SSID.
#[snafu(display("{}", err_msg))] ssid: String,
ActivateClient { err_msg: String }, },
/// Failed to retrieve network state.
#[snafu(display("Failed to add network for {}", ssid))] NoState {
Add { ssid: String }, /// Interface.
iface: String,
#[snafu(display("Failed to retrieve state for interface: {}", iface))] /// Underlying error source.
NoState { iface: String, source: io::Error }, source: IoError,
},
#[snafu(display("Failed to disable network {} for interface: {}", id, iface))] /// Failed to disable network.
Disable { id: String, iface: String }, Disable {
/// ID.
#[snafu(display("Failed to disconnect {}", iface))] id: String,
Disconnect { iface: String }, /// Interface.
iface: String,
#[snafu(display("Failed to generate wpa passphrase for {}: {}", ssid, source))] },
GenWpaPassphrase { ssid: String, source: io::Error }, /// Failed to disconnect interface.
Disconnect {
#[snafu(display("Failed to generate wpa passphrase for {}: {}", ssid, err_msg))] /// Interface.
GenWpaPassphraseWarning { ssid: String, err_msg: String }, iface: String,
},
#[snafu(display("No ID found for {} on interface: {}", ssid, iface))] /// Failed to execute wpa_passphrase command.
Id { ssid: String, iface: String }, GenWpaPassphrase {
/// SSID.
#[snafu(display("Could not access IP address for interface: {}", iface))] ssid: String,
NoIp { iface: String, source: io::Error }, /// Underlying error source.
source: IoError,
#[snafu(display("Could not find RSSI for interface: {}", iface))] },
Rssi { iface: String }, /// Failed to successfully generate wpa passphrase.
GenWpaPassphraseWarning {
#[snafu(display("Could not find signal quality (%) for interface: {}", iface))] /// SSID.
RssiPercent { iface: String }, ssid: String,
/// Error message describing context.
#[snafu(display("Could not find SSID for interface: {}", iface))] err_msg: String,
Ssid { iface: String }, },
/// Failed to retrieve ID for the given SSID and interface.
#[snafu(display("No state found for interface: {}", iface))] Id {
State { iface: String }, /// SSID.
ssid: String,
#[snafu(display("No status found for interface: {}", iface))] /// Interface.
Status { iface: String }, iface: String,
},
#[snafu(display("Could not find network traffic for interface: {}", iface))] /// Failed to retrieve IP address.
Traffic { iface: String }, NoIp {
/// Inteface.
#[snafu(display("No saved networks found for default interface"))] iface: String,
/// Underlying error source.
source: IoError,
},
/// Failed to retrieve RSSI.
Rssi {
/// Interface.
iface: String,
},
/// Failed to retrieve signal quality (%).
RssiPercent {
/// Interface.
iface: String,
},
/// Failed to retrieve SSID.
Ssid {
/// Interface.
iface: String,
},
/// Failed to retrieve state.
State {
/// Interface.
iface: String,
},
/// Failed to retrieve status.
Status {
/// Interface.
iface: String,
},
/// Failed to retieve network traffic.
Traffic {
/// Interface.
iface: String,
},
/// No saved network found for the default interface.
SavedNetworks, SavedNetworks,
/// No networks found in range.
#[snafu(display("No networks found in range of interface: {}", iface))] AvailableNetworks {
AvailableNetworks { iface: String }, /// Interface.
iface: String,
#[snafu(display("Missing expected parameters: {}", e))] },
MissingParams { e: Error }, /// Failed to set new password.
Modify {
#[snafu(display("Failed to set new password for network {} on {}", id, iface))] /// ID.
Modify { id: String, iface: String }, id: String,
/// Interface.
#[snafu(display("No IP found for interface: {}", iface))] iface: String,
Ip { iface: String }, },
/// Failed to retrieve IP address.
#[snafu(display("Failed to parse integer from string for RSSI value: {}", source))] Ip {
ParseString { source: std::num::ParseIntError }, /// Interface.
iface: String,
#[snafu(display( },
"Failed to retrieve network traffic measurement for {}: {}", /// Failed to parse integer from string.
iface, ParseInt(ParseIntError),
source /// Failed to retrieve network traffic measurement.
))] NoTraffic {
NoTraffic { iface: String, source: ProbeError }, /// Interface.
iface: String,
#[snafu(display("Failed to reassociate with WiFi network for interface: {}", iface))] /// Underlying error source.
Reassociate { iface: String }, source: ProbeError,
},
#[snafu(display("Failed to force reread of wpa_supplicant configuration file"))] /// Failed to reassociate with WiFi network.
Reassociate {
/// Interface.
iface: String,
},
/// Failed to force reread of wpa_supplicant configuration file.
Reconfigure, Reconfigure,
/// Failed to reconnect with WiFi network.
#[snafu(display("Failed to reconnect with WiFi network for interface: {}", iface))] Reconnect {
Reconnect { iface: String }, /// Interface.
iface: String,
#[snafu(display("Regex command failed"))] },
Regex { source: regex::Error }, /// Failed to execute Regex command.
Regex(RegexError),
#[snafu(display("Failed to delete network {} for interface: {}", id, iface))] /// Failed to delete network.
Delete { id: String, iface: String }, Delete {
/// ID.
#[snafu(display("Failed to retrieve state of wlan0 service: {}", source))] id: String,
WlanState { source: io::Error }, /// Interface.
iface: String,
#[snafu(display("Failed to retrieve connection state of wlan0 interface: {}", source))] },
WlanOperstate { source: io::Error }, /// Failed to retrieve state of wlan0 service.
WlanState(IoError),
#[snafu(display("Failed to save configuration changes to file"))] /// Failed to retrieve connection state of wlan0 interface.
WlanOperstate(IoError),
/// Failed to save wpa_supplicant configuration changes to file.
Save, Save,
/// Failed to connect to network.
#[snafu(display("Failed to connect to network {} for interface: {}", id, iface))] Connect {
Connect { id: String, iface: String }, /// ID.
id: String,
#[snafu(display("Failed to start ap0 service: {}", source))] /// Interface.
StartAp0 { source: io::Error }, iface: String,
#[snafu(display("Failed to start wlan0 service: {}", source))]
StartWlan0 { source: io::Error },
#[snafu(display("JSON serialization failed: {}", source))]
SerdeSerialize { source: SerdeError },
#[snafu(display("Failed to open control interface for wpasupplicant"))]
WpaCtrlOpen {
#[snafu(source(from(failure::Error, std::convert::Into::into)))]
source: BoxError,
}, },
/// Failed to start systemctl service for a network interface.
#[snafu(display("Request to wpasupplicant via wpactrl failed"))] StartInterface {
WpaCtrlRequest { /// Underlying error source.
#[snafu(source(from(failure::Error, std::convert::Into::into)))] source: IoError,
source: BoxError, /// Interface.
iface: String,
}, },
/// Failed to execute wpa-ctrl command.
WpaCtrl(WpaError),
} }
impl From<NetworkError> for Error { impl std::error::Error for NetworkError {
fn from(err: NetworkError) -> Self { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &err { match *self {
NetworkError::ActivateAp { err_msg } => Error { NetworkError::Add { .. } => None,
code: ErrorCode::ServerError(-32015), NetworkError::NoState { ref source, .. } => Some(source),
message: err_msg.to_string(), NetworkError::Disable { .. } => None,
data: None, NetworkError::Disconnect { .. } => None,
}, NetworkError::GenWpaPassphrase { ref source, .. } => Some(source),
NetworkError::ActivateClient { err_msg } => Error { NetworkError::GenWpaPassphraseWarning { .. } => None,
code: ErrorCode::ServerError(-32017), NetworkError::Id { .. } => None,
message: err_msg.to_string(), NetworkError::NoIp { ref source, .. } => Some(source),
data: None, NetworkError::Rssi { .. } => None,
}, NetworkError::RssiPercent { .. } => None,
NetworkError::Add { ssid } => Error { NetworkError::Ssid { .. } => None,
code: ErrorCode::ServerError(-32000), NetworkError::State { .. } => None,
message: format!("Failed to add network for {}", ssid), NetworkError::Status { .. } => None,
data: None, NetworkError::Traffic { .. } => None,
}, NetworkError::SavedNetworks => None,
NetworkError::NoState { iface, source } => Error { NetworkError::AvailableNetworks { .. } => None,
code: ErrorCode::ServerError(-32022), NetworkError::Modify { .. } => None,
message: format!( NetworkError::Ip { .. } => None,
"Failed to retrieve interface state for {}: {}", NetworkError::ParseInt(ref source) => Some(source),
iface, source NetworkError::NoTraffic { ref source, .. } => Some(source),
), NetworkError::Reassociate { .. } => None,
data: None, NetworkError::Reconfigure { .. } => None,
}, NetworkError::Reconnect { .. } => None,
NetworkError::Disable { id, iface } => Error { NetworkError::Regex(ref source) => Some(source),
code: ErrorCode::ServerError(-32029), NetworkError::Delete { .. } => None,
message: format!("Failed to disable network {} for {}", id, iface), NetworkError::WlanState(ref source) => Some(source),
data: None, NetworkError::WlanOperstate(ref source) => Some(source),
}, NetworkError::Save => None,
NetworkError::Disconnect { iface } => Error { NetworkError::Connect { .. } => None,
code: ErrorCode::ServerError(-32032), NetworkError::StartInterface { ref source, .. } => Some(source),
message: format!("Failed to disconnect {}", iface), NetworkError::WpaCtrl(ref source) => Some(source),
data: None, }
}, }
NetworkError::GenWpaPassphrase { ssid, source } => Error { }
code: ErrorCode::ServerError(-32025),
message: format!("Failed to generate wpa passphrase for {}: {}", ssid, source), impl std::fmt::Display for NetworkError {
data: None, fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
}, match *self {
NetworkError::GenWpaPassphraseWarning { ssid, err_msg } => Error { NetworkError::Add { ref ssid } => {
code: ErrorCode::ServerError(-32036), write!(f, "Failed to add network for {}", ssid)
message: format!( }
NetworkError::NoState { ref iface, .. } => {
write!(f, "Failed to retrieve state for interface: {}", iface)
}
NetworkError::Disable { ref id, ref iface } => {
write!(
f,
"Failed to disable network {} for interface: {}",
id, iface
)
}
NetworkError::Disconnect { ref iface } => {
write!(f, "Failed to disconnect {}", iface)
}
NetworkError::GenWpaPassphrase { ref ssid, .. } => {
write!(f, "Failed to generate wpa passphrase for {}", ssid)
}
NetworkError::GenWpaPassphraseWarning {
ref ssid,
ref err_msg,
} => {
write!(
f,
"Failed to generate wpa passphrase for {}: {}", "Failed to generate wpa passphrase for {}: {}",
ssid, err_msg ssid, err_msg
), )
data: None, }
}, NetworkError::Id {
NetworkError::Id { iface, ssid } => Error { ref ssid,
code: ErrorCode::ServerError(-32026), ref iface,
message: format!("No ID found for {} on interface {}", ssid, iface), } => {
data: None, write!(f, "No ID found for {} on interface: {}", ssid, iface)
}, }
NetworkError::NoIp { iface, source } => Error { NetworkError::NoIp { ref iface, .. } => {
code: ErrorCode::ServerError(-32001), write!(f, "Could not access IP address for interface: {}", iface)
message: format!("Failed to retrieve IP address for {}: {}", iface, source), }
data: None, NetworkError::Rssi { ref iface } => {
}, write!(f, "Could not find RSSI for interface: {}", iface)
NetworkError::Rssi { iface } => Error { }
code: ErrorCode::ServerError(-32002), NetworkError::RssiPercent { ref iface } => {
message: format!( write!(
"Failed to retrieve RSSI for {}. Interface may not be connected", f,
"Could not find signal quality (%) for interface: {}",
iface
)
}
NetworkError::Ssid { ref iface } => {
write!(f, "Could not find SSID for interface: {}", iface)
}
NetworkError::State { ref iface } => {
write!(f, "No state found for interface: {}", iface)
}
NetworkError::Status { ref iface } => {
write!(f, "No status found for interface: {}", iface)
}
NetworkError::Traffic { ref iface } => {
write!(f, "Could not find network traffic for interface: {}", iface)
}
NetworkError::SavedNetworks => {
write!(f, "No saved networks found for default interface")
}
NetworkError::AvailableNetworks { ref iface } => {
write!(f, "No networks found in range of interface: {}", iface)
}
NetworkError::Modify { ref id, ref iface } => {
write!(
f,
"Failed to set new password for network {} on {}",
id, iface
)
}
NetworkError::Ip { ref iface } => {
write!(f, "No IP found for interface: {}", iface)
}
NetworkError::ParseInt(_) => {
write!(f, "Failed to parse integer from string for RSSI value")
}
NetworkError::NoTraffic { ref iface, .. } => {
write!(
f,
"Failed to retrieve network traffic measurement for {}",
iface
)
}
NetworkError::Reassociate { ref iface } => {
write!(
f,
"Failed to reassociate with WiFi network for interface: {}",
iface
)
}
NetworkError::Reconfigure => {
write!(
f,
"Failed to force reread of wpa_supplicant configuration file"
)
}
NetworkError::Reconnect { ref iface } => {
write!(
f,
"Failed to reconnect with WiFi network for interface: {}",
iface
)
}
NetworkError::Regex(_) => write!(f, "Regex command failed"),
NetworkError::Delete { ref id, ref iface } => {
write!(
f,
"Failed to delete network {} for interface: {}",
id, iface
)
}
NetworkError::WlanState(_) => write!(f, "Failed to retrieve state of wlan0 service"),
NetworkError::WlanOperstate(_) => {
write!(f, "Failed to retrieve connection state of wlan0 interface")
}
NetworkError::Save => write!(f, "Failed to save configuration changes to file"),
NetworkError::Connect { ref id, ref iface } => {
write!(
f,
"Failed to connect to network {} for interface: {}",
id, iface
)
}
NetworkError::StartInterface { ref iface, .. } => write!(
f,
"Failed to start systemctl service for {} interface",
iface iface
), ),
data: None, NetworkError::WpaCtrl(_) => write!(f, "WpaCtrl command failed"),
},
NetworkError::RssiPercent { iface } => Error {
code: ErrorCode::ServerError(-32034),
message: format!(
"Failed to retrieve signal quality (%) for {}. Interface may not be connected",
iface
),
data: None,
},
NetworkError::Ssid { iface } => Error {
code: ErrorCode::ServerError(-32003),
message: format!(
"Failed to retrieve SSID for {}. Interface may not be connected",
iface
),
data: None,
},
NetworkError::State { iface } => Error {
code: ErrorCode::ServerError(-32023),
message: format!("No state found for {}. Interface may not exist", iface),
data: None,
},
NetworkError::Status { iface } => Error {
code: ErrorCode::ServerError(-32024),
message: format!("No status found for {}. Interface may not exist", iface),
data: None,
},
NetworkError::Traffic { iface } => Error {
code: ErrorCode::ServerError(-32004),
message: format!(
"No network traffic statistics found for {}. Interface may not exist",
iface
),
data: None,
},
NetworkError::SavedNetworks => Error {
code: ErrorCode::ServerError(-32005),
message: "No saved networks found".to_string(),
data: None,
},
NetworkError::AvailableNetworks { iface } => Error {
code: ErrorCode::ServerError(-32006),
message: format!("No networks found in range of {}", iface),
data: None,
},
NetworkError::MissingParams { e } => e.clone(),
NetworkError::Modify { id, iface } => Error {
code: ErrorCode::ServerError(-32033),
message: format!("Failed to set new password for network {} on {}", id, iface),
data: None,
},
NetworkError::Ip { iface } => Error {
code: ErrorCode::ServerError(-32007),
message: format!("No IP address found for {}", iface),
data: None,
},
NetworkError::ParseString { source } => Error {
code: ErrorCode::ServerError(-32035),
message: format!(
"Failed to parse integer from string for RSSI value: {}",
source
),
data: None,
},
NetworkError::NoTraffic { iface, source } => Error {
code: ErrorCode::ServerError(-32015),
message: format!(
"Failed to retrieve network traffic statistics for {}: {}",
iface, source
),
data: None,
},
NetworkError::Reassociate { iface } => Error {
code: ErrorCode::ServerError(-32008),
message: format!("Failed to reassociate with WiFi network for {}", iface),
data: None,
},
NetworkError::Reconfigure => Error {
code: ErrorCode::ServerError(-32030),
message: "Failed to force reread of wpa_supplicant configuration file".to_string(),
data: None,
},
NetworkError::Reconnect { iface } => Error {
code: ErrorCode::ServerError(-32009),
message: format!("Failed to reconnect with WiFi network for {}", iface),
data: None,
},
NetworkError::Regex { source } => Error {
code: ErrorCode::ServerError(-32010),
message: format!("Regex command error: {}", source),
data: None,
},
NetworkError::Delete { id, iface } => Error {
code: ErrorCode::ServerError(-32028),
message: format!("Failed to delete network {} for {}", id, iface),
data: None,
},
NetworkError::WlanState { source } => Error {
code: ErrorCode::ServerError(-32011),
message: format!("Failed to retrieve state of wlan0 service: {}", source),
data: None,
},
NetworkError::WlanOperstate { source } => Error {
code: ErrorCode::ServerError(-32021),
message: format!(
"Failed to retrieve connection state of wlan0 interface: {}",
source
),
data: None,
},
NetworkError::Save => Error {
code: ErrorCode::ServerError(-32031),
message: "Failed to save configuration changes to file".to_string(),
data: None,
},
NetworkError::Connect { id, iface } => Error {
code: ErrorCode::ServerError(-32027),
message: format!("Failed to connect to network {} for {}", id, iface),
data: None,
},
NetworkError::StartAp0 { source } => Error {
code: ErrorCode::ServerError(-32016),
message: format!("Failed to start ap0 service: {}", source),
data: None,
},
NetworkError::StartWlan0 { source } => Error {
code: ErrorCode::ServerError(-32018),
message: format!("Failed to start wlan0 service: {}", source),
data: None,
},
NetworkError::SerdeSerialize { source } => Error {
code: ErrorCode::ServerError(-32012),
message: format!("JSON serialization failed: {}", source),
data: None,
},
NetworkError::WpaCtrlOpen { source } => Error {
code: ErrorCode::ServerError(-32013),
message: format!(
"Failed to open control interface for wpasupplicant: {}",
source
),
data: None,
},
NetworkError::WpaCtrlRequest { source } => Error {
code: ErrorCode::ServerError(-32014),
message: format!("WPA supplicant request failed: {}", source),
data: None,
},
} }
} }
} }
impl From<WpaError> for NetworkError {
fn from(err: WpaError) -> Self {
NetworkError::WpaCtrl(err)
}
}
impl From<ParseIntError> for NetworkError {
fn from(err: ParseIntError) -> Self {
NetworkError::ParseInt(err)
}
}
impl From<RegexError> for NetworkError {
fn from(err: RegexError) -> Self {
NetworkError::Regex(err)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +0,0 @@
use std::process;
use log::error;
fn main() {
// initalize the logger
env_logger::init();
// handle errors returned from `run`
if let Err(e) = peach_network::run() {
error!("Application error: {}", e);
process::exit(1);
}
}

View File

@ -1,6 +1,6 @@
//! Retrieve network data and modify interface state. //! Retrieve network data and modify interface state.
//! //!
//! This module contains the core logic of the `peach-network` microservice and //! This module contains the core logic of the `peach-network` and
//! provides convenience wrappers for a range of `wpasupplicant` commands, //! provides convenience wrappers for a range of `wpasupplicant` commands,
//! many of which are ordinarily executed using `wpa_cli` (a WPA command line //! many of which are ordinarily executed using `wpa_cli` (a WPA command line
//! client). //! client).
@ -11,8 +11,8 @@
//! Switching between client mode and access point mode is achieved by making //! Switching between client mode and access point mode is achieved by making
//! system calls to systemd (via `systemctl`). Further networking functionality //! system calls to systemd (via `systemctl`). Further networking functionality
//! is provided by making system calls to retrieve interface state and write //! is provided by making system calls to retrieve interface state and write
//! access point credentials to `wpa_supplicant-wlan0.conf`. //! access point credentials to `wpa_supplicant-<wlan_iface>.conf`.
//!
use std::{ use std::{
fs::OpenOptions, fs::OpenOptions,
io::prelude::*, io::prelude::*,
@ -21,72 +21,58 @@ use std::{
str, str,
}; };
use crate::error::{
GenWpaPassphrase, NetworkError, NoIp, NoState, NoTraffic, ParseString, SerdeSerialize,
StartAp0, StartWlan0, WlanState, WpaCtrlOpen, WpaCtrlRequest,
};
use probes::network; use probes::network;
use serde::{Deserialize, Serialize};
use snafu::ResultExt;
#[cfg(feature = "miniserde_support")]
use miniserde::{Deserialize, Serialize};
#[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize};
use crate::error::NetworkError;
use crate::utils; use crate::utils;
/// 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,
}
/// Network SSID.
#[derive(Debug, Serialize)]
pub struct Network {
pub ssid: String,
}
/// Access point data retrieved via scan. /// Access point data retrieved via scan.
#[derive(Debug, Serialize)] #[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct Scan { pub struct Scan {
/// Frequency.
pub frequency: String, pub frequency: String,
/// Protocol.
pub protocol: String, pub protocol: String,
/// Signal strength.
pub signal_level: String, pub signal_level: String,
/// SSID.
pub ssid: String, pub ssid: String,
} }
/// Status data for a network interface. /// Status data for a network interface.
#[derive(Debug, Serialize)] #[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct Status { pub struct Status {
/// MAC address.
pub address: Option<String>, pub address: Option<String>,
/// Basic Service Set Identifier (BSSID).
pub bssid: Option<String>, pub bssid: Option<String>,
/// Frequency.
pub freq: Option<String>, pub freq: Option<String>,
/// Group cipher.
pub group_cipher: Option<String>, pub group_cipher: Option<String>,
/// Local ID.
pub id: Option<String>, pub id: Option<String>,
/// IP address.
pub ip_address: Option<String>, pub ip_address: Option<String>,
/// Key management.
pub key_mgmt: Option<String>, pub key_mgmt: Option<String>,
/// Mode.
pub mode: Option<String>, pub mode: Option<String>,
/// Pairwise cipher.
pub pairwise_cipher: Option<String>, pub pairwise_cipher: Option<String>,
/// SSID.
pub ssid: Option<String>, pub ssid: Option<String>,
/// WPA state.
pub wpa_state: Option<String>, pub wpa_state: Option<String>,
} }
@ -109,19 +95,16 @@ impl Status {
} }
/// Received and transmitted network traffic (bytes). /// Received and transmitted network traffic (bytes).
#[derive(Debug, Serialize)] #[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct Traffic { pub struct Traffic {
/// Total bytes received.
pub received: u64, pub received: u64,
/// Total bytes transmitted.
pub transmitted: u64, pub transmitted: u64,
} }
/// SSID and password for a wireless access point.
#[derive(Debug, Deserialize)]
pub struct WiFi {
pub ssid: String,
pub pass: String,
}
/* GET - Methods for retrieving data */ /* GET - Methods for retrieving data */
/// Retrieve list of available wireless access points for a given network /// Retrieve list of available wireless access points for a given network
@ -132,22 +115,15 @@ pub struct WiFi {
/// * `iface` - A string slice holding the name of a wireless network interface /// * `iface` - A string slice holding the name of a wireless network interface
/// ///
/// If the scan results include one or more access points for the given network /// If the scan results include one or more access points for the given network
/// interface, an `Ok` `Result` type is returned containing `Some(String)` - /// interface, an `Ok` `Result` type is returned containing `Some(Vec<Scan>)`.
/// where `String` is a serialized vector of `Scan` structs containing /// The vector of `Scan` structs contains data for the in-range access points.
/// data for the in-range access points. If no access points are found, /// If no access points are found, a `None` type is returned in the `Result`.
/// a `None` type is returned in the `Result`. In the event of an error, a /// In the event of an error, a `NetworkError` is returned in the `Result`.
/// `NetworkError` is returned in the `Result`. The `NetworkError` is then pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError> {
/// enumerated to a specific error type and an appropriate JSON RPC response is
/// sent to the caller.
///
pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new() let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
.ctrl_path(wpa_path) wpa.request("SCAN")?;
.open() let networks = wpa.request("SCAN_RESULTS")?;
.context(WpaCtrlOpen)?;
wpa.request("SCAN").context(WpaCtrlRequest)?;
let networks = wpa.request("SCAN_RESULTS").context(WpaCtrlRequest)?;
let mut scan = Vec::new(); let mut scan = Vec::new();
for network in networks.lines() { for network in networks.lines() {
let v: Vec<&str> = network.split('\t').collect(); let v: Vec<&str> = network.split('\t').collect();
@ -178,8 +154,7 @@ pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
if scan.is_empty() { if scan.is_empty() {
Ok(None) Ok(None)
} else { } else {
let results = serde_json::to_string(&scan).context(SerdeSerialize)?; Ok(Some(scan))
Ok(Some(results))
} }
} }
@ -195,17 +170,11 @@ pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
/// found in the list of saved networks, an `Ok` `Result` type is returned /// found in the list of saved networks, an `Ok` `Result` type is returned
/// containing `Some(String)` - where `String` is the network identifier. /// containing `Some(String)` - where `String` is the network identifier.
/// If no match is found, a `None` type is returned in the `Result`. In the /// If no match is found, a `None` type is returned in the `Result`. In the
/// event of an error, a `NetworkError` is returned in the `Result`. The /// event of an error, a `NetworkError` is returned in the `Result`.
/// `NetworkError` is then enumerated to a specific error type and an
/// appropriate JSON RPC response is sent to the caller.
///
pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> { pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new() let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
.ctrl_path(wpa_path) let networks = wpa.request("LIST_NETWORKS")?;
.open()
.context(WpaCtrlOpen)?;
let networks = wpa.request("LIST_NETWORKS").context(WpaCtrlRequest)?;
let mut id = Vec::new(); let mut id = Vec::new();
for network in networks.lines() { for network in networks.lines() {
let v: Vec<&str> = network.split('\t').collect(); let v: Vec<&str> = network.split('\t').collect();
@ -233,13 +202,13 @@ pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
/// an `Ok` `Result` type is returned containing `Some(String)` - where `String` /// an `Ok` `Result` type is returned containing `Some(String)` - where `String`
/// is the IP address of the interface. If no match is found, a `None` type is /// is the IP address of the interface. If no match is found, a `None` type is
/// returned in the `Result`. In the event of an error, a `NetworkError` is /// returned in the `Result`. In the event of an error, a `NetworkError` is
/// returned in the `Result`. The `NetworkError` is then enumerated to a /// returned in the `Result`.
/// specific error type and an appropriate JSON RPC response is sent to the
/// caller.
///
pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> { pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
let net_if: String = iface.to_string(); let net_if: String = iface.to_string();
let ifaces = get_if_addrs::get_if_addrs().context(NoIp { iface: net_if })?; let ifaces = get_if_addrs::get_if_addrs().map_err(|source| NetworkError::NoIp {
iface: net_if,
source,
})?;
let ip = ifaces let ip = ifaces
.iter() .iter()
.find(|&i| i.name == iface) .find(|&i| i.name == iface)
@ -260,16 +229,11 @@ pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
/// is the RSSI (Received Signal Strength Indicator) of the connection measured /// is the RSSI (Received Signal Strength Indicator) of the connection measured
/// in dBm. If signal strength is not found, a `None` type is returned in the /// in dBm. If signal strength is not found, a `None` type is returned in the
/// `Result`. In the event of an error, a `NetworkError` is returned in the /// `Result`. In the event of an error, a `NetworkError` is returned in the
/// `Result`. The `NetworkError` is then enumerated to a specific error type and /// `Result`.
/// an appropriate JSON RPC response is sent to the caller.
///
pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> { pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new() let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
.ctrl_path(wpa_path) let status = wpa.request("SIGNAL_POLL")?;
.open()
.context(WpaCtrlOpen)?;
let status = wpa.request("SIGNAL_POLL").context(WpaCtrlRequest)?;
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?; let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
if rssi.is_none() { if rssi.is_none() {
@ -292,22 +256,17 @@ pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
/// is the RSSI (Received Signal Strength Indicator) of the connection measured /// is the RSSI (Received Signal Strength Indicator) of the connection measured
/// as a percentage. If signal strength is not found, a `None` type is returned /// as a percentage. If signal strength is not found, a `None` type is returned
/// in the `Result`. In the event of an error, a `NetworkError` is returned in /// in the `Result`. In the event of an error, a `NetworkError` is returned in
/// the `Result`. The `NetworkError` is then enumerated to a specific error type /// the `Result`.
/// and an appropriate JSON RPC response is sent to the caller.
///
pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> { pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new() let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
.ctrl_path(wpa_path) let status = wpa.request("SIGNAL_POLL")?;
.open()
.context(WpaCtrlOpen)?;
let status = wpa.request("SIGNAL_POLL").context(WpaCtrlRequest)?;
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?; let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
match rssi { match rssi {
Some(rssi) => { Some(rssi) => {
// parse the string to a signed integer (for math) // parse the string to a signed integer (for math)
let rssi_parsed = rssi.parse::<i32>().context(ParseString)?; let rssi_parsed = rssi.parse::<i32>()?;
// perform rssi (dBm) to quality (%) conversion // perform rssi (dBm) to quality (%) conversion
let quality_percent = 2 * (rssi_parsed + 100); let quality_percent = 2 * (rssi_parsed + 100);
// convert signal quality integer to string // convert signal quality integer to string
@ -327,32 +286,27 @@ pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
/// ///
/// If the wpasupplicant configuration file contains credentials for one or /// If the wpasupplicant configuration file contains credentials for one or
/// more access points, an `Ok` `Result` type is returned containing /// more access points, an `Ok` `Result` type is returned containing
/// `Some(String)` - where `String` is a serialized vector of `Network` structs /// `Some(Vec<Network>)`. The vector of `Network` structs contains the SSIDs
/// containing the SSIDs of all saved networks. If no network credentials are /// of all saved networks. If no network credentials are found, a `None` type
/// found, a `None` type is returned in the `Result`. In the event of an error, /// is returned in the `Result`. In the event of an error, a `NetworkError` is
/// a `NetworkError` is returned in the `Result`. The `NetworkError` is then /// returned in the `Result`.
/// enumerated to a specific error type and an appropriate JSON RPC response is pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
/// sent to the caller. let mut wpa = wpactrl::WpaCtrl::builder().open()?;
/// let networks = wpa.request("LIST_NETWORKS")?;
pub fn saved_networks() -> Result<Option<String>, NetworkError> {
let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?;
let networks = wpa.request("LIST_NETWORKS").context(WpaCtrlRequest)?;
let mut ssids = Vec::new(); let mut ssids = Vec::new();
for network in networks.lines() { for network in networks.lines() {
let v: Vec<&str> = network.split('\t').collect(); let v: Vec<&str> = network.split('\t').collect();
let len = v.len(); let len = v.len();
if len > 1 { if len > 1 {
let ssid = v[1].trim().to_string(); let ssid = v[1].trim().to_string();
let response = Network { ssid }; ssids.push(ssid)
ssids.push(response)
} }
} }
if ssids.is_empty() { if ssids.is_empty() {
Ok(None) Ok(None)
} else { } else {
let results = serde_json::to_string(&ssids).context(SerdeSerialize)?; Ok(Some(ssids))
Ok(Some(results))
} }
} }
@ -366,17 +320,11 @@ pub fn saved_networks() -> Result<Option<String>, NetworkError> {
/// an `Ok` `Result` type is returned containing `Some(String)` - where `String` /// an `Ok` `Result` type is returned containing `Some(String)` - where `String`
/// is the SSID of the associated network. If SSID is not found, a `None` type /// is the SSID of the associated network. If SSID is not found, a `None` type
/// is returned in the `Result`. In the event of an error, a `NetworkError` is /// is returned in the `Result`. In the event of an error, a `NetworkError` is
/// returned in the `Result`. The `NetworkError` is then enumerated to a /// returned in the `Result`.
/// specific error type and an appropriate JSON RPC response is sent to the
/// caller.
///
pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> { pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new() let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
.ctrl_path(wpa_path) let status = wpa.request("STATUS")?;
.open()
.context(WpaCtrlOpen)?;
let status = wpa.request("STATUS").context(WpaCtrlRequest)?;
// pass the regex pattern and status output to the regex finder // pass the regex pattern and status output to the regex finder
let ssid = utils::regex_finder(r"\nssid=(.*)\n", &status)?; let ssid = utils::regex_finder(r"\nssid=(.*)\n", &status)?;
@ -394,9 +342,7 @@ pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
/// returned containing `Some(String)` - where `String` is the state of the /// returned containing `Some(String)` - where `String` is the state of the
/// network interface. If state is not found, a `None` type is returned in the /// network interface. If state is not found, a `None` type is returned in the
/// `Result`. In the event of an error, a `NetworkError` is returned in the /// `Result`. In the event of an error, a `NetworkError` is returned in the
/// `Result`. The `NetworkError` is then enumerated to a specific error type and /// `Result`.
/// an appropriate JSON RPC response is sent to the caller.
///
pub fn state(iface: &str) -> Result<Option<String>, NetworkError> { pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
// construct the interface operstate path // construct the interface operstate path
let iface_path: String = format!("/sys/class/net/{}/operstate", iface); let iface_path: String = format!("/sys/class/net/{}/operstate", iface);
@ -404,7 +350,10 @@ pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
let output = Command::new("cat") let output = Command::new("cat")
.arg(iface_path) .arg(iface_path)
.output() .output()
.context(NoState { iface })?; .map_err(|source| NetworkError::NoState {
iface: iface.to_string(),
source,
})?;
if !output.stdout.is_empty() { if !output.stdout.is_empty() {
// unwrap the command result and convert to String // unwrap the command result and convert to String
let mut state = String::from_utf8(output.stdout).unwrap(); let mut state = String::from_utf8(output.stdout).unwrap();
@ -427,17 +376,11 @@ pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
/// returned containing `Some(Status)` - where `Status` is a `struct` /// returned containing `Some(Status)` - where `Status` is a `struct`
/// containing the aggregated interface data in named fields. If status is not /// containing the aggregated interface data in named fields. If status is not
/// found, a `None` type is returned in the `Result`. In the event of an error, /// found, a `None` type is returned in the `Result`. In the event of an error,
/// a `NetworkError` is returned in the `Result`. The `NetworkError` is then /// a `NetworkError` is returned in the `Result`.
/// enumerated to a specific error type and an appropriate JSON RPC response is
/// sent to the caller.
///
pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> { pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new() let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
.ctrl_path(wpa_path) let wpa_status = wpa.request("STATUS")?;
.open()
.context(WpaCtrlOpen)?;
let wpa_status = wpa.request("STATUS").context(WpaCtrlRequest)?;
// pass the regex pattern and status output to the regex finder // pass the regex pattern and status output to the regex finder
let state = utils::regex_finder(r"wpa_state=(.*)\n", &wpa_status)?; let state = utils::regex_finder(r"wpa_state=(.*)\n", &wpa_status)?;
@ -486,16 +429,16 @@ pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
/// * `iface` - A string slice holding the name of a wireless network interface /// * `iface` - A string slice holding the name of a wireless network interface
/// ///
/// If the network traffic statistics are found for the given interface, an `Ok` /// If the network traffic statistics are found for the given interface, an `Ok`
/// `Result` type is returned containing `Some(String)` - where `String` is a /// `Result` type is returned containing `Some(Traffic)`. The `Traffic` `struct`
/// serialized `Traffic` `struct` with fields for received and transmitted /// includes fields for received and transmitted network data statistics. If
/// network data statistics. If network traffic statistics are not found for the /// network traffic statistics are not found for the given interface, a `None`
/// given interface, a `None` type is returned in the `Result`. In the event of /// type is returned in the `Result`. In the event of an error, a `NetworkError`
/// an error, a `NetworkError` is returned in the `Result`. The `NetworkError` /// is returned in the `Result`.
/// is then enumerated to a specific error type and an appropriate JSON RPC pub fn traffic(iface: &str) -> Result<Option<Traffic>, NetworkError> {
/// response is sent to the caller. let network = network::read().map_err(|source| NetworkError::NoTraffic {
/// iface: iface.to_string(),
pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> { source,
let network = network::read().context(NoTraffic { iface })?; })?;
// iterate through interfaces returned in network data // iterate through interfaces returned in network data
for (interface, traffic) in network.interfaces { for (interface, traffic) in network.interfaces {
if interface == iface { if interface == iface {
@ -505,9 +448,7 @@ pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
received, received,
transmitted, transmitted,
}; };
// TODO: add test for SerdeSerialize error return Ok(Some(traffic));
let t = serde_json::to_string(&traffic).context(SerdeSerialize)?;
return Ok(Some(t));
} }
} }
@ -516,42 +457,25 @@ pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
/* SET - Methods for modifying state */ /* SET - Methods for modifying state */
/// Activate wireless access point. /// Start network interface service.
/// ///
/// A `systemctl `command is invoked which starts the `ap0` interface service. /// A `systemctl `command is invoked which starts the service for the given
/// If the command executes successfully, an `Ok` `Result` type is returned. /// network interface. If the command executes successfully, an `Ok` `Result`
/// In the event of an error, a `NetworkError` is returned in the `Result`. /// type is returned. In the event of an error, a `NetworkError` is returned
/// The `NetworkError` is then enumerated to a specific error type and an /// in the `Result`.
/// appropriate JSON RPC response is sent to the caller. pub fn start_iface_service(iface: &str) -> Result<(), NetworkError> {
/// let iface_service = format!("wpa_supplicant@{}.service", &iface);
pub fn activate_ap() -> Result<(), NetworkError> {
// start the ap0 interface service // start the interface service
Command::new("sudo") Command::new("sudo")
.arg("/usr/bin/systemctl") .arg("/usr/bin/systemctl")
.arg("start") .arg("start")
.arg("wpa_supplicant@ap0.service") .arg(iface_service)
.output() .output()
.context(StartAp0)?; .map_err(|source| NetworkError::StartInterface {
source,
Ok(()) iface: iface.to_string(),
} })?;
/// Activate wireless client.
///
/// A `systemctl` command is invoked which starts the `wlan0` interface service.
/// If the command executes successfully, an `Ok` `Result` type is returned.
/// In the event of an error, a `NetworkError` is returned in the `Result`.
/// The `NetworkError` is then enumerated to a specific error type and an
/// appropriate JSON RPC response is sent to the caller.
///
pub fn activate_client() -> Result<(), NetworkError> {
// start the wlan0 interface service
Command::new("sudo")
.arg("/usr/bin/systemctl")
.arg("start")
.arg("wpa_supplicant@wlan0.service")
.output()
.context(StartWlan0)?;
Ok(()) Ok(())
} }
@ -560,34 +484,36 @@ pub fn activate_client() -> Result<(), NetworkError> {
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `wlan_iface` - A local wireless interface.
/// * `wifi` - An instance of the `WiFi` `struct` with fields `ssid` and `pass` /// * `wifi` - An instance of the `WiFi` `struct` with fields `ssid` and `pass`
/// ///
/// If configuration parameters are successfully generated from the provided /// If configuration parameters are successfully generated from the provided
/// SSID and password and appended to `wpa_supplicant-wlan0.conf`, an `Ok` /// SSID and password and appended to `wpa_supplicant-<wlan_iface>.conf` (where
/// `Result` type is returned. In the event of an error, a `NetworkError` is /// `<wlan_iface>` is the provided interface parameter), an `Ok` `Result` type
/// returned in the `Result`. The `NetworkError` is then enumerated to a /// is returned. In the event of an error, a `NetworkError` is returned in the
/// specific error type and an appropriate JSON RPC response is sent to the /// `Result`.
/// caller. pub fn add(wlan_iface: &str, ssid: &str, pass: &str) -> Result<(), NetworkError> {
///
pub fn add(wifi: &WiFi) -> Result<(), NetworkError> {
// generate configuration based on provided ssid & password // generate configuration based on provided ssid & password
let output = Command::new("wpa_passphrase") let output = Command::new("wpa_passphrase")
.arg(&wifi.ssid) .arg(&ssid)
.arg(&wifi.pass) .arg(&pass)
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.output() .output()
.context(GenWpaPassphrase { ssid: &wifi.ssid })?; .map_err(|source| NetworkError::GenWpaPassphrase {
ssid: ssid.to_string(),
source,
})?;
// prepend newline to wpa_details to safeguard against malformed supplicant // prepend newline to wpa_details to safeguard against malformed supplicant
let mut wpa_details = "\n".as_bytes().to_vec(); let mut wpa_details = "\n".as_bytes().to_vec();
wpa_details.extend(&*(output.stdout)); wpa_details.extend(&*(output.stdout));
// append wpa_passphrase output to wpa_supplicant-wlan0.conf if successful let wlan_config = format!("/etc/wpa_supplicant/wpa_supplicant-{}.conf", wlan_iface);
// append wpa_passphrase output to wpa_supplicant-<wlan_iface>.conf if successful
if output.status.success() { if output.status.success() {
// open file in append mode // open file in append mode
let file = OpenOptions::new() let file = OpenOptions::new().append(true).open(wlan_config);
.append(true)
.open("/etc/wpa_supplicant/wpa_supplicant-wlan0.conf");
let _file = match file { let _file = match file {
// if file exists & open succeeds, write wifi configuration // if file exists & open succeeds, write wifi configuration
@ -601,40 +527,41 @@ pub fn add(wifi: &WiFi) -> Result<(), NetworkError> {
} else { } else {
let err_msg = String::from_utf8_lossy(&output.stdout); let err_msg = String::from_utf8_lossy(&output.stdout);
Err(NetworkError::GenWpaPassphraseWarning { Err(NetworkError::GenWpaPassphraseWarning {
ssid: wifi.ssid.to_string(), ssid: ssid.to_string(),
err_msg: err_msg.to_string(), err_msg: err_msg.to_string(),
}) })
} }
} }
/// Deploy the access point if the `wlan0` interface is `up` without an active /// Deploy an access point if the wireless interface is `up` without an active
/// connection. /// connection.
/// ///
/// The status of the `wlan0` service and the state of the `wlan0` interface /// The status of the wireless service and the state of the wireless interface
/// are checked. If the service is active but the interface is down (ie. not /// are checked. If the service is active but the interface is down (ie. not
/// currently connected to an access point), then the access point is activated /// currently connected to an access point), then the access point is activated
/// by calling the `activate_ap()` function. /// by calling the `activate_ap()` function.
/// pub fn check_iface(wlan_iface: &str, ap_iface: &str) -> Result<(), NetworkError> {
pub fn check_iface() -> Result<(), NetworkError> { let wpa_service = format!("wpa_supplicant@{}.service", &wlan_iface);
// returns 0 if the service is currently active
let wlan0_status = Command::new("/usr/bin/systemctl")
.arg("is-active")
.arg("wpa_supplicant@wlan0.service")
.status()
.context(WlanState)?;
// returns the current state of the wlan0 interface // returns 0 if the service is currently active
let iface_state = state("wlan0")?; let wlan_status = Command::new("/usr/bin/systemctl")
.arg("is-active")
.arg(wpa_service)
.status()
.map_err(NetworkError::WlanState)?;
// returns the current state of the wlan interface
let iface_state = state(wlan_iface)?;
// returns down if the interface is not currently connected to an ap // returns down if the interface is not currently connected to an ap
let wlan0_state = match iface_state { let wlan_state = match iface_state {
Some(state) => state, Some(state) => state,
None => "error".to_string(), None => "error".to_string(),
}; };
// if wlan0 is active but not connected, start the ap0 service // if wlan is active but not connected, start the ap service
if wlan0_status.success() && wlan0_state == "down" { if wlan_status.success() && wlan_state == "down" {
activate_ap()? start_iface_service(ap_iface)?
} }
Ok(()) Ok(())
@ -651,18 +578,12 @@ pub fn check_iface() -> Result<(), NetworkError> {
/// If the network connection is successfully activated for the access point /// If the network connection is successfully activated for the access point
/// represented by the given network identifier on the given wireless interface, /// represented by the given network identifier on the given wireless interface,
/// an `Ok` `Result`type is returned. In the event of an error, a `NetworkError` /// an `Ok` `Result`type is returned. In the event of an error, a `NetworkError`
/// is returned in the `Result`. The `NetworkError` is then enumerated to a /// is returned in the `Result`.
/// specific error type and an appropriate JSON RPC response is sent to the
/// caller.
///
pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> { pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new() let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let select = format!("SELECT {}", id); let select = format!("SELECT {}", id);
wpa.request(&select).context(WpaCtrlRequest)?; wpa.request(&select)?;
Ok(()) Ok(())
} }
@ -676,18 +597,12 @@ pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
/// If the network configuration parameters are successfully deleted for /// If the network configuration parameters are successfully deleted for
/// the access point represented by the given network identifier, an `Ok` /// the access point represented by the given network identifier, an `Ok`
/// `Result`type is returned. In the event of an error, a `NetworkError` is /// `Result`type is returned. In the event of an error, a `NetworkError` is
/// returned in the `Result`. The `NetworkError` is then enumerated to a /// returned in the `Result`.
/// specific error type and an appropriate JSON RPC response is sent to the
/// caller.
///
pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> { pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new() let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let remove = format!("REMOVE_NETWORK {}", id); let remove = format!("REMOVE_NETWORK {}", id);
wpa.request(&remove).context(WpaCtrlRequest)?; wpa.request(&remove)?;
Ok(()) Ok(())
} }
@ -701,17 +616,12 @@ pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
/// If the network connection is successfully disabled for the access point /// If the network connection is successfully disabled for the access point
/// represented by the given network identifier, an `Ok` `Result`type is /// represented by the given network identifier, an `Ok` `Result`type is
/// returned. In the event of an error, a `NetworkError` is returned in the /// returned. In the event of an error, a `NetworkError` is returned in the
/// `Result`. The `NetworkError` is then enumerated to a specific error type and /// `Result`.
/// an appropriate JSON RPC response is sent to the caller.
///
pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> { pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new() let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let disable = format!("DISABLE_NETWORK {}", id); let disable = format!("DISABLE_NETWORK {}", id);
wpa.request(&disable).context(WpaCtrlRequest)?; wpa.request(&disable)?;
Ok(()) Ok(())
} }
@ -723,18 +633,12 @@ pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
/// ///
/// If the network connection is successfully disconnected for the given /// If the network connection is successfully disconnected for the given
/// wireless interface, an `Ok` `Result` type is returned. In the event of an /// wireless interface, an `Ok` `Result` type is returned. In the event of an
/// error, a `NetworkError` is returned in the `Result`. The `NetworkError` is /// error, a `NetworkError` is returned in the `Result`.
/// then enumerated to a specific error type and an appropriate JSON RPC
/// response is sent to the caller.
///
pub fn disconnect(iface: &str) -> Result<(), NetworkError> { pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new() let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let disconnect = "DISCONNECT".to_string(); let disconnect = "DISCONNECT".to_string();
wpa.request(&disconnect).context(WpaCtrlRequest)?; wpa.request(&disconnect)?;
Ok(()) Ok(())
} }
@ -748,18 +652,12 @@ pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
/// ///
/// If the password is successfully updated for the access point represented by /// If the password is successfully updated for the access point represented by
/// the given network identifier, an `Ok` `Result` type is returned. In the /// the given network identifier, an `Ok` `Result` type is returned. In the
/// event of an error, a `NetworkError` is returned in the `Result`. The /// event of an error, a `NetworkError` is returned in the `Result`.
/// `NetworkError` is then enumerated to a specific error type and an
/// appropriate JSON RPC response is sent to the caller.
///
pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> { pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new() let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
.ctrl_path(wpa_path)
.open()
.context(WpaCtrlOpen)?;
let new_pass = format!("NEW_PASSWORD {} {}", id, pass); let new_pass = format!("NEW_PASSWORD {} {}", id, pass);
wpa.request(&new_pass).context(WpaCtrlRequest)?; wpa.request(&new_pass)?;
Ok(()) Ok(())
} }
@ -771,17 +669,11 @@ pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
/// ///
/// If the network connection is successfully reassociated for the given /// If the network connection is successfully reassociated for the given
/// wireless interface, an `Ok` `Result` type is returned. In the event of an /// wireless interface, an `Ok` `Result` type is returned. In the event of an
/// error, a `NetworkError` is returned in the `Result`. The `NetworkError` is /// error, a `NetworkError` is returned in the `Result`.
/// then enumerated to a specific error type and an appropriate JSON RPC
/// response is sent to the caller.
///
pub fn reassociate(iface: &str) -> Result<(), NetworkError> { pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new() let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
.ctrl_path(wpa_path) wpa.request("REASSOCIATE")?;
.open()
.context(WpaCtrlOpen)?;
wpa.request("REASSOCIATE").context(WpaCtrlRequest)?;
Ok(()) Ok(())
} }
@ -790,13 +682,10 @@ pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
/// If the reconfigure command is successfully executed, indicating a reread /// If the reconfigure command is successfully executed, indicating a reread
/// of the `wpa_supplicant.conf` file by the `wpa_supplicant` process, an `Ok` /// of the `wpa_supplicant.conf` file by the `wpa_supplicant` process, an `Ok`
/// `Result` type is returned. In the event of an error, a `NetworkError` is /// `Result` type is returned. In the event of an error, a `NetworkError` is
/// returned in the `Result`. The `NetworkError` is then enumerated to a /// returned in the `Result`.
/// specific error type and an appropriate JSON RPC response is sent to the
/// caller.
///
pub fn reconfigure() -> Result<(), NetworkError> { pub fn reconfigure() -> Result<(), NetworkError> {
let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?; let mut wpa = wpactrl::WpaCtrl::builder().open()?;
wpa.request("RECONFIGURE").context(WpaCtrlRequest)?; wpa.request("RECONFIGURE")?;
Ok(()) Ok(())
} }
@ -808,18 +697,12 @@ pub fn reconfigure() -> Result<(), NetworkError> {
/// ///
/// If the network connection is successfully disconnected and reconnected for /// If the network connection is successfully disconnected and reconnected for
/// the given wireless interface, an `Ok` `Result` type is returned. In the /// the given wireless interface, an `Ok` `Result` type is returned. In the
/// event of an error, a `NetworkError` is returned in the `Result`. The /// event of an error, a `NetworkError` is returned in the `Result`.
/// `NetworkError` is then enumerated to a specific error type and an
/// appropriate JSON RPC response is sent to the caller.
///
pub fn reconnect(iface: &str) -> Result<(), NetworkError> { pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = wpactrl::WpaCtrl::new() let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
.ctrl_path(wpa_path) wpa.request("DISCONNECT")?;
.open() wpa.request("RECONNECT")?;
.context(WpaCtrlOpen)?;
wpa.request("DISCONNECT").context(WpaCtrlRequest)?;
wpa.request("RECONNECT").context(WpaCtrlRequest)?;
Ok(()) Ok(())
} }
@ -827,12 +710,9 @@ pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
/// ///
/// If wireless network configuration updates are successfully save to the /// If wireless network configuration updates are successfully save to the
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the /// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
/// event of an error, a `NetworkError` is returned in the `Result`. The /// event of an error, a `NetworkError` is returned in the `Result`.
/// `NetworkError` is then enumerated to a specific error type and an
/// appropriate JSON RPC response is sent to the caller.
///
pub fn save() -> Result<(), NetworkError> { pub fn save() -> Result<(), NetworkError> {
let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?; let mut wpa = wpactrl::WpaCtrl::builder().open()?;
wpa.request("SAVE_CONFIG").context(WpaCtrlRequest)?; wpa.request("SAVE_CONFIG")?;
Ok(()) Ok(())
} }

View File

@ -1,7 +1,6 @@
use regex::Regex; use regex::Regex;
use snafu::ResultExt;
use crate::error::*; use crate::error::NetworkError;
/// Return matches for a given Regex pattern and text /// Return matches for a given Regex pattern and text
/// ///
@ -11,7 +10,7 @@ use crate::error::*;
/// * `text` - A string slice containing the text to be matched on /// * `text` - A string slice containing the text to be matched on
/// ///
pub fn regex_finder(pattern: &str, text: &str) -> Result<Option<String>, NetworkError> { pub fn regex_finder(pattern: &str, text: &str) -> Result<Option<String>, NetworkError> {
let re = Regex::new(pattern).context(Regex)?; let re = Regex::new(pattern)?;
let caps = re.captures(text); let caps = re.captures(text);
let result = caps.map(|caps| caps[1].to_string()); let result = caps.map(|caps| caps[1].to_string());

View File

@ -1,4 +0,0 @@
[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] [package]
name = "peach-oled" name = "peach-oled"
version = "0.1.3" version = "0.1.4"
authors = ["Andrew Reid <gnomad@cryptolab.net>"] authors = ["Andrew Reid <gnomad@cryptolab.net>"]
edition = "2018" edition = "2018"
description = "Write and draw to OLED display using JSON-RPC over HTTP." description = "Write and draw to OLED display using JSON-RPC over HTTP."
@ -27,18 +27,16 @@ travis-ci = { repository = "peachcloud/peach-oled", branch = "master" }
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
jsonrpc-core = "11.0.0"
jsonrpc-http-server = "11.0.0"
linux-embedded-hal = "0.2.2"
embedded-graphics = "0.4.7" embedded-graphics = "0.4.7"
tinybmp = "0.1.0" env_logger = "0.9"
ssd1306 = "0.2.6" jsonrpc-core = "18"
serde = { version = "1.0.87", features = ["derive"] } jsonrpc-http-server = "18"
serde_json = "1.0.39" linux-embedded-hal = "0.2.2"
log = "0.4.0" log = "0.4"
env_logger = "0.6.1" serde = { version = "1", features = ["derive"] }
snafu = "0.4.1"
nix="0.11" nix="0.11"
ssd1306 = "0.2.6"
tinybmp = "0.1.0"
[dev-dependencies] [dev-dependencies]
jsonrpc-test = "11.0.0" jsonrpc-test = "18"

View File

@ -1,6 +1,6 @@
# peach-oled # peach-oled
[![Build Status](https://travis-ci.com/peachcloud/peach-oled.svg?branch=master)](https://travis-ci.com/peachcloud/peach-oled) ![Generic badge](https://img.shields.io/badge/version-0.1.3-<COLOR>.svg) [![Build Status](https://travis-ci.com/peachcloud/peach-oled.svg?branch=master)](https://travis-ci.com/peachcloud/peach-oled) ![Generic badge](https://img.shields.io/badge/version-0.1.4-<COLOR>.svg)
OLED microservice module for PeachCloud. Write to a 128x64 OLED display with SDD1306 driver (I2C) using [JSON-RPC](https://www.jsonrpc.org/specification) over http. OLED microservice module for PeachCloud. Write to a 128x64 OLED display with SDD1306 driver (I2C) using [JSON-RPC](https://www.jsonrpc.org/specification) over http.

View File

@ -1,44 +1,68 @@
use std::error; use std::{error, fmt};
use jsonrpc_core::{types::error::Error, ErrorCode}; use jsonrpc_core::types::error::Error as JsonRpcError;
use linux_embedded_hal as hal; use jsonrpc_core::ErrorCode;
use snafu::Snafu; use linux_embedded_hal::i2cdev::linux::LinuxI2CError;
pub type BoxError = Box<dyn error::Error>; #[derive(Debug)]
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum OledError { pub enum OledError {
#[snafu(display("Failed to create interface for I2C device: {}", source))]
I2CError { I2CError {
source: hal::i2cdev::linux::LinuxI2CError, source: LinuxI2CError,
}, },
#[snafu(display("Coordinate {} out of range {}: {}", coord, range, value))]
InvalidCoordinate { InvalidCoordinate {
coord: String, coord: String,
range: String, range: String,
value: i32, value: i32,
}, },
InvalidFontSize {
// TODO: implement for validate() in src/lib.rs font: String,
#[snafu(display("Font size invalid: {}", font))] },
InvalidFontSize { font: String }, InvalidString {
len: usize,
#[snafu(display("String length out of range 0-21: {}", len))] },
InvalidString { len: usize }, MissingParameter {
source: JsonRpcError,
#[snafu(display("Missing expected parameter: {}", e))] },
MissingParameter { e: Error }, ParseError {
source: JsonRpcError,
#[snafu(display("Failed to parse parameter: {}", e))] },
ParseError { e: Error },
} }
impl From<OledError> for Error { impl error::Error for OledError {}
impl fmt::Display for OledError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
OledError::ParseError { ref source } => {
write!(f, "Failed to parse parameter: {}", source)
}
OledError::MissingParameter { ref source } => {
write!(f, "Missing expected parameter: {}", source)
}
OledError::InvalidString { len } => {
write!(f, "String length out of range 0-21: {}", len)
}
OledError::InvalidFontSize { ref font } => {
write!(f, "Invalid font size: {}", font)
}
OledError::InvalidCoordinate {
ref coord,
ref range,
value,
} => {
write!(f, "Coordinate {} out of range {}: {}", coord, range, value)
}
OledError::I2CError { ref source } => {
write!(f, "Failed to create interface for I2C device: {}", source)
}
}
}
}
impl From<OledError> for JsonRpcError {
fn from(err: OledError) -> Self { fn from(err: OledError) -> Self {
match &err { match &err {
OledError::I2CError { source } => Error { OledError::I2CError { source } => JsonRpcError {
code: ErrorCode::ServerError(-32000), code: ErrorCode::ServerError(-32000),
message: format!("Failed to create interface for I2C device: {}", source), message: format!("Failed to create interface for I2C device: {}", source),
data: None, data: None,
@ -47,7 +71,7 @@ impl From<OledError> for Error {
coord, coord,
value, value,
range, range,
} => Error { } => JsonRpcError {
code: ErrorCode::ServerError(-32001), code: ErrorCode::ServerError(-32001),
message: format!( message: format!(
"Validation error: coordinate {} out of range {}: {}", "Validation error: coordinate {} out of range {}: {}",
@ -55,18 +79,18 @@ impl From<OledError> for Error {
), ),
data: None, data: None,
}, },
OledError::InvalidFontSize { font } => Error { OledError::InvalidFontSize { font } => JsonRpcError {
code: ErrorCode::ServerError(-32002), code: ErrorCode::ServerError(-32002),
message: format!("Validation error: {} is not an accepted font size. Use 6x8, 6x12, 8x16 or 12x16 instead", font), message: format!("Validation error: {} is not an accepted font size. Use 6x8, 6x12, 8x16 or 12x16 instead", font),
data: None, data: None,
}, },
OledError::InvalidString { len } => Error { OledError::InvalidString { len } => JsonRpcError {
code: ErrorCode::ServerError(-32003), code: ErrorCode::ServerError(-32003),
message: format!("Validation error: string length {} out of range 0-21", len), message: format!("Validation error: string length {} out of range 0-21", len),
data: None, data: None,
}, },
OledError::MissingParameter { e } => e.clone(), OledError::MissingParameter { source } => source.clone(),
OledError::ParseError { e } => e.clone(), OledError::ParseError { source } => source.clone(),
} }
} }
} }

View File

@ -6,21 +6,21 @@ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
use embedded_graphics::coord::Coord; use embedded_graphics::{
use embedded_graphics::fonts::{Font12x16, Font6x12, Font6x8, Font8x16}; coord::Coord,
use embedded_graphics::image::Image1BPP; fonts::{Font12x16, Font6x12, Font6x8, Font8x16},
use embedded_graphics::prelude::*; image::Image1BPP,
prelude::*,
};
use hal::I2cdev; use hal::I2cdev;
use jsonrpc_core::{types::error::Error, IoHandler, Params, Value}; use jsonrpc_core::{types::error::Error, IoHandler, Params, Value};
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder}; use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
use linux_embedded_hal as hal; use linux_embedded_hal as hal;
use log::{debug, error, info}; use log::{debug, error, info};
use serde::Deserialize; use serde::Deserialize;
use snafu::{ensure, ResultExt}; use ssd1306::{prelude::*, Builder};
use ssd1306::prelude::*;
use ssd1306::Builder;
use crate::error::{BoxError, I2CError, InvalidCoordinate, InvalidString, OledError}; use crate::error::OledError;
// define the Graphic struct for receiving draw commands // define the Graphic struct for receiving draw commands
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -47,80 +47,55 @@ pub struct On {
on: bool, on: bool,
} }
fn validate(m: &Msg) -> Result<(), OledError> { fn validate(msg: &Msg) -> Result<(), OledError> {
ensure!( if msg.string.len() > 21 {
m.string.len() <= 21, Err(OledError::InvalidString {
InvalidString { len: msg.string.len(),
len: m.string.len() })
} } else if msg.x_coord < 0 || msg.x_coord > 128 {
); Err(OledError::InvalidCoordinate {
ensure!(
m.x_coord >= 0,
InvalidCoordinate {
coord: "x".to_string(), coord: "x".to_string(),
range: "0-128".to_string(), range: "0-128".to_string(),
value: m.x_coord, value: msg.x_coord,
} })
); } else if msg.y_coord < 0 || msg.y_coord > 147 {
Err(OledError::InvalidCoordinate {
ensure!(
m.x_coord < 129,
InvalidCoordinate {
coord: "x".to_string(),
range: "0-128".to_string(),
value: m.x_coord,
}
);
ensure!(
m.y_coord >= 0,
InvalidCoordinate {
coord: "y".to_string(), coord: "y".to_string(),
range: "0-47".to_string(), range: "0-47".to_string(),
value: m.y_coord, value: msg.y_coord,
} })
); } else {
ensure!(
m.y_coord < 148,
InvalidCoordinate {
coord: "y".to_string(),
range: "0-47".to_string(),
value: m.y_coord,
}
);
Ok(()) Ok(())
} }
}
pub fn run() -> Result<(), BoxError> { pub fn run() -> Result<(), OledError> {
info!("Starting up."); info!("Starting up.");
debug!("Creating interface for I2C device."); debug!("Creating interface for I2C device.");
let i2c = I2cdev::new("/dev/i2c-1").context(I2CError)?; let i2c = I2cdev::new("/dev/i2c-1").map_err(|source| OledError::I2CError { source })?;
let mut disp: GraphicsMode<_> = Builder::new().connect_i2c(i2c).into(); let mut display: GraphicsMode<_> = Builder::new().connect_i2c(i2c).into();
info!("Initializing the display."); info!("Initializing the display.");
disp.init().unwrap_or_else(|_| { display.init().unwrap_or_else(|_| {
error!("Problem initializing the OLED display."); error!("Problem initializing the OLED display.");
process::exit(1); process::exit(1);
}); });
debug!("Flushing the display."); debug!("Flushing the display.");
disp.flush().unwrap_or_else(|_| { display.flush().unwrap_or_else(|_| {
error!("Problem flushing the OLED display."); error!("Problem flushing the OLED display.");
process::exit(1); process::exit(1);
}); });
let oled = Arc::new(Mutex::new(disp)); let oled = Arc::new(Mutex::new(display));
let oled_clone = Arc::clone(&oled); let oled_clone = Arc::clone(&oled);
info!("Creating JSON-RPC I/O handler."); info!("Creating JSON-RPC I/O handler.");
let mut io = IoHandler::default(); let mut io = IoHandler::default();
io.add_method("clear", move |_| { io.add_sync_method("clear", move |_| {
let mut oled = oled_clone.lock().unwrap(); let mut oled = oled_clone.lock().unwrap();
info!("Clearing the display."); info!("Clearing the display.");
oled.clear(); oled.clear();
@ -134,21 +109,20 @@ pub fn run() -> Result<(), BoxError> {
let oled_clone = Arc::clone(&oled); let oled_clone = Arc::clone(&oled);
io.add_method("draw", move |params: Params| { io.add_sync_method("draw", move |params: Params| {
let g: Result<Graphic, Error> = params.parse(); let graphic: Graphic = params.parse()?;
let g: Graphic = g?;
// TODO: add simple byte validation function // TODO: add simple byte validation function
let mut oled = oled_clone.lock().unwrap(); let mut oled = oled_clone.lock().unwrap();
info!("Drawing image to the display."); info!("Drawing image to the display.");
let im = let image = Image1BPP::new(&graphic.bytes, graphic.width, graphic.height)
Image1BPP::new(&g.bytes, g.width, g.height).translate(Coord::new(g.x_coord, g.y_coord)); .translate(Coord::new(graphic.x_coord, graphic.y_coord));
oled.draw(im.into_iter()); oled.draw(image.into_iter());
Ok(Value::String("success".into())) Ok(Value::String("success".into()))
}); });
let oled_clone = Arc::clone(&oled); let oled_clone = Arc::clone(&oled);
io.add_method("flush", move |_| { io.add_sync_method("flush", move |_| {
let mut oled = oled_clone.lock().unwrap(); let mut oled = oled_clone.lock().unwrap();
info!("Flushing the display."); info!("Flushing the display.");
oled.flush().unwrap_or_else(|_| { oled.flush().unwrap_or_else(|_| {
@ -160,9 +134,9 @@ pub fn run() -> Result<(), BoxError> {
let oled_clone = Arc::clone(&oled); let oled_clone = Arc::clone(&oled);
io.add_method("ping", |_| Ok(Value::String("success".to_string()))); io.add_sync_method("ping", |_| Ok(Value::String("success".to_string())));
io.add_method("power", move |params: Params| { io.add_sync_method("power", move |params: Params| {
let o: Result<On, Error> = params.parse(); let o: Result<On, Error> = params.parse();
let o: On = o?; let o: On = o?;
let mut oled = oled_clone.lock().unwrap(); let mut oled = oled_clone.lock().unwrap();
@ -180,37 +154,36 @@ pub fn run() -> Result<(), BoxError> {
let oled_clone = Arc::clone(&oled); let oled_clone = Arc::clone(&oled);
io.add_method("write", move |params: Params| { io.add_sync_method("write", move |params: Params| {
info!("Received a 'write' request."); info!("Received a 'write' request.");
let m: Result<Msg, Error> = params.parse(); let msg = params.parse()?;
let m: Msg = m?; validate(&msg)?;
validate(&m)?;
let mut oled = oled_clone.lock().unwrap(); let mut oled = oled_clone.lock().unwrap();
info!("Writing to the display."); info!("Writing to the display.");
if m.font_size == "6x8" { if msg.font_size == "6x8" {
oled.draw( oled.draw(
Font6x8::render_str(&m.string) Font6x8::render_str(&msg.string)
.translate(Coord::new(m.x_coord, m.y_coord)) .translate(Coord::new(msg.x_coord, msg.y_coord))
.into_iter(), .into_iter(),
); );
} else if m.font_size == "6x12" { } else if msg.font_size == "6x12" {
oled.draw( oled.draw(
Font6x12::render_str(&m.string) Font6x12::render_str(&msg.string)
.translate(Coord::new(m.x_coord, m.y_coord)) .translate(Coord::new(msg.x_coord, msg.y_coord))
.into_iter(), .into_iter(),
); );
} else if m.font_size == "8x16" { } else if msg.font_size == "8x16" {
oled.draw( oled.draw(
Font8x16::render_str(&m.string) Font8x16::render_str(&msg.string)
.translate(Coord::new(m.x_coord, m.y_coord)) .translate(Coord::new(msg.x_coord, msg.y_coord))
.into_iter(), .into_iter(),
); );
} else if m.font_size == "12x16" { } else if msg.font_size == "12x16" {
oled.draw( oled.draw(
Font12x16::render_str(&m.string) Font12x16::render_str(&msg.string)
.translate(Coord::new(m.x_coord, m.y_coord)) .translate(Coord::new(msg.x_coord, msg.y_coord))
.into_iter(), .into_iter(),
); );
} }
@ -255,7 +228,7 @@ mod tests {
fn rpc_success() { fn rpc_success() {
let rpc = { let rpc = {
let mut io = IoHandler::new(); let mut io = IoHandler::new();
io.add_method("rpc_success_response", |_| { io.add_sync_method("rpc_success_response", |_| {
Ok(Value::String("success".into())) Ok(Value::String("success".into()))
}); });
test_rpc::Rpc::from(io) test_rpc::Rpc::from(io)
@ -269,7 +242,7 @@ mod tests {
fn rpc_internal_error() { fn rpc_internal_error() {
let rpc = { let rpc = {
let mut io = IoHandler::new(); let mut io = IoHandler::new();
io.add_method("rpc_internal_error", |_| Err(Error::internal_error())); io.add_sync_method("rpc_internal_error", |_| Err(Error::internal_error()));
test_rpc::Rpc::from(io) test_rpc::Rpc::from(io)
}; };
@ -287,7 +260,7 @@ mod tests {
fn rpc_i2c_io_error() { fn rpc_i2c_io_error() {
let rpc = { let rpc = {
let mut io = IoHandler::new(); let mut io = IoHandler::new();
io.add_method("rpc_i2c_io_error", |_| { io.add_sync_method("rpc_i2c_io_error", |_| {
let io_err = IoError::new(ErrorKind::PermissionDenied, "oh no!"); let io_err = IoError::new(ErrorKind::PermissionDenied, "oh no!");
let source = LinuxI2CError::Io(io_err); let source = LinuxI2CError::Io(io_err);
Err(Error::from(OledError::I2CError { source })) Err(Error::from(OledError::I2CError { source }))
@ -310,7 +283,7 @@ mod tests {
fn rpc_i2c_nix_error() { fn rpc_i2c_nix_error() {
let rpc = { let rpc = {
let mut io = IoHandler::new(); let mut io = IoHandler::new();
io.add_method("rpc_i2c_nix_error", |_| { io.add_sync_method("rpc_i2c_nix_error", |_| {
let nix_err = NixError::InvalidPath; let nix_err = NixError::InvalidPath;
let source = LinuxI2CError::Nix(nix_err); let source = LinuxI2CError::Nix(nix_err);
Err(Error::from(OledError::I2CError { source })) Err(Error::from(OledError::I2CError { source }))
@ -333,7 +306,7 @@ mod tests {
fn rpc_invalid_coord() { fn rpc_invalid_coord() {
let rpc = { let rpc = {
let mut io = IoHandler::new(); let mut io = IoHandler::new();
io.add_method("rpc_invalid_coord", |_| { io.add_sync_method("rpc_invalid_coord", |_| {
Err(Error::from(OledError::InvalidCoordinate { Err(Error::from(OledError::InvalidCoordinate {
coord: "x".to_string(), coord: "x".to_string(),
range: "0-128".to_string(), range: "0-128".to_string(),
@ -357,7 +330,7 @@ mod tests {
fn rpc_invalid_fontsize() { fn rpc_invalid_fontsize() {
let rpc = { let rpc = {
let mut io = IoHandler::new(); let mut io = IoHandler::new();
io.add_method("rpc_invalid_fontsize", |_| { io.add_sync_method("rpc_invalid_fontsize", |_| {
Err(Error::from(OledError::InvalidFontSize { Err(Error::from(OledError::InvalidFontSize {
font: "24x32".to_string(), font: "24x32".to_string(),
})) }))
@ -379,7 +352,7 @@ mod tests {
fn rpc_invalid_string() { fn rpc_invalid_string() {
let rpc = { let rpc = {
let mut io = IoHandler::new(); let mut io = IoHandler::new();
io.add_method("rpc_invalid_string", |_| { io.add_sync_method("rpc_invalid_string", |_| {
Err(Error::from(OledError::InvalidString { len: 22 })) Err(Error::from(OledError::InvalidString { len: 22 }))
}); });
test_rpc::Rpc::from(io) test_rpc::Rpc::from(io)
@ -399,15 +372,15 @@ mod tests {
fn rpc_invalid_params() { fn rpc_invalid_params() {
let rpc = { let rpc = {
let mut io = IoHandler::new(); let mut io = IoHandler::new();
io.add_method("rpc_invalid_params", |_| { io.add_sync_method("rpc_invalid_params", |_| {
let e = Error { let source = Error {
code: ErrorCode::InvalidParams, code: ErrorCode::InvalidParams,
message: String::from("invalid params"), message: String::from("invalid params"),
data: Some(Value::String( data: Some(Value::String(
"Invalid params: invalid type: null, expected struct Msg.".into(), "Invalid params: invalid type: null, expected struct Msg.".into(),
)), )),
}; };
Err(Error::from(OledError::MissingParameter { e })) Err(Error::from(OledError::MissingParameter { source }))
}); });
test_rpc::Rpc::from(io) test_rpc::Rpc::from(io)
}; };
@ -427,13 +400,13 @@ mod tests {
fn rpc_parse_error() { fn rpc_parse_error() {
let rpc = { let rpc = {
let mut io = IoHandler::new(); let mut io = IoHandler::new();
io.add_method("rpc_parse_error", |_| { io.add_sync_method("rpc_parse_error", |_| {
let e = Error { let source = Error {
code: ErrorCode::ParseError, code: ErrorCode::ParseError,
message: String::from("Parse error"), message: String::from("Parse error"),
data: None, data: None,
}; };
Err(Error::from(OledError::ParseError { e })) Err(Error::from(OledError::ParseError { source }))
}); });
test_rpc::Rpc::from(io) test_rpc::Rpc::from(io)
}; };

View File

@ -1,4 +0,0 @@
[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,4 +0,0 @@
[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,40 +1,31 @@
[package] [package]
name = "peach-stats" name = "peach-stats"
version = "0.1.3" version = "0.2.0"
authors = ["Andrew Reid <gnomad@cryptolab.net>"] authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2018" edition = "2018"
description = "Query system statistics using JSON-RPC over HTTP. Provides a JSON-RPC wrapper around the probes and systemstat crates." description = "Query system statistics. Provides a wrapper around the probes and systemstat crates."
keywords = ["peachcloud", "system stats", "system statistics", "disk", "memory"]
homepage = "https://opencollective.com/peachcloud" homepage = "https://opencollective.com/peachcloud"
repository = "https://github.com/peachcloud/peach-stats" repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-stats"
readme = "README.md" readme = "README.md"
license = "AGPL-3.0-only" license = "LGPL-3.0-only"
publish = false publish = false
[package.metadata.deb]
depends = "$auto"
extended-description = """\
peach-stats is a system statistics microservice module for PeachCloud. \
Query system statistics using JSON-RPC over HTTP. Provides a JSON-RPC \
wrapper around the probes and systemstat crates."""
maintainer-scripts="debian"
systemd-units = { unit-name = "peach-stats" }
assets = [
["target/release/peach-stats", "usr/bin/", "755"],
["README.md", "usr/share/doc/peach-stats/README", "644"],
]
[badges] [badges]
travis-ci = { repository = "peachcloud/peach-stats", branch = "master" }
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
env_logger = "0.9"
jsonrpc-core = "18"
jsonrpc-http-server = "18"
log = "0.4" log = "0.4"
miniserde = "0.1.15" miniserde = { version = "0.1.15", optional = true }
probes = "0.4.1" probes = "0.4.1"
serde = { version = "1.0.130", features = ["derive"], optional = true }
systemstat = "0.1.10" systemstat = "0.1.10"
[dev-dependencies] [features]
jsonrpc-test = "18" default = []
# Provide `Serialize` and `Deserialize` traits for library structs using `miniserde`
miniserde_support = ["miniserde"]
# Provide `Serialize` and `Deserialize` traits for library structs using `serde`
serde_support = ["serde"]

View File

@ -1,109 +1,47 @@
# peach-stats # peach-stats
[![Build Status](https://travis-ci.com/peachcloud/peach-stats.svg?branch=master)](https://travis-ci.com/peachcloud/peach-stats) ![Generic badge](https://img.shields.io/badge/version-0.1.3-<COLOR>.svg) ![Generic badge](https://img.shields.io/badge/version-0.2.0-<COLOR>.svg)
System statistics microservice module for PeachCloud. Provides a JSON-RPC wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates. System statistics library for PeachCloud. Provides a wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates.
### JSON-RPC API Currently offers the following statistics and associated data structures:
| Method | Description | Returns | - CPU: `user`, `system`, `nice`, `idle` (as values or percentages)
| --- | --- | --- | - Disk usage: `filesystem`, `one_k_blocks`, `one_k_blocks_used`,
| `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` | `one_k_blocks_free`, `used_percentage`, `mountpoint`
| `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` | - Load average: `one`, `five`, `fifteen`
| `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` | - Memory: `total`, `free`, `used`
| `load_average` | Load average statistics | `one`, `five`, `fifteen` | - Uptime: `seconds`
| `mem_stats` | Memory statistics | `total`, `free`, `used` |
| `ping` | Microservice status | `success` if running |
| `uptime` | System uptime | `secs` |
### Environment ## Example Usage
The JSON-RPC HTTP server address and port can be configured with the `PEACH_STATS_SERVER` environment variable: ```rust
use peach_stats::{stats, StatsError};
`export PEACH_STATS_SERVER=127.0.0.1:5000` fn main() -> Result<(), StatsError> {
let cpu = stats::cpu_stats()?;
let cpu_percentages = stats::cpu_stats_percent()?;
let disks = stats::disk_usage()?;
let load = stats::load_average()?;
let mem = stats::mem_stats()?;
let uptime = stats::uptime()?;
When not set, the value defaults to `127.0.0.1:5113`. // do things with the retrieved values...
Logging is made available with `env_logger`: Ok(())
}
```
`export RUST_LOG=info` ## Feature Flags
Other logging levels include `debug`, `warn` and `error`. Feature flags are used to offer `Serialize` and `Deserialize` implementations for all `struct` data types provided by this library. These traits are not provided by default. A choice of `miniserde` and `serde` is provided.
### Setup Define the desired feature in the `Cargo.toml` manifest of your project:
Clone this repo: ```toml
peach-stats = { version = "0.1.0", features = ["miniserde_support"] }
```
`git clone https://github.com/peachcloud/peach-stats.git` ## License
Move into the repo and compile a release build:
`cd peach-stats`
`cargo build --release`
Run the binary:
`./target/release/peach-stats`
### Debian Packaging
A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-stats` to be easily bundled as a Debian package (`.deb`). The `cargo-deb` [crate](https://crates.io/crates/cargo-deb) can be used to achieve this.
Install `cargo-deb`:
`cargo install cargo-deb`
Move into the repo:
`cd peach-stats`
Build the package:
`cargo deb`
The output will be written to `target/debian/peach-stats_0.1.0_arm64.deb` (or similar).
Build the package (aarch64):
`cargo deb --target aarch64-unknown-linux-gnu`
Install the package as follows:
`sudo dpkg -i target/debian/peach-stats_0.1.0_arm64.deb`
The service will be automatically enabled and started.
Uninstall the service:
`sudo apt-get remove peach-stats`
Remove configuration files (not removed with `apt-get remove`):
`sudo apt-get purge peach-stats`
### Example Usage
**Get CPU Statistics**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "cpu_stats", "id":1 }' 127.0.0.1:5113`
Server responds with:
`{"jsonrpc":"2.0","result":"{\"user\":4661083,\"system\":1240371,\"idle\":326838290,\"nice\":0}","id":1}`
**Get System Uptime**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "uptime", "id":1 }' 127.0.0.1:5113`
Server responds with:
`{"jsonrpc":"2.0","result":"{\"secs\":840968}","id":1}`
### Licensing
AGPL-3.0
LGPL-3.0.

View File

@ -1,27 +0,0 @@
[Unit]
Description=Query system statistics using JSON-RPC over HTTP.
[Service]
Type=simple
User=peach-stats
Environment="RUST_LOG=error"
ExecStart=/usr/bin/peach-stats
Restart=always
CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYS_BOOT CAP_SYS_TIME CAP_KILL CAP_WAKE_ALARM CAP_LINUX_IMMUTABLE CAP_BLOCK_SUSPEND CAP_LEASE CAP_SYS_NICE CAP_SYS_RESOURCE CAP_RAWIO CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_* CAP_FOWNER CAP_IPC_OWNER CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_AUDIT_*
InaccessibleDirectories=/home
LockPersonality=yes
NoNewPrivileges=yes
PrivateDevices=yes
PrivateTmp=yes
PrivateUsers=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectSystem=yes
ReadOnlyDirectories=/var
RestrictAddressFamilies=~AF_INET6 AF_UNIX
SystemCallFilter=~@reboot @clock @debug @module @mount @swap @resources @privileged
[Install]
WantedBy=multi-user.target

View File

@ -1,69 +1,44 @@
use std::{error, fmt, io}; //! Custom error type for `peach-stats`.
use jsonrpc_core::{types::error::Error, ErrorCode};
use probes::ProbeError; use probes::ProbeError;
use std::{error, fmt, io::Error as IoError};
/// Custom error type encapsulating all possible errors when retrieving system
/// statistics.
#[derive(Debug)] #[derive(Debug)]
pub enum StatError { pub enum StatsError {
CpuStat { source: ProbeError }, /// Failed to retrieve CPU statistics.
DiskUsage { source: ProbeError }, CpuStat(ProbeError),
LoadAvg { source: ProbeError }, /// Failed to retrieve disk usage statistics.
MemStat { source: ProbeError }, DiskUsage(ProbeError),
Uptime { source: io::Error }, /// Failed to retrieve load average statistics.
LoadAvg(ProbeError),
/// Failed to retrieve memory usage statistics.
MemStat(ProbeError),
/// Failed to retrieve system uptime.
Uptime(IoError),
} }
impl error::Error for StatError {} impl error::Error for StatsError {}
impl fmt::Display for StatError { impl fmt::Display for StatsError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self { match *self {
StatError::CpuStat { ref source } => { StatsError::CpuStat(ref source) => {
write!(f, "Failed to retrieve CPU statistics: {}", source) write!(f, "Failed to retrieve CPU statistics: {}", source)
} }
StatError::DiskUsage { ref source } => { StatsError::DiskUsage(ref source) => {
write!(f, "Failed to retrieve disk usage statistics: {}", source) write!(f, "Failed to retrieve disk usage statistics: {}", source)
} }
StatError::LoadAvg { ref source } => { StatsError::LoadAvg(ref source) => {
write!(f, "Failed to retrieve load average statistics: {}", source) write!(f, "Failed to retrieve load average statistics: {}", source)
} }
StatError::MemStat { ref source } => { StatsError::MemStat(ref source) => {
write!(f, "Failed to retrieve memory statistics: {}", source) write!(f, "Failed to retrieve memory statistics: {}", source)
} }
StatError::Uptime { ref source } => { StatsError::Uptime(ref source) => {
write!(f, "Failed to retrieve system uptime: {}", source) write!(f, "Failed to retrieve system uptime: {}", source)
} }
} }
} }
} }
impl From<StatError> for Error {
fn from(err: StatError) -> Self {
match &err {
StatError::CpuStat { source } => Error {
code: ErrorCode::ServerError(-32001),
message: format!("Failed to retrieve CPU statistics: {}", source),
data: None,
},
StatError::DiskUsage { source } => Error {
code: ErrorCode::ServerError(-32001),
message: format!("Failed to retrieve disk usage statistics: {}", source),
data: None,
},
StatError::LoadAvg { source } => Error {
code: ErrorCode::ServerError(-32001),
message: format!("Failed to retrieve load average statistics: {}", source),
data: None,
},
StatError::MemStat { source } => Error {
code: ErrorCode::ServerError(-32001),
message: format!("Failed to retrieve memory statistics: {}", source),
data: None,
},
StatError::Uptime { source } => Error {
code: ErrorCode::ServerError(-32001),
message: format!("Failed to retrieve system uptime: {}", source),
data: None,
},
}
}
}

View File

@ -1,103 +1,48 @@
mod error; #![warn(missing_docs)]
mod stats;
mod structs;
use std::{env, result::Result}; //! # peach-stats
//!
//! System statistics retrieval library; designed for use with the PeachCloud platform.
//!
//! Currently offers the following statistics and associated data structures:
//!
//! - CPU: `user`, `system`, `nice`, `idle` (as values or percentages)
//! - Disk usage: `filesystem`, `one_k_blocks`, `one_k_blocks_used`,
//! `one_k_blocks_free`, `used_percentage`, `mountpoint`
//! - Load average: `one`, `five`, `fifteen`
//! - Memory: `total`, `free`, `used`
//! - Uptime: `seconds`
//!
//! ## Example Usage
//!
//! ```rust
//! use peach_stats::{stats, StatsError};
//!
//! fn main() -> Result<(), StatsError> {
//! let cpu = stats::cpu_stats()?;
//! let cpu_percentages = stats::cpu_stats_percent()?;
//! let disks = stats::disk_usage()?;
//! let load = stats::load_average()?;
//! let mem = stats::mem_stats()?;
//! let uptime = stats::uptime()?;
//!
//! Ok(())
//! }
//! ```
//!
//! ## Feature Flags
//!
//! Feature flags are used to offer `Serialize` and `Deserialize` implementations
//! for all `struct` data types provided by this library. These traits are not
//! provided by default. A choice of `miniserde` and `serde` is provided.
//!
//! Define the desired feature in the `Cargo.toml` manifest of your project:
//!
//! ```toml
//! peach-stats = { version = "0.1.0", features = ["miniserde_support"] }
//! ```
use jsonrpc_core::{IoHandler, Value}; pub mod error;
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder}; pub mod stats;
use log::info;
use crate::error::StatError; pub use crate::error::StatsError;
pub fn run() -> Result<(), StatError> {
info!("Starting up.");
info!("Creating JSON-RPC I/O handler.");
let mut io = IoHandler::default();
io.add_method("cpu_stats", move |_| async {
info!("Fetching CPU statistics.");
let stats = stats::cpu_stats()?;
Ok(Value::String(stats))
});
io.add_method("cpu_stats_percent", move |_| async {
info!("Fetching CPU statistics as percentages.");
let stats = stats::cpu_stats_percent()?;
Ok(Value::String(stats))
});
io.add_method("disk_usage", move |_| async {
info!("Fetching disk usage statistics.");
let disks = stats::disk_usage()?;
Ok(Value::String(disks))
});
io.add_method("load_average", move |_| async {
info!("Fetching system load average statistics.");
let avg = stats::load_average()?;
Ok(Value::String(avg))
});
io.add_method("mem_stats", move |_| async {
info!("Fetching current memory statistics.");
let mem = stats::mem_stats()?;
Ok(Value::String(mem))
});
io.add_method("ping", |_| async {
Ok(Value::String("success".to_string()))
});
io.add_method("uptime", move |_| async {
info!("Fetching system uptime.");
let uptime = stats::uptime()?;
Ok(Value::String(uptime))
});
let http_server = env::var("PEACH_OLED_STATS").unwrap_or_else(|_| "127.0.0.1:5113".to_string());
info!("Starting JSON-RPC server on {}.", http_server);
let server = ServerBuilder::new(io)
.cors(DomainsValidation::AllowOnly(vec![
AccessControlAllowOrigin::Null,
]))
.start_http(
&http_server
.parse()
.expect("Invalid HTTP address and port combination"),
)
.expect("Unable to start RPC server");
info!("Listening for requests.");
server.wait();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use jsonrpc_test as test_rpc;
// test to ensure correct success response
#[test]
fn rpc_success() {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_success_response", |_| async {
Ok(Value::String("success".into()))
});
test_rpc::Rpc::from(io)
};
assert_eq!(rpc.request("rpc_success_response", &()), r#""success""#);
}
}

View File

@ -1,14 +0,0 @@
use std::process;
use log::error;
fn main() {
// initialize the logger
env_logger::init();
// handle errors returned from `run`
if let Err(e) = peach_stats::run() {
error!("Application error: {}", e);
process::exit(1);
}
}

View File

@ -1,14 +1,96 @@
//! System statistics retrieval functions and associated data types.
use std::result::Result; use std::result::Result;
use miniserde::json; #[cfg(feature = "miniserde_support")]
use miniserde::{Deserialize, Serialize};
#[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize};
use probes::{cpu, disk_usage, load, memory}; use probes::{cpu, disk_usage, load, memory};
use systemstat::{Platform, System}; use systemstat::{Platform, System};
use crate::error::StatError; use crate::error::StatsError;
use crate::structs::{CpuStat, CpuStatPercentages, DiskUsage, LoadAverage, MemStat};
pub fn cpu_stats() -> Result<String, StatError> { /// CPU statistics.
let cpu_stats = cpu::proc::read().map_err(|source| StatError::CpuStat { source })?; #[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct CpuStat {
/// Time spent running user space (application) code.
pub user: u64,
/// Time spent running kernel code.
pub system: u64,
/// Time spent doing nothing.
pub idle: u64,
/// Time spent running user space processes which have been niced.
pub nice: u64,
}
/// CPU statistics as percentages.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct CpuStatPercentages {
/// Time spent running user space (application) code.
pub user: f32,
/// Time spent running kernel code.
pub system: f32,
/// Time spent doing nothing.
pub idle: f32,
/// Time spent running user space processes which have been niced.
pub nice: f32,
}
/// Disk usage statistics.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct DiskUsage {
/// Filesystem device path.
pub filesystem: Option<String>,
/// Total amount of disk space as a number of 1,000 kilobyte blocks.
pub one_k_blocks: u64,
/// Total amount of used disk space as a number of 1,000 kilobyte blocks.
pub one_k_blocks_used: u64,
/// Total amount of free / available disk space as a number of 1,000 kilobyte blocks.
pub one_k_blocks_free: u64,
/// Total amount of used disk space as a percentage.
pub used_percentage: u32,
/// Mountpoint of the disk / partition.
pub mountpoint: String,
}
/// Load average statistics.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct LoadAverage {
/// Average computational work performed over the past minute.
pub one: f32,
/// Average computational work performed over the past five minutes.
pub five: f32,
/// Average computational work performed over the past fifteen minutes.
pub fifteen: f32,
}
/// Memory statistics.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct MemStat {
/// Total amount of physical memory in kilobytes.
pub total: u64,
/// Total amount of free / available physical memory in kilobytes.
pub free: u64,
/// Total amount of used physical memory in kilobytes.
pub used: u64,
}
/// Retrieve the current CPU statistics.
pub fn cpu_stats() -> Result<CpuStat, StatsError> {
let cpu_stats = cpu::proc::read().map_err(StatsError::CpuStat)?;
let s = cpu_stats.stat; let s = cpu_stats.stat;
let cpu = CpuStat { let cpu = CpuStat {
user: s.user, user: s.user,
@ -16,13 +98,13 @@ pub fn cpu_stats() -> Result<String, StatError> {
nice: s.nice, nice: s.nice,
idle: s.idle, idle: s.idle,
}; };
let json_cpu = json::to_string(&cpu);
Ok(json_cpu) Ok(cpu)
} }
pub fn cpu_stats_percent() -> Result<String, StatError> { /// Retrieve the current CPU statistics as percentages.
let cpu_stats = cpu::proc::read().map_err(|source| StatError::CpuStat { source })?; pub fn cpu_stats_percent() -> Result<CpuStatPercentages, StatsError> {
let cpu_stats = cpu::proc::read().map_err(StatsError::CpuStat)?;
let s = cpu_stats.stat.in_percentages(); let s = cpu_stats.stat.in_percentages();
let cpu = CpuStatPercentages { let cpu = CpuStatPercentages {
user: s.user, user: s.user,
@ -30,13 +112,13 @@ pub fn cpu_stats_percent() -> Result<String, StatError> {
nice: s.nice, nice: s.nice,
idle: s.idle, idle: s.idle,
}; };
let json_cpu = json::to_string(&cpu);
Ok(json_cpu) Ok(cpu)
} }
pub fn disk_usage() -> Result<String, StatError> { /// Retrieve the current disk usage statistics for each available disk / partition.
let disks = disk_usage::read().map_err(|source| StatError::DiskUsage { source })?; pub fn disk_usage() -> Result<Vec<DiskUsage>, StatsError> {
let disks = disk_usage::read().map_err(StatsError::DiskUsage)?;
let mut disk_usages = Vec::new(); let mut disk_usages = Vec::new();
for d in disks { for d in disks {
let disk = DiskUsage { let disk = DiskUsage {
@ -49,42 +131,39 @@ pub fn disk_usage() -> Result<String, StatError> {
}; };
disk_usages.push(disk); disk_usages.push(disk);
} }
let json_disks = json::to_string(&disk_usages);
Ok(json_disks) Ok(disk_usages)
} }
pub fn load_average() -> Result<String, StatError> { /// Retrieve the current load average statistics.
let l = load::read().map_err(|source| StatError::LoadAvg { source })?; pub fn load_average() -> Result<LoadAverage, StatsError> {
let l = load::read().map_err(StatsError::LoadAvg)?;
let load_avg = LoadAverage { let load_avg = LoadAverage {
one: l.one, one: l.one,
five: l.five, five: l.five,
fifteen: l.fifteen, fifteen: l.fifteen,
}; };
let json_load_avg = json::to_string(&load_avg);
Ok(json_load_avg) Ok(load_avg)
} }
pub fn mem_stats() -> Result<String, StatError> { /// Retrieve the current memory usage statistics.
let m = memory::read().map_err(|source| StatError::MemStat { source })?; pub fn mem_stats() -> Result<MemStat, StatsError> {
let m = memory::read().map_err(StatsError::MemStat)?;
let mem = MemStat { let mem = MemStat {
total: m.total(), total: m.total(),
free: m.free(), free: m.free(),
used: m.used(), used: m.used(),
}; };
let json_mem = json::to_string(&mem);
Ok(json_mem) Ok(mem)
} }
pub fn uptime() -> Result<String, StatError> { /// Retrieve the system uptime in seconds.
pub fn uptime() -> Result<u64, StatsError> {
let sys = System::new(); let sys = System::new();
let uptime = sys let uptime = sys.uptime().map_err(StatsError::Uptime)?;
.uptime()
.map_err(|source| StatError::Uptime { source })?;
let uptime_secs = uptime.as_secs(); let uptime_secs = uptime.as_secs();
let json_uptime = json::to_string(&uptime_secs);
Ok(json_uptime) Ok(uptime_secs)
} }

View File

@ -1,41 +0,0 @@
use miniserde::Serialize;
#[derive(Debug, Serialize)]
pub struct CpuStat {
pub user: u64,
pub system: u64,
pub idle: u64,
pub nice: u64,
}
#[derive(Debug, Serialize)]
pub struct CpuStatPercentages {
pub user: f32,
pub system: f32,
pub idle: f32,
pub nice: f32,
}
#[derive(Debug, Serialize)]
pub struct DiskUsage {
pub filesystem: Option<String>,
pub one_k_blocks: u64,
pub one_k_blocks_used: u64,
pub one_k_blocks_free: u64,
pub used_percentage: u32,
pub mountpoint: String,
}
#[derive(Debug, Serialize)]
pub struct LoadAverage {
pub one: f32,
pub five: f32,
pub fifteen: f32,
}
#[derive(Debug, Serialize)]
pub struct MemStat {
pub total: u64,
pub free: u64,
pub used: u64,
}

View File

@ -1,4 +0,0 @@
[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] [package]
name = "peach-web" name = "peach-web"
version = "0.4.11" version = "0.4.17"
authors = ["Andrew Reid <gnomad@cryptolab.net>"] authors = ["Andrew Reid <gnomad@cryptolab.net>"]
edition = "2018" 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." 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,6 +21,7 @@ maintainer-scripts="debian"
systemd-units = { unit-name = "peach-web" } systemd-units = { unit-name = "peach-web" }
assets = [ assets = [
["target/release/peach-web", "/usr/bin/", "755"], ["target/release/peach-web", "/usr/bin/", "755"],
["Rocket.toml", "/usr/share/peach-web/Rocket.toml", "644"],
["templates/**/*", "/usr/share/peach-web/templates/", "644"], ["templates/**/*", "/usr/share/peach-web/templates/", "644"],
["static/*", "/usr/share/peach-web/static/", "644"], ["static/*", "/usr/share/peach-web/static/", "644"],
["static/css/*", "/usr/share/peach-web/static/css/", "644"], ["static/css/*", "/usr/share/peach-web/static/css/", "644"],
@ -38,17 +39,18 @@ maintenance = { status = "actively-developed" }
env_logger = "0.8" env_logger = "0.8"
log = "0.4" log = "0.4"
nest = "1.0.0" nest = "1.0.0"
openssl = { version = "0.10", features = ["vendored"] }
peach-lib = { path = "../peach-lib" } 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" percent-encoding = "2.1.0"
regex = "1"
rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] } rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
snafu = "0.6" snafu = "0.6"
tera = { version = "1.12.1", features = ["builtins"] } tera = { version = "1.12.1", features = ["builtins"] }
websocket = "0.26"
regex = "1"
xdg = "2.2.0" xdg = "2.2.0"
openssl = { version = "0.10", features = ["vendored"] }
[dependencies.rocket_dyn_templates] [dependencies.rocket_dyn_templates]
version = "0.1.0-rc.1" version = "0.1.0-rc.1"

View File

@ -1,6 +1,6 @@
# peach-web # 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.4.6-<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 ## Web Interface for PeachCloud
@ -39,12 +39,22 @@ _Note: Networking functionality requires peach-network microservice to be runnin
### Environment ### Environment
**Deployment Mode**
The web application deployment mode is configured with the `ROCKET_ENV` environment variable: The web application deployment mode is configured with the `ROCKET_ENV` environment variable:
`export ROCKET_ENV=stage` `export ROCKET_ENV=stage`
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. 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 `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`
**Logging**
Logging is made available with `env_logger`: Logging is made available with `env_logger`:
`export RUST_LOG=info` `export RUST_LOG=info`
@ -87,6 +97,20 @@ Remove configuration files (not removed with `apt-get remove`):
`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. `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.
### 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.
### Licensing ### Licensing
AGPL-3.0 AGPL-3.0

View File

@ -1,5 +1,10 @@
[default]
secret_key = "VYVUDivXvu8g6llxeJd9F92pMfocml5xl/Jjv5Sk4yw="
[development] [development]
template_dir = "templates/" template_dir = "templates/"
disable_auth = true
[production] [production]
template_dir = "templates/" template_dir = "templates/"
disable_auth = false

View File

@ -5,54 +5,18 @@ set -e
adduser --quiet --system peach-web adduser --quiet --system peach-web
usermod -g peach 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 # create nginx config
cat <<EOF > /etc/nginx/sites-enabled/default cat <<EOF > /etc/nginx/sites-enabled/default
server { server {
listen 80 default_server; listen 80 default_server;
server_name peach.local www.peach.local; 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 # remove trailing slash if found
rewrite ^/(.*)/$ /$1 permanent; rewrite ^/(.*)/$ /$1 permanent;
location / { location / {
proxy_pass http://127.0.0.1:3000; 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 EOF

View File

@ -1,10 +1,13 @@
use log::info; use log::info;
use rocket::form::{Form, FromForm}; use rocket::form::{Form, FromForm};
use rocket::request::FlashMessage; use rocket::http::{Cookie, CookieJar, Status};
use rocket::request::{self, FlashMessage, FromRequest, Request};
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
use rocket::serde::json::Json; use rocket::serde::{
use rocket::serde::{Deserialize, Serialize}; json::{Json, Value},
use rocket::{get, post}; Deserialize, Serialize,
};
use rocket::{get, post, Config};
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use peach_lib::error::PeachError; use peach_lib::error::PeachError;
@ -12,9 +15,6 @@ use peach_lib::password_utils;
use crate::error::PeachWebError; use crate::error::PeachWebError;
use crate::utils::{build_json_response, TemplateOrRedirect}; use crate::utils::{build_json_response, TemplateOrRedirect};
use rocket::http::{Cookie, CookieJar, Status};
use rocket::request::{self, FromRequest, Request};
use rocket::serde::json::Value;
// HELPERS AND STRUCTS FOR AUTHENTICATION WITH COOKIES // HELPERS AND STRUCTS FOR AUTHENTICATION WITH COOKIES
@ -42,6 +42,18 @@ impl<'r> FromRequest<'r> for Authenticated {
type Error = LoginError; type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> { async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
// 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)
} else {
let authenticated = req let authenticated = req
.cookies() .cookies()
.get_private(AUTH_COOKIE_KEY) .get_private(AUTH_COOKIE_KEY)
@ -53,6 +65,7 @@ impl<'r> FromRequest<'r> for Authenticated {
} }
} }
} }
}
// HELPERS AND ROUTES FOR /login // HELPERS AND ROUTES FOR /login

View File

@ -40,7 +40,7 @@ pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> {
if dns_form.enable_dyndns { if dns_form.enable_dyndns {
let full_dynamic_domain = 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 // check if this is a new domain or if its already registered
let is_new_domain = check_is_new_dyndns_domain(&full_dynamic_domain); let is_new_domain = check_is_new_dyndns_domain(&full_dynamic_domain)?;
if is_new_domain { if is_new_domain {
match dyndns_client::register_domain(&full_dynamic_domain) { match dyndns_client::register_domain(&full_dynamic_domain) {
Ok(_) => { Ok(_) => {
@ -52,7 +52,7 @@ pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> {
info!("Failed to register dyndns domain: {:?}", err); info!("Failed to register dyndns domain: {:?}", err);
// json response for failed update // json response for failed update
let msg: String = match err { let msg: String = match err {
PeachError::JsonRpcClientCore { source } => { PeachError::JsonRpcClientCore(source) => {
match source { match source {
Error(ErrorKind::JsonRpcError(err), _state) => match err.code { Error(ErrorKind::JsonRpcError(err), _state) => match err.code {
ErrorCode::ServerError(-32030) => { ErrorCode::ServerError(-32030) => {

View File

@ -3,8 +3,8 @@ use rocket::{
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
serde::json::Value,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use serde::Serialize; use serde::Serialize;
use std::{ use std::{
@ -12,13 +12,16 @@ use std::{
process::{Command, Output}, process::{Command, Output},
}; };
use peach_lib::config_manager::load_peach_config; use peach_lib::{
use peach_lib::stats_client::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat}; config_manager::load_peach_config, dyndns_client, network_client, oled_client, sbot_client,
use peach_lib::{dyndns_client, network_client, oled_client, sbot_client, stats_client}; };
use peach_stats::{
stats,
stats::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat},
};
use crate::routes::authentication::Authenticated; use crate::routes::authentication::Authenticated;
use crate::utils::build_json_response; use crate::utils::build_json_response;
use rocket::serde::json::Value;
// HELPERS AND ROUTES FOR /status // HELPERS AND ROUTES FOR /status
@ -34,7 +37,6 @@ pub struct StatusContext {
pub mem_stats: Option<MemStat>, pub mem_stats: Option<MemStat>,
pub network_ping: String, pub network_ping: String,
pub oled_ping: String, pub oled_ping: String,
pub stats_ping: String,
pub dyndns_enabled: bool, pub dyndns_enabled: bool,
pub dyndns_is_online: bool, pub dyndns_is_online: bool,
pub config_is_valid: bool, pub config_is_valid: bool,
@ -46,9 +48,12 @@ pub struct StatusContext {
impl StatusContext { impl StatusContext {
pub fn build() -> StatusContext { pub fn build() -> StatusContext {
// convert result to Option<CpuStatPercentages>, discard any error // convert result to Option<CpuStatPercentages>, discard any error
let cpu_stat_percent = stats_client::cpu_stats_percent().ok(); let cpu_stat_percent = stats::cpu_stats_percent().ok();
let load_average = stats_client::load_average().ok(); let load_average = stats::load_average().ok();
let mem_stats = stats_client::mem_stats().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 network_ping = match network_client::ping() { let network_ping = match network_client::ping() {
Ok(_) => "ONLINE".to_string(), Ok(_) => "ONLINE".to_string(),
Err(_) => "OFFLINE".to_string(), Err(_) => "OFFLINE".to_string(),
@ -57,22 +62,21 @@ impl StatusContext {
Ok(_) => "ONLINE".to_string(), Ok(_) => "ONLINE".to_string(),
Err(_) => "OFFLINE".to_string(), Err(_) => "OFFLINE".to_string(),
}; };
let stats_ping = match stats_client::ping() {
Ok(_) => "ONLINE".to_string(), let uptime = match stats::uptime() {
Err(_) => "OFFLINE".to_string(), Ok(secs) => {
}; let uptime_mins = secs / 60;
let uptime = match stats_client::uptime() { uptime_mins.to_string()
Ok(mins) => mins, }
Err(_) => "Unavailable".to_string(), 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> // serialize disk usage data into Vec<DiskUsage>
let disk_usage_stats = match stats_client::disk_usage() { let disk_usage_stats = match stats::disk_usage() {
Ok(disks) => { Ok(disks) => disks,
let partitions: Vec<DiskUsage> = serde_json::from_str(disks.as_str())
.expect("Failed to deserialize disk_usage response");
partitions
}
Err(_) => Vec::new(), Err(_) => Vec::new(),
}; };
@ -84,9 +88,6 @@ 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 // dyndns_is_online & config_is_valid
let dyndns_enabled: bool; let dyndns_enabled: bool;
let dyndns_is_online: bool; let dyndns_is_online: bool;
@ -139,7 +140,6 @@ impl StatusContext {
mem_stats, mem_stats,
network_ping, network_ping,
oled_ping, oled_ping,
stats_ping,
dyndns_enabled, dyndns_enabled,
dyndns_is_online, dyndns_is_online,
config_is_valid, config_is_valid,

View File

@ -2,25 +2,77 @@ use rocket::{get, request::FlashMessage};
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use serde::Serialize; use serde::Serialize;
use peach_lib::network_client; use peach_network::{
use peach_lib::stats_client::Traffic; network,
network::{Status, Traffic},
};
use crate::routes::authentication::Authenticated; use crate::routes::authentication::Authenticated;
// HELPERS AND ROUTES FOR /status/network // HELPERS AND ROUTES FOR /status/network
#[derive(Debug, Serialize)]
pub struct IfaceTraffic {
pub rx: u64,
pub rx_unit: Option<String>,
pub tx: u64,
pub tx_unit: Option<String>,
}
impl IfaceTraffic {
fn default() -> Self {
IfaceTraffic {
rx: 0,
rx_unit: None,
tx: 0,
tx_unit: None,
}
}
}
fn convert_traffic(traffic: Traffic) -> Option<IfaceTraffic> {
let mut t = IfaceTraffic::default();
// modify traffic values & assign measurement units
// based on received and transmitted values.
// if received > 999 MB, convert it to GB
if traffic.received > 1_047_527_424 {
t.rx = traffic.received / 1_073_741_824;
t.rx_unit = Some("GB".to_string());
} else if traffic.received > 0 {
// otherwise, convert it to MB
t.rx = (traffic.received / 1024) / 1024;
t.rx_unit = Some("MB".to_string());
} else {
t.rx = 0;
t.rx_unit = Some("MB".to_string());
}
if traffic.transmitted > 1_047_527_424 {
t.tx = traffic.transmitted / 1_073_741_824;
t.tx_unit = Some("GB".to_string());
} else if traffic.transmitted > 0 {
t.tx = (traffic.transmitted / 1024) / 1024;
t.tx_unit = Some("MB".to_string());
} else {
t.tx = 0;
t.tx_unit = Some("MB".to_string());
}
Some(t)
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct NetworkContext { pub struct NetworkContext {
pub ap_ip: String, pub ap_ip: String,
pub ap_ssid: String, pub ap_ssid: String,
pub ap_state: String, pub ap_state: String,
pub ap_traffic: Option<Traffic>, pub ap_traffic: Option<IfaceTraffic>,
pub wlan_ip: String, pub wlan_ip: String,
pub wlan_rssi: Option<String>, pub wlan_rssi: Option<String>,
pub wlan_ssid: String, pub wlan_ssid: String,
pub wlan_state: String, pub wlan_state: String,
pub wlan_status: String, pub wlan_status: Option<Status>,
pub wlan_traffic: Option<Traffic>, pub wlan_traffic: Option<IfaceTraffic>,
pub flash_name: Option<String>, pub flash_name: Option<String>,
pub flash_msg: Option<String>, pub flash_msg: Option<String>,
// page title for header in navbar // page title for header in navbar
@ -31,101 +83,47 @@ pub struct NetworkContext {
impl NetworkContext { impl NetworkContext {
pub fn build() -> NetworkContext { pub fn build() -> NetworkContext {
let ap_ip = match network_client::ip("ap0") { let ap_ip = match network::ip("ap0") {
Ok(ip) => ip, Ok(Some(ip)) => ip,
Err(_) => "x.x.x.x".to_string(), _ => "x.x.x.x".to_string(),
}; };
let ap_ssid = match network_client::ssid("ap0") { let ap_ssid = match network::ssid("ap0") {
Ok(ssid) => ssid, Ok(Some(ssid)) => ssid,
Err(_) => "Not currently activated".to_string(), _ => "Not currently activated".to_string(),
}; };
let ap_state = match network_client::state("ap0") { let ap_state = match network::state("ap0") {
Ok(state) => state, Ok(Some(state)) => state,
Err(_) => "Interface unavailable".to_string(), _ => "Interface unavailable".to_string(),
}; };
let ap_traffic = match network_client::traffic("ap0") { let ap_traffic = match network::traffic("ap0") {
Ok(traffic) => { // convert bytes to mb or gb and add appropriate units
let mut t = traffic; Ok(Some(traffic)) => convert_traffic(traffic),
// modify traffic values & assign measurement unit _ => None,
// 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") { let wlan_ip = match network::ip("wlan0") {
Ok(ip) => ip, Ok(Some(ip)) => ip,
Err(_) => "x.x.x.x".to_string(), _ => "x.x.x.x".to_string(),
}; };
let wlan_rssi = match network_client::rssi_percent("wlan0") { let wlan_rssi = match network::rssi_percent("wlan0") {
Ok(rssi) => Some(rssi), Ok(rssi) => rssi,
Err(_) => None, _ => None,
}; };
let wlan_ssid = match network_client::ssid("wlan0") { let wlan_ssid = match network::ssid("wlan0") {
Ok(ssid) => ssid, Ok(Some(ssid)) => ssid,
Err(_) => "Not connected".to_string(), _ => "Not connected".to_string(),
}; };
let wlan_state = match network_client::state("wlan0") { let wlan_state = match network::state("wlan0") {
Ok(state) => state, Ok(Some(state)) => state,
Err(_) => "Interface unavailable".to_string(), _ => "Interface unavailable".to_string(),
}; };
let wlan_status = match network_client::status("wlan0") { let wlan_status = match network::status("wlan0") {
Ok(status) => status, Ok(status) => status,
Err(_) => "Interface unavailable".to_string(), _ => None,
}; };
let wlan_traffic = match network_client::traffic("wlan0") { let wlan_traffic = match network::traffic("wlan0") {
Ok(traffic) => { // convert bytes to mb or gb and add appropriate units
let mut t = traffic; Ok(Some(traffic)) => convert_traffic(traffic),
// modify traffic values & assign measurement unit _ => None,
// 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 { NetworkContext {

View File

@ -4,17 +4,29 @@ use std::io::Read;
use rocket::http::{ContentType, Status}; use rocket::http::{ContentType, Status};
use rocket::local::blocking::Client; use rocket::local::blocking::Client;
use rocket::serde::json::{json, Value}; use rocket::serde::json::{json, Value};
use rocket::{Build, Config, Rocket};
use crate::utils::build_json_response; use crate::utils::build_json_response;
use super::init_rocket; use super::init_rocket;
// define authentication mode
const DISABLE_AUTH: bool = true;
/// Wrapper around `init_rocket()` to simplify the process of invoking the application with the desired authentication status. This is particularly useful for testing purposes.
fn init_test_rocket(disable_auth: bool) -> Rocket<Build> {
// set authentication based on provided `disable_auth` value
Config::figment().merge(("disable_auth", disable_auth));
init_rocket()
}
// helper function to test correct retrieval and content of a file // helper function to test correct retrieval and content of a file
fn test_query_file<T>(path: &str, file: T, status: Status) fn test_query_file<T>(path: &str, file: T, status: Status)
where where
T: Into<Option<&'static str>>, T: Into<Option<&'static str>>,
{ {
let client = Client::tracked(init_rocket()).unwrap(); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).unwrap();
let response = client.get(path).dispatch(); let response = client.get(path).dispatch();
assert_eq!(response.status(), status); assert_eq!(response.status(), status);
@ -39,7 +51,7 @@ fn read_file_content(path: &str) -> Vec<u8> {
#[test] #[test]
fn index_html() { fn index_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/").dispatch(); let response = client.get("/").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -54,7 +66,7 @@ fn index_html() {
#[test] #[test]
fn help_html() { fn help_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/help").dispatch(); let response = client.get("/help").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -64,7 +76,7 @@ fn help_html() {
#[test] #[test]
fn login_html() { fn login_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/login").dispatch(); let response = client.get("/login").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -74,7 +86,7 @@ fn login_html() {
#[test] #[test]
fn logout_html() { fn logout_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/logout").dispatch(); let response = client.get("/logout").dispatch();
// check for 303 status (redirect to "/login") // check for 303 status (redirect to "/login")
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
@ -83,7 +95,7 @@ fn logout_html() {
#[test] #[test]
fn power_html() { fn power_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/power").dispatch(); let response = client.get("/power").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -97,7 +109,7 @@ NOTE: these tests are comment-out for the moment, due to the fact that they invo
#[test] #[test]
fn reboot() { fn reboot() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/power/reboot").dispatch(); let response = client.get("/power/reboot").dispatch();
// check for redirect // check for redirect
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
@ -105,7 +117,7 @@ fn reboot() {
#[test] #[test]
fn shutdown() { fn shutdown() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/power/shutdown").dispatch(); let response = client.get("/power/shutdown").dispatch();
// check for redirect // check for redirect
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
@ -116,7 +128,7 @@ fn shutdown() {
#[test] #[test]
fn block() { fn block() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.post("/scuttlebutt/block") .post("/scuttlebutt/block")
.header(ContentType::Form) .header(ContentType::Form)
@ -127,7 +139,7 @@ fn block() {
#[test] #[test]
fn blocks_html() { fn blocks_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/scuttlebutt/blocks").dispatch(); let response = client.get("/scuttlebutt/blocks").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -137,7 +149,7 @@ fn blocks_html() {
#[test] #[test]
fn follow() { fn follow() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.post("/scuttlebutt/follow") .post("/scuttlebutt/follow")
.header(ContentType::Form) .header(ContentType::Form)
@ -149,7 +161,7 @@ fn follow() {
#[test] #[test]
fn follows_html() { fn follows_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/scuttlebutt/follows").dispatch(); let response = client.get("/scuttlebutt/follows").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -159,7 +171,7 @@ fn follows_html() {
#[test] #[test]
fn followers_html() { fn followers_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/scuttlebutt/followers").dispatch(); let response = client.get("/scuttlebutt/followers").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -169,7 +181,7 @@ fn followers_html() {
#[test] #[test]
fn friends_html() { fn friends_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/scuttlebutt/friends").dispatch(); let response = client.get("/scuttlebutt/friends").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -179,7 +191,7 @@ fn friends_html() {
#[test] #[test]
fn peers_html() { fn peers_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/scuttlebutt/peers").dispatch(); let response = client.get("/scuttlebutt/peers").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -189,7 +201,7 @@ fn peers_html() {
#[test] #[test]
fn private_html() { fn private_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/scuttlebutt/private").dispatch(); let response = client.get("/scuttlebutt/private").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -199,7 +211,7 @@ fn private_html() {
#[test] #[test]
fn profile_html() { fn profile_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/scuttlebutt/profile").dispatch(); let response = client.get("/scuttlebutt/profile").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -209,7 +221,7 @@ fn profile_html() {
#[test] #[test]
fn publish_post() { fn publish_post() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.post("/scuttlebutt/publish") .post("/scuttlebutt/publish")
.header(ContentType::Form) .header(ContentType::Form)
@ -220,7 +232,7 @@ fn publish_post() {
#[test] #[test]
fn unfollow() { fn unfollow() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.post("/scuttlebutt/unfollow") .post("/scuttlebutt/unfollow")
.header(ContentType::Form) .header(ContentType::Form)
@ -233,7 +245,7 @@ fn unfollow() {
#[test] #[test]
fn admin_settings_menu_html() { fn admin_settings_menu_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/admin").dispatch(); let response = client.get("/settings/admin").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -245,7 +257,7 @@ fn admin_settings_menu_html() {
#[test] #[test]
fn add_admin_html() { fn add_admin_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/admin/add").dispatch(); let response = client.get("/settings/admin/add").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -258,7 +270,7 @@ fn add_admin_html() {
#[test] #[test]
fn add_admin() { fn add_admin() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.post("/settings/admin/add") .post("/settings/admin/add")
.header(ContentType::Form) .header(ContentType::Form)
@ -270,21 +282,21 @@ fn add_admin() {
#[test] #[test]
fn change_password_html() { fn change_password_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/admin/change_password").dispatch(); let response = client.get("/settings/admin/change_password").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
let body = response.into_string().unwrap(); let body = response.into_string().unwrap();
assert!(body.contains("Change Password")); assert!(body.contains("Change Password"));
assert!(body.contains("Old Password")); assert!(body.contains("Current password"));
assert!(body.contains("Enter New Password")); assert!(body.contains("New password"));
assert!(body.contains("Re-Enter New Password")); assert!(body.contains("New password duplicate"));
assert!(body.contains("Save")); assert!(body.contains("Save"));
} }
#[test] #[test]
fn configure_admin_html() { fn configure_admin_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/admin/configure").dispatch(); let response = client.get("/settings/admin/configure").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -296,7 +308,7 @@ fn configure_admin_html() {
#[test] #[test]
fn forgot_password_html() { fn forgot_password_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/admin/forgot_password").dispatch(); let response = client.get("/settings/admin/forgot_password").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -308,7 +320,7 @@ fn forgot_password_html() {
#[test] #[test]
fn network_settings_menu_html() { fn network_settings_menu_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/network").dispatch(); let response = client.get("/settings/network").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -318,7 +330,7 @@ fn network_settings_menu_html() {
#[test] #[test]
fn deploy_ap() { fn deploy_ap() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/network/ap/activate").dispatch(); let response = client.get("/settings/network/ap/activate").dispatch();
// check for 303 status (redirect) // check for 303 status (redirect)
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
@ -327,7 +339,7 @@ fn deploy_ap() {
#[test] #[test]
fn dns_settings_html() { fn dns_settings_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/network/dns").dispatch(); let response = client.get("/settings/network/dns").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -341,7 +353,7 @@ fn dns_settings_html() {
#[test] #[test]
fn list_aps_html() { fn list_aps_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/network/wifi").dispatch(); let response = client.get("/settings/network/wifi").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -353,7 +365,7 @@ fn list_aps_html() {
// TODO: needs further testing once template has been refactored // TODO: needs further testing once template has been refactored
#[test] #[test]
fn ap_details_html() { fn ap_details_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/network/wifi?ssid=Home").dispatch(); let response = client.get("/settings/network/wifi?ssid=Home").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -363,7 +375,7 @@ fn ap_details_html() {
#[test] #[test]
fn deploy_client() { fn deploy_client() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/network/wifi/activate").dispatch(); let response = client.get("/settings/network/wifi/activate").dispatch();
// check for 303 status (redirect) // check for 303 status (redirect)
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
@ -372,7 +384,7 @@ fn deploy_client() {
#[test] #[test]
fn add_ap_html() { fn add_ap_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/network/wifi/add").dispatch(); let response = client.get("/settings/network/wifi/add").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -386,7 +398,7 @@ fn add_ap_html() {
#[test] #[test]
fn add_ap_ssid_html() { fn add_ap_ssid_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.get("/settings/network/wifi/add?ssid=Home") .get("/settings/network/wifi/add?ssid=Home")
.dispatch(); .dispatch();
@ -402,7 +414,7 @@ fn add_ap_ssid_html() {
#[test] #[test]
fn add_credentials() { fn add_credentials() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.post("/settings/network/wifi/add") .post("/settings/network/wifi/add")
.header(ContentType::Form) .header(ContentType::Form)
@ -414,7 +426,7 @@ fn add_credentials() {
#[test] #[test]
fn forget_wifi() { fn forget_wifi() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.post("/settings/network/wifi/forget") .post("/settings/network/wifi/forget")
.header(ContentType::Form) .header(ContentType::Form)
@ -426,7 +438,7 @@ fn forget_wifi() {
#[test] #[test]
fn modify_password() { fn modify_password() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.post("/settings/network/wifi/modify") .post("/settings/network/wifi/modify")
.header(ContentType::Form) .header(ContentType::Form)
@ -438,7 +450,7 @@ fn modify_password() {
#[test] #[test]
fn data_usage_html() { fn data_usage_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/network/wifi/usage").dispatch(); let response = client.get("/settings/network/wifi/usage").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -453,7 +465,7 @@ fn data_usage_html() {
#[test] #[test]
fn scuttlebutt_settings_menu_html() { fn scuttlebutt_settings_menu_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/scuttlebutt").dispatch(); let response = client.get("/settings/scuttlebutt").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -472,7 +484,7 @@ fn scuttlebutt_settings_menu_html() {
#[test] #[test]
fn status_html() { fn status_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/status").dispatch(); let response = client.get("/status").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -485,7 +497,7 @@ fn status_html() {
#[test] #[test]
fn network_status_html() { fn network_status_html() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/status/network").dispatch(); let response = client.get("/status/network").dispatch();
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML)); assert_eq!(response.content_type(), Some(ContentType::HTML));
@ -502,7 +514,7 @@ fn network_status_html() {
#[test] #[test]
fn activate_ap() { fn activate_ap() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.post("/api/v1/network/activate_ap") .post("/api/v1/network/activate_ap")
.header(ContentType::JSON) .header(ContentType::JSON)
@ -513,7 +525,7 @@ fn activate_ap() {
#[test] #[test]
fn activate_client() { fn activate_client() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.post("/api/v1/network/activate_client") .post("/api/v1/network/activate_client")
.header(ContentType::JSON) .header(ContentType::JSON)
@ -524,7 +536,7 @@ fn activate_client() {
#[test] #[test]
fn return_ip() { fn return_ip() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.get("/api/v1/network/ip") .get("/api/v1/network/ip")
.header(ContentType::JSON) .header(ContentType::JSON)
@ -538,7 +550,7 @@ fn return_ip() {
#[test] #[test]
fn return_rssi() { fn return_rssi() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.get("/api/v1/network/rssi") .get("/api/v1/network/rssi")
.header(ContentType::JSON) .header(ContentType::JSON)
@ -551,7 +563,7 @@ fn return_rssi() {
#[test] #[test]
fn return_ssid() { fn return_ssid() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.get("/api/v1/network/ssid") .get("/api/v1/network/ssid")
.header(ContentType::JSON) .header(ContentType::JSON)
@ -564,7 +576,7 @@ fn return_ssid() {
#[test] #[test]
fn return_state() { fn return_state() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.get("/api/v1/network/state") .get("/api/v1/network/state")
.header(ContentType::JSON) .header(ContentType::JSON)
@ -579,7 +591,7 @@ fn return_state() {
#[test] #[test]
fn return_status() { fn return_status() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.get("/api/v1/network/status") .get("/api/v1/network/status")
.header(ContentType::JSON) .header(ContentType::JSON)
@ -592,7 +604,7 @@ fn return_status() {
#[test] #[test]
fn scan_networks() { fn scan_networks() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.get("/api/v1/network/wifi") .get("/api/v1/network/wifi")
.header(ContentType::JSON) .header(ContentType::JSON)
@ -605,7 +617,7 @@ fn scan_networks() {
#[test] #[test]
fn add_wifi() { fn add_wifi() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.post("/api/v1/network/wifi") .post("/api/v1/network/wifi")
.header(ContentType::JSON) .header(ContentType::JSON)
@ -619,7 +631,7 @@ fn add_wifi() {
#[test] #[test]
fn remove_wifi() { fn remove_wifi() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.post("/api/v1/network/wifi/forget") .post("/api/v1/network/wifi/forget")
.header(ContentType::JSON) .header(ContentType::JSON)
@ -633,7 +645,7 @@ fn remove_wifi() {
#[test] #[test]
fn new_password() { fn new_password() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.post("/api/v1/network/wifi/modify") .post("/api/v1/network/wifi/modify")
.header(ContentType::JSON) .header(ContentType::JSON)
@ -647,7 +659,7 @@ fn new_password() {
#[test] #[test]
fn ping_pong() { fn ping_pong() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance"); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client let response = client
.get("/api/v1/ping") .get("/api/v1/ping")
.header(ContentType::JSON) .header(ContentType::JSON)
@ -709,7 +721,7 @@ fn invalid_path() {
#[test] #[test]
fn invalid_get_request() { fn invalid_get_request() {
let client = Client::tracked(init_rocket()).unwrap(); let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).unwrap();
// try to get a path that doesn't exist // try to get a path that doesn't exist
let res = client let res = client

View File

@ -220,12 +220,18 @@ body {
} }
.capsule-container { .capsule-container {
margin-left: 2rem; margin-left: 1rem;
margin-right: 2rem; margin-right: 1rem;
padding-top: 1rem;
padding-bottom: 1rem; padding-bottom: 1rem;
} }
@media only screen and (min-width: 600px) {
.capsule-container {
margin-left: 0;
margin-right: 0;
}
}
/* /*
* CARDS * CARDS
*/ */
@ -235,6 +241,7 @@ body {
max-height: 90vh; max-height: 90vh;
position: relative; position: relative;
width: 100%; width: 100%;
margin-top: 1rem;
} }
@media only screen and (min-width: 600px) { @media only screen and (min-width: 600px) {
@ -248,8 +255,6 @@ body {
.card-container { .card-container {
justify-content: center; justify-content: center;
padding: 0.5rem; padding: 0.5rem;
/* padding-top: 1rem; */
/* padding-bottom: 1rem; */
} }
.form-container { .form-container {
@ -560,6 +565,7 @@ html {
font-size: var(--font-size-6); font-size: var(--font-size-6);
margin-left: 2rem; margin-left: 2rem;
margin-right: 2rem; margin-right: 2rem;
margin-top: 1rem;
} }
/* /*

View File

@ -1,16 +1,28 @@
/* /*
* behavioural layer for the `change_password.html.tera` template,
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 // catch click of 'Save' button and make POST request
PEACH.add = function() { PEACH_AUTH.changePassword = function() {
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
document.body.addEventListener('submit', function(e) { document.body.addEventListener('submit', function(e) {
// prevent redirect on button press (default behavior) // prevent redirect on button press (default behavior)
e.preventDefault(); e.preventDefault();
// capture form data // capture form data
var formElement = document.querySelector("form"); var formElement = document.querySelector("form");
// create form data object from the wifiCreds form element // create form data object from the changePassword form element
var formData = new FormData(formElement); var formData = new FormData(formElement);
var object = {}; var object = {};
// assign values from form // assign values from form
@ -22,7 +34,7 @@ PEACH.add = function() {
var jsonData = JSON.stringify(object); var jsonData = JSON.stringify(object);
// write in-progress status message to ui // write in-progress status message to ui
PEACH.flashMsg("info", "Saving new password."); PEACH.flashMsg("info", "Saving new password.");
// send add_wifi POST request // send change_password POST request
fetch("/api/v1/admin/change_password", { fetch("/api/v1/admin/change_password", {
method: "post", method: "post",
headers: { headers: {
@ -41,5 +53,5 @@ PEACH.add = function() {
}); });
} }
var addInstance = PEACH; var changePassInstance = PEACH_AUTH;
addInstance.add(); changePassInstance.changePassword();

View File

@ -43,5 +43,4 @@ PEACH.flashMsg = function(status, msg) {
} }
} }
var addInstance = PEACH; var commonInstance = PEACH;
addInstance.add();

View File

@ -1,9 +1,9 @@
/* /*
behavioural layer for the `configure_dns.html.tera` template, behavioural layer for the `configure_dns.html.tera` template,
corresponding to the web route `/network/dns` corresponding to the web route `/settings/network/dns`
- intercept button click for add (form submission of credentials) - intercept button click for save (form submission of dns settings)
- perform json api call - perform json api call
- update the dom - update the dom
@ -12,14 +12,14 @@ corresponding to the web route `/network/dns`
var PEACH_DNS = {}; var PEACH_DNS = {};
// catch click of 'Add' button and make POST request // catch click of 'Add' button and make POST request
PEACH_DNS.add = function() { PEACH_DNS.configureDns = function() {
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
document.body.addEventListener('submit', function(e) { document.body.addEventListener('submit', function(e) {
// prevent redirect on button press (default behavior) // prevent redirect on button press (default behavior)
e.preventDefault(); e.preventDefault();
// capture form data // capture form data
var formElement = document.querySelector("form"); var formElement = document.querySelector("form");
// create form data object from the wifiCreds form element // create form data object from the configureDNS form element
var formData = new FormData(formElement); var formData = new FormData(formElement);
var object = {}; var object = {};
// set checkbox to false (the value is only passed to formData if it is "on") // set checkbox to false (the value is only passed to formData if it is "on")
@ -36,7 +36,7 @@ PEACH_DNS.add = function() {
console.log(object); console.log(object);
var jsonData = JSON.stringify(object); var jsonData = JSON.stringify(object);
// write in-progress status message to ui // write in-progress status message to ui
PEACH_DNS.flashMsg("info", "Saving new DNS configurations"); PEACH.flashMsg("info", "Saving new DNS configurations");
// send add_wifi POST request // send add_wifi POST request
fetch("/api/v1/network/dns/configure", { fetch("/api/v1/network/dns/configure", {
method: "post", method: "post",
@ -50,49 +50,14 @@ PEACH_DNS.add = function() {
}) })
.then( (jsonData) => { .then( (jsonData) => {
// write json response message to ui // write json response message to ui
PEACH_DNS.flashMsg(jsonData.status, jsonData.msg); PEACH.flashMsg(jsonData.status, jsonData.msg);
let statusIndicator = document.getElementById("dyndns-status-indicator"); let statusIndicator = document.getElementById("dyndns-status-indicator");
statusIndicator.remove(); // only remove the "dyndns-status-indicator" element if it exists
if (statusIndicator != null ) statusIndicator.remove();
}) })
}, false); }, false);
}); });
} }
// display a message by appending a paragraph element var configureDnsInstance = PEACH_DNS;
PEACH_DNS.flashMsg = function(status, msg) { configureDnsInstance.configureDns();
// 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 addInstance = PEACH_DNS;
addInstance.add();

View File

@ -10,7 +10,6 @@ corresponding to the web route `/network/wifi/add`
methods: methods:
PEACH_NETWORK.add(); PEACH_NETWORK.add();
PEACH_NETWORK.flashMsg(status, msg);
*/ */
@ -34,7 +33,7 @@ PEACH_NETWORK.add = function() {
// perform json serialization // perform json serialization
var jsonData = JSON.stringify(object); var jsonData = JSON.stringify(object);
// write in-progress status message to ui // write in-progress status message to ui
PEACH_NETWORK.flashMsg("info", "Adding WiFi credentials..."); PEACH.flashMsg("info", "Adding WiFi credentials...");
// send add_wifi POST request // send add_wifi POST request
fetch("/api/v1/network/wifi", { fetch("/api/v1/network/wifi", {
method: "post", method: "post",
@ -48,46 +47,11 @@ PEACH_NETWORK.add = function() {
}) })
.then( (jsonData) => { .then( (jsonData) => {
// write json response message to ui // write json response message to ui
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); PEACH.flashMsg(jsonData.status, jsonData.msg);
}) })
}, false); }, false);
}); });
} }
// display a message by appending a paragraph element
PEACH_NETWORK.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 addInstance = PEACH_NETWORK; var addInstance = PEACH_NETWORK;
addInstance.add(); addInstance.add();

View File

@ -1,7 +1,7 @@
/* /*
behavioural layer for the `network_card.html.tera` template, behavioural layer for the `network_card.html.tera` template,
corresponding to the web route `/network` corresponding to the web route `/settings/network`
- intercept form submissions - intercept form submissions
- perform json api calls - perform json api calls
@ -11,10 +11,8 @@ methods:
PEACH_NETWORK.activateAp(); PEACH_NETWORK.activateAp();
PEACH_NETWORK.activateClient(); PEACH_NETWORK.activateClient();
PEACH_NETWORK.apOnline(); PEACH_NETWORK.apMode();
PEACH_NETWORK.clientOffline(); PEACH_NETWORK.clientMode();
PEACH_NETWORK.clientOnline();
PEACH_NETWORK.flashMsg(status, msg);
*/ */
@ -29,7 +27,7 @@ PEACH_NETWORK.activateAp = function() {
// prevent form submission (default behavior) // prevent form submission (default behavior)
e.preventDefault(); e.preventDefault();
// write in-progress status message to ui // write in-progress status message to ui
PEACH_NETWORK.flashMsg("info", "Deploying access point..."); PEACH.flashMsg("info", "Deploying access point...");
// send activate_ap POST request // send activate_ap POST request
fetch("/api/v1/network/activate_ap", { fetch("/api/v1/network/activate_ap", {
method: "post", method: "post",
@ -44,10 +42,10 @@ PEACH_NETWORK.activateAp = function() {
.then( (jsonData) => { .then( (jsonData) => {
console.log(jsonData.msg); console.log(jsonData.msg);
// write json response message to ui // write json response message to ui
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); PEACH.flashMsg(jsonData.status, jsonData.msg);
// if ap activation is successful, update the ui // if ap activation is successful, update the ui
if (jsonData.status === "success") { if (jsonData.status === "success") {
PEACH_NETWORK.apOnline(); PEACH_NETWORK.apMode();
} }
}) })
}, false); }, false);
@ -64,7 +62,7 @@ PEACH_NETWORK.activateClient = function() {
// prevent form submission (default behavior) // prevent form submission (default behavior)
e.preventDefault(); e.preventDefault();
// write in-progress status message to ui // write in-progress status message to ui
PEACH_NETWORK.flashMsg("info", "Enabling WiFi client..."); PEACH.flashMsg("info", "Enabling WiFi client...");
// send activate_ap POST request // send activate_ap POST request
fetch("/api/v1/network/activate_client", { fetch("/api/v1/network/activate_client", {
method: "post", method: "post",
@ -79,10 +77,10 @@ PEACH_NETWORK.activateClient = function() {
.then( (jsonData) => { .then( (jsonData) => {
console.log(jsonData.msg); console.log(jsonData.msg);
// write json response message to ui // write json response message to ui
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); PEACH.flashMsg(jsonData.status, jsonData.msg);
// if client activation is successful, update the ui // if client activation is successful, update the ui
if (jsonData.status === "success") { if (jsonData.status === "success") {
PEACH_NETWORK.clientOnline(); PEACH_NETWORK.clientMode();
} }
}) })
}, false); }, false);
@ -90,21 +88,12 @@ PEACH_NETWORK.activateClient = function() {
}); });
} }
// update ui for access point mode (status: online) // replace 'Deploy Access Point' button with 'Enable WiFi' button
PEACH_NETWORK.apOnline = function() { PEACH_NETWORK.apMode = function() {
console.log('Activating AP Mode');
// update network mode and status (icon & label)
let i = document.getElementById("netModeIcon");
i.className = "center icon icon-active";
i.src = "icons/router.svg";
let l = document.getElementById("netModeLabel");
l.textContent = "ONLINE";
// create Enable WiFi button and add it to button div // create Enable WiFi button and add it to button div
var wifiButton = document.createElement("A"); var wifiButton = document.createElement("A");
wifiButton.className = "button center"; wifiButton.className = "button center";
wifiButton.href = "/network/wifi/activate"; wifiButton.href = "/settings/network/wifi/activate";
wifiButton.id = "connectWifi"; wifiButton.id = "connectWifi";
var label = "Enable WiFi"; var label = "Enable WiFi";
var buttonText = document.createTextNode(label); var buttonText = document.createTextNode(label);
@ -114,88 +103,31 @@ PEACH_NETWORK.apOnline = function() {
let buttons = document.getElementById("buttons"); let buttons = document.getElementById("buttons");
buttons.appendChild(wifiButton); buttons.appendChild(wifiButton);
// remove the old 'Activate Access Point' button // remove the old 'Deploy Access Point' button
let apButton = document.getElementById("deployAccessPoint"); let apButton = document.getElementById("deployAccessPoint");
apButton.style = "display: none;"; apButton.remove();
} }
// update ui for wifi client mode (status: online) // replace 'Enable WiFi' button with 'Deploy Access Point' button
PEACH_NETWORK.clientOnline = function() { PEACH_NETWORK.clientMode = function() {
console.log('Activating Client Mode'); // 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);
// update network mode and status (icon & label) // append the new button to the buttons div
let i = document.getElementById("netModeIcon"); let buttons = document.getElementById("buttons");
i.className = "center icon icon-active"; buttons.appendChild(apButton);
i.src = "icons/wifi.svg";
let l = document.getElementById("netModeLabel");
l.textContent = "ONLINE";
// TODO: think about updates for buttons (transition from ap mode) // remove the old 'Enable Wifi' button
} let wifiButton = document.getElementById("connectWifi");
wifiButton.remove();
// update ui for wifi client mode (status: offline)
PEACH_NETWORK.clientOffline = function() {
console.log('Activating Client Mode');
// update network mode and status (icon & label)
let i = document.getElementById("netModeIcon");
i.className = "center icon icon-inactive";
i.src = "icons/wifi.svg";
let l = document.getElementById("netModeLabel");
l.textContent = "OFFLINE";
// TODO: think about updates for buttons (transition from ap mode)
}
// display a message by appending a paragraph element
PEACH_NETWORK.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 above the three icon grid div
var gridDiv = document.getElementById("gridDiv");
gridDiv.parentNode.insertBefore(flashDiv, gridDiv);
}
} }
var networkInstance = PEACH_NETWORK; var networkInstance = PEACH_NETWORK;
networkInstance.activateAp(); networkInstance.activateAp();
networkInstance.activateClient(); networkInstance.activateClient();
/*
async function exampleFetch() {
const response = await fetch('/api/v1/network/state');
const myJson = await response.json();
//const jsonData = JSON.parse(myJson);
console.log(myJson.data.wlan0);
//var state = document.createElement("P");
//state.innerText = ""jsonData.wlan0;
//document.body.appendChild(state);
}
exampleFetch()
*/

View File

@ -1,7 +1,7 @@
/* /*
behavioural layer for the `network_detail.html.tera` template, behavioural layer for the `network_detail.html.tera` template,
corresponding to the web route `/network/wifi?<ssid>` corresponding to the web route `/settings/network/wifi?<ssid>`
- intercept button clicks for connect, disconnect and forget - intercept button clicks for connect, disconnect and forget
- perform json api call - perform json api call
@ -12,7 +12,6 @@ methods:
PEACH_NETWORK.connect(); PEACH_NETWORK.connect();
PEACH_NETWORK.disconnect(); PEACH_NETWORK.disconnect();
PEACH_NETWORK.forget(); PEACH_NETWORK.forget();
PEACH_NETWORK.flashMsg(status, msg);
*/ */
@ -33,7 +32,7 @@ PEACH_NETWORK.connect = function() {
// perform json serialization // perform json serialization
var jsonData = JSON.stringify(ssidData); var jsonData = JSON.stringify(ssidData);
// write in-progress status message to ui // write in-progress status message to ui
PEACH_NETWORK.flashMsg("info", "Connecting to access point..."); PEACH.flashMsg("info", "Connecting to access point...");
// send add_wifi POST request // send add_wifi POST request
fetch("/api/v1/network/wifi/connect", { fetch("/api/v1/network/wifi/connect", {
method: "post", method: "post",
@ -47,7 +46,7 @@ PEACH_NETWORK.connect = function() {
}) })
.then( (jsonData) => { .then( (jsonData) => {
// write json response message to ui // write json response message to ui
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); PEACH.flashMsg(jsonData.status, jsonData.msg);
}) })
}, false); }, false);
}; };
@ -69,7 +68,7 @@ PEACH_NETWORK.disconnect = function() {
// perform json serialization // perform json serialization
var jsonData = JSON.stringify(ssidData); var jsonData = JSON.stringify(ssidData);
// write in-progress status message to ui // write in-progress status message to ui
PEACH_NETWORK.flashMsg("info", "Disconnecting from access point..."); PEACH.flashMsg("info", "Disconnecting from access point...");
// send disconnect_wifi POST request // send disconnect_wifi POST request
fetch("/api/v1/network/wifi/disconnect", { fetch("/api/v1/network/wifi/disconnect", {
method: "post", method: "post",
@ -83,7 +82,7 @@ PEACH_NETWORK.disconnect = function() {
}) })
.then( (jsonData) => { .then( (jsonData) => {
// write json response message to ui // write json response message to ui
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); PEACH.flashMsg(jsonData.status, jsonData.msg);
}) })
}, false); }, false);
}; };
@ -105,7 +104,7 @@ PEACH_NETWORK.forget = function() {
// perform json serialization // perform json serialization
var jsonData = JSON.stringify(ssidData); var jsonData = JSON.stringify(ssidData);
// write in-progress status message to ui // write in-progress status message to ui
PEACH_NETWORK.flashMsg("info", "Removing credentials for access point..."); PEACH.flashMsg("info", "Removing credentials for access point...");
// send forget_ap POST request // send forget_ap POST request
fetch("/api/v1/network/wifi/forget", { fetch("/api/v1/network/wifi/forget", {
method: "post", method: "post",
@ -119,48 +118,13 @@ PEACH_NETWORK.forget = function() {
}) })
.then( (jsonData) => { .then( (jsonData) => {
// write json response message to ui // write json response message to ui
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); PEACH.flashMsg(jsonData.status, jsonData.msg);
}) })
}, false); }, false);
}; };
}); });
} }
// display a message by appending a paragraph element
PEACH_NETWORK.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 detailInstance = PEACH_NETWORK; var detailInstance = PEACH_NETWORK;
detailInstance.connect(); detailInstance.connect();
detailInstance.disconnect(); detailInstance.disconnect();

View File

@ -9,7 +9,6 @@ behavioural layer for the `network_modify.html.tera` template
methods: methods:
PEACH_NETWORK.modify(); PEACH_NETWORK.modify();
PEACH_NETWORK.flashMsg(status, msg);
*/ */
@ -33,7 +32,7 @@ PEACH_NETWORK.modify = function() {
// perform json serialization // perform json serialization
var jsonData = JSON.stringify(object); var jsonData = JSON.stringify(object);
// write in-progress status message to ui // write in-progress status message to ui
PEACH_NETWORK.flashMsg("info", "Updating WiFi password..."); PEACH.flashMsg("info", "Updating WiFi password...");
// send new_password POST request // send new_password POST request
fetch("/api/v1/network/wifi/modify", { fetch("/api/v1/network/wifi/modify", {
method: "post", method: "post",
@ -47,46 +46,11 @@ PEACH_NETWORK.modify = function() {
}) })
.then( (jsonData) => { .then( (jsonData) => {
// write json response message to ui // write json response message to ui
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); PEACH.flashMsg(jsonData.status, jsonData.msg);
}) })
}, false); }, false);
}); });
} }
// display a message by appending a paragraph element
PEACH_NETWORK.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 modifyInstance = PEACH_NETWORK; var modifyInstance = PEACH_NETWORK;
modifyInstance.modify(); modifyInstance.modify();

View File

@ -1,7 +1,7 @@
/* /*
behavioural layer for the `network_usage.html.tera` template, behavioural layer for the `network_usage.html.tera` template,
corresponding to the web route `/network/wifi/usage` corresponding to the web route `/settings/network/wifi/usage`
- intercept form submissions - intercept form submissions
- perform json api calls - perform json api calls
@ -13,7 +13,6 @@ methods:
PEACH_NETWORK.resetUsage(); PEACH_NETWORK.resetUsage();
PEACH_NETWORK.toggleWarning(); PEACH_NETWORK.toggleWarning();
PEACH_NETWORK.toggleCutoff(); PEACH_NETWORK.toggleCutoff();
PEACH_NETWORK.flashMsg(status, msg);
*/ */
@ -51,7 +50,7 @@ PEACH_NETWORK.updateAlerts = function() {
}) })
.then( (jsonData) => { .then( (jsonData) => {
// write json response message to ui // write json response message to ui
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); PEACH.flashMsg(jsonData.status, jsonData.msg);
}) })
}, false); }, false);
}); });
@ -79,7 +78,7 @@ PEACH_NETWORK.resetUsage = function() {
.then( (jsonData) => { .then( (jsonData) => {
console.log(jsonData.msg); console.log(jsonData.msg);
// write json response message to ui // write json response message to ui
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg); PEACH.flashMsg(jsonData.status, jsonData.msg);
// if reset is successful, update the ui // if reset is successful, update the ui
if (jsonData.status === "success") { if (jsonData.status === "success") {
console.log(jsonData.data); console.log(jsonData.data);
@ -133,39 +132,6 @@ PEACH_NETWORK.toggleCutoff = function() {
}); });
}; };
// display a message by appending a paragraph element
PEACH_NETWORK.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");
buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling);
}
}
var usageInstance = PEACH_NETWORK; var usageInstance = PEACH_NETWORK;
usageInstance.resetUsage(); usageInstance.resetUsage();
usageInstance.toggleWarning(); usageInstance.toggleWarning();

View File

@ -11,7 +11,6 @@ methods:
PEACH_DEVICE.reboot(); PEACH_DEVICE.reboot();
PEACH_DEVICE.shutdown(); PEACH_DEVICE.shutdown();
PEACH_DEVICE.flashMsg(status, msg);
*/ */
@ -26,7 +25,7 @@ PEACH_DEVICE.reboot = function() {
// prevent redirect on button press (default behavior) // prevent redirect on button press (default behavior)
e.preventDefault(); e.preventDefault();
// write reboot flash message // write reboot flash message
PEACH_DEVICE.flashMsg("success", "Rebooting the device..."); PEACH.flashMsg("success", "Rebooting the device...");
// send reboot_device POST request // send reboot_device POST request
fetch("/api/v1/admin/reboot", { fetch("/api/v1/admin/reboot", {
method: "post", method: "post",
@ -41,7 +40,7 @@ PEACH_DEVICE.reboot = function() {
.then( (jsonData) => { .then( (jsonData) => {
console.log(jsonData.msg); console.log(jsonData.msg);
// write json response message to ui // write json response message to ui
PEACH_DEVICE.flashMsg(jsonData.status, jsonData.msg); PEACH.flashMsg(jsonData.status, jsonData.msg);
}) })
}, false); }, false);
} }
@ -57,7 +56,7 @@ PEACH_DEVICE.shutdown = function() {
// prevent form submission (default behavior) // prevent form submission (default behavior)
e.preventDefault(); e.preventDefault();
// write shutdown flash message // write shutdown flash message
PEACH_DEVICE.flashMsg("success", "Shutting down the device..."); PEACH.flashMsg("success", "Shutting down the device...");
// send shutdown_device POST request // send shutdown_device POST request
fetch("/api/v1/shutdown", { fetch("/api/v1/shutdown", {
method: "post", method: "post",
@ -72,48 +71,13 @@ PEACH_DEVICE.shutdown = function() {
.then( (jsonData) => { .then( (jsonData) => {
console.log(jsonData.msg); console.log(jsonData.msg);
// write json response message to ui // write json response message to ui
PEACH_DEVICE.flashMsg(jsonData.status, jsonData.msg); PEACH.flashMsg(jsonData.status, jsonData.msg);
}) })
}, false); }, false);
} }
}); });
} }
// display a message by appending a paragraph element
PEACH_DEVICE.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 deviceInstance = PEACH_DEVICE; var deviceInstance = PEACH_DEVICE;
deviceInstance.reboot(); deviceInstance.reboot();
deviceInstance.shutdown(); deviceInstance.shutdown();

View File

@ -2,15 +2,17 @@
* behavioural layer for the `reset_password.html.tera` template, * behavioural layer for the `reset_password.html.tera` template,
*/ */
var PEACH_AUTH = {};
// catch click of 'Save' button and make POST request // catch click of 'Save' button and make POST request
PEACH.add = function() { PEACH_AUTH.resetPassword = function() {
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
document.body.addEventListener('submit', function(e) { document.body.addEventListener('submit', function(e) {
// prevent redirect on button press (default behavior) // prevent redirect on button press (default behavior)
e.preventDefault(); e.preventDefault();
// capture form data // capture form data
var formElement = document.querySelector("form"); var formElement = document.querySelector("form");
// create form data object from the wifiCreds form element // create form data object from the changePassword form element
var formData = new FormData(formElement); var formData = new FormData(formElement);
var object = {}; var object = {};
// assign values from form // assign values from form
@ -41,5 +43,5 @@ PEACH.add = function() {
}); });
} }
var addInstance = PEACH; var resetPassInstance = PEACH_AUTH;
addInstance.add(); resetPassInstance.resetPassword();

View File

@ -1,9 +1,11 @@
{%- extends "nav" -%} {%- extends "nav" -%}
{%- block card %} {%- block card %}
<div class="card center"> <div class="card center">
<div class="card-container capsule info-border"> <div class="capsule-container">
<div class="capsule info-border">
<p>No PeachCloud resource exists for this URL. Please ensure that the URL in the address bar is correct.</p> <p>No PeachCloud resource exists for this URL. Please ensure that the URL in the address bar is correct.</p>
<p>Click the back arrow in the top-left or the PeachCloud logo at the bottom of your screen to return Home.</p> <p>Click the back arrow in the top-left or the PeachCloud logo at the bottom of your screen to return Home.</p>
</div> </div>
</div> </div>
</div>
{%- endblock card -%} {%- endblock card -%}

View File

@ -4,47 +4,22 @@
<div class="card center"> <div class="card center">
<div class="form-container"> <div class="form-container">
<form id="changePassword" action="/settings/change_password" method="post"> <form id="changePassword" action="/settings/change_password" method="post">
<div class="input-wrapper"> <!-- input for current password -->
<!-- input for old password --> <input id="currentPassword" class="center input" name="current_password" type="password" placeholder="Current password" title="Current password" autofocus>
<label id="old_password" class="label-small input-label font-near-black"> <!-- input for new password -->
<label class="label-small input-label font-gray" for="old_password" style="padding-top: 0.25rem;">Old Password</label> <input id="newPassword" class="center input" name="new_password1" type="password" placeholder="New password" title="New password">
<input id="old_password" class="form-input" style="margin-bottom: 0;" <!-- input for duplicate new password -->
name="old_password" type="password" title="old password" value=""> <input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" placeholder="Re-enter new password" title="New password duplicate">
</label>
</div>
<div class="input-wrapper">
<!-- input for new password1 -->
<label id="new_password1" class="label-small input-label font-near-black">
<label class="label-small input-label font-gray" for="new_password1" style="padding-top: 0.25rem;">Enter New Password</label>
<input id="new_password1" class="form-input" style="margin-bottom: 0;"
name="new_password1" title="new_password1" type="password" value="">
</label>
</div>
<div class="input-wrapper">
<!-- input for new password2 -->
<label id="new_password2" class="label-small input-label font-near-black">
<label class="label-small input-label font-gray" for="new_password2" style="padding-top: 0.25rem;">Re-Enter New Password</label>
<input id="new_password2" class="form-input" style="margin-bottom: 0;"
name="new_password2" title="new_password2" type="password" value="">
</label>
</div>
<div id="buttonDiv"> <div id="buttonDiv">
<input id="changePasswordButton" class="button button-primary center" title="Add" type="submit" value="Save"> <input id="savePassword" class="button button-primary center" title="Add" type="submit" value="Save">
</div>
<a class="button button-secondary center" href="/settings/admin" title="Cancel">Cancel</a> <a class="button button-secondary center" href="/settings/admin" title="Cancel">Cancel</a>
</div>
</form> </form>
<!-- FLASH MESSAGE --> <!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %} {% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED --> <!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %} {% include "snippets/noscript" %}
</div> </div>
</div> </div>
<script type="text/javascript" src="/js/change_password.js"></script> <script type="text/javascript" src="/js/change_password.js"></script>
{%- endblock card -%} {%- endblock card -%}

View File

@ -1,25 +1,20 @@
{%- extends "nav" -%} {%- extends "nav" -%}
{%- block card %} {%- block card %}
<!--PUBLIC PAGE FOR SENDING A NEW TEMPORARY PASSWORD TO BE USED TO RESET YOUR PASSWORD --> <!-- PASSWORD RESET REQUEST CARD -->
<div class="card center"> <div class="card center">
<p class="text-notice" style="width: 80%; margin:auto; margin-bottom: 35px; margin-top: 20px;"> <div class="capsule capsule-container info-border">
Click the button below to send a new temporary password which can be used to change your device password. <p class="card-text">Click the button below to send a new temporary password which can be used to change your device password.
<br/><br/> </br></br>
The temporary password will be sent in an SSB private message to the admin of this device. The temporary password will be sent in an SSB private message to the admin of this device.</p>
</p> </div>
<form id="sendPasswordReset" action="/send_password_reset" method="post"> <form id="sendPasswordReset" action="/send_password_reset" method="post">
<div id="buttonDiv"> <div id="buttonDiv">
<input type="submit" class="button center button-secondary" value="Send Password Reset" title="Send Password Reset Link"/> <input class="button button-primary center" style="margin-top: 1rem;" type="submit" value="Send Password Reset" title="Send Password Reset Link"/>
</div> </div>
</form> </form>
<!-- FLASH MESSAGE --> <!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %} {% include "snippets/flash_message" %}
<!-- NO SCRIPT FOR WHEN JS IS DISABLED --> <!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
{% include "snippets/noscript" %} {% include "snippets/noscript" %}
</div>
</div> </div>
{%- endblock card -%} {%- endblock card -%}

View File

@ -2,12 +2,10 @@
{%- block card %} {%- block card %}
<!-- ADMIN SETTINGS MENU --> <!-- ADMIN SETTINGS MENU -->
<div class="card center"> <div class="card center">
<div class="card-container">
<!-- BUTTONS --> <!-- BUTTONS -->
<div id="settingsButtons"> <div id="settingsButtons">
<a id="change" class="button button-primary center" href="/settings/admin/change_password" title="Change Password">Change Password</a> <a id="change" class="button button-primary center" href="/settings/admin/change_password" title="Change Password">Change Password</a>
<a id="configure" class="button button-primary center" href="/settings/admin/configure" title="Configure Admin">Configure Admin</a> <a id="configure" class="button button-primary center" href="/settings/admin/configure" title="Configure Admin">Configure Admin</a>
</div> </div>
</div> </div>
</div>
{%- endblock card -%} {%- endblock card -%}

View File

@ -2,7 +2,6 @@
{%- block card %} {%- block card %}
<!-- SETTINGS MENU --> <!-- SETTINGS MENU -->
<div class="card center"> <div class="card center">
<div class="card-container">
<!-- BUTTONS --> <!-- BUTTONS -->
<div id="settingsButtons"> <div id="settingsButtons">
<a id="network" class="button button-primary center" href="/settings/network" title="Network Settings">Network</a> <a id="network" class="button button-primary center" href="/settings/network" title="Network Settings">Network</a>
@ -10,5 +9,4 @@
<a id="admin" class="button button-primary center" href="/settings/admin" title="Administrator Settings">Administration</a> <a id="admin" class="button button-primary center" href="/settings/admin" title="Administrator Settings">Administration</a>
</div> </div>
</div> </div>
</div>
{%- endblock card -%} {%- endblock card -%}

View File

@ -2,7 +2,6 @@
{%- block card %} {%- block card %}
<!-- SCUTTLEBUTT SETTINGS MENU --> <!-- SCUTTLEBUTT SETTINGS MENU -->
<div class="card center"> <div class="card center">
<div class="card-container">
<!-- BUTTONS --> <!-- BUTTONS -->
<div id="settingsButtons"> <div id="settingsButtons">
<a id="networkKey" class="button button-primary center" href="/settings/scuttlebutt/network_key" title="Set Network Key">Set Network Key</a> <a id="networkKey" class="button button-primary center" href="/settings/scuttlebutt/network_key" title="Set Network Key">Set Network Key</a>
@ -16,5 +15,4 @@
<a id="restart" class="button button-primary center" href="/settings/scuttlebutt/restart" title="Restart Sbot">Restart Sbot</a> <a id="restart" class="button button-primary center" href="/settings/scuttlebutt/restart" title="Restart Sbot">Restart Sbot</a>
</div> </div>
</div> </div>
</div>
{%- endblock card -%} {%- endblock card -%}

View File

@ -42,11 +42,11 @@
</div> </div>
</div> </div>
<!-- PEACH-STATS STATUS STACK --> <!-- PEACH-STATS STATUS STACK -->
<div class="stack capsule{% if stats_ping == "ONLINE" %} success-border{% else %} warning-border{% endif %}"> <div class="stack capsule success-border">
<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"> <img id="statsIcon" class="icon icon-medium" alt="Stats" title="System statistics microservice status" src="/icons/chart.svg">
<div class="stack" style="padding-top: 0.5rem;"> <div class="stack" style="padding-top: 0.5rem;">
<label class="label-small font-near-black">Statistics</label> <label class="label-small font-near-black">Statistics</label>
<label class="label-small font-near-black">{{ stats_ping }}</label> <label class="label-small font-near-black">AVAILABLE</label>
</div> </div>
</div> </div>
{# Display status for dynsdns, config & sbot #} {# Display status for dynsdns, config & sbot #}