80 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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#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: PeachCloud/peach-workspace#38
2021-12-07 08:00:51 +00:00
79c94e6af0 replace snafu with custom error type 2021-12-01 14:26:30 +02:00
30f00524f4 remove unneeded dependency 2021-11-30 13:55:11 +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
406206bab3 update lockfile 2021-11-25 12:20:11 +02:00
c72c57c345 update lockfile 2021-11-25 09:58:49 +02:00
bf9dd7f567 bump version 2021-11-25 09:58:30 +02:00
e6a6fcdc89 remove snafu context and improve error handling 2021-11-25 09:58:00 +02:00
b2f7747357 implement custom error type 2021-11-25 09:56:21 +02:00
f17ae95b21 remove snafu 2021-11-25 09:56:10 +02:00
91180ed1fe clippy tidy-up 2021-11-25 09:55:56 +02:00
60 changed files with 2187 additions and 3189 deletions

3
.gitignore vendored
View File

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

880
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
@ -9,4 +8,4 @@ fn main() {
info!("Running peach-dyndns-updater"); info!("Running peach-dyndns-updater");
let result = dyndns_update_ip(); let result = dyndns_update_ip();
info!("result: {:?}", result); info!("result: {:?}", result);
} }

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,24 +1,19 @@
[package] [package]
name = "peach-lib" name = "peach-lib"
version = "1.2.15" version = "1.3.2"
authors = ["Andrew Reid <gnomad@cryptolab.net>"] authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2018" edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[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_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_yaml = "0.8" serde_yaml = "0.8"
env_logger = "0.6" sha3 = "0.10.0"
snafu = "0.6"
regex = "1"
chrono = "0.4.19"
rand="0.8.4"
fslock="0.1.6"

View File

@ -4,12 +4,12 @@
//! //!
//! The configuration file is located at: "/var/lib/peachcloud/config.yml" //! The configuration file is located at: "/var/lib/peachcloud/config.yml"
use fslock::{LockFile};
use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use fslock::LockFile;
use serde::{Deserialize, Serialize};
use crate::error::PeachError; use crate::error::PeachError;
use crate::error::*;
// main configuration file // main configuration file
pub const YAML_PATH: &str = "/var/lib/peachcloud/config.yml"; pub const YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
@ -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,
@ -48,8 +56,9 @@ fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachErro
let yaml_str = serde_yaml::to_string(&peach_config)?; let yaml_str = serde_yaml::to_string(&peach_config)?;
fs::write(YAML_PATH, yaml_str).context(WriteConfigError { fs::write(YAML_PATH, yaml_str).map_err(|source| PeachError::Write {
file: YAML_PATH.to_string(), source,
path: YAML_PATH.to_string(),
})?; })?;
// unlock file lock // unlock file lock
@ -62,28 +71,28 @@ 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 {
let contents = fs::read_to_string(YAML_PATH).context(ReadConfigError { let contents = fs::read_to_string(YAML_PATH).map_err(|source| PeachError::Read {
file: YAML_PATH.to_string(), source,
path: YAML_PATH.to_string(),
})?; })?;
peach_config = serde_yaml::from_str(&contents)?; serde_yaml::from_str(&contents)?
} };
Ok(peach_config) Ok(peach_config)
} }
@ -120,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;
@ -176,4 +197,4 @@ pub fn get_temporary_password_hash() -> Result<String, PeachError> {
} else { } else {
Err(PeachError::PasswordNotSet) Err(PeachError::PasswordNotSet)
} }
} }

View File

@ -9,27 +9,19 @@
//! //!
//! 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 crate::config_manager::{load_peach_config, set_peach_dyndns_config}; use std::ffi::OsStr;
use crate::error::PeachError; use std::{fs, fs::OpenOptions, io::Write, process::Command, str::FromStr};
use crate::error::{
ChronoParseError, DecodeNsUpdateOutputError, DecodePublicIpError, GetPublicIpError,
NsCommandError, SaveDynDnsResultError, SaveTsigKeyError,
};
use chrono::prelude::*; use chrono::prelude::*;
use jsonrpc_client_core::{expand_params, jsonrpc_client}; use jsonrpc_client_core::{expand_params, jsonrpc_client};
use jsonrpc_client_http::HttpTransport; use jsonrpc_client_http::HttpTransport;
use log::{debug, info}; use log::{debug, info};
use regex::Regex; use regex::Regex;
use snafu::ResultExt;
use std::fs; use crate::config_manager::get_dyndns_server_address;
use std::fs::OpenOptions; use crate::{config_manager, error::PeachError};
use std::io::Write;
use std::process::{Command, Stdio};
use std::str::FromStr;
use std::str::ParseBoolError;
/// 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";
@ -37,20 +29,21 @@ pub const DYNDNS_LOG_PATH: &str = "/var/lib/peachcloud/peach-dyndns/latest_resul
/// helper function which saves dyndns TSIG key returned by peach-dyndns-server to /var/lib/peachcloud/peach-dyndns/tsig.key /// helper function which saves dyndns TSIG key returned by peach-dyndns-server to /var/lib/peachcloud/peach-dyndns/tsig.key
pub fn save_dyndns_key(key: &str) -> Result<(), PeachError> { pub fn save_dyndns_key(key: &str) -> Result<(), PeachError> {
// create directory if it doesn't exist // create directory if it doesn't exist
fs::create_dir_all(PEACH_DYNDNS_CONFIG_PATH).context(SaveTsigKeyError { fs::create_dir_all(PEACH_DYNDNS_CONFIG_PATH)?;
path: PEACH_DYNDNS_CONFIG_PATH.to_string(), //.context(SaveTsigKeyError {
})?; //path: PEACH_DYNDNS_CONFIG_PATH.to_string(),
//})?;
// write key text // write key text
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.open(TSIG_KEY_PATH) // TODO: consider adding context msg
.context(SaveTsigKeyError { .open(TSIG_KEY_PATH)?;
path: TSIG_KEY_PATH.to_string(), writeln!(file, "{}", key).map_err(|source| PeachError::Write {
})?; source,
writeln!(file, "{}", key).context(SaveTsigKeyError {
path: TSIG_KEY_PATH.to_string(), path: TSIG_KEY_PATH.to_string(),
})?; })?;
Ok(()) Ok(())
} }
@ -61,30 +54,26 @@ 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);
info!("Performing register_domain call to peach-dyndns-server"); info!("Performing register_domain call to peach-dyndns-server");
let res = client.register_domain(domain).call(); let key = client.register_domain(domain).call()?;
match res { // save new TSIG key
Ok(key) => { save_dyndns_key(&key)?;
// save new TSIG key // save new configuration values
save_dyndns_key(&key)?; let set_config_result =
// save new configuration values config_manager::set_peach_dyndns_config(domain, &http_server, TSIG_KEY_PATH, true);
let set_config_result = match set_config_result {
set_peach_dyndns_config(domain, PEACH_DYNDNS_URL, TSIG_KEY_PATH, true); Ok(_) => {
match set_config_result { let response = "success".to_string();
Ok(_) => { Ok(response)
let response = "success".to_string();
Ok(response)
}
Err(err) => Err(err),
}
} }
Err(err) => Err(PeachError::JsonRpcClientCore { source: err }), Err(err) => Err(err),
} }
} }
@ -92,67 +81,57 @@ 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);
info!("Performing register_domain call to peach-dyndns-server"); info!("Performing register_domain call to peach-dyndns-server");
let res = client.is_domain_available(domain).call(); let domain_availability = client.is_domain_available(domain).call()?;
info!("res: {:?}", res); info!("Domain availability: {:?}", domain_availability);
match res { // convert availability status to a bool
Ok(result_str) => { let available: bool = FromStr::from_str(&domain_availability)?;
let result: Result<bool, ParseBoolError> = FromStr::from_str(&result_str);
match result { Ok(available)
Ok(result_bool) => Ok(result_bool),
Err(err) => Err(PeachError::PeachParseBoolError { source: err }),
}
}
Err(err) => Err(PeachError::JsonRpcClientCore { source: err }),
}
} }
/// Helper function to get public ip address of PeachCloud device. /// Helper function to get public ip address of PeachCloud device.
fn get_public_ip_address() -> Result<String, PeachError> { fn get_public_ip_address() -> Result<String, PeachError> {
// TODO: consider other ways to get public IP address // TODO: consider other ways to get public IP address
let output = Command::new("/usr/bin/curl") let output = Command::new("/usr/bin/curl").arg("ifconfig.me").output()?;
.arg("ifconfig.me") let command_output = String::from_utf8(output.stdout)?;
.output() Ok(command_output)
.context(GetPublicIpError)?;
let command_output = std::str::from_utf8(&output.stdout).context(DecodePublicIpError)?;
Ok(command_output.to_string())
} }
/// 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 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()
.context(NsCommandError)?;
// 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);
@ -163,16 +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,
); );
write!(nsupdate_command.stdin.as_ref().unwrap(), "{}", ns_commands).unwrap(); info!("ns_commands: {:?}", ns_commands);
let nsupdate_output = nsupdate_command info!("creating nsupdate temp file");
.wait_with_output() let temp_file_path = "/var/lib/peachcloud/nsupdate.sh";
.context(NsCommandError)?; // write ns_commands to temp_file
info!("output: {:?}", nsupdate_output); fs::write(temp_file_path, ns_commands)?;
nsupdate_command.arg(temp_file_path);
let nsupdate_output = nsupdate_command.output()?;
let args: Vec<&OsStr> = nsupdate_command.get_args().collect();
info!("nsupdate command: {:?}", args);
// 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");
@ -182,9 +165,8 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
Ok(true) Ok(true)
} else { } else {
info!("nsupdate failed, returning error"); info!("nsupdate failed, returning error");
let err_msg = let err_msg = String::from_utf8(nsupdate_output.stdout)?;
String::from_utf8(nsupdate_output.stdout).context(DecodeNsUpdateOutputError)?; Err(PeachError::NsUpdate { msg: err_msg })
Err(PeachError::NsUpdateError { msg: err_msg })
} }
} }
} }
@ -195,9 +177,12 @@ pub fn log_successful_nsupdate() -> Result<bool, PeachError> {
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.open(DYNDNS_LOG_PATH) // TODO: possibly add a context msg here ("failed to open dynamic dns success log")
.context(SaveDynDnsResultError)?; .open(DYNDNS_LOG_PATH)?;
write!(file, "{}", now_timestamp).context(SaveDynDnsResultError)?; write!(file, "{}", now_timestamp).map_err(|source| PeachError::Write {
source,
path: DYNDNS_LOG_PATH.to_string(),
})?;
Ok(true) Ok(true)
} }
@ -207,12 +192,19 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
if !log_exists { if !log_exists {
Ok(None) Ok(None)
} else { } else {
let contents = let contents = fs::read_to_string(DYNDNS_LOG_PATH).map_err(|source| PeachError::Read {
fs::read_to_string(DYNDNS_LOG_PATH).expect("Something went wrong reading the file"); source,
path: DYNDNS_LOG_PATH.to_string(),
})?;
// replace newline if found // replace newline if found
let contents = contents.replace("\n", ""); // TODO: maybe we can use `.trim()` instead
let time_ran_dt = DateTime::parse_from_rfc3339(&contents).context(ChronoParseError { let contents = contents.replace('\n', "");
msg: "Error parsing dyndns time from latest_result.log".to_string(), // TODO: consider adding additional context?
let time_ran_dt = DateTime::parse_from_rfc3339(&contents).map_err(|source| {
PeachError::ParseDateTime {
source,
path: DYNDNS_LOG_PATH.to_string(),
}
})?; })?;
let current_time: DateTime<Utc> = Utc::now(); let current_time: DateTime<Utc> = Utc::now();
let duration = current_time.signed_duration_since(time_ran_dt); let duration = current_time.signed_duration_since(time_ran_dt);
@ -225,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);
@ -260,10 +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> {
let peach_config = load_peach_config().unwrap(); let peach_config = config_manager::load_peach_config()?;
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,136 +1,222 @@
//! Basic error handling for the network, OLED, stats and dyndns JSON-RPC clients. #![warn(missing_docs)]
pub use snafu::ResultExt;
use snafu::Snafu;
use std::error;
pub type BoxError = Box<dyn error::Error>;
#[derive(Debug, Snafu)] //! Error handling for various aspects of the PeachCloud system, including the network, OLED, stats and dyndns JSON-RPC clients, as well as the configuration manager, sbot client and password utilities.
#[snafu(visibility(pub(crate)))]
use std::{io, str, string};
/// This type represents all possible errors that can occur when interacting with the PeachCloud library.
#[derive(Debug)]
pub enum PeachError { pub enum PeachError {
#[snafu(display("{}", source))] /// Represents all other cases of `std::io::Error`.
JsonRpcHttp { source: jsonrpc_client_http::Error }, Io(io::Error),
#[snafu(display("{}", source))]
JsonRpcClientCore { source: jsonrpc_client_core::Error }, /// Represents a JSON-RPC core error returned from a JSON-RPC client.
#[snafu(display("{}", source))] JsonRpcClientCore(jsonrpc_client_core::Error),
Serde { source: serde_json::error::Error },
#[snafu(display("{}", source))] /// Represents a JSON-RPC core error returned from a JSON-RPC server.
PeachParseBoolError { source: std::str::ParseBoolError }, JsonRpcCore(jsonrpc_core::Error),
#[snafu(display("{}", source))]
SetConfigError { source: serde_yaml::Error }, /// Represents a JSON-RPC HTTP error returned from a JSON-RPC client.
#[snafu(display("Failed to read: {}", file))] JsonRpcHttp(jsonrpc_client_http::Error),
ReadConfigError {
source: std::io::Error, /// Represents a failure to update the nameserver.
file: String, NsUpdate {
}, /// A message describing the context of the attempted nameserver update.
#[snafu(display("Failed to save: {}", file))]
WriteConfigError {
source: std::io::Error,
file: String,
},
#[snafu(display("Failed to save tsig key: {} {}", path, source))]
SaveTsigKeyError {
source: std::io::Error,
path: String,
},
#[snafu(display("{}", msg))]
NsUpdateError { msg: String },
#[snafu(display("Failed to run nsupdate: {}", source))]
NsCommandError { source: std::io::Error },
#[snafu(display("Failed to get public IP address: {}", source))]
GetPublicIpError { source: std::io::Error },
#[snafu(display("Failed to decode public ip: {}", source))]
DecodePublicIpError { source: std::str::Utf8Error },
#[snafu(display("Failed to decode nsupdate output: {}", source))]
DecodeNsUpdateOutputError { source: std::string::FromUtf8Error },
#[snafu(display("{}", source))]
YamlError { source: serde_yaml::Error },
#[snafu(display("{:?}", err))]
JsonRpcCore { err: jsonrpc_core::Error },
#[snafu(display("Error creating regex: {}", source))]
RegexError { source: regex::Error },
#[snafu(display("Failed to decode utf8: {}", source))]
FromUtf8Error { source: std::string::FromUtf8Error },
#[snafu(display("Encountered Utf8Error: {}", source))]
Utf8Error { source: std::str::Utf8Error },
#[snafu(display("Stdio error: {}: {}", msg, source))]
StdIoError { source: std::io::Error, msg: String },
#[snafu(display("Failed to parse time from {} {}", source, msg))]
ChronoParseError {
source: chrono::ParseError,
msg: String, msg: String,
}, },
#[snafu(display("Failed to save dynamic dns success log: {}", source))]
SaveDynDnsResultError { source: std::io::Error }, /// Represents a failure to parse a string slice to a boolean value.
#[snafu(display("New passwords do not match"))] ParseBool(str::ParseBoolError),
PasswordsDoNotMatch,
#[snafu(display("No admin password is set"))] /// Represents a failure to parse a `DateTime`. Includes the error source and the file path
/// used in the parse attempt.
ParseDateTime {
/// The underlying source of the error.
source: chrono::ParseError,
/// The file path for the parse attempt.
path: String,
},
/// Represents the submission of an incorrect admin password.
PasswordIncorrect,
/// Represents the submission of two passwords which do not match.
PasswordMismatch,
/// Represents an unset admin password (empty password hash value) in the config file.
PasswordNotSet, PasswordNotSet,
#[snafu(display("The supplied password was not correct"))]
InvalidPassword, /// Represents a failure to read from input. Includes the error source and the file path used
#[snafu(display("Error saving new password: {}", msg))] /// in the read attempt.
FailedToSetNewPassword { msg: String }, Read {
#[snafu(display("Error calling sbotcli: {}", msg))] /// The underlying source of the error.
SbotCliError { msg: String }, source: io::Error,
#[snafu(display("Error deleting ssb admin id, id not found"))] /// The file path for the read attempt.
SsbAdminIdNotFound { id: String }, path: String,
},
/// Represents a failure to parse or compile a regular expression.
Regex(regex::Error),
/// Represents a failure to successfully execute an sbot command.
SbotCli {
/// The `stderr` output from the sbot command.
msg: String,
},
/// Represents a failure to serialize or deserialize JSON.
SerdeJson(serde_json::error::Error),
/// Represents a failure to serialize or deserialize YAML.
SerdeYaml(serde_yaml::Error),
/// Represents a failure to find the given SSB ID in the config file.
SsbAdminIdNotFound {
/// An SSB ID (public key).
id: String,
},
/// Represents a failure to interpret a sequence of u8 as a string slice.
Utf8ToStr(str::Utf8Error),
/// Represents a failure to interpret a sequence of u8 as a String.
Utf8ToString(string::FromUtf8Error),
/// Represents a failure to write to output. Includes the error source and the file path used
/// in the write attempt.
Write {
/// The underlying source of the error.
source: io::Error,
/// The file path for the write attemp.
path: String,
},
} }
impl From<jsonrpc_client_http::Error> for PeachError { impl std::error::Error for PeachError {
fn from(err: jsonrpc_client_http::Error) -> PeachError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
PeachError::JsonRpcHttp { source: err } match *self {
PeachError::Io(_) => None,
PeachError::JsonRpcClientCore(_) => None,
PeachError::JsonRpcCore(_) => None,
PeachError::JsonRpcHttp(_) => None,
PeachError::NsUpdate { .. } => None,
PeachError::ParseBool(_) => None,
PeachError::ParseDateTime { ref source, .. } => Some(source),
PeachError::PasswordIncorrect => None,
PeachError::PasswordMismatch => None,
PeachError::PasswordNotSet => None,
PeachError::Read { ref source, .. } => Some(source),
PeachError::Regex(_) => None,
PeachError::SbotCli { .. } => None,
PeachError::SerdeJson(_) => None,
PeachError::SerdeYaml(_) => None,
PeachError::SsbAdminIdNotFound { .. } => None,
PeachError::Utf8ToStr(_) => None,
PeachError::Utf8ToString(_) => None,
PeachError::Write { ref source, .. } => Some(source),
}
} }
} }
impl From<jsonrpc_client_core::Error> for PeachError { impl std::fmt::Display for PeachError {
fn from(err: jsonrpc_client_core::Error) -> PeachError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
PeachError::JsonRpcClientCore { source: err } match *self {
} PeachError::Io(ref err) => err.fmt(f),
} PeachError::JsonRpcClientCore(ref err) => err.fmt(f),
PeachError::JsonRpcCore(ref err) => {
impl From<serde_json::error::Error> for PeachError { write!(f, "{:?}", err)
fn from(err: serde_json::error::Error) -> PeachError { }
PeachError::Serde { source: err } PeachError::JsonRpcHttp(ref err) => err.fmt(f),
} PeachError::NsUpdate { ref msg } => {
} write!(f, "Nameserver error: {}", msg)
}
impl From<serde_yaml::Error> for PeachError { PeachError::ParseBool(ref err) => err.fmt(f),
fn from(err: serde_yaml::Error) -> PeachError { PeachError::ParseDateTime { ref path, .. } => {
PeachError::YamlError { source: err } write!(f, "Date/time parse error: {}", path)
}
PeachError::PasswordIncorrect => {
write!(f, "Password error: user-supplied password is incorrect")
}
PeachError::PasswordMismatch => {
write!(f, "Password error: user-supplied passwords do not match")
}
PeachError::PasswordNotSet => {
write!(
f,
"Password error: hash value in YAML configuration file is empty"
)
}
PeachError::Read { ref path, .. } => {
write!(f, "Read error: {}", path)
}
PeachError::Regex(ref err) => err.fmt(f),
PeachError::SbotCli { ref msg } => {
write!(f, "Sbot error: {}", msg)
}
PeachError::SerdeJson(ref err) => err.fmt(f),
PeachError::SerdeYaml(ref err) => err.fmt(f),
PeachError::SsbAdminIdNotFound { ref id } => {
write!(f, "Config error: SSB admin ID `{}` not found", id)
}
PeachError::Utf8ToStr(ref err) => err.fmt(f),
PeachError::Utf8ToString(ref err) => err.fmt(f),
PeachError::Write { ref path, .. } => {
write!(f, "Write error: {}", path)
}
}
} }
} }
impl From<std::io::Error> for PeachError { impl From<std::io::Error> for PeachError {
fn from(err: std::io::Error) -> PeachError { fn from(err: std::io::Error) -> PeachError {
PeachError::StdIoError { PeachError::Io(err)
source: err, }
msg: "".to_string(), }
}
impl From<jsonrpc_client_core::Error> for PeachError {
fn from(err: jsonrpc_client_core::Error) -> PeachError {
PeachError::JsonRpcClientCore(err)
}
}
impl From<jsonrpc_client_http::Error> for PeachError {
fn from(err: jsonrpc_client_http::Error) -> PeachError {
PeachError::JsonRpcHttp(err)
}
}
impl From<str::ParseBoolError> for PeachError {
fn from(err: str::ParseBoolError) -> PeachError {
PeachError::ParseBool(err)
} }
} }
impl From<regex::Error> for PeachError { impl From<regex::Error> for PeachError {
fn from(err: regex::Error) -> PeachError { fn from(err: regex::Error) -> PeachError {
PeachError::RegexError { source: err } PeachError::Regex(err)
} }
} }
impl From<std::string::FromUtf8Error> for PeachError { impl From<serde_json::error::Error> for PeachError {
fn from(err: std::string::FromUtf8Error) -> PeachError { fn from(err: serde_json::error::Error) -> PeachError {
PeachError::FromUtf8Error { source: err } PeachError::SerdeJson(err)
} }
} }
impl From<std::str::Utf8Error> for PeachError { impl From<serde_yaml::Error> for PeachError {
fn from(err: std::str::Utf8Error) -> PeachError { fn from(err: serde_yaml::Error) -> PeachError {
PeachError::Utf8Error { source: err } PeachError::SerdeYaml(err)
} }
} }
impl From<chrono::ParseError> for PeachError { impl From<str::Utf8Error> for PeachError {
fn from(err: chrono::ParseError) -> PeachError { fn from(err: str::Utf8Error) -> PeachError {
PeachError::ChronoParseError { PeachError::Utf8ToStr(err)
source: err, }
msg: "".to_string(), }
}
impl From<string::FromUtf8Error> for PeachError {
fn from(err: string::FromUtf8Error) -> PeachError {
PeachError::Utf8ToString(err)
} }
} }

View File

@ -1,7 +1,3 @@
// this is to ignore a clippy warning that suggests
// to replace code with the same code that is already there (possibly a bug)
#![allow(clippy::nonstandard_macro_braces)]
pub mod config_manager; pub mod config_manager;
pub mod dyndns_client; pub mod dyndns_client;
pub mod error; pub mod error;

View File

@ -9,9 +9,6 @@
//! Several helper methods are also included here which bundle multiple client //! Several helper methods are also included here which bundle multiple client
//! calls to achieve the desired functionality. //! calls to achieve the desired functionality.
// TODO: fix these clippy errors so this allow can be removed
#![allow(clippy::needless_borrow)]
use std::env; use std::env;
use jsonrpc_client_core::{expand_params, jsonrpc_client}; use jsonrpc_client_core::{expand_params, jsonrpc_client};
@ -166,9 +163,9 @@ pub fn disable(iface: &str, ssid: &str) -> std::result::Result<String, PeachErro
let mut client = PeachNetworkClient::new(transport_handle); let mut client = PeachNetworkClient::new(transport_handle);
info!("Performing id call to peach-network microservice."); info!("Performing id call to peach-network microservice.");
let id = client.id(&iface, &ssid).call()?; let id = client.id(iface, ssid).call()?;
info!("Performing disable call to peach-network microservice."); info!("Performing disable call to peach-network microservice.");
client.disable(&id, &iface).call()?; client.disable(&id, iface).call()?;
let response = "success".to_string(); let response = "success".to_string();
@ -194,12 +191,12 @@ pub fn forget(iface: &str, ssid: &str) -> std::result::Result<String, PeachError
let mut client = PeachNetworkClient::new(transport_handle); let mut client = PeachNetworkClient::new(transport_handle);
info!("Performing id call to peach-network microservice."); info!("Performing id call to peach-network microservice.");
let id = client.id(&iface, &ssid).call()?; let id = client.id(iface, ssid).call()?;
info!("Performing delete call to peach-network microservice."); info!("Performing delete call to peach-network microservice.");
// WEIRD BUG: the parameters below are technically in the wrong order: // WEIRD BUG: the parameters below are technically in the wrong order:
// it should be id first and then iface, but somehow they get twisted. // it should be id first and then iface, but somehow they get twisted.
// i don't understand computers. // i don't understand computers.
client.delete(&iface, &id).call()?; client.delete(iface, &id).call()?;
info!("Performing save call to peach-network microservice."); info!("Performing save call to peach-network microservice.");
client.save().call()?; client.save().call()?;
@ -357,8 +354,7 @@ pub fn saved_ap(ssid: &str) -> std::result::Result<bool, PeachError> {
// retrieve a list of access points with saved credentials // retrieve a list of access points with saved credentials
let saved_aps = match client.saved_networks().call() { let saved_aps = match client.saved_networks().call() {
Ok(ssids) => { Ok(ssids) => {
let networks: Vec<Networks> = serde_json::from_str(ssids.as_str()) let networks: Vec<Networks> = serde_json::from_str(ssids.as_str())?;
.expect("Failed to deserialize saved_networks response");
networks networks
} }
// return an empty vector if there are no saved access point credentials // return an empty vector if there are no saved access point credentials
@ -479,7 +475,7 @@ pub fn traffic(iface: &str) -> std::result::Result<Traffic, PeachError> {
let mut client = PeachNetworkClient::new(transport_handle); let mut client = PeachNetworkClient::new(transport_handle);
let response = client.traffic(iface).call()?; let response = client.traffic(iface).call()?;
let t: Traffic = serde_json::from_str(&response).unwrap(); let t: Traffic = serde_json::from_str(&response)?;
Ok(t) Ok(t)
} }
@ -506,13 +502,13 @@ pub fn update(iface: &str, ssid: &str, pass: &str) -> std::result::Result<String
// get the id of the network // get the id of the network
info!("Performing id call to peach-network microservice."); info!("Performing id call to peach-network microservice.");
let id = client.id(&iface, &ssid).call()?; let id = client.id(iface, ssid).call()?;
// delete the old credentials // delete the old credentials
// WEIRD BUG: the parameters below are technically in the wrong order: // WEIRD BUG: the parameters below are technically in the wrong order:
// it should be id first and then iface, but somehow they get twisted. // it should be id first and then iface, but somehow they get twisted.
// i don't understand computers. // i don't understand computers.
info!("Performing delete call to peach-network microservice."); info!("Performing delete call to peach-network microservice.");
client.delete(&iface, &id).call()?; client.delete(iface, &id).call()?;
// save the updates to wpa_supplicant.conf // save the updates to wpa_supplicant.conf
info!("Performing save call to peach-network microservice."); info!("Performing save call to peach-network microservice.");
client.save().call()?; client.save().call()?;

View File

@ -1,23 +1,17 @@
use crate::config_manager::{get_peachcloud_domain, load_peach_config, use nanorand::{Rng, WyRand};
set_admin_password_hash, get_admin_password_hash, use sha3::{Digest, Sha3_256};
get_temporary_password_hash, set_temporary_password_hash};
use crate::error::PeachError; use crate::{config_manager, error::PeachError, sbot_client};
use crate::sbot_client;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use std::iter;
use crypto::digest::Digest;
use crypto::sha3::Sha3;
/// Returns Ok(()) if the supplied password is correct, /// Returns Ok(()) if the supplied password is correct,
/// and returns Err if the supplied password is incorrect. /// and returns Err if the supplied password is incorrect.
pub fn verify_password(password: &str) -> Result<(), PeachError> { pub fn verify_password(password: &str) -> Result<(), PeachError> {
let real_admin_password_hash = get_admin_password_hash()?; let real_admin_password_hash = config_manager::get_admin_password_hash()?;
let password_hash = hash_password(&password.to_string()); let password_hash = hash_password(&password.to_string());
if real_admin_password_hash == password_hash { if real_admin_password_hash == password_hash {
Ok(()) Ok(())
} else { } else {
Err(PeachError::InvalidPassword) Err(PeachError::PasswordIncorrect)
} }
} }
@ -29,71 +23,60 @@ pub fn validate_new_passwords(new_password1: &str, new_password2: &str) -> Resul
if new_password1 == new_password2 { if new_password1 == new_password2 {
Ok(()) Ok(())
} else { } else {
Err(PeachError::PasswordsDoNotMatch) Err(PeachError::PasswordMismatch)
} }
} }
/// Sets a new password for the admin user /// Sets a new password for the admin user
pub fn set_new_password(new_password: &str) -> Result<(), PeachError> { pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
let new_password_hash = hash_password(&new_password.to_string()); let new_password_hash = hash_password(&new_password.to_string());
let result = set_admin_password_hash(&new_password_hash); config_manager::set_admin_password_hash(&new_password_hash)?;
match result {
Ok(_) => { Ok(())
Ok(())
},
Err(_err) => {
Err(PeachError::FailedToSetNewPassword { msg: "failed to save password hash".to_string() })
}
}
} }
/// 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
/// which can be used to reset the permanent password /// which can be used to reset the permanent password
pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> { pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> {
let new_password_hash = hash_password(&new_password.to_string()); let new_password_hash = hash_password(&new_password.to_string());
let result = set_temporary_password_hash(&new_password_hash); config_manager::set_temporary_password_hash(&new_password_hash)?;
match result {
Ok(_) => { Ok(())
Ok(())
},
Err(_err) => {
Err(PeachError::FailedToSetNewPassword { msg: "failed to save temporary password hash".to_string() })
}
}
} }
/// Returns Ok(()) if the supplied temp_password is correct, /// Returns Ok(()) if the supplied temp_password is correct,
/// and returns Err if the supplied temp_password is incorrect /// and returns Err if the supplied temp_password is incorrect
pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> { pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> {
let temporary_admin_password_hash = get_temporary_password_hash()?; let temporary_admin_password_hash = config_manager::get_temporary_password_hash()?;
let password_hash = hash_password(&password.to_string()); let password_hash = hash_password(&password.to_string());
if temporary_admin_password_hash == password_hash { if temporary_admin_password_hash == password_hash {
Ok(()) Ok(())
} else { } else {
Err(PeachError::InvalidPassword) Err(PeachError::PasswordIncorrect)
} }
} }
/// 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 = get_peachcloud_domain()?; let domain = config_manager::get_peachcloud_domain()?;
// then send temporary password as a private ssb message to admin // then send temporary password as a private ssb message to admin
let mut msg = format!( let mut msg = format!(
@ -117,7 +100,7 @@ using this link: http://peach.local/reset_password",
}; };
msg += &remote_link; msg += &remote_link;
// finally send the message to the admins // finally send the message to the admins
let peach_config = load_peach_config()?; let peach_config = config_manager::load_peach_config()?;
for ssb_admin_id in peach_config.ssb_admin_ids { for ssb_admin_id in peach_config.ssb_admin_ids {
sbot_client::private_message(&msg, &ssb_admin_id)?; sbot_client::private_message(&msg, &ssb_admin_id)?;
} }

View File

@ -1,9 +1,11 @@
//! Interfaces for monitoring and configuring go-sbot using sbotcli. //! Interfaces for monitoring and configuring go-sbot using sbotcli.
//!
use crate::error::PeachError;
use serde::{Deserialize, Serialize};
use std::process::Command; use std::process::Command;
use serde::{Deserialize, Serialize};
use crate::error::PeachError;
pub fn is_sbot_online() -> Result<bool, PeachError> { pub fn is_sbot_online() -> Result<bool, PeachError> {
let output = Command::new("/usr/bin/systemctl") let output = Command::new("/usr/bin/systemctl")
.arg("status") .arg("status")
@ -36,7 +38,7 @@ pub fn post(msg: &str) -> Result<(), PeachError> {
Ok(()) Ok(())
} else { } else {
let stderr = std::str::from_utf8(&output.stderr)?; let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCliError { Err(PeachError::SbotCli {
msg: format!("Error making ssb post: {}", stderr), msg: format!("Error making ssb post: {}", stderr),
}) })
} }
@ -83,7 +85,7 @@ pub fn update_pub_name(new_name: &str) -> Result<(), PeachError> {
Ok(()) Ok(())
} else { } else {
let stderr = std::str::from_utf8(&output.stderr)?; let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCliError { Err(PeachError::SbotCli {
msg: format!("Error updating pub name: {}", stderr), msg: format!("Error updating pub name: {}", stderr),
}) })
} }
@ -102,7 +104,7 @@ pub fn private_message(msg: &str, recipient: &str) -> Result<(), PeachError> {
Ok(()) Ok(())
} else { } else {
let stderr = std::str::from_utf8(&output.stderr)?; let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCliError { Err(PeachError::SbotCli {
msg: format!("Error sending ssb private message: {}", stderr), msg: format!("Error sending ssb private message: {}", stderr),
}) })
} }

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),
data: None,
},
NetworkError::GenWpaPassphraseWarning { ssid, err_msg } => Error {
code: ErrorCode::ServerError(-32036),
message: format!(
"Failed to generate wpa passphrase for {}: {}",
ssid, err_msg
),
data: None,
},
NetworkError::Id { iface, ssid } => Error {
code: ErrorCode::ServerError(-32026),
message: format!("No ID found for {} on interface {}", ssid, iface),
data: None,
},
NetworkError::NoIp { iface, source } => Error {
code: ErrorCode::ServerError(-32001),
message: format!("Failed to retrieve IP address for {}: {}", iface, source),
data: None,
},
NetworkError::Rssi { iface } => Error {
code: ErrorCode::ServerError(-32002),
message: format!(
"Failed to retrieve RSSI for {}. Interface may not be connected",
iface
),
data: None,
},
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 std::fmt::Display for NetworkError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
NetworkError::Add { ref ssid } => {
write!(f, "Failed to add network for {}", ssid)
}
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 {}: {}",
ssid, err_msg
)
}
NetworkError::Id {
ref ssid,
ref iface,
} => {
write!(f, "No ID found for {} on interface: {}", ssid, iface)
}
NetworkError::NoIp { ref iface, .. } => {
write!(f, "Could not access IP address for interface: {}", iface)
}
NetworkError::Rssi { ref iface } => {
write!(f, "Could not find RSSI for interface: {}", iface)
}
NetworkError::RssiPercent { ref iface } => {
write!(
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
),
NetworkError::WpaCtrl(_) => write!(f, "WpaCtrl command failed"),
}
}
}
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,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.12" 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"],
@ -40,6 +41,8 @@ log = "0.4"
nest = "1.0.0" nest = "1.0.0"
openssl = { version = "0.10", features = ["vendored"] } 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" regex = "1"
rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] } rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] }

View File

@ -97,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,3 +1,6 @@
[default]
secret_key = "VYVUDivXvu8g6llxeJd9F92pMfocml5xl/Jjv5Sk4yw="
[development] [development]
template_dir = "templates/" template_dir = "templates/"
disable_auth = true disable_auth = true

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

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

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