Compare commits
155 Commits
lib_error_
...
ssb_status
Author | SHA1 | Date | |
---|---|---|---|
528db7189a | |||
2bfba66dab | |||
43344566de | |||
bfb53747db | |||
d0321d17d0 | |||
f3ddbcf07c | |||
680044cba8 | |||
66555f19bf | |||
792779f60f | |||
44b68a8b71 | |||
205dd145b4 | |||
7346c37c86 | |||
72fbbe83f0 | |||
b4a930e774 | |||
8c4cf6261e | |||
8f5b257ed1 | |||
3bb00c4eb7 | |||
5d75aebf0d | |||
4d2a3771b8 | |||
ed6da528a2 | |||
aca687974a | |||
6e4b8faf40 | |||
552c4b419e | |||
6fb4a2406b | |||
65dbc6bdd4 | |||
dbab6f1762 | |||
d3ae25934c | |||
c6f68de516 | |||
2ccd7e65d3 | |||
bf3325a41e | |||
e4b3479417 | |||
0561b6a9be | |||
166f4d25ae | |||
a5f0d991fa | |||
60a0d7f293 | |||
d8c40e0724 | |||
f4ad230d58 | |||
b0b21ad8a0 | |||
08ee9cd776 | |||
cfd50ca359 | |||
fd94ba27ac | |||
bb5cd0f0d3 | |||
72b7281587 | |||
cbb4027099 | |||
5e1520aa3f | |||
a8f3730b7c | |||
c1432bd29e | |||
eb77290a93 | |||
5dcba8e2ad | |||
69ba400b69 | |||
2a7c893d94 | |||
2135ab1a5b | |||
6f5cefa367 | |||
c6f8591600 | |||
cd1fb697f7 | |||
a5415aad99 | |||
037e5c34b6 | |||
699f2b13c9 | |||
c3fbc5cd73 | |||
4a27892ab6 | |||
4adf5547c9 | |||
bdfbd7057f | |||
171d051710 | |||
1ea0ea2ed1 | |||
42774674e5 | |||
57ed0ab66a | |||
49ad74595c | |||
17d52c771f | |||
6792e4702d | |||
446927f587 | |||
567b0bbc2a | |||
3ab3e65eb7 | |||
a0e80fcda7 | |||
731bc1958b | |||
58f2ddde05 | |||
4b0b2626a4 | |||
a05e67c22f | |||
c75608fb1a | |||
068d3430d7 | |||
62793f401e | |||
b8f394b901 | |||
9324b3ec0b | |||
f43fbf19f5 | |||
29cc40be48 | |||
570f6a679b | |||
399af51ccc | |||
94bac00664 | |||
c41dae8d04 | |||
e34df3b656 | |||
3399a3c80f | |||
1c26cb70fa | |||
c79bd4b19f | |||
7743511923 | |||
10833078fa | |||
244a2132fa | |||
f737236abc | |||
b5ce677a5b | |||
4d6dbd511e | |||
7fe4715014 | |||
dd33fdd47d | |||
1986d31461 | |||
a824be53b9 | |||
287082381e | |||
9f40378fce | |||
4f5eb3aa04 | |||
f4113f0632 | |||
8032b83c41 | |||
dfa1306b2d | |||
2f7c7aac8f | |||
46b7c0fc2b | |||
f62c8f0b51 | |||
06e48deb3a | |||
fb6d0317b6 | |||
cfbf052d27 | |||
d240741958 | |||
33486b4e1d | |||
8c3fecb875 | |||
0907fbc474 | |||
b747ff6db2 | |||
220c7fd540 | |||
ed7e172efb | |||
bc0f2d595b | |||
61b33d1613 | |||
c3fa188400 | |||
a1444cf478 | |||
79c94e6af0 | |||
cd8e5737c4 | |||
2429ea8fdd | |||
c8d0a2ddf6 | |||
adc1a5bd77 | |||
d760f9f92c | |||
5c4ef4a529 | |||
35ff408365 | |||
c2b785f54b | |||
1dc740eeae | |||
b3c6138e03 | |||
b59e62f920 | |||
3325706dcb | |||
bc28a84ad4 | |||
bb34bdd653 | |||
ac98bde760 | |||
361b159299 | |||
7344d6f4e0 | |||
8d18e712a1 | |||
116afe78fd | |||
7e3c500b1e | |||
b59eb22082 | |||
ee1da0599c | |||
e5f9a9be83 | |||
e54ff8829a | |||
554997a5c0 | |||
da51070ccd | |||
925051a379 | |||
380ee2683a | |||
bae3b7c2ce |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
.idea
|
||||
target
|
||||
*peachdeploy.sh
|
||||
*vpsdeploy.sh
|
||||
*bindeploy.sh
|
||||
|
1084
Cargo.lock
generated
1084
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,6 @@ members = [
|
||||
"peach-menu",
|
||||
"peach-monitor",
|
||||
"peach-stats",
|
||||
"peach-probe",
|
||||
"peach-jsonrpc-server",
|
||||
"peach-dyndns-updater"
|
||||
]
|
||||
|
@ -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" }
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "peach-config"
|
||||
version = "0.1.10"
|
||||
version = "0.1.17"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
|
||||
edition = "2018"
|
||||
description = "Command line tool for installing, updating and configuring PeachCloud"
|
||||
@ -35,3 +35,5 @@ structopt = "0.3.13"
|
||||
clap = "2.33.3"
|
||||
log = "0.4"
|
||||
lazy_static = "1.4.0"
|
||||
peach-lib = { path = "../peach-lib" }
|
||||
rpassword = "5.0"
|
||||
|
@ -8,7 +8,7 @@ dtparam=i2c_arm=on
|
||||
# Apply device tree overlay to enable pull-up resistors for buttons
|
||||
device_tree_overlay=overlays/mygpio.dtbo
|
||||
|
||||
kernel=vmlinuz-4.19.0-17-arm64
|
||||
kernel=vmlinuz-4.19.0-18-arm64
|
||||
# For details on the initramfs directive, see
|
||||
# https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=10532
|
||||
initramfs initrd.img-4.19.0-17-arm64
|
||||
initramfs initrd.img-4.19.0-18-arm64
|
||||
|
35
peach-config/src/change_password.rs
Normal file
35
peach-config/src/change_password.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,15 +3,12 @@
|
||||
pub const CONF: &str = "/var/lib/peachcloud/conf";
|
||||
|
||||
// List of package names which are installed via apt-get
|
||||
pub const SERVICES: [&str; 11] = [
|
||||
"peach-oled",
|
||||
"peach-network",
|
||||
"peach-stats",
|
||||
pub const SERVICES: [&str; 8] = [
|
||||
"peach-web",
|
||||
"peach-probe",
|
||||
"peach-menu",
|
||||
"peach-buttons",
|
||||
"peach-monitor",
|
||||
"peach-probe",
|
||||
"peach-oled",
|
||||
"peach-dyndns-updater",
|
||||
"peach-go-sbot",
|
||||
"peach-config",
|
||||
|
@ -1,4 +1,5 @@
|
||||
#![allow(clippy::nonstandard_macro_braces)]
|
||||
use peach_lib::error::PeachError;
|
||||
pub use snafu::ResultExt;
|
||||
use snafu::Snafu;
|
||||
|
||||
@ -30,6 +31,10 @@ pub enum PeachConfigError {
|
||||
},
|
||||
#[snafu(display("Error serializing json: {}", source))]
|
||||
SerdeError { source: serde_json::Error },
|
||||
#[snafu(display("Error changing password: {}", source))]
|
||||
ChangePasswordError { source: PeachError },
|
||||
#[snafu(display("Entered passwords did not match. Please try again."))]
|
||||
InvalidPassword,
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for PeachConfigError {
|
||||
|
@ -1,6 +1,8 @@
|
||||
mod change_password;
|
||||
mod constants;
|
||||
mod error;
|
||||
mod generate_manifest;
|
||||
mod set_permissions;
|
||||
mod setup_networking;
|
||||
mod setup_peach;
|
||||
mod setup_peach_deb;
|
||||
@ -12,10 +14,6 @@ use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use structopt::StructOpt;
|
||||
|
||||
use crate::generate_manifest::generate_manifest;
|
||||
use crate::setup_peach::setup_peach;
|
||||
use crate::update::update;
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(
|
||||
name = "peach-config",
|
||||
@ -44,6 +42,14 @@ enum PeachConfig {
|
||||
/// Updates all PeachCloud microservices
|
||||
#[structopt(name = "update")]
|
||||
Update(UpdateOpts),
|
||||
|
||||
/// Changes the password for the peach-web interface
|
||||
#[structopt(name = "changepassword")]
|
||||
ChangePassword(ChangePasswordOpts),
|
||||
|
||||
/// Updates file permissions on PeachCloud device
|
||||
#[structopt(name = "permissions")]
|
||||
SetPermissions,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
@ -76,6 +82,14 @@ pub struct UpdateOpts {
|
||||
list: bool,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub struct ChangePasswordOpts {
|
||||
/// Optional argument to specify password as CLI argument
|
||||
/// if not specified, this command asks for user input for the passwords
|
||||
#[structopt(short, long)]
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
arg_enum! {
|
||||
/// enum options for real-time clock choices
|
||||
#[derive(Debug)]
|
||||
@ -99,28 +113,48 @@ fn main() {
|
||||
if let Some(subcommand) = opt.commands {
|
||||
match subcommand {
|
||||
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(_) => {}
|
||||
Err(err) => {
|
||||
error!("peach-config encountered an error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
PeachConfig::Manifest => match generate_manifest() {
|
||||
PeachConfig::Manifest => match generate_manifest::generate_manifest() {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"peach-config countered an error generating manifest: {}",
|
||||
"peach-config encountered an error generating manifest: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
},
|
||||
PeachConfig::Update(opts) => match update(opts) {
|
||||
PeachConfig::Update(opts) => match update::update(opts) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!("peach-config encountered an error during update: {}", err)
|
||||
}
|
||||
},
|
||||
PeachConfig::ChangePassword(opts) => {
|
||||
match change_password::set_peach_web_password(opts) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"peach-config encountered an error during password update: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
PeachConfig::SetPermissions => match set_permissions::set_permissions() {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"peach-config ecountered an error updating file permissions: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
peach-config/src/set_permissions.rs
Normal file
21
peach-config/src/set_permissions.rs
Normal 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(())
|
||||
}
|
@ -68,6 +68,7 @@ pub fn setup_peach(
|
||||
"libssl-dev",
|
||||
"nginx",
|
||||
"wget",
|
||||
"dnsutils",
|
||||
"-y",
|
||||
])?;
|
||||
|
||||
|
@ -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" }
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "peach-dyndns-updater"
|
||||
version = "0.1.6"
|
||||
version = "0.1.8"
|
||||
authors = ["Max Fowler <mfowler@commoninternet.net>"]
|
||||
edition = "2018"
|
||||
description = "Sytemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate."
|
||||
|
29
peach-dyndns-updater/bindeploy.sh
Executable file
29
peach-dyndns-updater/bindeploy.sh
Executable 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
|
||||
|
@ -1,6 +1,5 @@
|
||||
use log::info;
|
||||
use peach_lib::dyndns_client::dyndns_update_ip;
|
||||
use log::{info};
|
||||
|
||||
|
||||
fn main() {
|
||||
// initalize the logger
|
||||
@ -9,4 +8,4 @@ fn main() {
|
||||
info!("Running peach-dyndns-updater");
|
||||
let result = dyndns_update_ip();
|
||||
info!("result: {:?}", result);
|
||||
}
|
||||
}
|
||||
|
25
peach-jsonrpc-server/Cargo.toml
Normal file
25
peach-jsonrpc-server/Cargo.toml
Normal 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"
|
||||
peach-stats = { path = "../peach-stats", features = ["serde_support"] }
|
||||
serde_json = "1.0.74"
|
||||
|
||||
[dev-dependencies]
|
||||
jsonrpc-test = "18"
|
72
peach-jsonrpc-server/README.md
Normal file
72
peach-jsonrpc-server/README.md
Normal 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
|
58
peach-jsonrpc-server/src/error.rs
Normal file
58
peach-jsonrpc-server/src/error.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use std::fmt;
|
||||
|
||||
use jsonrpc_core::{Error as JsonRpcError, ErrorCode};
|
||||
use serde_json::error::Error as SerdeJsonError;
|
||||
|
||||
use peach_stats::StatsError;
|
||||
|
||||
/// Custom error type encapsulating all possible errors for a JSON-RPC server
|
||||
/// and associated methods.
|
||||
#[derive(Debug)]
|
||||
pub enum JsonRpcServerError {
|
||||
/// Failed to serialize a string from a data structure.
|
||||
Serde(SerdeJsonError),
|
||||
/// 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::Serde(ref source) => {
|
||||
write!(f, "{}", source)
|
||||
}
|
||||
JsonRpcServerError::Stats(ref source) => {
|
||||
write!(f, "{}", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonRpcServerError> for JsonRpcError {
|
||||
fn from(err: JsonRpcServerError) -> Self {
|
||||
match &err {
|
||||
JsonRpcServerError::Serde(source) => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32002),
|
||||
message: format!("{}", source),
|
||||
data: None,
|
||||
},
|
||||
JsonRpcServerError::Stats(source) => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32001),
|
||||
message: format!("{}", source),
|
||||
data: None,
|
||||
},
|
||||
JsonRpcServerError::MissingParameter(source) => source.clone(),
|
||||
JsonRpcServerError::ParseParameter(source) => source.clone(),
|
||||
}
|
||||
}
|
||||
}
|
140
peach-jsonrpc-server/src/lib.rs
Normal file
140
peach-jsonrpc-server/src/lib.rs
Normal file
@ -0,0 +1,140 @@
|
||||
//! # 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 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 = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?;
|
||||
|
||||
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 = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?;
|
||||
|
||||
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 = serde_json::to_string(&disks).map_err(JsonRpcServerError::Serde)?;
|
||||
|
||||
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 = serde_json::to_string(&avg).map_err(JsonRpcServerError::Serde)?;
|
||||
|
||||
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 = serde_json::to_string(&mem).map_err(JsonRpcServerError::Serde)?;
|
||||
|
||||
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 = serde_json::to_string(&uptime).map_err(JsonRpcServerError::Serde)?;
|
||||
|
||||
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"
|
||||
}"#
|
||||
);
|
||||
}
|
||||
}
|
34
peach-jsonrpc-server/src/main.rs
Normal file
34
peach-jsonrpc-server/src/main.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -1,19 +1,19 @@
|
||||
[package]
|
||||
name = "peach-lib"
|
||||
version = "1.3.0"
|
||||
version = "1.3.2"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
chrono = "0.4.19"
|
||||
fslock="0.1.6"
|
||||
jsonrpc-client-core = "0.5"
|
||||
jsonrpc-client-http = "0.5"
|
||||
jsonrpc-core = "8.0.1"
|
||||
log = "0.4"
|
||||
nanorand = "0.6.1"
|
||||
regex = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
rust-crypto = "0.2.36"
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.8"
|
||||
regex = "1"
|
||||
chrono = "0.4.19"
|
||||
rand="0.8.4"
|
||||
fslock="0.1.6"
|
||||
sha3 = "0.10.0"
|
||||
|
@ -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)
|
||||
pub const LOCK_FILE_PATH: &str = "/var/lib/peachcloud/config.lock";
|
||||
|
||||
// default values
|
||||
pub const DEFAULT_DYN_SERVER_ADDRESS: &str = "http://dynserver.dyn.peachcloud.org";
|
||||
pub const DEFAULT_DYN_NAMESERVER: &str = "ns.peachcloud.org";
|
||||
|
||||
// we make use of Serde default values in order to make PeachCloud
|
||||
// robust and keep running even with a not fully complete config.yml
|
||||
// main type which represents all peachcloud configurations
|
||||
@ -29,6 +33,10 @@ pub struct PeachConfig {
|
||||
#[serde(default)]
|
||||
pub dyn_dns_server_address: String,
|
||||
#[serde(default)]
|
||||
pub dyn_use_custom_server: bool,
|
||||
#[serde(default)]
|
||||
pub dyn_nameserver: String,
|
||||
#[serde(default)]
|
||||
pub dyn_tsig_key_path: String,
|
||||
#[serde(default)] // default is false
|
||||
pub dyn_enabled: bool,
|
||||
@ -63,20 +71,19 @@ fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachErro
|
||||
pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
|
||||
let peach_config_exists = std::path::Path::new(YAML_PATH).exists();
|
||||
|
||||
let peach_config: PeachConfig;
|
||||
|
||||
// if this is the first time loading peach_config, we can create a default here
|
||||
if !peach_config_exists {
|
||||
peach_config = PeachConfig {
|
||||
let peach_config: PeachConfig = if !peach_config_exists {
|
||||
PeachConfig {
|
||||
external_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_enabled: false,
|
||||
ssb_admin_ids: Vec::new(),
|
||||
admin_password_hash: "".to_string(),
|
||||
temporary_password_hash: "".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
// otherwise we load peach config from disk
|
||||
else {
|
||||
@ -84,8 +91,8 @@ pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
|
||||
source,
|
||||
path: YAML_PATH.to_string(),
|
||||
})?;
|
||||
peach_config = serde_yaml::from_str(&contents)?;
|
||||
}
|
||||
serde_yaml::from_str(&contents)?
|
||||
};
|
||||
|
||||
Ok(peach_config)
|
||||
}
|
||||
@ -122,6 +129,18 @@ pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_dyndns_server_address() -> Result<String, PeachError> {
|
||||
let peach_config = load_peach_config()?;
|
||||
// if the user is using a custom dyn server then load the address from the config
|
||||
if peach_config.dyn_use_custom_server {
|
||||
Ok(peach_config.dyn_dns_server_address)
|
||||
}
|
||||
// otherwise hardcode the address
|
||||
else {
|
||||
Ok(DEFAULT_DYN_SERVER_ADDRESS.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result<PeachConfig, PeachError> {
|
||||
let mut peach_config = load_peach_config()?;
|
||||
peach_config.dyn_enabled = enabled_value;
|
||||
|
@ -9,13 +9,8 @@
|
||||
//!
|
||||
//! The domain for dyndns updates is stored in /var/lib/peachcloud/config.yml
|
||||
//! The tsig key for authenticating the updates is stored in /var/lib/peachcloud/peach-dyndns/tsig.key
|
||||
use std::{
|
||||
fs,
|
||||
fs::OpenOptions,
|
||||
io::Write,
|
||||
process::{Command, Stdio},
|
||||
str::FromStr,
|
||||
};
|
||||
use std::ffi::OsStr;
|
||||
use std::{fs, fs::OpenOptions, io::Write, process::Command, str::FromStr};
|
||||
|
||||
use chrono::prelude::*;
|
||||
use jsonrpc_client_core::{expand_params, jsonrpc_client};
|
||||
@ -23,13 +18,10 @@ use jsonrpc_client_http::HttpTransport;
|
||||
use log::{debug, info};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::{
|
||||
config_manager::{load_peach_config, set_peach_dyndns_config},
|
||||
error::PeachError,
|
||||
};
|
||||
use crate::config_manager::get_dyndns_server_address;
|
||||
use crate::{config_manager, error::PeachError};
|
||||
|
||||
/// constants for dyndns configuration
|
||||
pub const PEACH_DYNDNS_URL: &str = "http://dynserver.dyn.peachcloud.org";
|
||||
pub const TSIG_KEY_PATH: &str = "/var/lib/peachcloud/peach-dyndns/tsig.key";
|
||||
pub const PEACH_DYNDNS_CONFIG_PATH: &str = "/var/lib/peachcloud/peach-dyndns";
|
||||
pub const DYNDNS_LOG_PATH: &str = "/var/lib/peachcloud/peach-dyndns/latest_result.log";
|
||||
@ -62,9 +54,10 @@ pub fn save_dyndns_key(key: &str) -> Result<(), PeachError> {
|
||||
pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError> {
|
||||
debug!("Creating HTTP transport for dyndns client.");
|
||||
let transport = HttpTransport::new().standalone()?;
|
||||
let http_server = PEACH_DYNDNS_URL;
|
||||
debug!("Creating HTTP transport handle on {}.", http_server);
|
||||
let transport_handle = transport.handle(http_server)?;
|
||||
let http_server = get_dyndns_server_address()?;
|
||||
info!("Using dyndns http server address: {:?}", http_server);
|
||||
debug!("Creating HTTP transport handle on {}.", &http_server);
|
||||
let transport_handle = transport.handle(&http_server)?;
|
||||
info!("Creating client for peach-dyndns service.");
|
||||
let mut client = PeachDynDnsClient::new(transport_handle);
|
||||
|
||||
@ -73,7 +66,8 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError>
|
||||
// save new TSIG key
|
||||
save_dyndns_key(&key)?;
|
||||
// save new configuration values
|
||||
let set_config_result = set_peach_dyndns_config(domain, PEACH_DYNDNS_URL, TSIG_KEY_PATH, true);
|
||||
let set_config_result =
|
||||
config_manager::set_peach_dyndns_config(domain, &http_server, TSIG_KEY_PATH, true);
|
||||
match set_config_result {
|
||||
Ok(_) => {
|
||||
let response = "success".to_string();
|
||||
@ -87,9 +81,9 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError>
|
||||
pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError> {
|
||||
debug!("Creating HTTP transport for dyndns client.");
|
||||
let transport = HttpTransport::new().standalone()?;
|
||||
let http_server = PEACH_DYNDNS_URL;
|
||||
debug!("Creating HTTP transport handle on {}.", http_server);
|
||||
let transport_handle = transport.handle(http_server)?;
|
||||
let http_server = get_dyndns_server_address()?;
|
||||
debug!("Creating HTTP transport handle on {}.", &http_server);
|
||||
let transport_handle = transport.handle(&http_server)?;
|
||||
info!("Creating client for peach_network service.");
|
||||
let mut client = PeachDynDnsClient::new(transport_handle);
|
||||
|
||||
@ -113,31 +107,31 @@ fn get_public_ip_address() -> Result<String, PeachError> {
|
||||
/// Reads dyndns configurations from config.yml
|
||||
/// and then uses nsupdate to update the IP address for the configured domain
|
||||
pub fn dyndns_update_ip() -> Result<bool, PeachError> {
|
||||
info!("Running dyndns_update_ip");
|
||||
let peach_config = load_peach_config()?;
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
info!(
|
||||
"Using config:
|
||||
dyn_tsig_key_path: {:?}
|
||||
dyn_domain: {:?}
|
||||
dyn_dns_server_address: {:?}
|
||||
dyn_enabled: {:?}
|
||||
dyn_nameserver: {:?}
|
||||
",
|
||||
peach_config.dyn_tsig_key_path,
|
||||
peach_config.dyn_domain,
|
||||
peach_config.dyn_dns_server_address,
|
||||
peach_config.dyn_enabled,
|
||||
peach_config.dyn_nameserver,
|
||||
);
|
||||
if !peach_config.dyn_enabled {
|
||||
info!("dyndns is not enabled, not updating");
|
||||
Ok(false)
|
||||
} else {
|
||||
// call nsupdate passing appropriate configs
|
||||
let mut nsupdate_command = Command::new("/usr/bin/nsupdate")
|
||||
let mut nsupdate_command = Command::new("/usr/bin/nsupdate");
|
||||
nsupdate_command
|
||||
.arg("-k")
|
||||
.arg(&peach_config.dyn_tsig_key_path)
|
||||
.arg("-v")
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()?;
|
||||
.arg("-v");
|
||||
// pass nsupdate commands via stdin
|
||||
let public_ip_address = get_public_ip_address()?;
|
||||
info!("found public ip address: {}", public_ip_address);
|
||||
@ -148,20 +142,20 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
|
||||
update delete {DOMAIN} A
|
||||
update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS}
|
||||
send",
|
||||
NAMESERVER = "ns.peachcloud.org",
|
||||
NAMESERVER = peach_config.dyn_nameserver,
|
||||
ZONE = peach_config.dyn_domain,
|
||||
DOMAIN = peach_config.dyn_domain,
|
||||
PUBLIC_IP_ADDRESS = public_ip_address,
|
||||
);
|
||||
let mut nsupdate_stdin = nsupdate_command.stdin.take().ok_or(PeachError::NsUpdate {
|
||||
msg: "unable to capture stdin handle for `nsupdate` command".to_string(),
|
||||
})?;
|
||||
write!(nsupdate_stdin, "{}", ns_commands).map_err(|source| PeachError::Write {
|
||||
source,
|
||||
path: peach_config.dyn_tsig_key_path.to_string(),
|
||||
})?;
|
||||
let nsupdate_output = nsupdate_command.wait_with_output()?;
|
||||
info!("nsupdate output: {:?}", nsupdate_output);
|
||||
info!("ns_commands: {:?}", ns_commands);
|
||||
info!("creating nsupdate temp file");
|
||||
let temp_file_path = "/var/lib/peachcloud/nsupdate.sh";
|
||||
// write ns_commands to temp_file
|
||||
fs::write(temp_file_path, ns_commands)?;
|
||||
nsupdate_command.arg(temp_file_path);
|
||||
let nsupdate_output = nsupdate_command.output()?;
|
||||
let args: Vec<&OsStr> = nsupdate_command.get_args().collect();
|
||||
info!("nsupdate command: {:?}", args);
|
||||
// We only return a successful result if nsupdate was successful
|
||||
if nsupdate_output.status.success() {
|
||||
info!("nsupdate succeeded, returning ok");
|
||||
@ -204,7 +198,7 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
|
||||
})?;
|
||||
// replace newline if found
|
||||
// TODO: maybe we can use `.trim()` instead
|
||||
let contents = contents.replace("\n", "");
|
||||
let contents = contents.replace('\n', "");
|
||||
// TODO: consider adding additional context?
|
||||
let time_ran_dt = DateTime::parse_from_rfc3339(&contents).map_err(|source| {
|
||||
PeachError::ParseDateTime {
|
||||
@ -223,20 +217,15 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
|
||||
/// and has successfully run recently (in the last six minutes)
|
||||
pub fn is_dns_updater_online() -> Result<bool, PeachError> {
|
||||
// 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;
|
||||
// then check if it has successfully run within the last 6 minutes (60*6 seconds)
|
||||
let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?;
|
||||
let ran_recently: bool;
|
||||
match num_seconds_since_successful_update {
|
||||
Some(seconds) => {
|
||||
ran_recently = seconds < (60 * 6);
|
||||
}
|
||||
let ran_recently: bool = match num_seconds_since_successful_update {
|
||||
Some(seconds) => seconds < (60 * 6),
|
||||
// if the value is None, then the last time it ran successfully is unknown
|
||||
None => {
|
||||
ran_recently = false;
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
};
|
||||
// debug log
|
||||
info!("is_dyndns_enabled: {:?}", is_enabled);
|
||||
info!("dyndns_ran_recently: {:?}", ran_recently);
|
||||
@ -258,11 +247,10 @@ pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
// helper function which checks if a dyndns domain is new
|
||||
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> bool {
|
||||
// TODO: return `Result<bool, PeachError>` and replace `unwrap` with `?` operator
|
||||
let peach_config = load_peach_config().unwrap();
|
||||
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> Result<bool, PeachError> {
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
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 {
|
||||
|
@ -1,7 +1,5 @@
|
||||
use std::iter;
|
||||
|
||||
use crypto::{digest::Digest, sha3::Sha3};
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use nanorand::{Rng, WyRand};
|
||||
use sha3::{Digest, Sha3_256};
|
||||
|
||||
use crate::{config_manager, error::PeachError, sbot_client};
|
||||
|
||||
@ -9,7 +7,7 @@ use crate::{config_manager, error::PeachError, sbot_client};
|
||||
/// and returns Err if the supplied password is incorrect.
|
||||
pub fn verify_password(password: &str) -> Result<(), PeachError> {
|
||||
let real_admin_password_hash = config_manager::get_admin_password_hash()?;
|
||||
let password_hash = hash_password(&password.to_string());
|
||||
let password_hash = hash_password(password);
|
||||
if real_admin_password_hash == password_hash {
|
||||
Ok(())
|
||||
} else {
|
||||
@ -31,7 +29,7 @@ pub fn validate_new_passwords(new_password1: &str, new_password2: &str) -> Resul
|
||||
|
||||
/// Sets a new password for the admin user
|
||||
pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
|
||||
let new_password_hash = hash_password(&new_password.to_string());
|
||||
let new_password_hash = hash_password(new_password);
|
||||
config_manager::set_admin_password_hash(&new_password_hash)?;
|
||||
|
||||
Ok(())
|
||||
@ -39,15 +37,19 @@ pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
|
||||
|
||||
/// Creates a hash from a password string
|
||||
pub fn hash_password(password: &str) -> String {
|
||||
let mut hasher = Sha3::sha3_256();
|
||||
hasher.input_str(password);
|
||||
hasher.result_str()
|
||||
let mut hasher = Sha3_256::new();
|
||||
// write input message
|
||||
hasher.update(password);
|
||||
// read hash digest
|
||||
let result = hasher.finalize();
|
||||
// convert `u8` to `String`
|
||||
result[0].to_string()
|
||||
}
|
||||
|
||||
/// Sets a new temporary password for the admin user
|
||||
/// which can be used to reset the permanent password
|
||||
pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> {
|
||||
let new_password_hash = hash_password(&new_password.to_string());
|
||||
let new_password_hash = hash_password(new_password);
|
||||
config_manager::set_temporary_password_hash(&new_password_hash)?;
|
||||
|
||||
Ok(())
|
||||
@ -57,7 +59,7 @@ pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError>
|
||||
/// and returns Err if the supplied temp_password is incorrect
|
||||
pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> {
|
||||
let temporary_admin_password_hash = config_manager::get_temporary_password_hash()?;
|
||||
let password_hash = hash_password(&password.to_string());
|
||||
let password_hash = hash_password(password);
|
||||
if temporary_admin_password_hash == password_hash {
|
||||
Ok(())
|
||||
} else {
|
||||
@ -68,13 +70,10 @@ pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> {
|
||||
/// Generates a temporary password and sends it via ssb dm
|
||||
/// to the ssb id configured to be the admin of the peachcloud device
|
||||
pub fn send_password_reset() -> Result<(), PeachError> {
|
||||
// first generate a new random password of ascii characters
|
||||
let mut rng = thread_rng();
|
||||
let temporary_password: String = iter::repeat(())
|
||||
.map(|()| rng.sample(Alphanumeric))
|
||||
.map(char::from)
|
||||
.take(10)
|
||||
.collect();
|
||||
// initialise random number generator
|
||||
let mut rng = WyRand::new();
|
||||
// generate a new password of random numbers
|
||||
let temporary_password = rng.generate::<u64>().to_string();
|
||||
// save this string as a new temporary password
|
||||
set_new_temporary_password(&temporary_password)?;
|
||||
let domain = config_manager::get_peachcloud_domain()?;
|
||||
|
@ -67,7 +67,7 @@ pub fn create_invite(uses: i32) -> Result<String, PeachError> {
|
||||
.arg(uses.to_string())
|
||||
.output()?;
|
||||
let text_output = std::str::from_utf8(&output.stdout)?;
|
||||
let output = text_output.replace("\n", "");
|
||||
let output = text_output.replace('\n', "");
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
|
@ -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" }
|
@ -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" }
|
@ -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" }
|
@ -1,44 +1,32 @@
|
||||
[package]
|
||||
name = "peach-network"
|
||||
version = "0.2.12"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
|
||||
edition = "2018"
|
||||
description = "Query and configure network interfaces using JSON-RPC over HTTP."
|
||||
version = "0.4.2"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
edition = "2021"
|
||||
description = "Query and configure network interfaces."
|
||||
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"
|
||||
license = "AGPL-3.0-only"
|
||||
license = "LGPL-3.0-only"
|
||||
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]
|
||||
travis-ci = { repository = "peachcloud/peach-network", branch = "master" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.6"
|
||||
failure = "0.1"
|
||||
get_if_addrs = "0.5.3"
|
||||
jsonrpc-core = "11"
|
||||
jsonrpc-http-server = "11"
|
||||
log = "0.4"
|
||||
probes = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
snafu = "0.6"
|
||||
miniserde = { version = "0.1.15", optional = true }
|
||||
probes = "0.4.1"
|
||||
serde = { version = "1.0.130", features = ["derive"], optional = true }
|
||||
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]
|
||||
jsonrpc-test = "11"
|
||||
[features]
|
||||
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"]
|
||||
|
@ -1,178 +1,46 @@
|
||||
# peach-network
|
||||
|
||||
[](https://travis-ci.com/peachcloud/peach-network) 
|
||||

|
||||
|
||||
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).
|
||||
|
||||
_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 |
|
||||
| --- | --- | --- |
|
||||
| `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 |
|
||||
```rust
|
||||
use peach_network::{network, NetworkError};
|
||||
|
||||
Methods for **modifying state**:
|
||||
fn main() -> Result<(), NetworkError> {
|
||||
let wlan_iface = "wlan0";
|
||||
|
||||
| Method | Parameters | Description |
|
||||
| --- | --- | --- |
|
||||
| `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` |
|
||||
let wlan_ip = network::ip(wlan_iface)?;
|
||||
let wlan_ssid = network::ssid(wlan_iface)?;
|
||||
|
||||
### 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`.
|
||||
|
||||
### 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
|
||||
LGPL-3.0.
|
||||
|
@ -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
|
@ -1,351 +1,367 @@
|
||||
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 serde_json::error::Error as SerdeError;
|
||||
use snafu::Snafu;
|
||||
use regex::Error as RegexError;
|
||||
use wpactrl::WpaError;
|
||||
|
||||
pub type BoxError = Box<dyn error::Error>;
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
#[snafu(visibility(pub(crate)))]
|
||||
/// Custom error type encapsulating all possible errors when querying
|
||||
/// network interfaces and modifying their state.
|
||||
#[derive(Debug)]
|
||||
pub enum NetworkError {
|
||||
#[snafu(display("{}", err_msg))]
|
||||
ActivateAp { err_msg: String },
|
||||
|
||||
#[snafu(display("{}", err_msg))]
|
||||
ActivateClient { err_msg: String },
|
||||
|
||||
#[snafu(display("Failed to add network for {}", ssid))]
|
||||
Add { ssid: String },
|
||||
|
||||
#[snafu(display("Failed to retrieve state for interface: {}", iface))]
|
||||
NoState { iface: String, source: io::Error },
|
||||
|
||||
#[snafu(display("Failed to disable network {} for interface: {}", id, iface))]
|
||||
Disable { id: String, iface: String },
|
||||
|
||||
#[snafu(display("Failed to disconnect {}", iface))]
|
||||
Disconnect { iface: String },
|
||||
|
||||
#[snafu(display("Failed to generate wpa passphrase for {}: {}", ssid, source))]
|
||||
GenWpaPassphrase { ssid: String, source: io::Error },
|
||||
|
||||
#[snafu(display("Failed to generate wpa passphrase for {}: {}", ssid, err_msg))]
|
||||
GenWpaPassphraseWarning { ssid: String, err_msg: String },
|
||||
|
||||
#[snafu(display("No ID found for {} on interface: {}", ssid, iface))]
|
||||
Id { ssid: String, iface: String },
|
||||
|
||||
#[snafu(display("Could not access IP address for interface: {}", iface))]
|
||||
NoIp { iface: String, source: io::Error },
|
||||
|
||||
#[snafu(display("Could not find RSSI for interface: {}", iface))]
|
||||
Rssi { iface: String },
|
||||
|
||||
#[snafu(display("Could not find signal quality (%) for interface: {}", iface))]
|
||||
RssiPercent { iface: String },
|
||||
|
||||
#[snafu(display("Could not find SSID for interface: {}", iface))]
|
||||
Ssid { iface: String },
|
||||
|
||||
#[snafu(display("No state found for interface: {}", iface))]
|
||||
State { iface: String },
|
||||
|
||||
#[snafu(display("No status found for interface: {}", iface))]
|
||||
Status { iface: String },
|
||||
|
||||
#[snafu(display("Could not find network traffic for interface: {}", iface))]
|
||||
Traffic { iface: String },
|
||||
|
||||
#[snafu(display("No saved networks found for default interface"))]
|
||||
/// Failed to add network.
|
||||
Add {
|
||||
/// SSID.
|
||||
ssid: String,
|
||||
},
|
||||
/// Failed to retrieve network state.
|
||||
NoState {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
/// Underlying error source.
|
||||
source: IoError,
|
||||
},
|
||||
/// Failed to disable network.
|
||||
Disable {
|
||||
/// ID.
|
||||
id: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to disconnect interface.
|
||||
Disconnect {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to execute wpa_passphrase command.
|
||||
GenWpaPassphrase {
|
||||
/// SSID.
|
||||
ssid: String,
|
||||
/// Underlying error source.
|
||||
source: IoError,
|
||||
},
|
||||
/// Failed to successfully generate wpa passphrase.
|
||||
GenWpaPassphraseWarning {
|
||||
/// SSID.
|
||||
ssid: String,
|
||||
/// Error message describing context.
|
||||
err_msg: String,
|
||||
},
|
||||
/// Failed to retrieve ID for the given SSID and interface.
|
||||
Id {
|
||||
/// SSID.
|
||||
ssid: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve IP address.
|
||||
NoIp {
|
||||
/// Inteface.
|
||||
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,
|
||||
|
||||
#[snafu(display("No networks found in range of interface: {}", iface))]
|
||||
AvailableNetworks { iface: String },
|
||||
|
||||
#[snafu(display("Missing expected parameters: {}", e))]
|
||||
MissingParams { e: Error },
|
||||
|
||||
#[snafu(display("Failed to set new password for network {} on {}", id, iface))]
|
||||
Modify { id: String, iface: String },
|
||||
|
||||
#[snafu(display("No IP found for interface: {}", iface))]
|
||||
Ip { iface: String },
|
||||
|
||||
#[snafu(display("Failed to parse integer from string for RSSI value: {}", source))]
|
||||
ParseString { source: std::num::ParseIntError },
|
||||
|
||||
#[snafu(display(
|
||||
"Failed to retrieve network traffic measurement for {}: {}",
|
||||
iface,
|
||||
source
|
||||
))]
|
||||
NoTraffic { iface: String, source: ProbeError },
|
||||
|
||||
#[snafu(display("Failed to reassociate with WiFi network for interface: {}", iface))]
|
||||
Reassociate { iface: String },
|
||||
|
||||
#[snafu(display("Failed to force reread of wpa_supplicant configuration file"))]
|
||||
/// No networks found in range.
|
||||
AvailableNetworks {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to set new password.
|
||||
Modify {
|
||||
/// ID.
|
||||
id: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve IP address.
|
||||
Ip {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to parse integer from string.
|
||||
ParseInt(ParseIntError),
|
||||
/// Failed to retrieve network traffic measurement.
|
||||
NoTraffic {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
/// Underlying error source.
|
||||
source: ProbeError,
|
||||
},
|
||||
/// Failed to reassociate with WiFi network.
|
||||
Reassociate {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to force reread of wpa_supplicant configuration file.
|
||||
Reconfigure,
|
||||
|
||||
#[snafu(display("Failed to reconnect with WiFi network for interface: {}", iface))]
|
||||
Reconnect { iface: String },
|
||||
|
||||
#[snafu(display("Regex command failed"))]
|
||||
Regex { source: regex::Error },
|
||||
|
||||
#[snafu(display("Failed to delete network {} for interface: {}", id, iface))]
|
||||
Delete { id: String, iface: String },
|
||||
|
||||
#[snafu(display("Failed to retrieve state of wlan0 service: {}", source))]
|
||||
WlanState { source: io::Error },
|
||||
|
||||
#[snafu(display("Failed to retrieve connection state of wlan0 interface: {}", source))]
|
||||
WlanOperstate { source: io::Error },
|
||||
|
||||
#[snafu(display("Failed to save configuration changes to file"))]
|
||||
Save,
|
||||
|
||||
#[snafu(display("Failed to connect to network {} for interface: {}", id, iface))]
|
||||
Connect { id: String, iface: String },
|
||||
|
||||
#[snafu(display("Failed to start ap0 service: {}", source))]
|
||||
StartAp0 { source: io::Error },
|
||||
|
||||
#[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 reconnect with WiFi network.
|
||||
Reconnect {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
|
||||
#[snafu(display("Request to wpasupplicant via wpactrl failed"))]
|
||||
WpaCtrlRequest {
|
||||
#[snafu(source(from(failure::Error, std::convert::Into::into)))]
|
||||
source: BoxError,
|
||||
/// Failed to execute Regex command.
|
||||
Regex(RegexError),
|
||||
/// Failed to delete network.
|
||||
Delete {
|
||||
/// ID.
|
||||
id: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve state of wlan0 service.
|
||||
WlanState(IoError),
|
||||
/// Failed to retrieve connection state of wlan0 interface.
|
||||
WlanOperstate(IoError),
|
||||
/// Failed to save wpa_supplicant configuration changes to file.
|
||||
Save(IoError),
|
||||
/// Failed to connect to network.
|
||||
Connect {
|
||||
/// ID.
|
||||
id: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to start systemctl service for a network interface.
|
||||
StartInterface {
|
||||
/// Underlying error source.
|
||||
source: IoError,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to execute wpa-ctrl command.
|
||||
WpaCtrl(WpaError),
|
||||
}
|
||||
|
||||
impl From<NetworkError> for Error {
|
||||
fn from(err: NetworkError) -> Self {
|
||||
match &err {
|
||||
NetworkError::ActivateAp { err_msg } => Error {
|
||||
code: ErrorCode::ServerError(-32015),
|
||||
message: err_msg.to_string(),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::ActivateClient { err_msg } => Error {
|
||||
code: ErrorCode::ServerError(-32017),
|
||||
message: err_msg.to_string(),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Add { ssid } => Error {
|
||||
code: ErrorCode::ServerError(-32000),
|
||||
message: format!("Failed to add network for {}", ssid),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::NoState { iface, source } => Error {
|
||||
code: ErrorCode::ServerError(-32022),
|
||||
message: format!(
|
||||
"Failed to retrieve interface state for {}: {}",
|
||||
iface, source
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Disable { id, iface } => Error {
|
||||
code: ErrorCode::ServerError(-32029),
|
||||
message: format!("Failed to disable network {} for {}", id, iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Disconnect { iface } => Error {
|
||||
code: ErrorCode::ServerError(-32032),
|
||||
message: format!("Failed to disconnect {}", iface),
|
||||
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::error::Error for NetworkError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match *self {
|
||||
NetworkError::Add { .. } => None,
|
||||
NetworkError::NoState { ref source, .. } => Some(source),
|
||||
NetworkError::Disable { .. } => None,
|
||||
NetworkError::Disconnect { .. } => None,
|
||||
NetworkError::GenWpaPassphrase { ref source, .. } => Some(source),
|
||||
NetworkError::GenWpaPassphraseWarning { .. } => None,
|
||||
NetworkError::Id { .. } => None,
|
||||
NetworkError::NoIp { ref source, .. } => Some(source),
|
||||
NetworkError::Rssi { .. } => None,
|
||||
NetworkError::RssiPercent { .. } => None,
|
||||
NetworkError::Ssid { .. } => None,
|
||||
NetworkError::State { .. } => None,
|
||||
NetworkError::Status { .. } => None,
|
||||
NetworkError::Traffic { .. } => None,
|
||||
NetworkError::SavedNetworks => None,
|
||||
NetworkError::AvailableNetworks { .. } => None,
|
||||
NetworkError::Modify { .. } => None,
|
||||
NetworkError::Ip { .. } => None,
|
||||
NetworkError::ParseInt(ref source) => Some(source),
|
||||
NetworkError::NoTraffic { ref source, .. } => Some(source),
|
||||
NetworkError::Reassociate { .. } => None,
|
||||
NetworkError::Reconfigure { .. } => None,
|
||||
NetworkError::Reconnect { .. } => None,
|
||||
NetworkError::Regex(ref source) => Some(source),
|
||||
NetworkError::Delete { .. } => None,
|
||||
NetworkError::WlanState(ref source) => Some(source),
|
||||
NetworkError::WlanOperstate(ref source) => Some(source),
|
||||
NetworkError::Save(ref source) => Some(source),
|
||||
NetworkError::Connect { .. } => None,
|
||||
NetworkError::StartInterface { ref source, .. } => Some(source),
|
||||
NetworkError::WpaCtrl(ref source) => Some(source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(ref source) => write!(
|
||||
f,
|
||||
"Failed to save configuration changes to file: {}",
|
||||
source
|
||||
),
|
||||
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
@ -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);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
//! 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,
|
||||
//! many of which are ordinarily executed using `wpa_cli` (a WPA command line
|
||||
//! client).
|
||||
@ -11,8 +11,8 @@
|
||||
//! Switching between client mode and access point mode is achieved by making
|
||||
//! system calls to systemd (via `systemctl`). Further networking functionality
|
||||
//! 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::{
|
||||
fs::OpenOptions,
|
||||
io::prelude::*,
|
||||
@ -21,72 +21,58 @@ use std::{
|
||||
str,
|
||||
};
|
||||
|
||||
use crate::error::{
|
||||
GenWpaPassphrase, NetworkError, NoIp, NoState, NoTraffic, ParseString, SerdeSerialize,
|
||||
StartAp0, StartWlan0, WlanState, WpaCtrlOpen, WpaCtrlRequest,
|
||||
};
|
||||
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;
|
||||
|
||||
/// 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.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct Scan {
|
||||
/// Frequency.
|
||||
pub frequency: String,
|
||||
/// Protocol.
|
||||
pub protocol: String,
|
||||
/// Signal strength.
|
||||
pub signal_level: String,
|
||||
/// SSID.
|
||||
pub ssid: String,
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// MAC address.
|
||||
pub address: Option<String>,
|
||||
/// Basic Service Set Identifier (BSSID).
|
||||
pub bssid: Option<String>,
|
||||
/// Frequency.
|
||||
pub freq: Option<String>,
|
||||
/// Group cipher.
|
||||
pub group_cipher: Option<String>,
|
||||
/// Local ID.
|
||||
pub id: Option<String>,
|
||||
/// IP address.
|
||||
pub ip_address: Option<String>,
|
||||
/// Key management.
|
||||
pub key_mgmt: Option<String>,
|
||||
/// Mode.
|
||||
pub mode: Option<String>,
|
||||
/// Pairwise cipher.
|
||||
pub pairwise_cipher: Option<String>,
|
||||
/// SSID.
|
||||
pub ssid: Option<String>,
|
||||
/// WPA state.
|
||||
pub wpa_state: Option<String>,
|
||||
}
|
||||
|
||||
@ -109,19 +95,16 @@ impl Status {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// Total bytes received.
|
||||
pub received: u64,
|
||||
/// Total bytes transmitted.
|
||||
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 */
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// If the scan results include one or more access points for the given network
|
||||
/// interface, an `Ok` `Result` type is returned containing `Some(String)` -
|
||||
/// where `String` is a serialized vector of `Scan` structs containing
|
||||
/// data for the in-range access points. If no access points are 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
|
||||
/// 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> {
|
||||
/// interface, an `Ok` `Result` type is returned containing `Some(Vec<Scan>)`.
|
||||
/// The vector of `Scan` structs contains data for the in-range access points.
|
||||
/// If no access points are found, a `None` type is returned in the `Result`.
|
||||
/// In the event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
wpa.request("SCAN").context(WpaCtrlRequest)?;
|
||||
let networks = wpa.request("SCAN_RESULTS").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
wpa.request("SCAN")?;
|
||||
let networks = wpa.request("SCAN_RESULTS")?;
|
||||
let mut scan = Vec::new();
|
||||
for network in networks.lines() {
|
||||
let v: Vec<&str> = network.split('\t').collect();
|
||||
@ -162,7 +138,7 @@ pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
// we only want to return the auth / crypto flags
|
||||
if flags_vec[0] != "[ESS]" {
|
||||
// parse auth / crypto flag and assign it to protocol
|
||||
protocol.push_str(flags_vec[0].replace("[", "").replace("]", "").as_str());
|
||||
protocol.push_str(flags_vec[0].replace('[', "").replace(']', "").as_str());
|
||||
}
|
||||
let ssid = v[4].to_string();
|
||||
let response = Scan {
|
||||
@ -178,8 +154,7 @@ pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
if scan.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let results = serde_json::to_string(&scan).context(SerdeSerialize)?;
|
||||
Ok(Some(results))
|
||||
Ok(Some(scan))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
/// containing `Some(String)` - where `String` is the network identifier.
|
||||
/// 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
|
||||
/// `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let networks = wpa.request("LIST_NETWORKS").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let networks = wpa.request("LIST_NETWORKS")?;
|
||||
let mut id = Vec::new();
|
||||
for network in networks.lines() {
|
||||
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`
|
||||
/// 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`. The `NetworkError` is then enumerated to a
|
||||
/// specific error type and an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
/// returned in the `Result`.
|
||||
pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
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
|
||||
.iter()
|
||||
.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
|
||||
/// 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`. The `NetworkError` is then enumerated to a specific error type and
|
||||
/// an appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// `Result`.
|
||||
pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let status = wpa.request("SIGNAL_POLL").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let status = wpa.request("SIGNAL_POLL")?;
|
||||
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
|
||||
|
||||
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
|
||||
/// 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
|
||||
/// the `Result`. The `NetworkError` is then enumerated to a specific error type
|
||||
/// and an appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// the `Result`.
|
||||
pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let status = wpa.request("SIGNAL_POLL").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let status = wpa.request("SIGNAL_POLL")?;
|
||||
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
|
||||
|
||||
match rssi {
|
||||
Some(rssi) => {
|
||||
// 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
|
||||
let quality_percent = 2 * (rssi_parsed + 100);
|
||||
// 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
|
||||
/// more access points, an `Ok` `Result` type is returned containing
|
||||
/// `Some(String)` - where `String` is a serialized vector of `Network` structs
|
||||
/// containing the SSIDs of all saved networks. If no network credentials are
|
||||
/// 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
|
||||
/// enumerated to a specific error type and an appropriate JSON RPC response is
|
||||
/// sent to the caller.
|
||||
///
|
||||
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)?;
|
||||
/// `Some(Vec<Network>)`. The vector of `Network` structs contains the SSIDs
|
||||
/// of all saved networks. If no network credentials are found, a `None` type
|
||||
/// is returned in the `Result`. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`.
|
||||
pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
||||
let networks = wpa.request("LIST_NETWORKS")?;
|
||||
let mut ssids = Vec::new();
|
||||
for network in networks.lines() {
|
||||
let v: Vec<&str> = network.split('\t').collect();
|
||||
let len = v.len();
|
||||
if len > 1 {
|
||||
let ssid = v[1].trim().to_string();
|
||||
let response = Network { ssid };
|
||||
ssids.push(response)
|
||||
ssids.push(ssid)
|
||||
}
|
||||
}
|
||||
|
||||
if ssids.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let results = serde_json::to_string(&ssids).context(SerdeSerialize)?;
|
||||
Ok(Some(results))
|
||||
Ok(Some(ssids))
|
||||
}
|
||||
}
|
||||
|
||||
@ -366,17 +320,11 @@ pub fn saved_networks() -> Result<Option<String>, NetworkError> {
|
||||
/// 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 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 an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
/// returned in the `Result`.
|
||||
pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let status = wpa.request("STATUS").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let status = wpa.request("STATUS")?;
|
||||
|
||||
// pass the regex pattern and status output to the regex finder
|
||||
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
|
||||
/// 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`. The `NetworkError` is then enumerated to a specific error type and
|
||||
/// an appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// `Result`.
|
||||
pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
// construct the interface operstate path
|
||||
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")
|
||||
.arg(iface_path)
|
||||
.output()
|
||||
.context(NoState { iface })?;
|
||||
.map_err(|source| NetworkError::NoState {
|
||||
iface: iface.to_string(),
|
||||
source,
|
||||
})?;
|
||||
if !output.stdout.is_empty() {
|
||||
// unwrap the command result and convert to String
|
||||
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`
|
||||
/// 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,
|
||||
/// 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.
|
||||
///
|
||||
/// a `NetworkError` is returned in the `Result`.
|
||||
pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let wpa_status = wpa.request("STATUS").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let wpa_status = wpa.request("STATUS")?;
|
||||
|
||||
// pass the regex pattern and status output to the regex finder
|
||||
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
|
||||
///
|
||||
/// If the network traffic statistics are found for the given interface, an `Ok`
|
||||
/// `Result` type is returned containing `Some(String)` - where `String` is a
|
||||
/// serialized `Traffic` `struct` with fields for received and transmitted
|
||||
/// network data statistics. If network traffic statistics are not found for the
|
||||
/// given interface, 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 enumerated to a specific error type and an appropriate JSON RPC
|
||||
/// response is sent to the caller.
|
||||
///
|
||||
pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let network = network::read().context(NoTraffic { iface })?;
|
||||
/// `Result` type is returned containing `Some(Traffic)`. The `Traffic` `struct`
|
||||
/// includes fields for received and transmitted network data statistics. If
|
||||
/// network traffic statistics are not found for the given interface, a `None`
|
||||
/// type is returned in the `Result`. In the event of an error, a `NetworkError`
|
||||
/// is returned in the `Result`.
|
||||
pub fn traffic(iface: &str) -> Result<Option<Traffic>, NetworkError> {
|
||||
let network = network::read().map_err(|source| NetworkError::NoTraffic {
|
||||
iface: iface.to_string(),
|
||||
source,
|
||||
})?;
|
||||
// iterate through interfaces returned in network data
|
||||
for (interface, traffic) in network.interfaces {
|
||||
if interface == iface {
|
||||
@ -505,9 +448,7 @@ pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
received,
|
||||
transmitted,
|
||||
};
|
||||
// TODO: add test for SerdeSerialize error
|
||||
let t = serde_json::to_string(&traffic).context(SerdeSerialize)?;
|
||||
return Ok(Some(t));
|
||||
return Ok(Some(traffic));
|
||||
}
|
||||
}
|
||||
|
||||
@ -516,42 +457,25 @@ pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
|
||||
/* SET - Methods for modifying state */
|
||||
|
||||
/// Activate wireless access point.
|
||||
/// Start network interface service.
|
||||
///
|
||||
/// A `systemctl `command is invoked which starts the `ap0` 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_ap() -> Result<(), NetworkError> {
|
||||
// start the ap0 interface service
|
||||
/// A `systemctl `command is invoked which starts the service for the given
|
||||
/// network interface. If the command executes successfully, an `Ok` `Result`
|
||||
/// type is returned. In the event of an error, a `NetworkError` is returned
|
||||
/// in the `Result`.
|
||||
pub fn start_iface_service(iface: &str) -> Result<(), NetworkError> {
|
||||
let iface_service = format!("wpa_supplicant@{}.service", &iface);
|
||||
|
||||
// start the interface service
|
||||
Command::new("sudo")
|
||||
.arg("/usr/bin/systemctl")
|
||||
.arg("start")
|
||||
.arg("wpa_supplicant@ap0.service")
|
||||
.arg(iface_service)
|
||||
.output()
|
||||
.context(StartAp0)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
.map_err(|source| NetworkError::StartInterface {
|
||||
source,
|
||||
iface: iface.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -560,81 +484,82 @@ pub fn activate_client() -> Result<(), NetworkError> {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `wlan_iface` - A local wireless interface.
|
||||
/// * `wifi` - An instance of the `WiFi` `struct` with fields `ssid` and `pass`
|
||||
///
|
||||
/// If configuration parameters are successfully generated from the provided
|
||||
/// SSID and password and appended to `wpa_supplicant-wlan0.conf`, 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 add(wifi: &WiFi) -> Result<(), NetworkError> {
|
||||
/// SSID and password and appended to `wpa_supplicant-<wlan_iface>.conf` (where
|
||||
/// `<wlan_iface>` is the provided interface parameter), an `Ok` `Result` type
|
||||
/// is returned. In the event of an error, a `NetworkError` is returned in the
|
||||
/// `Result`.
|
||||
pub fn add(wlan_iface: &str, ssid: &str, pass: &str) -> Result<(), NetworkError> {
|
||||
// generate configuration based on provided ssid & password
|
||||
let output = Command::new("wpa_passphrase")
|
||||
.arg(&wifi.ssid)
|
||||
.arg(&wifi.pass)
|
||||
.arg(&ssid)
|
||||
.arg(&pass)
|
||||
.stdout(Stdio::piped())
|
||||
.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
|
||||
let mut wpa_details = "\n".as_bytes().to_vec();
|
||||
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() {
|
||||
// open file in append mode
|
||||
let file = OpenOptions::new()
|
||||
let mut file = OpenOptions::new()
|
||||
.append(true)
|
||||
.open("/etc/wpa_supplicant/wpa_supplicant-wlan0.conf");
|
||||
.open(wlan_config)
|
||||
// TODO: create the file if it doesn't exist
|
||||
.map_err(NetworkError::Save)?;
|
||||
|
||||
file.write(&wpa_details).map_err(NetworkError::Save)?;
|
||||
|
||||
let _file = match file {
|
||||
// if file exists & open succeeds, write wifi configuration
|
||||
Ok(mut f) => f.write(&wpa_details),
|
||||
// TODO: handle this better: create file if not found
|
||||
// & seed with 'ctrl_interace' & 'update_config' settings
|
||||
// config file could also be copied from peach/config fs location
|
||||
Err(e) => panic!("Failed to write to file: {}", e),
|
||||
};
|
||||
Ok(())
|
||||
} else {
|
||||
let err_msg = String::from_utf8_lossy(&output.stdout);
|
||||
Err(NetworkError::GenWpaPassphraseWarning {
|
||||
ssid: wifi.ssid.to_string(),
|
||||
ssid: ssid.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.
|
||||
///
|
||||
/// 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
|
||||
/// currently connected to an access point), then the access point is activated
|
||||
/// by calling the `activate_ap()` function.
|
||||
///
|
||||
pub fn check_iface() -> Result<(), NetworkError> {
|
||||
// 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)?;
|
||||
pub fn check_iface(wlan_iface: &str, ap_iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_service = format!("wpa_supplicant@{}.service", &wlan_iface);
|
||||
|
||||
// returns the current state of the wlan0 interface
|
||||
let iface_state = state("wlan0")?;
|
||||
// returns 0 if the service is currently active
|
||||
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
|
||||
let wlan0_state = match iface_state {
|
||||
let wlan_state = match iface_state {
|
||||
Some(state) => state,
|
||||
None => "error".to_string(),
|
||||
};
|
||||
|
||||
// if wlan0 is active but not connected, start the ap0 service
|
||||
if wlan0_status.success() && wlan0_state == "down" {
|
||||
activate_ap()?
|
||||
// if wlan is active but not connected, start the ap service
|
||||
if wlan_status.success() && wlan_state == "down" {
|
||||
start_iface_service(ap_iface)?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -651,18 +576,12 @@ pub fn check_iface() -> Result<(), NetworkError> {
|
||||
/// If the network connection is successfully activated for the access point
|
||||
/// 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`
|
||||
/// 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.
|
||||
///
|
||||
/// is returned in the `Result`.
|
||||
pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let select = format!("SELECT {}", id);
|
||||
wpa.request(&select).context(WpaCtrlRequest)?;
|
||||
wpa.request(&select)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -676,18 +595,12 @@ pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
/// If the network configuration parameters are successfully deleted for
|
||||
/// the access point represented by the given network identifier, 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.
|
||||
///
|
||||
/// returned in the `Result`.
|
||||
pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let remove = format!("REMOVE_NETWORK {}", id);
|
||||
wpa.request(&remove).context(WpaCtrlRequest)?;
|
||||
wpa.request(&remove)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -701,17 +614,12 @@ pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
/// If the network connection is successfully disabled for the access point
|
||||
/// represented by the given network identifier, 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.
|
||||
///
|
||||
/// `Result`.
|
||||
pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let disable = format!("DISABLE_NETWORK {}", id);
|
||||
wpa.request(&disable).context(WpaCtrlRequest)?;
|
||||
wpa.request(&disable)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -723,18 +631,44 @@ pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
///
|
||||
/// If the network connection is successfully disconnected for the given
|
||||
/// wireless interface, 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.
|
||||
///
|
||||
/// error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let disconnect = "DISCONNECT".to_string();
|
||||
wpa.request(&disconnect).context(WpaCtrlRequest)?;
|
||||
wpa.request(&disconnect)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Forget credentials for the given network SSID and interface.
|
||||
/// Look up the network identified for the given SSID, delete the credentials
|
||||
/// and then save.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `iface` - A string slice holding the name of a wireless network interface
|
||||
/// * `ssid` - A string slice holding the SSID for a wireless access point
|
||||
///
|
||||
/// If the credentials are successfully deleted and saved, an `Ok` `Result`
|
||||
/// type is returned. In the event of an error, a `NetworkError` is returned
|
||||
/// in the `Result`.
|
||||
pub fn forget(iface: &str, ssid: &str) -> Result<(), NetworkError> {
|
||||
// get the id of the network
|
||||
let id_opt = id(iface, ssid)?;
|
||||
let id = id_opt.ok_or(NetworkError::Id {
|
||||
ssid: ssid.to_string(),
|
||||
iface: iface.to_string(),
|
||||
})?;
|
||||
// delete the old credentials
|
||||
// TODO: i've switched these back to the "correct" order
|
||||
// WEIRD BUG: the parameters below are technically in the wrong order:
|
||||
// it should be id first and then iface, but somehow they get twisted.
|
||||
// i don't understand computers.
|
||||
//delete(&iface, &id)?;
|
||||
delete(&id, iface)?;
|
||||
// save the updates to wpa_supplicant.conf
|
||||
save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -748,18 +682,12 @@ pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
|
||||
///
|
||||
/// If the password is successfully updated for the access point represented by
|
||||
/// the given network identifier, 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.
|
||||
///
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let new_pass = format!("NEW_PASSWORD {} {}", id, pass);
|
||||
wpa.request(&new_pass).context(WpaCtrlRequest)?;
|
||||
wpa.request(&new_pass)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -771,17 +699,11 @@ pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
|
||||
///
|
||||
/// If the network connection is successfully reassociated for the given
|
||||
/// wireless interface, 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.
|
||||
///
|
||||
/// error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
wpa.request("REASSOCIATE").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
wpa.request("REASSOCIATE")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -790,13 +712,10 @@ pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
|
||||
/// If the reconfigure command is successfully executed, indicating a reread
|
||||
/// 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
|
||||
/// 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.
|
||||
///
|
||||
/// returned in the `Result`.
|
||||
pub fn reconfigure() -> Result<(), NetworkError> {
|
||||
let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?;
|
||||
wpa.request("RECONFIGURE").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
||||
wpa.request("RECONFIGURE")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -808,31 +727,37 @@ pub fn reconfigure() -> Result<(), NetworkError> {
|
||||
///
|
||||
/// If the network connection is successfully disconnected and reconnected for
|
||||
/// the given wireless interface, 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.
|
||||
///
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
wpa.request("DISCONNECT").context(WpaCtrlRequest)?;
|
||||
wpa.request("RECONNECT").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
wpa.request("DISCONNECT")?;
|
||||
wpa.request("RECONNECT")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save configuration updates to the `wpa_supplicant` configuration file.
|
||||
///
|
||||
/// If wireless network configuration updates are successfully save to the
|
||||
/// If wireless network configuration updates are successfully saved to the
|
||||
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`. The
|
||||
/// `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn save() -> Result<(), NetworkError> {
|
||||
let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?;
|
||||
wpa.request("SAVE_CONFIG").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
||||
wpa.request("SAVE_CONFIG")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update password for an access point and save configuration updates to the
|
||||
/// `wpa_supplicant` configuration file.
|
||||
///
|
||||
/// If wireless network configuration updates are successfully saved to the
|
||||
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn update(iface: &str, ssid: &str, pass: &str) -> Result<(), NetworkError> {
|
||||
// delete the old credentials and save the changes
|
||||
forget(iface, ssid)?;
|
||||
// add the new credentials
|
||||
add(iface, ssid, pass)?;
|
||||
reconfigure()?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
use regex::Regex;
|
||||
use snafu::ResultExt;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::error::NetworkError;
|
||||
|
||||
/// 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
|
||||
///
|
||||
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 result = caps.map(|caps| caps[1].to_string());
|
||||
|
||||
|
@ -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" }
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "peach-oled"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
|
||||
edition = "2018"
|
||||
description = "Write and draw to OLED display using JSON-RPC over HTTP."
|
||||
@ -27,18 +27,16 @@ travis-ci = { repository = "peachcloud/peach-oled", branch = "master" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
jsonrpc-core = "11.0.0"
|
||||
jsonrpc-http-server = "11.0.0"
|
||||
linux-embedded-hal = "0.2.2"
|
||||
embedded-graphics = "0.4.7"
|
||||
tinybmp = "0.1.0"
|
||||
ssd1306 = "0.2.6"
|
||||
serde = { version = "1.0.87", features = ["derive"] }
|
||||
serde_json = "1.0.39"
|
||||
log = "0.4.0"
|
||||
env_logger = "0.6.1"
|
||||
snafu = "0.4.1"
|
||||
env_logger = "0.9"
|
||||
jsonrpc-core = "18"
|
||||
jsonrpc-http-server = "18"
|
||||
linux-embedded-hal = "0.2.2"
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
nix="0.11"
|
||||
ssd1306 = "0.2.6"
|
||||
tinybmp = "0.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
jsonrpc-test = "11.0.0"
|
||||
jsonrpc-test = "18"
|
||||
|
@ -1,6 +1,6 @@
|
||||
# peach-oled
|
||||
|
||||
[](https://travis-ci.com/peachcloud/peach-oled) 
|
||||
[](https://travis-ci.com/peachcloud/peach-oled) 
|
||||
|
||||
OLED microservice module for PeachCloud. Write to a 128x64 OLED display with SDD1306 driver (I2C) using [JSON-RPC](https://www.jsonrpc.org/specification) over http.
|
||||
|
||||
|
@ -1,44 +1,68 @@
|
||||
use std::error;
|
||||
use std::{error, fmt};
|
||||
|
||||
use jsonrpc_core::{types::error::Error, ErrorCode};
|
||||
use linux_embedded_hal as hal;
|
||||
use snafu::Snafu;
|
||||
use jsonrpc_core::types::error::Error as JsonRpcError;
|
||||
use jsonrpc_core::ErrorCode;
|
||||
use linux_embedded_hal::i2cdev::linux::LinuxI2CError;
|
||||
|
||||
pub type BoxError = Box<dyn error::Error>;
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
#[snafu(visibility(pub(crate)))]
|
||||
#[derive(Debug)]
|
||||
pub enum OledError {
|
||||
#[snafu(display("Failed to create interface for I2C device: {}", source))]
|
||||
I2CError {
|
||||
source: hal::i2cdev::linux::LinuxI2CError,
|
||||
source: LinuxI2CError,
|
||||
},
|
||||
|
||||
#[snafu(display("Coordinate {} out of range {}: {}", coord, range, value))]
|
||||
InvalidCoordinate {
|
||||
coord: String,
|
||||
range: String,
|
||||
value: i32,
|
||||
},
|
||||
|
||||
// TODO: implement for validate() in src/lib.rs
|
||||
#[snafu(display("Font size invalid: {}", font))]
|
||||
InvalidFontSize { font: String },
|
||||
|
||||
#[snafu(display("String length out of range 0-21: {}", len))]
|
||||
InvalidString { len: usize },
|
||||
|
||||
#[snafu(display("Missing expected parameter: {}", e))]
|
||||
MissingParameter { e: Error },
|
||||
|
||||
#[snafu(display("Failed to parse parameter: {}", e))]
|
||||
ParseError { e: Error },
|
||||
InvalidFontSize {
|
||||
font: String,
|
||||
},
|
||||
InvalidString {
|
||||
len: usize,
|
||||
},
|
||||
MissingParameter {
|
||||
source: JsonRpcError,
|
||||
},
|
||||
ParseError {
|
||||
source: JsonRpcError,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<OledError> for Error {
|
||||
impl error::Error for OledError {}
|
||||
|
||||
impl fmt::Display for OledError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
OledError::ParseError { ref source } => {
|
||||
write!(f, "Failed to parse parameter: {}", source)
|
||||
}
|
||||
OledError::MissingParameter { ref source } => {
|
||||
write!(f, "Missing expected parameter: {}", source)
|
||||
}
|
||||
OledError::InvalidString { len } => {
|
||||
write!(f, "String length out of range 0-21: {}", len)
|
||||
}
|
||||
OledError::InvalidFontSize { ref font } => {
|
||||
write!(f, "Invalid font size: {}", font)
|
||||
}
|
||||
OledError::InvalidCoordinate {
|
||||
ref coord,
|
||||
ref range,
|
||||
value,
|
||||
} => {
|
||||
write!(f, "Coordinate {} out of range {}: {}", coord, range, value)
|
||||
}
|
||||
OledError::I2CError { ref source } => {
|
||||
write!(f, "Failed to create interface for I2C device: {}", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OledError> for JsonRpcError {
|
||||
fn from(err: OledError) -> Self {
|
||||
match &err {
|
||||
OledError::I2CError { source } => Error {
|
||||
OledError::I2CError { source } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32000),
|
||||
message: format!("Failed to create interface for I2C device: {}", source),
|
||||
data: None,
|
||||
@ -47,7 +71,7 @@ impl From<OledError> for Error {
|
||||
coord,
|
||||
value,
|
||||
range,
|
||||
} => Error {
|
||||
} => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32001),
|
||||
message: format!(
|
||||
"Validation error: coordinate {} out of range {}: {}",
|
||||
@ -55,18 +79,18 @@ impl From<OledError> for Error {
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
OledError::InvalidFontSize { font } => Error {
|
||||
OledError::InvalidFontSize { font } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32002),
|
||||
message: format!("Validation error: {} is not an accepted font size. Use 6x8, 6x12, 8x16 or 12x16 instead", font),
|
||||
data: None,
|
||||
},
|
||||
OledError::InvalidString { len } => Error {
|
||||
OledError::InvalidString { len } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32003),
|
||||
message: format!("Validation error: string length {} out of range 0-21", len),
|
||||
data: None,
|
||||
},
|
||||
OledError::MissingParameter { e } => e.clone(),
|
||||
OledError::ParseError { e } => e.clone(),
|
||||
OledError::MissingParameter { source } => source.clone(),
|
||||
OledError::ParseError { source } => source.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,23 +6,23 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use embedded_graphics::coord::Coord;
|
||||
use embedded_graphics::fonts::{Font12x16, Font6x12, Font6x8, Font8x16};
|
||||
use embedded_graphics::image::Image1BPP;
|
||||
use embedded_graphics::prelude::*;
|
||||
use embedded_graphics::{
|
||||
coord::Coord,
|
||||
fonts::{Font12x16, Font6x12, Font6x8, Font8x16},
|
||||
image::Image1BPP,
|
||||
prelude::*,
|
||||
};
|
||||
use hal::I2cdev;
|
||||
use jsonrpc_core::{types::error::Error, IoHandler, Params, Value};
|
||||
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
|
||||
use linux_embedded_hal as hal;
|
||||
use log::{debug, error, info};
|
||||
use serde::Deserialize;
|
||||
use snafu::{ensure, ResultExt};
|
||||
use ssd1306::prelude::*;
|
||||
use ssd1306::Builder;
|
||||
use ssd1306::{prelude::*, Builder};
|
||||
|
||||
use crate::error::{BoxError, I2CError, InvalidCoordinate, InvalidString, OledError};
|
||||
use crate::error::OledError;
|
||||
|
||||
//define the Graphic struct for receiving draw commands
|
||||
// define the Graphic struct for receiving draw commands
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Graphic {
|
||||
bytes: Vec<u8>,
|
||||
@ -32,7 +32,7 @@ pub struct Graphic {
|
||||
y_coord: i32,
|
||||
}
|
||||
|
||||
//define the Msg struct for receiving write commands
|
||||
// define the Msg struct for receiving write commands
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Msg {
|
||||
x_coord: i32,
|
||||
@ -41,86 +41,61 @@ pub struct Msg {
|
||||
font_size: String,
|
||||
}
|
||||
|
||||
//definte the On struct for receiving power on/off commands
|
||||
// definte the On struct for receiving power on/off commands
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct On {
|
||||
on: bool,
|
||||
}
|
||||
|
||||
fn validate(m: &Msg) -> Result<(), OledError> {
|
||||
ensure!(
|
||||
m.string.len() <= 21,
|
||||
InvalidString {
|
||||
len: m.string.len()
|
||||
}
|
||||
);
|
||||
|
||||
ensure!(
|
||||
m.x_coord >= 0,
|
||||
InvalidCoordinate {
|
||||
fn validate(msg: &Msg) -> Result<(), OledError> {
|
||||
if msg.string.len() > 21 {
|
||||
Err(OledError::InvalidString {
|
||||
len: msg.string.len(),
|
||||
})
|
||||
} else if msg.x_coord < 0 || msg.x_coord > 128 {
|
||||
Err(OledError::InvalidCoordinate {
|
||||
coord: "x".to_string(),
|
||||
range: "0-128".to_string(),
|
||||
value: m.x_coord,
|
||||
}
|
||||
);
|
||||
|
||||
ensure!(
|
||||
m.x_coord < 129,
|
||||
InvalidCoordinate {
|
||||
coord: "x".to_string(),
|
||||
range: "0-128".to_string(),
|
||||
value: m.x_coord,
|
||||
}
|
||||
);
|
||||
|
||||
ensure!(
|
||||
m.y_coord >= 0,
|
||||
InvalidCoordinate {
|
||||
value: msg.x_coord,
|
||||
})
|
||||
} else if msg.y_coord < 0 || msg.y_coord > 147 {
|
||||
Err(OledError::InvalidCoordinate {
|
||||
coord: "y".to_string(),
|
||||
range: "0-47".to_string(),
|
||||
value: m.y_coord,
|
||||
}
|
||||
);
|
||||
|
||||
ensure!(
|
||||
m.y_coord < 148,
|
||||
InvalidCoordinate {
|
||||
coord: "y".to_string(),
|
||||
range: "0-47".to_string(),
|
||||
value: m.y_coord,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
value: msg.y_coord,
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), BoxError> {
|
||||
pub fn run() -> Result<(), OledError> {
|
||||
info!("Starting up.");
|
||||
|
||||
debug!("Creating interface for I2C device.");
|
||||
let i2c = I2cdev::new("/dev/i2c-1").context(I2CError)?;
|
||||
let i2c = I2cdev::new("/dev/i2c-1").map_err(|source| OledError::I2CError { source })?;
|
||||
|
||||
let mut disp: GraphicsMode<_> = Builder::new().connect_i2c(i2c).into();
|
||||
let mut display: GraphicsMode<_> = Builder::new().connect_i2c(i2c).into();
|
||||
|
||||
info!("Initializing the display.");
|
||||
disp.init().unwrap_or_else(|_| {
|
||||
display.init().unwrap_or_else(|_| {
|
||||
error!("Problem initializing the OLED display.");
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
debug!("Flushing the display.");
|
||||
disp.flush().unwrap_or_else(|_| {
|
||||
display.flush().unwrap_or_else(|_| {
|
||||
error!("Problem flushing the OLED display.");
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
let oled = Arc::new(Mutex::new(disp));
|
||||
let oled = Arc::new(Mutex::new(display));
|
||||
let oled_clone = Arc::clone(&oled);
|
||||
|
||||
info!("Creating JSON-RPC I/O handler.");
|
||||
let mut io = IoHandler::default();
|
||||
|
||||
io.add_method("clear", move |_| {
|
||||
io.add_sync_method("clear", move |_| {
|
||||
let mut oled = oled_clone.lock().unwrap();
|
||||
info!("Clearing the display.");
|
||||
oled.clear();
|
||||
@ -134,21 +109,20 @@ pub fn run() -> Result<(), BoxError> {
|
||||
|
||||
let oled_clone = Arc::clone(&oled);
|
||||
|
||||
io.add_method("draw", move |params: Params| {
|
||||
let g: Result<Graphic, Error> = params.parse();
|
||||
let g: Graphic = g?;
|
||||
io.add_sync_method("draw", move |params: Params| {
|
||||
let graphic: Graphic = params.parse()?;
|
||||
// TODO: add simple byte validation function
|
||||
let mut oled = oled_clone.lock().unwrap();
|
||||
info!("Drawing image to the display.");
|
||||
let im =
|
||||
Image1BPP::new(&g.bytes, g.width, g.height).translate(Coord::new(g.x_coord, g.y_coord));
|
||||
oled.draw(im.into_iter());
|
||||
let image = Image1BPP::new(&graphic.bytes, graphic.width, graphic.height)
|
||||
.translate(Coord::new(graphic.x_coord, graphic.y_coord));
|
||||
oled.draw(image.into_iter());
|
||||
Ok(Value::String("success".into()))
|
||||
});
|
||||
|
||||
let oled_clone = Arc::clone(&oled);
|
||||
|
||||
io.add_method("flush", move |_| {
|
||||
io.add_sync_method("flush", move |_| {
|
||||
let mut oled = oled_clone.lock().unwrap();
|
||||
info!("Flushing the display.");
|
||||
oled.flush().unwrap_or_else(|_| {
|
||||
@ -160,9 +134,9 @@ pub fn run() -> Result<(), BoxError> {
|
||||
|
||||
let oled_clone = Arc::clone(&oled);
|
||||
|
||||
io.add_method("ping", |_| Ok(Value::String("success".to_string())));
|
||||
io.add_sync_method("ping", |_| Ok(Value::String("success".to_string())));
|
||||
|
||||
io.add_method("power", move |params: Params| {
|
||||
io.add_sync_method("power", move |params: Params| {
|
||||
let o: Result<On, Error> = params.parse();
|
||||
let o: On = o?;
|
||||
let mut oled = oled_clone.lock().unwrap();
|
||||
@ -180,37 +154,36 @@ pub fn run() -> Result<(), BoxError> {
|
||||
|
||||
let oled_clone = Arc::clone(&oled);
|
||||
|
||||
io.add_method("write", move |params: Params| {
|
||||
io.add_sync_method("write", move |params: Params| {
|
||||
info!("Received a 'write' request.");
|
||||
let m: Result<Msg, Error> = params.parse();
|
||||
let m: Msg = m?;
|
||||
validate(&m)?;
|
||||
let msg = params.parse()?;
|
||||
validate(&msg)?;
|
||||
|
||||
let mut oled = oled_clone.lock().unwrap();
|
||||
|
||||
info!("Writing to the display.");
|
||||
if m.font_size == "6x8" {
|
||||
if msg.font_size == "6x8" {
|
||||
oled.draw(
|
||||
Font6x8::render_str(&m.string)
|
||||
.translate(Coord::new(m.x_coord, m.y_coord))
|
||||
Font6x8::render_str(&msg.string)
|
||||
.translate(Coord::new(msg.x_coord, msg.y_coord))
|
||||
.into_iter(),
|
||||
);
|
||||
} else if m.font_size == "6x12" {
|
||||
} else if msg.font_size == "6x12" {
|
||||
oled.draw(
|
||||
Font6x12::render_str(&m.string)
|
||||
.translate(Coord::new(m.x_coord, m.y_coord))
|
||||
Font6x12::render_str(&msg.string)
|
||||
.translate(Coord::new(msg.x_coord, msg.y_coord))
|
||||
.into_iter(),
|
||||
);
|
||||
} else if m.font_size == "8x16" {
|
||||
} else if msg.font_size == "8x16" {
|
||||
oled.draw(
|
||||
Font8x16::render_str(&m.string)
|
||||
.translate(Coord::new(m.x_coord, m.y_coord))
|
||||
Font8x16::render_str(&msg.string)
|
||||
.translate(Coord::new(msg.x_coord, msg.y_coord))
|
||||
.into_iter(),
|
||||
);
|
||||
} else if m.font_size == "12x16" {
|
||||
} else if msg.font_size == "12x16" {
|
||||
oled.draw(
|
||||
Font12x16::render_str(&m.string)
|
||||
.translate(Coord::new(m.x_coord, m.y_coord))
|
||||
Font12x16::render_str(&msg.string)
|
||||
.translate(Coord::new(msg.x_coord, msg.y_coord))
|
||||
.into_iter(),
|
||||
);
|
||||
}
|
||||
@ -255,7 +228,7 @@ mod tests {
|
||||
fn rpc_success() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_success_response", |_| {
|
||||
io.add_sync_method("rpc_success_response", |_| {
|
||||
Ok(Value::String("success".into()))
|
||||
});
|
||||
test_rpc::Rpc::from(io)
|
||||
@ -269,7 +242,7 @@ mod tests {
|
||||
fn rpc_internal_error() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_internal_error", |_| Err(Error::internal_error()));
|
||||
io.add_sync_method("rpc_internal_error", |_| Err(Error::internal_error()));
|
||||
test_rpc::Rpc::from(io)
|
||||
};
|
||||
|
||||
@ -287,7 +260,7 @@ mod tests {
|
||||
fn rpc_i2c_io_error() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_i2c_io_error", |_| {
|
||||
io.add_sync_method("rpc_i2c_io_error", |_| {
|
||||
let io_err = IoError::new(ErrorKind::PermissionDenied, "oh no!");
|
||||
let source = LinuxI2CError::Io(io_err);
|
||||
Err(Error::from(OledError::I2CError { source }))
|
||||
@ -310,7 +283,7 @@ mod tests {
|
||||
fn rpc_i2c_nix_error() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_i2c_nix_error", |_| {
|
||||
io.add_sync_method("rpc_i2c_nix_error", |_| {
|
||||
let nix_err = NixError::InvalidPath;
|
||||
let source = LinuxI2CError::Nix(nix_err);
|
||||
Err(Error::from(OledError::I2CError { source }))
|
||||
@ -326,14 +299,14 @@ mod tests {
|
||||
}"#
|
||||
);
|
||||
}
|
||||
*/
|
||||
*/
|
||||
|
||||
// test to ensure correct InvalidCoordinate error response
|
||||
#[test]
|
||||
fn rpc_invalid_coord() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_invalid_coord", |_| {
|
||||
io.add_sync_method("rpc_invalid_coord", |_| {
|
||||
Err(Error::from(OledError::InvalidCoordinate {
|
||||
coord: "x".to_string(),
|
||||
range: "0-128".to_string(),
|
||||
@ -357,7 +330,7 @@ mod tests {
|
||||
fn rpc_invalid_fontsize() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_invalid_fontsize", |_| {
|
||||
io.add_sync_method("rpc_invalid_fontsize", |_| {
|
||||
Err(Error::from(OledError::InvalidFontSize {
|
||||
font: "24x32".to_string(),
|
||||
}))
|
||||
@ -379,7 +352,7 @@ mod tests {
|
||||
fn rpc_invalid_string() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_invalid_string", |_| {
|
||||
io.add_sync_method("rpc_invalid_string", |_| {
|
||||
Err(Error::from(OledError::InvalidString { len: 22 }))
|
||||
});
|
||||
test_rpc::Rpc::from(io)
|
||||
@ -399,15 +372,15 @@ mod tests {
|
||||
fn rpc_invalid_params() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_invalid_params", |_| {
|
||||
let e = Error {
|
||||
io.add_sync_method("rpc_invalid_params", |_| {
|
||||
let source = Error {
|
||||
code: ErrorCode::InvalidParams,
|
||||
message: String::from("invalid params"),
|
||||
data: Some(Value::String(
|
||||
"Invalid params: invalid type: null, expected struct Msg.".into(),
|
||||
)),
|
||||
};
|
||||
Err(Error::from(OledError::MissingParameter { e }))
|
||||
Err(Error::from(OledError::MissingParameter { source }))
|
||||
});
|
||||
test_rpc::Rpc::from(io)
|
||||
};
|
||||
@ -427,13 +400,13 @@ mod tests {
|
||||
fn rpc_parse_error() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_parse_error", |_| {
|
||||
let e = Error {
|
||||
io.add_sync_method("rpc_parse_error", |_| {
|
||||
let source = Error {
|
||||
code: ErrorCode::ParseError,
|
||||
message: String::from("Parse error"),
|
||||
data: None,
|
||||
};
|
||||
Err(Error::from(OledError::ParseError { e }))
|
||||
Err(Error::from(OledError::ParseError { source }))
|
||||
});
|
||||
test_rpc::Rpc::from(io)
|
||||
};
|
||||
|
@ -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" }
|
@ -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" }
|
@ -1,40 +1,31 @@
|
||||
[package]
|
||||
name = "peach-stats"
|
||||
version = "0.1.3"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
|
||||
version = "0.2.0"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
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"
|
||||
repository = "https://github.com/peachcloud/peach-stats"
|
||||
repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-stats"
|
||||
readme = "README.md"
|
||||
license = "AGPL-3.0-only"
|
||||
license = "LGPL-3.0-only"
|
||||
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]
|
||||
travis-ci = { repository = "peachcloud/peach-stats", branch = "master" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.9"
|
||||
jsonrpc-core = "18"
|
||||
jsonrpc-http-server = "18"
|
||||
log = "0.4"
|
||||
miniserde = "0.1.15"
|
||||
miniserde = { version = "0.1.15", optional = true }
|
||||
probes = "0.4.1"
|
||||
serde = { version = "1.0.130", features = ["derive"], optional = true }
|
||||
systemstat = "0.1.10"
|
||||
|
||||
[dev-dependencies]
|
||||
jsonrpc-test = "18"
|
||||
[features]
|
||||
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"]
|
||||
|
@ -1,109 +1,47 @@
|
||||
# peach-stats
|
||||
|
||||
[](https://travis-ci.com/peachcloud/peach-stats) 
|
||||

|
||||
|
||||
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_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` |
|
||||
- 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`
|
||||
|
||||
### 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`
|
||||
|
||||
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
|
||||
## License
|
||||
|
||||
LGPL-3.0.
|
||||
|
@ -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
|
@ -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 std::{error, fmt, io::Error as IoError};
|
||||
|
||||
/// Custom error type encapsulating all possible errors when retrieving system
|
||||
/// statistics.
|
||||
#[derive(Debug)]
|
||||
pub enum StatError {
|
||||
CpuStat { source: ProbeError },
|
||||
DiskUsage { source: ProbeError },
|
||||
LoadAvg { source: ProbeError },
|
||||
MemStat { source: ProbeError },
|
||||
Uptime { source: io::Error },
|
||||
pub enum StatsError {
|
||||
/// Failed to retrieve CPU statistics.
|
||||
CpuStat(ProbeError),
|
||||
/// Failed to retrieve disk usage statistics.
|
||||
DiskUsage(ProbeError),
|
||||
/// 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 {
|
||||
match *self {
|
||||
StatError::CpuStat { ref source } => {
|
||||
StatsError::CpuStat(ref 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)
|
||||
}
|
||||
StatError::LoadAvg { ref source } => {
|
||||
StatsError::LoadAvg(ref 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)
|
||||
}
|
||||
StatError::Uptime { ref source } => {
|
||||
StatsError::Uptime(ref 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,103 +1,48 @@
|
||||
mod error;
|
||||
mod stats;
|
||||
mod structs;
|
||||
#![warn(missing_docs)]
|
||||
|
||||
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};
|
||||
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
|
||||
use log::info;
|
||||
pub mod error;
|
||||
pub mod stats;
|
||||
|
||||
use crate::error::StatError;
|
||||
|
||||
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""#);
|
||||
}
|
||||
}
|
||||
pub use crate::error::StatsError;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -1,14 +1,96 @@
|
||||
//! System statistics retrieval functions and associated data types.
|
||||
|
||||
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 systemstat::{Platform, System};
|
||||
|
||||
use crate::error::StatError;
|
||||
use crate::structs::{CpuStat, CpuStatPercentages, DiskUsage, LoadAverage, MemStat};
|
||||
use crate::error::StatsError;
|
||||
|
||||
pub fn cpu_stats() -> Result<String, StatError> {
|
||||
let cpu_stats = cpu::proc::read().map_err(|source| StatError::CpuStat { source })?;
|
||||
/// CPU statistics.
|
||||
#[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 cpu = CpuStat {
|
||||
user: s.user,
|
||||
@ -16,13 +98,13 @@ pub fn cpu_stats() -> Result<String, StatError> {
|
||||
nice: s.nice,
|
||||
idle: s.idle,
|
||||
};
|
||||
let json_cpu = json::to_string(&cpu);
|
||||
|
||||
Ok(json_cpu)
|
||||
Ok(cpu)
|
||||
}
|
||||
|
||||
pub fn cpu_stats_percent() -> Result<String, StatError> {
|
||||
let cpu_stats = cpu::proc::read().map_err(|source| StatError::CpuStat { source })?;
|
||||
/// Retrieve the current CPU statistics as percentages.
|
||||
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 cpu = CpuStatPercentages {
|
||||
user: s.user,
|
||||
@ -30,13 +112,13 @@ pub fn cpu_stats_percent() -> Result<String, StatError> {
|
||||
nice: s.nice,
|
||||
idle: s.idle,
|
||||
};
|
||||
let json_cpu = json::to_string(&cpu);
|
||||
|
||||
Ok(json_cpu)
|
||||
Ok(cpu)
|
||||
}
|
||||
|
||||
pub fn disk_usage() -> Result<String, StatError> {
|
||||
let disks = disk_usage::read().map_err(|source| StatError::DiskUsage { source })?;
|
||||
/// Retrieve the current disk usage statistics for each available disk / partition.
|
||||
pub fn disk_usage() -> Result<Vec<DiskUsage>, StatsError> {
|
||||
let disks = disk_usage::read().map_err(StatsError::DiskUsage)?;
|
||||
let mut disk_usages = Vec::new();
|
||||
for d in disks {
|
||||
let disk = DiskUsage {
|
||||
@ -49,42 +131,39 @@ pub fn disk_usage() -> Result<String, StatError> {
|
||||
};
|
||||
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> {
|
||||
let l = load::read().map_err(|source| StatError::LoadAvg { source })?;
|
||||
/// Retrieve the current load average statistics.
|
||||
pub fn load_average() -> Result<LoadAverage, StatsError> {
|
||||
let l = load::read().map_err(StatsError::LoadAvg)?;
|
||||
let load_avg = LoadAverage {
|
||||
one: l.one,
|
||||
five: l.five,
|
||||
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> {
|
||||
let m = memory::read().map_err(|source| StatError::MemStat { source })?;
|
||||
/// Retrieve the current memory usage statistics.
|
||||
pub fn mem_stats() -> Result<MemStat, StatsError> {
|
||||
let m = memory::read().map_err(StatsError::MemStat)?;
|
||||
let mem = MemStat {
|
||||
total: m.total(),
|
||||
free: m.free(),
|
||||
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 uptime = sys
|
||||
.uptime()
|
||||
.map_err(|source| StatError::Uptime { source })?;
|
||||
let uptime = sys.uptime().map_err(StatsError::Uptime)?;
|
||||
let uptime_secs = uptime.as_secs();
|
||||
let json_uptime = json::to_string(&uptime_secs);
|
||||
|
||||
Ok(json_uptime)
|
||||
Ok(uptime_secs)
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
@ -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" }
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "peach-web"
|
||||
version = "0.4.11"
|
||||
version = "0.5.0"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
|
||||
edition = "2018"
|
||||
description = "peach-web is a web application which provides a web interface for monitoring and interacting with the PeachCloud device. This allows administration of the single-board computer (ie. Raspberry Pi) running PeachCloud, as well as the ssb-server and related plugins."
|
||||
@ -21,12 +21,12 @@ maintainer-scripts="debian"
|
||||
systemd-units = { unit-name = "peach-web" }
|
||||
assets = [
|
||||
["target/release/peach-web", "/usr/bin/", "755"],
|
||||
["Rocket.toml", "/usr/share/peach-web/Rocket.toml", "644"],
|
||||
["templates/**/*", "/usr/share/peach-web/templates/", "644"],
|
||||
["static/*", "/usr/share/peach-web/static/", "644"],
|
||||
["static/css/*", "/usr/share/peach-web/static/css/", "644"],
|
||||
["static/icons/*", "/usr/share/peach-web/static/icons/", "644"],
|
||||
["static/images/*", "/usr/share/peach-web/static/images/", "644"],
|
||||
["static/js/*", "/usr/share/peach-web/static/js/", "644"],
|
||||
["README.md", "/usr/share/doc/peach-web/README", "644"],
|
||||
]
|
||||
|
||||
@ -39,16 +39,13 @@ env_logger = "0.8"
|
||||
log = "0.4"
|
||||
nest = "1.0.0"
|
||||
peach-lib = { path = "../peach-lib" }
|
||||
percent-encoding = "2.1.0"
|
||||
peach-network = { path = "../peach-network", features = ["serde_support"] }
|
||||
peach-stats = { path = "../peach-stats", features = ["serde_support"] }
|
||||
rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
snafu = "0.6"
|
||||
tera = { version = "1.12.1", features = ["builtins"] }
|
||||
websocket = "0.26"
|
||||
regex = "1"
|
||||
xdg = "2.2.0"
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
|
||||
[dependencies.rocket_dyn_templates]
|
||||
version = "0.1.0-rc.1"
|
||||
|
@ -1,14 +1,14 @@
|
||||
# peach-web
|
||||
|
||||
[](https://travis-ci.com/peachcloud/peach-web) 
|
||||
[](https://travis-ci.com/peachcloud/peach-web) 
|
||||
|
||||
## Web Interface for PeachCloud
|
||||
|
||||
**peach-web** provides a web interface for the PeachCloud device. It serves static assets and exposes a JSON API for programmatic interactions.
|
||||
**peach-web** provides a web interface for the PeachCloud device.
|
||||
|
||||
Initial development is focused on administration of the device itself, beginning with networking functionality, with SSB-related administration to be integrated at a later stage.
|
||||
|
||||
The peach-web stack currently consists of [Rocket](https://rocket.rs/) (Rust web framework), [Tera](http://tera.netlify.com/) (Rust template engine), HTML, CSS and JavaScript.
|
||||
The peach-web stack currently consists of [Rocket](https://rocket.rs/) (Rust web framework), [Tera](http://tera.netlify.com/) (Rust template engine), HTML and CSS.
|
||||
|
||||
_Note: This is a work-in-progress._
|
||||
|
||||
@ -25,7 +25,7 @@ Move into the repo and compile:
|
||||
|
||||
Run the tests:
|
||||
|
||||
`cargo test`
|
||||
`ROCKET_DISABLE_AUTH=true ROCKET_STANDALONE_MODE=false cargo test`
|
||||
|
||||
Move back to the `peach-workspace` directory:
|
||||
|
||||
@ -35,15 +35,29 @@ Run the binary:
|
||||
|
||||
`./target/release/peach-web`
|
||||
|
||||
_Note: Networking functionality requires peach-network microservice to be running._
|
||||
|
||||
### Environment
|
||||
|
||||
The web application deployment mode is configured with the `ROCKET_ENV` environment variable:
|
||||
**Deployment Profile**
|
||||
|
||||
The web application deployment profile can be configured with the `ROCKET_ENV` environment variable:
|
||||
|
||||
`export ROCKET_ENV=stage`
|
||||
|
||||
Other deployment modes are `dev` and `prod`. Read the [Rocket Environment Configurations docs](https://rocket.rs/v0.5-rc/guide/configuration/#environment-variables) for further information.
|
||||
Default configuration parameters are defined in `Rocket.toml`. This file defines a set of default parameters, some of which are overwritten when running in `debug` mode (ie. `cargo run` or `cargo build`) or `release` mode (ie. `cargo run --release` or `cargo build --release`).
|
||||
|
||||
Read the [Rocket Environment Configurations docs](https://rocket.rs/v0.5-rc/guide/configuration/#environment-variables) for further information.
|
||||
|
||||
**Configuration Mode**
|
||||
|
||||
The web application can be run with a minimal set of routes and functionality (PeachPub - a simple sbot manager) or with the full-suite of capabilities, including network management and access to device statistics (PeachCloud). The mode is enabled by default (as defined in `Rocket.toml`) but can be overwritten using the `ROCKET_STANDALONE_MODE` environment variable: `true` or `false`. If the variable is unset or the value is incorrectly set, the application defaults to standalone mode.
|
||||
|
||||
**Authentication**
|
||||
|
||||
Authentication is disabled in `debug` mode and enabled by default when running the application in `release` mode. It can be disabled by setting the `ROCKET_DISABLE_AUTH` environment variable to `true`:
|
||||
|
||||
`export ROCKET_DISABLE_AUTH=true`
|
||||
|
||||
**Logging**
|
||||
|
||||
Logging is made available with `env_logger`:
|
||||
|
||||
@ -85,7 +99,21 @@ Remove configuration files (not removed with `apt-get remove`):
|
||||
|
||||
### Design
|
||||
|
||||
`peach-web` is built on the Rocket webserver and Tera templating engine. It presents a web interface for interacting with the device. HTML is rendered server-side. Request handlers call 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 `peach-` libraries and serve HTML and assets. Each Tera template is passed a context object. In the case of Rust, this object is a `struct` and must implement `Serialize`. The fields of the context object are available in the context of the template to be rendered.
|
||||
|
||||
### Configuration
|
||||
|
||||
Configuration variables are stored in /var/lib/peachcloud/config.yml.
|
||||
Peach-web also updates this file when changes are made to configurations via
|
||||
the web interface. peach-web has no database, so all configurations are stored in this file.
|
||||
|
||||
#### Dynamic DNS Configuration
|
||||
|
||||
Most users will want to use the default PeachCloud dynamic dns server.
|
||||
If the config dyn_use_custom_server=false, then default values will be used.
|
||||
If the config dyn_use_custom_server=true, then a value must also be set for dyn_dns_server_address (e.g. "http://peachdynserver.commoninternet.net").
|
||||
This value is the URL of the instance of peach-dyndns-server that requests will be sent to for domain registration.
|
||||
Using a custom value can here can be useful for testing.
|
||||
|
||||
### Licensing
|
||||
|
||||
|
@ -1,5 +1,11 @@
|
||||
[development]
|
||||
template_dir = "templates/"
|
||||
[default]
|
||||
secret_key = "VYVUDivXvu8g6llxeJd9F92pMfocml5xl/Jjv5Sk4yw="
|
||||
disable_auth = false
|
||||
standalone_mode = true
|
||||
|
||||
[production]
|
||||
[debug]
|
||||
template_dir = "templates/"
|
||||
disable_auth = true
|
||||
|
||||
[release]
|
||||
template_dir = "templates/"
|
||||
|
@ -5,54 +5,18 @@ set -e
|
||||
adduser --quiet --system peach-web
|
||||
usermod -g peach peach-web
|
||||
|
||||
# create secret passwords folder if it doesn't already exist
|
||||
mkdir -p /var/lib/peachcloud/passwords
|
||||
chown -R peach-web:peach /var/lib/peachcloud/passwords
|
||||
chmod -R u+rwX,go+rX,go-w /var/lib/peachcloud/passwords
|
||||
|
||||
# create nginx config
|
||||
cat <<EOF > /etc/nginx/sites-enabled/default
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name peach.local www.peach.local;
|
||||
|
||||
# nginx authentication
|
||||
auth_basic "If you have forgotten your password visit: http://peach.local/send_password_reset/";
|
||||
auth_basic_user_file /var/lib/peachcloud/passwords/htpasswd;
|
||||
|
||||
# remove trailing slash if found
|
||||
rewrite ^/(.*)/$ /$1 permanent;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
|
||||
# public routes
|
||||
location /send_password_reset {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /reset_password {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /public/ {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /js/ {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /css/ {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /icons/ {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
|
||||
}
|
||||
EOF
|
||||
|
||||
|
36
peach-web/src/context/dns.rs
Normal file
36
peach-web/src/context/dns.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use peach_lib::{config_manager, dyndns_client};
|
||||
use rocket::serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ConfigureDNSContext {
|
||||
pub external_domain: String,
|
||||
pub dyndns_subdomain: String,
|
||||
pub enable_dyndns: bool,
|
||||
pub is_dyndns_online: bool,
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl ConfigureDNSContext {
|
||||
pub fn build() -> ConfigureDNSContext {
|
||||
// TODO: replace `unwrap` with resilient error handling
|
||||
let peach_config = config_manager::load_peach_config().unwrap();
|
||||
let dyndns_fulldomain = peach_config.dyn_domain;
|
||||
let is_dyndns_online = dyndns_client::is_dns_updater_online().unwrap();
|
||||
let dyndns_subdomain =
|
||||
dyndns_client::get_dyndns_subdomain(&dyndns_fulldomain).unwrap_or(dyndns_fulldomain);
|
||||
|
||||
ConfigureDNSContext {
|
||||
external_domain: peach_config.external_domain,
|
||||
dyndns_subdomain,
|
||||
enable_dyndns: peach_config.dyn_enabled,
|
||||
is_dyndns_online,
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
2
peach-web/src/context/mod.rs
Normal file
2
peach-web/src/context/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod dns;
|
||||
pub mod network;
|
398
peach-web/src/context/network.rs
Normal file
398
peach-web/src/context/network.rs
Normal file
@ -0,0 +1,398 @@
|
||||
//! Data retrieval for the purpose of serving routes and hydrating
|
||||
//! network-related HTML templates.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rocket::{
|
||||
form::FromForm,
|
||||
serde::{Deserialize, Serialize},
|
||||
UriDisplayQuery,
|
||||
};
|
||||
|
||||
use peach_network::{
|
||||
network,
|
||||
network::{Scan, Status, Traffic},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
utils::{
|
||||
monitor,
|
||||
monitor::{Alert, Data, Threshold},
|
||||
},
|
||||
AP_IFACE, WLAN_IFACE,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AccessPoint {
|
||||
pub detail: Option<Scan>,
|
||||
pub signal: Option<i32>,
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
pub fn ap_state() -> String {
|
||||
match network::state(&*AP_IFACE) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm, UriDisplayQuery)]
|
||||
pub struct Ssid {
|
||||
pub ssid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct WiFi {
|
||||
pub ssid: String,
|
||||
pub pass: String,
|
||||
}
|
||||
|
||||
fn convert_traffic(traffic: Traffic) -> Option<IfaceTraffic> {
|
||||
// modify traffic values & assign measurement unit
|
||||
// based on received and transmitted values
|
||||
let (rx, rx_unit) = if traffic.received > 1_047_527_424 {
|
||||
// convert to GB
|
||||
(traffic.received / 1_073_741_824, "GB".to_string())
|
||||
} else if traffic.received > 0 {
|
||||
// otherwise, convert it to MB
|
||||
((traffic.received / 1024) / 1024, "MB".to_string())
|
||||
} else {
|
||||
(0, "MB".to_string())
|
||||
};
|
||||
|
||||
let (tx, tx_unit) = if traffic.transmitted > 1_047_527_424 {
|
||||
// convert to GB
|
||||
(traffic.transmitted / 1_073_741_824, "GB".to_string())
|
||||
} else if traffic.transmitted > 0 {
|
||||
((traffic.transmitted / 1024) / 1024, "MB".to_string())
|
||||
} else {
|
||||
(0, "MB".to_string())
|
||||
};
|
||||
|
||||
Some(IfaceTraffic {
|
||||
rx,
|
||||
rx_unit,
|
||||
tx,
|
||||
tx_unit,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IfaceTraffic {
|
||||
pub rx: u64,
|
||||
pub rx_unit: String,
|
||||
pub tx: u64,
|
||||
pub tx_unit: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NetworkAlertContext {
|
||||
pub alert: Alert,
|
||||
pub back: Option<String>,
|
||||
pub data_total: Option<Data>, // combined stored and current wifi traffic in bytes
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub threshold: Threshold,
|
||||
pub title: Option<String>,
|
||||
pub traffic: Option<IfaceTraffic>, // current wifi traffic in bytes (since boot)
|
||||
}
|
||||
|
||||
impl NetworkAlertContext {
|
||||
pub fn build() -> NetworkAlertContext {
|
||||
let alert = monitor::get_alerts().unwrap();
|
||||
// stored wifi data values as bytes
|
||||
let stored_traffic = monitor::get_data().unwrap();
|
||||
let threshold = monitor::get_thresholds().unwrap();
|
||||
|
||||
let (traffic, data_total) = match network::traffic(&*WLAN_IFACE) {
|
||||
// convert bytes to mb or gb and add appropriate units
|
||||
Ok(Some(t)) => {
|
||||
let current_traffic = t.received + t.transmitted;
|
||||
let traffic = convert_traffic(t);
|
||||
let total = stored_traffic.total + current_traffic;
|
||||
let data_total = Data { total };
|
||||
(traffic, Some(data_total))
|
||||
}
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
NetworkAlertContext {
|
||||
alert,
|
||||
back: None,
|
||||
data_total,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
threshold,
|
||||
title: None,
|
||||
traffic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NetworkDetailContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub selected: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub saved_aps: Vec<String>,
|
||||
pub wlan_ip: String,
|
||||
pub wlan_networks: HashMap<String, AccessPoint>,
|
||||
pub wlan_rssi: Option<String>,
|
||||
pub wlan_ssid: String,
|
||||
pub wlan_state: String,
|
||||
pub wlan_status: Option<Status>,
|
||||
pub wlan_traffic: Option<IfaceTraffic>,
|
||||
}
|
||||
|
||||
impl NetworkDetailContext {
|
||||
pub fn build() -> NetworkDetailContext {
|
||||
let wlan_ip = match network::ip(&*WLAN_IFACE) {
|
||||
Ok(Some(ip)) => ip,
|
||||
_ => "x.x.x.x".to_string(),
|
||||
};
|
||||
|
||||
// list of networks saved in wpa_supplicant.conf
|
||||
let wlan_list = match network::saved_networks() {
|
||||
Ok(Some(ssids)) => ssids,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
// list of networks saved in wpa_supplicant.conf
|
||||
let saved_aps = wlan_list.clone();
|
||||
|
||||
let wlan_rssi = match network::rssi_percent(&*WLAN_IFACE) {
|
||||
Ok(rssi) => rssi,
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
// list of networks currently in range (online & accessible)
|
||||
let wlan_scan = match network::available_networks(&*WLAN_IFACE) {
|
||||
Ok(Some(networks)) => networks,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
let wlan_ssid = match network::ssid(&*WLAN_IFACE) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not connected".to_string(),
|
||||
};
|
||||
|
||||
let wlan_state = match network::state(&*WLAN_IFACE) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
|
||||
let wlan_status = match network::status(&*WLAN_IFACE) {
|
||||
Ok(status) => status,
|
||||
// interface unavailable
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let wlan_traffic = match network::traffic(&*WLAN_IFACE) {
|
||||
// convert bytes to mb or gb and add appropriate units
|
||||
Ok(Some(traffic)) => convert_traffic(traffic),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// create a hashmap to combine wlan_list & wlan_scan without repetition
|
||||
let mut wlan_networks = HashMap::new();
|
||||
|
||||
for ap in wlan_scan {
|
||||
let ssid = ap.ssid.clone();
|
||||
let rssi = ap.signal_level.clone();
|
||||
// parse the string to a signed integer (for math)
|
||||
let rssi_parsed = rssi.parse::<i32>().unwrap();
|
||||
// perform rssi (dBm) to quality (%) conversion
|
||||
let quality_percent = 2 * (rssi_parsed + 100);
|
||||
let ap_detail = AccessPoint {
|
||||
detail: Some(ap),
|
||||
state: "Available".to_string(),
|
||||
signal: Some(quality_percent),
|
||||
};
|
||||
wlan_networks.insert(ssid, ap_detail);
|
||||
}
|
||||
|
||||
for network in wlan_list {
|
||||
// avoid repetition by checking that ssid is not already in list
|
||||
if !wlan_networks.contains_key(&network) {
|
||||
let ssid = network.clone();
|
||||
let net_detail = AccessPoint {
|
||||
detail: None,
|
||||
state: "Not in range".to_string(),
|
||||
signal: None,
|
||||
};
|
||||
wlan_networks.insert(ssid, net_detail);
|
||||
}
|
||||
}
|
||||
|
||||
NetworkDetailContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
selected: None,
|
||||
title: None,
|
||||
saved_aps,
|
||||
wlan_ip,
|
||||
wlan_networks,
|
||||
wlan_rssi,
|
||||
wlan_ssid,
|
||||
wlan_state,
|
||||
wlan_status,
|
||||
wlan_traffic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NetworkListContext {
|
||||
pub ap_state: String,
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub wlan_networks: HashMap<String, String>,
|
||||
pub wlan_ssid: String,
|
||||
}
|
||||
|
||||
impl NetworkListContext {
|
||||
pub fn build() -> NetworkListContext {
|
||||
// list of networks saved in wpa_supplicant.conf
|
||||
let wlan_list = match network::saved_networks() {
|
||||
Ok(Some(ssids)) => ssids,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
// list of networks currently in range (online & accessible)
|
||||
let wlan_scan = match network::available_networks(&*WLAN_IFACE) {
|
||||
Ok(Some(networks)) => networks,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
let wlan_ssid = match network::ssid(&*WLAN_IFACE) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not connected".to_string(),
|
||||
};
|
||||
|
||||
// create a hashmap to combine wlan_list & wlan_scan without repetition
|
||||
let mut wlan_networks = HashMap::new();
|
||||
for ap in wlan_scan {
|
||||
wlan_networks.insert(ap.ssid, "Available".to_string());
|
||||
}
|
||||
for network in wlan_list {
|
||||
// insert ssid (with state) only if it doesn't already exist
|
||||
wlan_networks
|
||||
.entry(network)
|
||||
.or_insert_with(|| "Not in range".to_string());
|
||||
}
|
||||
|
||||
let ap_state = match network::state(&*AP_IFACE) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
|
||||
NetworkListContext {
|
||||
ap_state,
|
||||
back: None,
|
||||
flash_msg: None,
|
||||
flash_name: None,
|
||||
title: None,
|
||||
wlan_networks,
|
||||
wlan_ssid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NetworkStatusContext {
|
||||
pub ap_ip: String,
|
||||
pub ap_ssid: String,
|
||||
pub ap_state: String,
|
||||
pub ap_traffic: Option<IfaceTraffic>,
|
||||
pub wlan_ip: String,
|
||||
pub wlan_rssi: Option<String>,
|
||||
pub wlan_ssid: String,
|
||||
pub wlan_state: String,
|
||||
pub wlan_status: Option<Status>,
|
||||
pub wlan_traffic: Option<IfaceTraffic>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
// passing in the ssid of a chosen access point
|
||||
pub selected: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub back: Option<String>,
|
||||
}
|
||||
|
||||
impl NetworkStatusContext {
|
||||
pub fn build() -> Self {
|
||||
let ap_ip = match network::ip(&*AP_IFACE) {
|
||||
Ok(Some(ip)) => ip,
|
||||
_ => "x.x.x.x".to_string(),
|
||||
};
|
||||
|
||||
let ap_ssid = match network::ssid(&*AP_IFACE) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not currently activated".to_string(),
|
||||
};
|
||||
|
||||
let ap_state = match network::state(&*AP_IFACE) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
|
||||
let ap_traffic = match network::traffic(&*AP_IFACE) {
|
||||
// convert bytes to mb or gb and add appropriate units
|
||||
Ok(Some(traffic)) => convert_traffic(traffic),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let wlan_ip = match network::ip(&*WLAN_IFACE) {
|
||||
Ok(Some(ip)) => ip,
|
||||
_ => "x.x.x.x".to_string(),
|
||||
};
|
||||
|
||||
let wlan_rssi = match network::rssi_percent(&*WLAN_IFACE) {
|
||||
Ok(rssi) => rssi,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let wlan_ssid = match network::ssid(&*WLAN_IFACE) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not connected".to_string(),
|
||||
};
|
||||
|
||||
let wlan_state = match network::state(&*WLAN_IFACE) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
|
||||
let wlan_status = match network::status(&*WLAN_IFACE) {
|
||||
Ok(status) => status,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let wlan_traffic = match network::traffic(&*WLAN_IFACE) {
|
||||
// convert bytes to mb or gb and add appropriate units
|
||||
Ok(Some(traffic)) => convert_traffic(traffic),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
NetworkStatusContext {
|
||||
ap_ip,
|
||||
ap_ssid,
|
||||
ap_state,
|
||||
ap_traffic,
|
||||
wlan_ip,
|
||||
wlan_rssi,
|
||||
wlan_ssid,
|
||||
wlan_state,
|
||||
wlan_status,
|
||||
wlan_traffic,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
selected: None,
|
||||
title: None,
|
||||
back: None,
|
||||
}
|
||||
}
|
||||
}
|
@ -2,35 +2,57 @@
|
||||
|
||||
use peach_lib::error::PeachError;
|
||||
use peach_lib::{serde_json, serde_yaml};
|
||||
use snafu::Snafu;
|
||||
use serde_json::error::Error as JsonError;
|
||||
use serde_yaml::Error as YamlError;
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
/// Custom error type encapsulating all possible errors for the web application.
|
||||
#[derive(Debug)]
|
||||
pub enum PeachWebError {
|
||||
#[snafu(display("Error loading serde json"))]
|
||||
Serde { source: serde_json::error::Error },
|
||||
#[snafu(display("Error loading peach-config yaml"))]
|
||||
YamlError { source: serde_yaml::Error },
|
||||
#[snafu(display("{}", msg))]
|
||||
FailedToRegisterDynDomain { msg: String },
|
||||
#[snafu(display("{}: {}", source, msg))]
|
||||
PeachLibError { source: PeachError, msg: String },
|
||||
Json(JsonError),
|
||||
Yaml(YamlError),
|
||||
FailedToRegisterDynDomain(String),
|
||||
PeachLib { source: PeachError, msg: String },
|
||||
}
|
||||
|
||||
impl From<serde_json::error::Error> for PeachWebError {
|
||||
fn from(err: serde_json::error::Error) -> PeachWebError {
|
||||
PeachWebError::Serde { source: err }
|
||||
impl std::error::Error for PeachWebError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match *self {
|
||||
PeachWebError::Json(ref source) => Some(source),
|
||||
PeachWebError::Yaml(ref source) => Some(source),
|
||||
PeachWebError::FailedToRegisterDynDomain(_) => None,
|
||||
PeachWebError::PeachLib { ref source, .. } => Some(source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_yaml::Error> for PeachWebError {
|
||||
fn from(err: serde_yaml::Error) -> PeachWebError {
|
||||
PeachWebError::YamlError { source: err }
|
||||
impl std::fmt::Display for PeachWebError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match *self {
|
||||
PeachWebError::Json(ref source) => write!(f, "Serde JSON error: {}", source),
|
||||
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
|
||||
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
|
||||
write!(f, "DYN DNS error: {}", msg)
|
||||
}
|
||||
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonError> for PeachWebError {
|
||||
fn from(err: JsonError) -> PeachWebError {
|
||||
PeachWebError::Json(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<YamlError> for PeachWebError {
|
||||
fn from(err: YamlError) -> PeachWebError {
|
||||
PeachWebError::Yaml(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PeachError> for PeachWebError {
|
||||
fn from(err: PeachError) -> PeachWebError {
|
||||
PeachWebError::PeachLibError {
|
||||
PeachWebError::PeachLib {
|
||||
source: err,
|
||||
msg: "".to_string(),
|
||||
}
|
||||
|
@ -24,145 +24,59 @@
|
||||
|
||||
#![feature(proc_macro_hygiene, decl_macro)]
|
||||
|
||||
mod context;
|
||||
pub mod error;
|
||||
mod router;
|
||||
pub mod routes;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod utils;
|
||||
|
||||
use log::{error, info};
|
||||
use std::process;
|
||||
|
||||
use rocket::{catchers, fs::FileServer, routes, Build, Rocket};
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use crate::routes::authentication::*;
|
||||
use crate::routes::catchers::*;
|
||||
use crate::routes::index::*;
|
||||
use crate::routes::scuttlebutt::*;
|
||||
use crate::routes::status::device::*;
|
||||
use crate::routes::status::network::*;
|
||||
use crate::routes::status::ping::*;
|
||||
|
||||
use crate::routes::settings::admin::*;
|
||||
use crate::routes::settings::dns::*;
|
||||
use crate::routes::settings::menu::*;
|
||||
use crate::routes::settings::network::*;
|
||||
use crate::routes::settings::scuttlebutt::*;
|
||||
use log::{debug, error, info};
|
||||
use rocket::{fairing::AdHoc, serde::Deserialize, Build, Rocket};
|
||||
|
||||
pub type BoxError = Box<dyn std::error::Error>;
|
||||
|
||||
/// Create rocket instance & mount all routes.
|
||||
fn init_rocket() -> Rocket<Build> {
|
||||
rocket::build()
|
||||
// GENERAL HTML ROUTES
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
help,
|
||||
home,
|
||||
login,
|
||||
login_post,
|
||||
logout,
|
||||
reboot_cmd,
|
||||
shutdown_cmd,
|
||||
power_menu,
|
||||
settings_menu,
|
||||
],
|
||||
)
|
||||
// STATUS HTML ROUTES
|
||||
.mount("/status", routes![device_status, network_status])
|
||||
// ADMIN SETTINGS HTML ROUTES
|
||||
.mount(
|
||||
"/settings/admin",
|
||||
routes![
|
||||
admin_menu,
|
||||
configure_admin,
|
||||
add_admin,
|
||||
add_admin_post,
|
||||
delete_admin_post,
|
||||
change_password,
|
||||
change_password_post,
|
||||
reset_password,
|
||||
reset_password_post,
|
||||
forgot_password_page,
|
||||
send_password_reset_post,
|
||||
],
|
||||
)
|
||||
// NETWORK SETTINGS HTML ROUTES
|
||||
.mount(
|
||||
"/settings/network",
|
||||
routes![
|
||||
add_credentials,
|
||||
connect_wifi,
|
||||
configure_dns,
|
||||
configure_dns_post,
|
||||
disconnect_wifi,
|
||||
deploy_ap,
|
||||
deploy_client,
|
||||
forget_wifi,
|
||||
network_home,
|
||||
add_ssid,
|
||||
add_wifi,
|
||||
network_detail,
|
||||
wifi_list,
|
||||
wifi_password,
|
||||
wifi_set_password,
|
||||
wifi_usage,
|
||||
wifi_usage_alerts,
|
||||
wifi_usage_reset,
|
||||
],
|
||||
)
|
||||
// SCUTTLEBUTT SETTINGS HTML ROUTES
|
||||
.mount("/settings/scuttlebutt", routes![ssb_settings_menu])
|
||||
// SCUTTLEBUTT SOCIAL HTML ROUTES
|
||||
.mount(
|
||||
"/scuttlebutt",
|
||||
routes![
|
||||
peers, friends, follows, followers, blocks, profile, private, follow, unfollow,
|
||||
block, publish,
|
||||
],
|
||||
)
|
||||
// GENERAL JSON API ROUTES
|
||||
.mount(
|
||||
"/api/v1",
|
||||
routes![ping_pong, ping_network, ping_oled, ping_stats,],
|
||||
)
|
||||
// ADMIN JSON API ROUTES
|
||||
.mount(
|
||||
"/api/v1/admin",
|
||||
routes![
|
||||
save_password_form_endpoint,
|
||||
reset_password_form_endpoint,
|
||||
reboot_device,
|
||||
shutdown_device,
|
||||
],
|
||||
)
|
||||
// NETWORK JSON API ROUTES
|
||||
.mount(
|
||||
"/api/v1/network",
|
||||
routes![
|
||||
activate_ap,
|
||||
activate_client,
|
||||
add_wifi_credentials,
|
||||
connect_ap,
|
||||
disconnect_ap,
|
||||
forget_ap,
|
||||
modify_password,
|
||||
reset_data_total,
|
||||
return_ip,
|
||||
return_rssi,
|
||||
return_ssid,
|
||||
return_state,
|
||||
return_status,
|
||||
scan_networks,
|
||||
update_wifi_alerts,
|
||||
save_dns_configuration_endpoint,
|
||||
],
|
||||
)
|
||||
.mount("/", FileServer::from("static"))
|
||||
.register("/", catchers![not_found, internal_error, forbidden])
|
||||
.attach(Template::fairing())
|
||||
/// Application configuration parameters.
|
||||
/// These values are extracted from Rocket's default configuration provider:
|
||||
/// `Config::figment()`. As such, the values are drawn from `Rocket.toml` or
|
||||
/// the TOML file path in the `ROCKET_CONFIG` environment variable. The TOML
|
||||
/// file parameters are automatically overruled by any `ROCKET_` variables
|
||||
/// which might be set.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RocketConfig {
|
||||
disable_auth: bool,
|
||||
standalone_mode: bool,
|
||||
}
|
||||
|
||||
static WLAN_IFACE: &str = "wlan0";
|
||||
static AP_IFACE: &str = "ap0";
|
||||
|
||||
pub fn init_rocket() -> Rocket<Build> {
|
||||
info!("Initializing Rocket");
|
||||
// build a basic rocket instance
|
||||
let rocket = rocket::build();
|
||||
|
||||
// return the default provider figment used by `rocket::build()`
|
||||
let figment = rocket.figment();
|
||||
|
||||
// deserialize configuration parameters into our `RocketConfig` struct (defined above)
|
||||
// since we're in the intialisation phase, panic if the extraction fails
|
||||
let config: RocketConfig = figment.extract().expect("configuration extraction failed");
|
||||
|
||||
debug!("{:?}", config);
|
||||
|
||||
info!("Mounting Rocket routes");
|
||||
let mounted_rocket = if config.standalone_mode {
|
||||
router::mount_peachpub_routes(rocket)
|
||||
} else {
|
||||
router::mount_peachcloud_routes(rocket)
|
||||
};
|
||||
|
||||
info!("Attaching application configuration to managed state");
|
||||
mounted_rocket.attach(AdHoc::config::<RocketConfig>())
|
||||
}
|
||||
|
||||
/// Launch the peach-web rocket server.
|
||||
@ -172,7 +86,6 @@ async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
// initialize rocket
|
||||
info!("Initializing Rocket");
|
||||
let rocket = init_rocket();
|
||||
|
||||
// launch rocket
|
||||
|
95
peach-web/src/router.rs
Normal file
95
peach-web/src/router.rs
Normal file
@ -0,0 +1,95 @@
|
||||
use rocket::{catchers, fs::FileServer, routes, Build, Rocket};
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use crate::routes::{
|
||||
authentication::*,
|
||||
catchers::*,
|
||||
index::*,
|
||||
scuttlebutt::*,
|
||||
settings::{admin::*, dns::*, menu::*, network::*, scuttlebutt::*},
|
||||
status::{device::*, network::*, scuttlebutt::*},
|
||||
};
|
||||
|
||||
/// Create a Rocket instance and mount PeachPub routes, fileserver and
|
||||
/// catchers. This gives us everything we need to run PeachPub and excludes
|
||||
/// settings and status routes related to networking and the device (memory,
|
||||
/// hard disk, CPU etc.).
|
||||
pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
rocket
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
help,
|
||||
home,
|
||||
login,
|
||||
login_post,
|
||||
logout,
|
||||
reboot_cmd,
|
||||
shutdown_cmd,
|
||||
power_menu,
|
||||
settings_menu,
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
"/settings/admin",
|
||||
routes![
|
||||
admin_menu,
|
||||
configure_admin,
|
||||
add_admin,
|
||||
add_admin_post,
|
||||
delete_admin_post,
|
||||
change_password,
|
||||
change_password_post,
|
||||
reset_password,
|
||||
reset_password_post,
|
||||
forgot_password_page,
|
||||
send_password_reset_post,
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
"/settings/scuttlebutt",
|
||||
routes![ssb_settings_menu, configure_sbot],
|
||||
)
|
||||
.mount(
|
||||
"/scuttlebutt",
|
||||
routes![
|
||||
peers, friends, follows, followers, blocks, profile, private, follow, unfollow,
|
||||
block, publish,
|
||||
],
|
||||
)
|
||||
.mount("/status", routes![scuttlebutt_status])
|
||||
.mount("/", FileServer::from("static"))
|
||||
.register("/", catchers![not_found, internal_error, forbidden])
|
||||
.attach(Template::fairing())
|
||||
}
|
||||
|
||||
/// Create a Rocket instance with PeachPub routes, fileserver and catchers by
|
||||
/// calling `mount_peachpub_routes()` and then mount all additional routes
|
||||
/// required to run a complete PeachCloud build.
|
||||
pub fn mount_peachcloud_routes(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
mount_peachpub_routes(rocket)
|
||||
.mount(
|
||||
"/settings/network",
|
||||
routes![
|
||||
add_credentials,
|
||||
connect_wifi,
|
||||
configure_dns,
|
||||
configure_dns_post,
|
||||
disconnect_wifi,
|
||||
deploy_ap,
|
||||
deploy_client,
|
||||
forget_wifi,
|
||||
network_home,
|
||||
add_ssid,
|
||||
add_wifi,
|
||||
network_detail,
|
||||
wifi_list,
|
||||
wifi_password,
|
||||
wifi_set_password,
|
||||
wifi_usage,
|
||||
wifi_usage_alerts,
|
||||
wifi_usage_reset,
|
||||
],
|
||||
)
|
||||
.mount("/status", routes![device_status, network_status])
|
||||
}
|
@ -1,20 +1,21 @@
|
||||
use log::info;
|
||||
use rocket::form::{Form, FromForm};
|
||||
use rocket::request::FlashMessage;
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::{get, post};
|
||||
use rocket_dyn_templates::Template;
|
||||
use rocket::{
|
||||
form::{Form, FromForm},
|
||||
get,
|
||||
http::{Cookie, CookieJar, Status},
|
||||
post,
|
||||
request::{self, FlashMessage, FromRequest, Request},
|
||||
response::{Flash, Redirect},
|
||||
serde::Deserialize,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
|
||||
use peach_lib::error::PeachError;
|
||||
use peach_lib::password_utils;
|
||||
use peach_lib::{error::PeachError, password_utils};
|
||||
|
||||
use crate::error::PeachWebError;
|
||||
use crate::utils::{build_json_response, TemplateOrRedirect};
|
||||
use rocket::http::{Cookie, CookieJar, Status};
|
||||
use rocket::request::{self, FromRequest, Request};
|
||||
use rocket::serde::json::Value;
|
||||
use crate::utils::TemplateOrRedirect;
|
||||
//use crate::DisableAuth;
|
||||
use crate::RocketConfig;
|
||||
|
||||
// HELPERS AND STRUCTS FOR AUTHENTICATION WITH COOKIES
|
||||
|
||||
@ -42,51 +43,46 @@ impl<'r> FromRequest<'r> for Authenticated {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let authenticated = req
|
||||
.cookies()
|
||||
.get_private(AUTH_COOKIE_KEY)
|
||||
.and_then(|cookie| cookie.value().parse().ok())
|
||||
.map(|_value: String| Authenticated {});
|
||||
match authenticated {
|
||||
Some(auth) => request::Outcome::Success(auth),
|
||||
None => request::Outcome::Failure((Status::Forbidden, LoginError::UserNotLoggedIn)),
|
||||
// retrieve auth state from managed state (returns `Option<bool>`).
|
||||
// this value is read from the Rocket.toml config file on start-up
|
||||
let authentication_is_disabled: bool = *req
|
||||
.rocket()
|
||||
.state::<RocketConfig>()
|
||||
.map(|config| (&config.disable_auth))
|
||||
.unwrap_or(&false);
|
||||
|
||||
if authentication_is_disabled {
|
||||
let auth = Authenticated {};
|
||||
request::Outcome::Success(auth)
|
||||
} else {
|
||||
let authenticated = req
|
||||
.cookies()
|
||||
.get_private(AUTH_COOKIE_KEY)
|
||||
.and_then(|cookie| cookie.value().parse().ok())
|
||||
.map(|_value: String| Authenticated {});
|
||||
match authenticated {
|
||||
Some(auth) => request::Outcome::Success(auth),
|
||||
None => request::Outcome::Failure((Status::Forbidden, LoginError::UserNotLoggedIn)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /login
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct LoginContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl LoginContext {
|
||||
pub fn build() -> LoginContext {
|
||||
LoginContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/login")]
|
||||
pub fn login(flash: Option<FlashMessage>) -> Template {
|
||||
let mut context = LoginContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Login".to_string());
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Login".to_string()));
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
Template::render("login", &context)
|
||||
|
||||
Template::render("login", &context.into_json())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
@ -106,8 +102,7 @@ pub fn verify_login_form(login_form: LoginForm) -> Result<(), PeachError> {
|
||||
|
||||
#[post("/login", data = "<login_form>")]
|
||||
pub fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) -> TemplateOrRedirect {
|
||||
let result = verify_login_form(login_form.into_inner());
|
||||
match result {
|
||||
match verify_login_form(login_form.into_inner()) {
|
||||
Ok(_) => {
|
||||
// if successful login, add a cookie indicating the user is authenticated
|
||||
// and redirect to home page
|
||||
@ -115,17 +110,18 @@ pub fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) -> Templ
|
||||
// is just admin (this is arbitrary).
|
||||
// If we had multiple users, we could put the user_id here.
|
||||
cookies.add_private(Cookie::new(AUTH_COOKIE_KEY, ADMIN_USERNAME));
|
||||
|
||||
TemplateOrRedirect::Redirect(Redirect::to("/"))
|
||||
}
|
||||
Err(_) => {
|
||||
// if unsuccessful login, render /login page again
|
||||
let mut context = LoginContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Login".to_string());
|
||||
context.flash_name = Some("error".to_string());
|
||||
let flash_msg = "Invalid password".to_string();
|
||||
context.flash_msg = Some(flash_msg);
|
||||
TemplateOrRedirect::Template(Template::render("login", &context))
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Login".to_string()));
|
||||
context.insert("flash_name", &("error".to_string()));
|
||||
context.insert("flash_msg", &("Invalid password".to_string()));
|
||||
|
||||
TemplateOrRedirect::Template(Template::render("login", &context.into_json()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -149,44 +145,6 @@ pub struct ResetPasswordForm {
|
||||
pub new_password2: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ResetPasswordContext {
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl ResetPasswordContext {
|
||||
pub fn build() -> ResetPasswordContext {
|
||||
ResetPasswordContext {
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ChangePasswordContext {
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl ChangePasswordContext {
|
||||
pub fn build() -> ChangePasswordContext {
|
||||
ChangePasswordContext {
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify, validate and save the submitted password. This function is publicly exposed for users who have forgotten their password.
|
||||
pub fn save_reset_password_form(password_form: ResetPasswordForm) -> Result<(), PeachWebError> {
|
||||
info!(
|
||||
@ -208,100 +166,63 @@ pub fn save_reset_password_form(password_form: ResetPasswordForm) -> Result<(),
|
||||
/// and is specifically for users who have forgotten their password.
|
||||
#[get("/reset_password")]
|
||||
pub fn reset_password(flash: Option<FlashMessage>) -> Template {
|
||||
let mut context = ResetPasswordContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Reset Password".to_string());
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Reset Password".to_string()));
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
Template::render("settings/admin/reset_password", &context)
|
||||
|
||||
Template::render("settings/admin/reset_password", &context.into_json())
|
||||
}
|
||||
|
||||
/// Password reset form request handler. This route is used by a user who is not logged in
|
||||
/// and is specifically for users who have forgotten their password.
|
||||
#[post("/reset_password", data = "<reset_password_form>")]
|
||||
pub fn reset_password_post(reset_password_form: Form<ResetPasswordForm>) -> Template {
|
||||
let result = save_reset_password_form(reset_password_form.into_inner());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Reset Password".to_string());
|
||||
context.flash_name = Some("success".to_string());
|
||||
let flash_msg = "New password is now saved. Return home to login".to_string();
|
||||
context.flash_msg = Some(flash_msg);
|
||||
Template::render("settings/admin/reset_password", &context)
|
||||
}
|
||||
Err(err) => {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Reset Password".to_string());
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some(format!("Failed to reset password: {}", err));
|
||||
Template::render("settings/admin/reset_password", &context)
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Reset Password".to_string()));
|
||||
|
||||
/// JSON password reset form request handler. This route is used by a user who is not logged in
|
||||
/// and is specifically for users who have forgotten their password.
|
||||
#[post("/reset_password", data = "<reset_password_form>")]
|
||||
pub fn reset_password_form_endpoint(reset_password_form: Json<ResetPasswordForm>) -> Value {
|
||||
let result = save_reset_password_form(reset_password_form.into_inner());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let status = "success".to_string();
|
||||
let msg = "New password is now saved. Return home to login.".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
Err(err) => {
|
||||
let status = "error".to_string();
|
||||
let msg = format!("{}", err);
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
}
|
||||
let (flash_name, flash_msg) = match save_reset_password_form(reset_password_form.into_inner()) {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"New password is now saved. Return home to login".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to reset password: {}", err),
|
||||
),
|
||||
};
|
||||
|
||||
context.insert("flash_name", &Some(flash_name));
|
||||
context.insert("flash_msg", &Some(flash_msg));
|
||||
|
||||
Template::render("settings/admin/reset_password", &context.into_json())
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /send_password_reset
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SendPasswordResetContext {
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl SendPasswordResetContext {
|
||||
pub fn build() -> SendPasswordResetContext {
|
||||
SendPasswordResetContext {
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Page for users who have forgotten their password.
|
||||
/// This route is used by a user who is not logged in
|
||||
/// to initiate the sending of a new password reset.
|
||||
#[get("/forgot_password")]
|
||||
pub fn forgot_password_page(flash: Option<FlashMessage>) -> Template {
|
||||
let mut context = SendPasswordResetContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Send Password Reset".to_string());
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Send Password Reset".to_string()));
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
Template::render("settings/admin/forgot_password", &context)
|
||||
|
||||
Template::render("settings/admin/forgot_password", &context.into_json())
|
||||
}
|
||||
|
||||
/// Send password reset request handler. This route is used by a user who is not logged in
|
||||
@ -310,27 +231,25 @@ pub fn forgot_password_page(flash: Option<FlashMessage>) -> Template {
|
||||
#[post("/send_password_reset")]
|
||||
pub fn send_password_reset_post() -> Template {
|
||||
info!("++ send password reset post");
|
||||
let result = password_utils::send_password_reset();
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Send Password Reset".to_string());
|
||||
context.flash_name = Some("success".to_string());
|
||||
let flash_msg =
|
||||
"A password reset link has been sent to the admin of this device".to_string();
|
||||
context.flash_msg = Some(flash_msg);
|
||||
Template::render("settings/admin/forgot_password", &context)
|
||||
}
|
||||
Err(err) => {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Send Password Reset".to_string());
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some(format!("Failed to send password reset link: {}", err));
|
||||
Template::render("settings/admin/forgot_password", &context)
|
||||
}
|
||||
}
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Send Password Reset".to_string()));
|
||||
|
||||
let (flash_name, flash_msg) = match password_utils::send_password_reset() {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"A password reset link has been sent to the admin of this device".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to send password reset link: {}", err),
|
||||
),
|
||||
};
|
||||
|
||||
context.insert("flash_name", &Some(flash_name));
|
||||
context.insert("flash_msg", &Some(flash_msg));
|
||||
|
||||
Template::render("settings/admin/forgot_password", &context.into_json())
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/change_password
|
||||
@ -362,63 +281,40 @@ pub fn save_password_form(password_form: PasswordForm) -> Result<(), PeachWebErr
|
||||
/// Change password request handler. This is used by a user who is already logged in.
|
||||
#[get("/change_password")]
|
||||
pub fn change_password(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/settings/admin".to_string());
|
||||
context.title = Some("Change Password".to_string());
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings/admin".to_string()));
|
||||
context.insert("title", &Some("Change Password".to_string()));
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
Template::render("settings/admin/change_password", &context)
|
||||
|
||||
Template::render("settings/admin/change_password", &context.into_json())
|
||||
}
|
||||
|
||||
/// Change password form request handler. This route is used by a user who is already logged in.
|
||||
#[post("/change_password", data = "<password_form>")]
|
||||
pub fn change_password_post(password_form: Form<PasswordForm>, _auth: Authenticated) -> Template {
|
||||
let result = save_password_form(password_form.into_inner());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/settings/admin".to_string());
|
||||
context.title = Some("Change Password".to_string());
|
||||
context.flash_name = Some("success".to_string());
|
||||
context.flash_msg = Some("New password is now saved".to_string());
|
||||
// template_dir is set in Rocket.toml
|
||||
Template::render("settings/admin/change_password", &context)
|
||||
}
|
||||
Err(err) => {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/settings/admin".to_string());
|
||||
context.title = Some("Change Password".to_string());
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some(format!("Failed to save new password: {}", err));
|
||||
Template::render("settings/admin/change_password", &context)
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings/admin".to_string()));
|
||||
context.insert("title", &Some("Change Password".to_string()));
|
||||
|
||||
/// JSON change password form request handler.
|
||||
#[post("/change_password", data = "<password_form>")]
|
||||
pub fn save_password_form_endpoint(
|
||||
password_form: Json<PasswordForm>,
|
||||
_auth: Authenticated,
|
||||
) -> Value {
|
||||
let result = save_password_form(password_form.into_inner());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let status = "success".to_string();
|
||||
let msg = "Your password was successfully changed".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
Err(err) => {
|
||||
let status = "error".to_string();
|
||||
let msg = format!("{}", err);
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
}
|
||||
let (flash_name, flash_msg) = match save_password_form(password_form.into_inner()) {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"New password is now saved".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to save new password: {}", err),
|
||||
),
|
||||
};
|
||||
|
||||
context.insert("flash_name", &Some(flash_name));
|
||||
context.insert("flash_msg", &Some(flash_msg));
|
||||
|
||||
Template::render("settings/admin/change_password", &context.into_json())
|
||||
}
|
||||
|
@ -1,69 +1,37 @@
|
||||
use rocket::{get, request::FlashMessage};
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::Serialize;
|
||||
use rocket::{get, request::FlashMessage, State};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
use crate::RocketConfig;
|
||||
|
||||
// HELPERS AND ROUTES FOR / (HOME PAGE)
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct HomeContext {
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl HomeContext {
|
||||
pub fn build() -> HomeContext {
|
||||
HomeContext {
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub fn home(_auth: Authenticated) -> Template {
|
||||
let context = HomeContext {
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
};
|
||||
Template::render("home", &context)
|
||||
pub fn home(_auth: Authenticated, config: &State<RocketConfig>) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("flash_name", &None::<()>);
|
||||
context.insert("flash_msg", &None::<()>);
|
||||
context.insert("title", &None::<()>);
|
||||
|
||||
// pass in mode from managed state so we can define appropriate urls in template
|
||||
context.insert("standalone_mode", &config.standalone_mode);
|
||||
|
||||
Template::render("home", &context.into_json())
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /help
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct HelpContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl HelpContext {
|
||||
pub fn build() -> HelpContext {
|
||||
HelpContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/help")]
|
||||
pub fn help(flash: Option<FlashMessage>) -> Template {
|
||||
let mut context = HelpContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Help".to_string());
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Help".to_string()));
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
Template::render("help", &context)
|
||||
|
||||
Template::render("help", &context.into_json())
|
||||
}
|
||||
|
@ -1,95 +1,69 @@
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::{
|
||||
form::{Form, FromForm},
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
serde::Deserialize,
|
||||
uri,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
|
||||
use peach_lib::config_manager;
|
||||
use peach_lib::config_manager::load_peach_config;
|
||||
|
||||
use crate::error::PeachWebError;
|
||||
use crate::routes::authentication::Authenticated;
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/admin
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AdminMenuContext {
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl AdminMenuContext {
|
||||
pub fn build() -> AdminMenuContext {
|
||||
AdminMenuContext {
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Administrator settings menu.
|
||||
#[get("/")]
|
||||
pub fn admin_menu(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = AdminMenuContext::build();
|
||||
// set back icon link to settings route
|
||||
context.back = Some("/settings".to_string());
|
||||
context.title = Some("Administrator Settings".to_string());
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings".to_string()));
|
||||
context.insert("title", &Some("Administrator Settings".to_string()));
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
Template::render("settings/admin/menu", &context)
|
||||
|
||||
Template::render("settings/admin/menu", &context.into_json())
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/admin/configure
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ConfigureAdminContext {
|
||||
pub ssb_admin_ids: Vec<String>,
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl ConfigureAdminContext {
|
||||
pub fn build() -> ConfigureAdminContext {
|
||||
let peach_config = load_peach_config().unwrap();
|
||||
let ssb_admin_ids = peach_config.ssb_admin_ids;
|
||||
ConfigureAdminContext {
|
||||
ssb_admin_ids,
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// View and delete currently configured admin.
|
||||
#[get("/configure")]
|
||||
pub fn configure_admin(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = ConfigureAdminContext::build();
|
||||
// set back icon link to settings route
|
||||
context.back = Some("/settings/admin".to_string());
|
||||
context.title = Some("Configure Admin".to_string());
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings/admin".to_string()));
|
||||
context.insert("title", &Some("Configure Admin".to_string()));
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
Template::render("settings/admin/configure_admin", &context)
|
||||
|
||||
// load the peach configuration vector
|
||||
match config_manager::load_peach_config() {
|
||||
Ok(config) => {
|
||||
// retrieve the vector of ssb admin ids
|
||||
let ssb_admin_ids = config.ssb_admin_ids;
|
||||
context.insert("ssb_admin_ids", &ssb_admin_ids);
|
||||
}
|
||||
// if load fails, overwrite the flash_name and flash_msg
|
||||
Err(e) => {
|
||||
context.insert("flash_name", &Some("error".to_string()));
|
||||
context.insert(
|
||||
"flash_msg",
|
||||
&Some(format!("Failed to load Peach config: {}", e)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Template::render("settings/admin/configure_admin", &context.into_json())
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/admin/add
|
||||
@ -99,25 +73,6 @@ pub struct AddAdminForm {
|
||||
pub ssb_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AddAdminContext {
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl AddAdminContext {
|
||||
pub fn build() -> AddAdminContext {
|
||||
AddAdminContext {
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_add_admin_form(admin_form: AddAdminForm) -> Result<(), PeachWebError> {
|
||||
let _result = config_manager::add_ssb_admin_id(&admin_form.ssb_id)?;
|
||||
// if the previous line didn't throw an error then it was a success
|
||||
@ -126,23 +81,24 @@ pub fn save_add_admin_form(admin_form: AddAdminForm) -> Result<(), PeachWebError
|
||||
|
||||
#[get("/add")]
|
||||
pub fn add_admin(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = AddAdminContext::build();
|
||||
context.back = Some("/settings/admin/configure".to_string());
|
||||
context.title = Some("Add Admin".to_string());
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings/admin/configure".to_string()));
|
||||
context.insert("title", &Some("Add Admin".to_string()));
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
|
||||
// template_dir is set in Rocket.toml
|
||||
Template::render("settings/admin/add_admin", &context)
|
||||
Template::render("settings/admin/add_admin", &context.into_json())
|
||||
}
|
||||
|
||||
#[post("/add", data = "<add_admin_form>")]
|
||||
pub fn add_admin_post(add_admin_form: Form<AddAdminForm>, _auth: Authenticated) -> Flash<Redirect> {
|
||||
let result = save_add_admin_form(add_admin_form.into_inner());
|
||||
let url = uri!(configure_admin);
|
||||
let url = uri!("/settings/admin/configure");
|
||||
match result {
|
||||
Ok(_) => Flash::success(Redirect::to(url), "Successfully added new admin"),
|
||||
Err(_) => Flash::error(Redirect::to(url), "Failed to add new admin"),
|
||||
|
@ -3,27 +3,20 @@ use rocket::{
|
||||
form::{Form, FromForm},
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
serde::{
|
||||
json::{Json, Value},
|
||||
Deserialize, Serialize,
|
||||
},
|
||||
serde::Deserialize,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use peach_lib::config_manager;
|
||||
use peach_lib::config_manager::load_peach_config;
|
||||
use peach_lib::dyndns_client;
|
||||
use peach_lib::dyndns_client::{
|
||||
check_is_new_dyndns_domain, get_dyndns_subdomain, get_full_dynamic_domain,
|
||||
is_dns_updater_online,
|
||||
use peach_lib::{
|
||||
config_manager, dyndns_client,
|
||||
error::PeachError,
|
||||
jsonrpc_client_core::{Error, ErrorKind},
|
||||
jsonrpc_core::types::error::ErrorCode,
|
||||
};
|
||||
use peach_lib::error::PeachError;
|
||||
use peach_lib::jsonrpc_client_core::{Error, ErrorKind};
|
||||
use peach_lib::jsonrpc_core::types::error::ErrorCode;
|
||||
|
||||
use crate::error::PeachWebError;
|
||||
use crate::routes::authentication::Authenticated;
|
||||
use crate::utils::build_json_response;
|
||||
use crate::{
|
||||
context::dns::ConfigureDNSContext, error::PeachWebError, routes::authentication::Authenticated,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct DnsForm {
|
||||
@ -36,11 +29,12 @@ pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> {
|
||||
// first save local configurations
|
||||
config_manager::set_external_domain(&dns_form.external_domain)?;
|
||||
config_manager::set_dyndns_enabled_value(dns_form.enable_dyndns)?;
|
||||
|
||||
// if dynamic dns is enabled and this is a new domain name, then register it
|
||||
if dns_form.enable_dyndns {
|
||||
let full_dynamic_domain = get_full_dynamic_domain(&dns_form.dynamic_domain);
|
||||
let full_dynamic_domain = dyndns_client::get_full_dynamic_domain(&dns_form.dynamic_domain);
|
||||
// check if this is a new domain or if its already registered
|
||||
let is_new_domain = check_is_new_dyndns_domain(&full_dynamic_domain);
|
||||
let is_new_domain = dyndns_client::check_is_new_dyndns_domain(&full_dynamic_domain)?;
|
||||
if is_new_domain {
|
||||
match dyndns_client::register_domain(&full_dynamic_domain) {
|
||||
Ok(_) => {
|
||||
@ -52,24 +46,22 @@ pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> {
|
||||
info!("Failed to register dyndns domain: {:?}", err);
|
||||
// json response for failed update
|
||||
let msg: String = match err {
|
||||
PeachError::JsonRpcClientCore { source } => {
|
||||
match source {
|
||||
Error(ErrorKind::JsonRpcError(err), _state) => match err.code {
|
||||
ErrorCode::ServerError(-32030) => {
|
||||
format!("Error registering domain: {} was previously registered", full_dynamic_domain)
|
||||
}
|
||||
_ => {
|
||||
format!("Failed to register dyndns domain {:?}", err)
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
format!("Failed to register dyndns domain: {:?}", source)
|
||||
}
|
||||
PeachError::JsonRpcClientCore(Error(
|
||||
ErrorKind::JsonRpcError(err),
|
||||
_state,
|
||||
)) => {
|
||||
if let ErrorCode::ServerError(-32030) = err.code {
|
||||
format!(
|
||||
"Error registering domain: {} was previously registered",
|
||||
full_dynamic_domain
|
||||
)
|
||||
} else {
|
||||
"Failed to register dyndns domain".to_string()
|
||||
}
|
||||
}
|
||||
_ => "Failed to register dyndns domain".to_string(),
|
||||
};
|
||||
Err(PeachWebError::FailedToRegisterDynDomain { msg })
|
||||
Err(PeachWebError::FailedToRegisterDynDomain(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,91 +74,44 @@ pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ConfigureDNSContext {
|
||||
pub external_domain: String,
|
||||
pub dyndns_subdomain: String,
|
||||
pub enable_dyndns: bool,
|
||||
pub is_dyndns_online: bool,
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl ConfigureDNSContext {
|
||||
pub fn build() -> ConfigureDNSContext {
|
||||
let peach_config = load_peach_config().unwrap();
|
||||
let dyndns_fulldomain = peach_config.dyn_domain;
|
||||
let is_dyndns_online = is_dns_updater_online().unwrap();
|
||||
let dyndns_subdomain =
|
||||
get_dyndns_subdomain(&dyndns_fulldomain).unwrap_or(dyndns_fulldomain);
|
||||
ConfigureDNSContext {
|
||||
external_domain: peach_config.external_domain,
|
||||
dyndns_subdomain,
|
||||
enable_dyndns: peach_config.dyn_enabled,
|
||||
is_dyndns_online,
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/dns")]
|
||||
pub fn configure_dns(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = ConfigureDNSContext::build();
|
||||
|
||||
// set back icon link to network route
|
||||
context.back = Some("/settings/network".to_string());
|
||||
context.title = Some("Configure DNS".to_string());
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
|
||||
Template::render("settings/network/configure_dns", &context)
|
||||
}
|
||||
|
||||
#[post("/dns", data = "<dns>")]
|
||||
pub fn configure_dns_post(dns: Form<DnsForm>, _auth: Authenticated) -> Template {
|
||||
let result = save_dns_configuration(dns.into_inner());
|
||||
|
||||
let mut context = ConfigureDNSContext::build();
|
||||
|
||||
// set back icon link to network route
|
||||
context.back = Some("/settings/network".to_string());
|
||||
context.title = Some("Configure DNS".to_string());
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let mut context = ConfigureDNSContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/settings/network".to_string());
|
||||
context.title = Some("Configure DNS".to_string());
|
||||
context.flash_name = Some("success".to_string());
|
||||
context.flash_msg = Some("New dynamic dns configuration is now enabled".to_string());
|
||||
Template::render("settings/network/configure_dns", &context)
|
||||
}
|
||||
Err(err) => {
|
||||
let mut context = ConfigureDNSContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/settings/network".to_string());
|
||||
context.title = Some("Configure DNS".to_string());
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some(format!("Failed to save dns configurations: {}", err));
|
||||
Template::render("settings/network/configure_dns", &context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/dns/configure", data = "<dns_form>")]
|
||||
pub fn save_dns_configuration_endpoint(dns_form: Json<DnsForm>, _auth: Authenticated) -> Value {
|
||||
let result = save_dns_configuration(dns_form.into_inner());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let status = "success".to_string();
|
||||
let msg = "New dynamic dns configuration is now enabled".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
Err(err) => {
|
||||
let status = "error".to_string();
|
||||
let msg = format!("{}", err);
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
}
|
||||
Template::render("settings/network/configure_dns", &context)
|
||||
}
|
||||
|
@ -1,41 +1,22 @@
|
||||
use rocket::{get, request::FlashMessage, serde::Serialize};
|
||||
use rocket_dyn_templates::Template;
|
||||
use rocket::{get, request::FlashMessage};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SettingsMenuContext {
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl SettingsMenuContext {
|
||||
pub fn build() -> SettingsMenuContext {
|
||||
SettingsMenuContext {
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// View and delete currently configured admin.
|
||||
#[get("/settings")]
|
||||
pub fn settings_menu(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = SettingsMenuContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Settings".to_string());
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/".to_string()));
|
||||
context.insert("title", &Some("Settings".to_string()));
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
Template::render("settings/menu", &context)
|
||||
|
||||
Template::render("settings/menu", &context.into_json())
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,41 +1,36 @@
|
||||
use rocket::{get, request::FlashMessage, serde::Serialize};
|
||||
use rocket_dyn_templates::Template;
|
||||
use rocket::{get, request::FlashMessage};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/scuttlebutt
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ScuttlebuttSettingsContext {
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl ScuttlebuttSettingsContext {
|
||||
pub fn build() -> ScuttlebuttSettingsContext {
|
||||
ScuttlebuttSettingsContext {
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scuttlebutt settings menu.
|
||||
#[get("/")]
|
||||
pub fn ssb_settings_menu(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = ScuttlebuttSettingsContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/settings".to_string());
|
||||
context.title = Some("Scuttlebutt Settings".to_string());
|
||||
// check to see if there is a flash message to display
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings".to_string()));
|
||||
context.insert("title", &Some("Scuttlebutt Settings".to_string()));
|
||||
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
Template::render("settings/scuttlebutt", &context)
|
||||
|
||||
Template::render("settings/scuttlebutt/menu", &context.into_json())
|
||||
}
|
||||
|
||||
/// Sbot configuration page (includes form for updating configuration parameters).
|
||||
#[get("/configure")]
|
||||
pub fn configure_sbot(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("back", &Some("/settings/scuttlebutt".to_string()));
|
||||
context.insert("title", &Some("Sbot Configuration".to_string()));
|
||||
|
||||
if let Some(flash) = flash {
|
||||
context.insert("flash_name", &Some(flash.kind().to_string()));
|
||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
||||
};
|
||||
|
||||
Template::render("settings/scuttlebutt/configure_sbot", &context.into_json())
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
use log::{debug, info, warn};
|
||||
use log::info;
|
||||
use rocket::{
|
||||
get, post,
|
||||
get,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
};
|
||||
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
@ -12,13 +11,15 @@ use std::{
|
||||
process::{Command, Output},
|
||||
};
|
||||
|
||||
use peach_lib::config_manager::load_peach_config;
|
||||
use peach_lib::stats_client::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat};
|
||||
use peach_lib::{dyndns_client, network_client, oled_client, sbot_client, stats_client};
|
||||
use peach_lib::{
|
||||
config_manager::load_peach_config, dyndns_client, network_client, oled_client, sbot_client,
|
||||
};
|
||||
use peach_stats::{
|
||||
stats,
|
||||
stats::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat},
|
||||
};
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
use crate::utils::build_json_response;
|
||||
use rocket::serde::json::Value;
|
||||
|
||||
// HELPERS AND ROUTES FOR /status
|
||||
|
||||
@ -34,7 +35,6 @@ pub struct StatusContext {
|
||||
pub mem_stats: Option<MemStat>,
|
||||
pub network_ping: String,
|
||||
pub oled_ping: String,
|
||||
pub stats_ping: String,
|
||||
pub dyndns_enabled: bool,
|
||||
pub dyndns_is_online: bool,
|
||||
pub config_is_valid: bool,
|
||||
@ -46,9 +46,12 @@ pub struct StatusContext {
|
||||
impl StatusContext {
|
||||
pub fn build() -> StatusContext {
|
||||
// convert result to Option<CpuStatPercentages>, discard any error
|
||||
let cpu_stat_percent = stats_client::cpu_stats_percent().ok();
|
||||
let load_average = stats_client::load_average().ok();
|
||||
let mem_stats = stats_client::mem_stats().ok();
|
||||
let cpu_stat_percent = stats::cpu_stats_percent().ok();
|
||||
let load_average = stats::load_average().ok();
|
||||
let mem_stats = stats::mem_stats().ok();
|
||||
// TODO: add `wpa_supplicant_status` to peach_network to replace this ping call
|
||||
// instead of: "is the network json-rpc server running?", we want to ask:
|
||||
// "is the wpa_supplicant systemd service functioning correctly?"
|
||||
let network_ping = match network_client::ping() {
|
||||
Ok(_) => "ONLINE".to_string(),
|
||||
Err(_) => "OFFLINE".to_string(),
|
||||
@ -57,22 +60,21 @@ impl StatusContext {
|
||||
Ok(_) => "ONLINE".to_string(),
|
||||
Err(_) => "OFFLINE".to_string(),
|
||||
};
|
||||
let stats_ping = match stats_client::ping() {
|
||||
Ok(_) => "ONLINE".to_string(),
|
||||
Err(_) => "OFFLINE".to_string(),
|
||||
};
|
||||
let uptime = match stats_client::uptime() {
|
||||
Ok(mins) => mins,
|
||||
|
||||
let uptime = match stats::uptime() {
|
||||
Ok(secs) => {
|
||||
let uptime_mins = secs / 60;
|
||||
uptime_mins.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>
|
||||
let disk_usage_stats = match stats_client::disk_usage() {
|
||||
Ok(disks) => {
|
||||
let partitions: Vec<DiskUsage> = serde_json::from_str(disks.as_str())
|
||||
.expect("Failed to deserialize disk_usage response");
|
||||
partitions
|
||||
}
|
||||
let disk_usage_stats = match stats::disk_usage() {
|
||||
Ok(disks) => disks,
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
|
||||
@ -84,9 +86,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
|
||||
let dyndns_enabled: bool;
|
||||
let dyndns_is_online: bool;
|
||||
@ -118,16 +117,11 @@ impl StatusContext {
|
||||
}
|
||||
|
||||
// test if go-sbot is running
|
||||
let sbot_is_online: bool;
|
||||
let sbot_is_online_result = sbot_client::is_sbot_online();
|
||||
match sbot_is_online_result {
|
||||
Ok(val) => {
|
||||
sbot_is_online = val;
|
||||
}
|
||||
Err(_err) => {
|
||||
sbot_is_online = false;
|
||||
}
|
||||
}
|
||||
let sbot_is_online: bool = match sbot_is_online_result {
|
||||
Ok(val) => val,
|
||||
Err(_err) => false,
|
||||
};
|
||||
|
||||
StatusContext {
|
||||
back: None,
|
||||
@ -139,7 +133,6 @@ impl StatusContext {
|
||||
mem_stats,
|
||||
network_ping,
|
||||
oled_ping,
|
||||
stats_ping,
|
||||
dyndns_enabled,
|
||||
dyndns_is_online,
|
||||
config_is_valid,
|
||||
@ -189,25 +182,6 @@ pub fn reboot_cmd(_auth: Authenticated) -> Flash<Redirect> {
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON request handler for device reboot.
|
||||
#[post("/api/v1/admin/reboot")]
|
||||
pub fn reboot_device(_auth: Authenticated) -> Value {
|
||||
match reboot() {
|
||||
Ok(_) => {
|
||||
debug!("Going down for reboot...");
|
||||
let status = "success".to_string();
|
||||
let msg = "Going down for reboot.".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Reboot failed");
|
||||
let status = "error".to_string();
|
||||
let msg = "Failed to reboot the device.".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /power/shutdown
|
||||
|
||||
/// Executes a system command to shutdown the device immediately.
|
||||
@ -227,25 +201,6 @@ pub fn shutdown_cmd(_auth: Authenticated) -> Flash<Redirect> {
|
||||
}
|
||||
}
|
||||
|
||||
// shutdown the device
|
||||
#[post("/power/shutdown")]
|
||||
pub fn shutdown_device(_auth: Authenticated) -> Value {
|
||||
match shutdown() {
|
||||
Ok(_) => {
|
||||
debug!("Going down for shutdown...");
|
||||
let status = "success".to_string();
|
||||
let msg = "Going down for shutdown.".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Shutdown failed");
|
||||
let status = "error".to_string();
|
||||
let msg = "Failed to shutdown the device.".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /power
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
@ -1,3 +1,3 @@
|
||||
pub mod device;
|
||||
pub mod network;
|
||||
pub mod ping;
|
||||
pub mod scuttlebutt;
|
||||
|
@ -1,164 +1,21 @@
|
||||
use rocket::{get, request::FlashMessage};
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::Serialize;
|
||||
|
||||
use peach_lib::network_client;
|
||||
use peach_lib::stats_client::Traffic;
|
||||
|
||||
use crate::context::network::NetworkStatusContext;
|
||||
use crate::routes::authentication::Authenticated;
|
||||
|
||||
// HELPERS AND ROUTES FOR /status/network
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NetworkContext {
|
||||
pub ap_ip: String,
|
||||
pub ap_ssid: String,
|
||||
pub ap_state: String,
|
||||
pub ap_traffic: Option<Traffic>,
|
||||
pub wlan_ip: String,
|
||||
pub wlan_rssi: Option<String>,
|
||||
pub wlan_ssid: String,
|
||||
pub wlan_state: String,
|
||||
pub wlan_status: String,
|
||||
pub wlan_traffic: Option<Traffic>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
// page title for header in navbar
|
||||
pub title: Option<String>,
|
||||
// url for back-arrow link
|
||||
pub back: Option<String>,
|
||||
}
|
||||
|
||||
impl NetworkContext {
|
||||
pub fn build() -> NetworkContext {
|
||||
let ap_ip = match network_client::ip("ap0") {
|
||||
Ok(ip) => ip,
|
||||
Err(_) => "x.x.x.x".to_string(),
|
||||
};
|
||||
let ap_ssid = match network_client::ssid("ap0") {
|
||||
Ok(ssid) => ssid,
|
||||
Err(_) => "Not currently activated".to_string(),
|
||||
};
|
||||
let ap_state = match network_client::state("ap0") {
|
||||
Ok(state) => state,
|
||||
Err(_) => "Interface unavailable".to_string(),
|
||||
};
|
||||
let ap_traffic = match network_client::traffic("ap0") {
|
||||
Ok(traffic) => {
|
||||
let mut t = traffic;
|
||||
// modify traffic values & assign measurement unit
|
||||
// based on received and transmitted values
|
||||
// if received > 999 MB, convert it to GB
|
||||
if t.received > 1_047_527_424 {
|
||||
t.received /= 1_073_741_824;
|
||||
t.rx_unit = Some("GB".to_string());
|
||||
} else if t.received > 0 {
|
||||
// otherwise, convert it to MB
|
||||
t.received = (t.received / 1024) / 1024;
|
||||
t.rx_unit = Some("MB".to_string());
|
||||
} else {
|
||||
t.received = 0;
|
||||
t.rx_unit = Some("MB".to_string());
|
||||
}
|
||||
|
||||
if t.transmitted > 1_047_527_424 {
|
||||
t.transmitted /= 1_073_741_824;
|
||||
t.tx_unit = Some("GB".to_string());
|
||||
} else if t.transmitted > 0 {
|
||||
t.transmitted = (t.transmitted / 1024) / 1024;
|
||||
t.tx_unit = Some("MB".to_string());
|
||||
} else {
|
||||
t.transmitted = 0;
|
||||
t.tx_unit = Some("MB".to_string());
|
||||
}
|
||||
Some(t)
|
||||
}
|
||||
Err(_) => None,
|
||||
};
|
||||
let wlan_ip = match network_client::ip("wlan0") {
|
||||
Ok(ip) => ip,
|
||||
Err(_) => "x.x.x.x".to_string(),
|
||||
};
|
||||
let wlan_rssi = match network_client::rssi_percent("wlan0") {
|
||||
Ok(rssi) => Some(rssi),
|
||||
Err(_) => None,
|
||||
};
|
||||
let wlan_ssid = match network_client::ssid("wlan0") {
|
||||
Ok(ssid) => ssid,
|
||||
Err(_) => "Not connected".to_string(),
|
||||
};
|
||||
let wlan_state = match network_client::state("wlan0") {
|
||||
Ok(state) => state,
|
||||
Err(_) => "Interface unavailable".to_string(),
|
||||
};
|
||||
let wlan_status = match network_client::status("wlan0") {
|
||||
Ok(status) => status,
|
||||
Err(_) => "Interface unavailable".to_string(),
|
||||
};
|
||||
let wlan_traffic = match network_client::traffic("wlan0") {
|
||||
Ok(traffic) => {
|
||||
let mut t = traffic;
|
||||
// modify traffic values & assign measurement unit
|
||||
// based on received and transmitted values
|
||||
// if received > 999 MB, convert it to GB
|
||||
if t.received > 1_047_527_424 {
|
||||
t.received /= 1_073_741_824;
|
||||
t.rx_unit = Some("GB".to_string());
|
||||
} else if t.received > 0 {
|
||||
// otherwise, convert it to MB
|
||||
t.received = (t.received / 1024) / 1024;
|
||||
t.rx_unit = Some("MB".to_string());
|
||||
} else {
|
||||
t.received = 0;
|
||||
t.rx_unit = Some("MB".to_string());
|
||||
}
|
||||
|
||||
if t.transmitted > 1_047_527_424 {
|
||||
t.transmitted /= 1_073_741_824;
|
||||
t.tx_unit = Some("GB".to_string());
|
||||
} else if t.transmitted > 0 {
|
||||
t.transmitted = (t.transmitted / 1024) / 1024;
|
||||
t.tx_unit = Some("MB".to_string());
|
||||
} else {
|
||||
t.transmitted = 0;
|
||||
t.tx_unit = Some("MB".to_string());
|
||||
}
|
||||
Some(t)
|
||||
}
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
NetworkContext {
|
||||
ap_ip,
|
||||
ap_ssid,
|
||||
ap_state,
|
||||
ap_traffic,
|
||||
wlan_ip,
|
||||
wlan_rssi,
|
||||
wlan_ssid,
|
||||
wlan_state,
|
||||
wlan_status,
|
||||
wlan_traffic,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
back: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/network")]
|
||||
pub fn network_status(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
// assign context through context_builder call
|
||||
let mut context = NetworkContext::build();
|
||||
let mut context = NetworkStatusContext::build();
|
||||
context.back = Some("/status".to_string());
|
||||
context.title = Some("Network Status".to_string());
|
||||
// check to see if there is a flash message to display
|
||||
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
// template_dir is set in Rocket.toml
|
||||
|
||||
Template::render("status/network", &context)
|
||||
}
|
||||
|
@ -1,78 +0,0 @@
|
||||
//! Helper routes for pinging services to check that they are active
|
||||
use log::{debug, warn};
|
||||
use rocket::get;
|
||||
use rocket::serde::json::Value;
|
||||
|
||||
use peach_lib::network_client;
|
||||
use peach_lib::oled_client;
|
||||
use peach_lib::stats_client;
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
use crate::utils::build_json_response;
|
||||
|
||||
/// Status route: useful for checking connectivity from web client.
|
||||
#[get("/ping")]
|
||||
pub fn ping_pong(_auth: Authenticated) -> Value {
|
||||
//pub fn ping_pong() -> Value {
|
||||
// ping pong
|
||||
let status = "success".to_string();
|
||||
let msg = "pong!".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
|
||||
/// Status route: check availability of `peach-network` microservice.
|
||||
#[get("/ping/network")]
|
||||
pub fn ping_network(_auth: Authenticated) -> Value {
|
||||
match network_client::ping() {
|
||||
Ok(_) => {
|
||||
debug!("peach-network responded successfully");
|
||||
let status = "success".to_string();
|
||||
let msg = "peach-network is available.".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("peach-network failed to respond");
|
||||
let status = "error".to_string();
|
||||
let msg = "peach-network is unavailable.".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Status route: check availability of `peach-oled` microservice.
|
||||
#[get("/ping/oled")]
|
||||
pub fn ping_oled(_auth: Authenticated) -> Value {
|
||||
match oled_client::ping() {
|
||||
Ok(_) => {
|
||||
debug!("peach-oled responded successfully");
|
||||
let status = "success".to_string();
|
||||
let msg = "peach-oled is available.".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("peach-oled failed to respond");
|
||||
let status = "error".to_string();
|
||||
let msg = "peach-oled is unavailable.".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Status route: check availability of `peach-stats` microservice.
|
||||
#[get("/ping/stats")]
|
||||
pub fn ping_stats(_auth: Authenticated) -> Value {
|
||||
match stats_client::ping() {
|
||||
Ok(_) => {
|
||||
debug!("peach-stats responded successfully");
|
||||
let status = "success".to_string();
|
||||
let msg = "peach-stats is available.".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("peach-stats failed to respond");
|
||||
let status = "error".to_string();
|
||||
let msg = "peach-stats is unavailable.".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
}
|
||||
}
|
16
peach-web/src/routes/status/scuttlebutt.rs
Normal file
16
peach-web/src/routes/status/scuttlebutt.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use rocket::get;
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
|
||||
// HELPERS AND ROUTES FOR /status/scuttlebutt
|
||||
|
||||
#[get("/scuttlebutt")]
|
||||
pub fn scuttlebutt_status(_auth: Authenticated) -> Template {
|
||||
let mut context = Context::new();
|
||||
context.insert("flash_name", &None::<()>);
|
||||
context.insert("flash_msg", &None::<()>);
|
||||
context.insert("title", &Some("Scuttlebutt Status"));
|
||||
|
||||
Template::render("status/scuttlebutt", &context.into_json())
|
||||
}
|
@ -3,18 +3,27 @@ use std::io::Read;
|
||||
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::local::blocking::Client;
|
||||
use rocket::serde::json::{json, Value};
|
||||
|
||||
use crate::utils::build_json_response;
|
||||
use rocket::{Build, Config, Rocket};
|
||||
|
||||
use super::init_rocket;
|
||||
|
||||
// define authentication mode
|
||||
const DISABLE_AUTH: bool = true;
|
||||
|
||||
/// Wrapper around `init_rocket()` to simplify the process of invoking the application with the desired authentication status. This is particularly useful for testing purposes.
|
||||
fn init_test_rocket(disable_auth: bool) -> Rocket<Build> {
|
||||
// set authentication based on provided `disable_auth` value
|
||||
Config::figment().merge(("disable_auth", disable_auth));
|
||||
|
||||
init_rocket()
|
||||
}
|
||||
|
||||
// helper function to test correct retrieval and content of a file
|
||||
fn test_query_file<T>(path: &str, file: T, status: Status)
|
||||
where
|
||||
T: Into<Option<&'static str>>,
|
||||
{
|
||||
let client = Client::tracked(init_rocket()).unwrap();
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).unwrap();
|
||||
let response = client.get(path).dispatch();
|
||||
assert_eq!(response.status(), status);
|
||||
|
||||
@ -39,7 +48,7 @@ fn read_file_content(path: &str) -> Vec<u8> {
|
||||
|
||||
#[test]
|
||||
fn index_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -54,7 +63,7 @@ fn index_html() {
|
||||
|
||||
#[test]
|
||||
fn help_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/help").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -64,7 +73,7 @@ fn help_html() {
|
||||
|
||||
#[test]
|
||||
fn login_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/login").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -74,7 +83,7 @@ fn login_html() {
|
||||
|
||||
#[test]
|
||||
fn logout_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/logout").dispatch();
|
||||
// check for 303 status (redirect to "/login")
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
@ -83,7 +92,7 @@ fn logout_html() {
|
||||
|
||||
#[test]
|
||||
fn power_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/power").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -97,7 +106,7 @@ NOTE: these tests are comment-out for the moment, due to the fact that they invo
|
||||
|
||||
#[test]
|
||||
fn reboot() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/power/reboot").dispatch();
|
||||
// check for redirect
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
@ -105,7 +114,7 @@ fn reboot() {
|
||||
|
||||
#[test]
|
||||
fn shutdown() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/power/shutdown").dispatch();
|
||||
// check for redirect
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
@ -116,7 +125,7 @@ fn shutdown() {
|
||||
|
||||
#[test]
|
||||
fn block() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client
|
||||
.post("/scuttlebutt/block")
|
||||
.header(ContentType::Form)
|
||||
@ -127,7 +136,7 @@ fn block() {
|
||||
|
||||
#[test]
|
||||
fn blocks_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/scuttlebutt/blocks").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -137,7 +146,7 @@ fn blocks_html() {
|
||||
|
||||
#[test]
|
||||
fn follow() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client
|
||||
.post("/scuttlebutt/follow")
|
||||
.header(ContentType::Form)
|
||||
@ -149,7 +158,7 @@ fn follow() {
|
||||
|
||||
#[test]
|
||||
fn follows_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/scuttlebutt/follows").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -159,7 +168,7 @@ fn follows_html() {
|
||||
|
||||
#[test]
|
||||
fn followers_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/scuttlebutt/followers").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -169,7 +178,7 @@ fn followers_html() {
|
||||
|
||||
#[test]
|
||||
fn friends_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/scuttlebutt/friends").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -179,7 +188,7 @@ fn friends_html() {
|
||||
|
||||
#[test]
|
||||
fn peers_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/scuttlebutt/peers").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -189,7 +198,7 @@ fn peers_html() {
|
||||
|
||||
#[test]
|
||||
fn private_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/scuttlebutt/private").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -199,7 +208,7 @@ fn private_html() {
|
||||
|
||||
#[test]
|
||||
fn profile_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/scuttlebutt/profile").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -209,7 +218,7 @@ fn profile_html() {
|
||||
|
||||
#[test]
|
||||
fn publish_post() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client
|
||||
.post("/scuttlebutt/publish")
|
||||
.header(ContentType::Form)
|
||||
@ -220,7 +229,7 @@ fn publish_post() {
|
||||
|
||||
#[test]
|
||||
fn unfollow() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client
|
||||
.post("/scuttlebutt/unfollow")
|
||||
.header(ContentType::Form)
|
||||
@ -233,7 +242,7 @@ fn unfollow() {
|
||||
|
||||
#[test]
|
||||
fn admin_settings_menu_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/admin").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -245,7 +254,7 @@ fn admin_settings_menu_html() {
|
||||
|
||||
#[test]
|
||||
fn add_admin_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/admin/add").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -258,7 +267,7 @@ fn add_admin_html() {
|
||||
|
||||
#[test]
|
||||
fn add_admin() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client
|
||||
.post("/settings/admin/add")
|
||||
.header(ContentType::Form)
|
||||
@ -270,21 +279,21 @@ fn add_admin() {
|
||||
|
||||
#[test]
|
||||
fn change_password_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/admin/change_password").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
let body = response.into_string().unwrap();
|
||||
assert!(body.contains("Change Password"));
|
||||
assert!(body.contains("Old Password"));
|
||||
assert!(body.contains("Enter New Password"));
|
||||
assert!(body.contains("Re-Enter New Password"));
|
||||
assert!(body.contains("Current password"));
|
||||
assert!(body.contains("New password"));
|
||||
assert!(body.contains("New password duplicate"));
|
||||
assert!(body.contains("Save"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configure_admin_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/admin/configure").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -296,7 +305,7 @@ fn configure_admin_html() {
|
||||
|
||||
#[test]
|
||||
fn forgot_password_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/admin/forgot_password").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -308,7 +317,7 @@ fn forgot_password_html() {
|
||||
|
||||
#[test]
|
||||
fn network_settings_menu_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/network").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -316,18 +325,35 @@ fn network_settings_menu_html() {
|
||||
assert!(body.contains("Network Configuration"));
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
NOTE: these tests are commented-out for the moment, due to the fact that they
|
||||
invoke system commands (resulting in a `sudo` password request during
|
||||
test execution). see if we can find a way to test the results without
|
||||
triggering the `systemctl` call.
|
||||
|
||||
#[test]
|
||||
fn deploy_ap() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/network/ap/activate").dispatch();
|
||||
// check for 303 status (redirect)
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.content_type(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deploy_client() {
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/network/wifi/activate").dispatch();
|
||||
// check for 303 status (redirect)
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.content_type(), None);
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn dns_settings_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/network/dns").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -341,7 +367,7 @@ fn dns_settings_html() {
|
||||
|
||||
#[test]
|
||||
fn list_aps_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/network/wifi").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -353,7 +379,7 @@ fn list_aps_html() {
|
||||
// TODO: needs further testing once template has been refactored
|
||||
#[test]
|
||||
fn ap_details_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/network/wifi?ssid=Home").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -361,18 +387,9 @@ fn ap_details_html() {
|
||||
//assert!(body.contains("Network not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deploy_client() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let response = client.get("/settings/network/wifi/activate").dispatch();
|
||||
// check for 303 status (redirect)
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.content_type(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_ap_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/network/wifi/add").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -386,7 +403,7 @@ fn add_ap_html() {
|
||||
|
||||
#[test]
|
||||
fn add_ap_ssid_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client
|
||||
.get("/settings/network/wifi/add?ssid=Home")
|
||||
.dispatch();
|
||||
@ -402,7 +419,7 @@ fn add_ap_ssid_html() {
|
||||
|
||||
#[test]
|
||||
fn add_credentials() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client
|
||||
.post("/settings/network/wifi/add")
|
||||
.header(ContentType::Form)
|
||||
@ -414,7 +431,7 @@ fn add_credentials() {
|
||||
|
||||
#[test]
|
||||
fn forget_wifi() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client
|
||||
.post("/settings/network/wifi/forget")
|
||||
.header(ContentType::Form)
|
||||
@ -426,7 +443,7 @@ fn forget_wifi() {
|
||||
|
||||
#[test]
|
||||
fn modify_password() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client
|
||||
.post("/settings/network/wifi/modify")
|
||||
.header(ContentType::Form)
|
||||
@ -438,7 +455,7 @@ fn modify_password() {
|
||||
|
||||
#[test]
|
||||
fn data_usage_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/network/wifi/usage").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -453,7 +470,7 @@ fn data_usage_html() {
|
||||
|
||||
#[test]
|
||||
fn scuttlebutt_settings_menu_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/settings/scuttlebutt").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -472,7 +489,7 @@ fn scuttlebutt_settings_menu_html() {
|
||||
|
||||
#[test]
|
||||
fn status_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/status").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -485,7 +502,7 @@ fn status_html() {
|
||||
|
||||
#[test]
|
||||
fn network_status_html() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
|
||||
let response = client.get("/status/network").dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::HTML));
|
||||
@ -497,179 +514,6 @@ fn network_status_html() {
|
||||
assert!(body.contains("DOWNLOAD"));
|
||||
assert!(body.contains("UPLOAD"));
|
||||
}
|
||||
|
||||
// JSON API ROUTES
|
||||
|
||||
#[test]
|
||||
fn activate_ap() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let response = client
|
||||
.post("/api/v1/network/activate_ap")
|
||||
.header(ContentType::JSON)
|
||||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::JSON));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn activate_client() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let response = client
|
||||
.post("/api/v1/network/activate_client")
|
||||
.header(ContentType::JSON)
|
||||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::JSON));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_ip() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let response = client
|
||||
.get("/api/v1/network/ip")
|
||||
.header(ContentType::JSON)
|
||||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::JSON));
|
||||
let body = response.into_string().unwrap();
|
||||
assert!(body.contains("wlan0"));
|
||||
assert!(body.contains("ap0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_rssi() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let response = client
|
||||
.get("/api/v1/network/rssi")
|
||||
.header(ContentType::JSON)
|
||||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::JSON));
|
||||
let body = response.into_string().unwrap();
|
||||
assert!(body.contains("Not currently connected to an access point."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_ssid() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let response = client
|
||||
.get("/api/v1/network/ssid")
|
||||
.header(ContentType::JSON)
|
||||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::JSON));
|
||||
let body = response.into_string().unwrap();
|
||||
assert!(body.contains("Not currently connected to an access point."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_state() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let response = client
|
||||
.get("/api/v1/network/state")
|
||||
.header(ContentType::JSON)
|
||||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::JSON));
|
||||
let body = response.into_string().unwrap();
|
||||
assert!(body.contains("ap0"));
|
||||
assert!(body.contains("wlan0"));
|
||||
assert!(body.contains("unavailable"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_status() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let response = client
|
||||
.get("/api/v1/network/status")
|
||||
.header(ContentType::JSON)
|
||||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::JSON));
|
||||
let body = response.into_string().unwrap();
|
||||
assert!(body.contains("Not currently connected to an access point."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_networks() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let response = client
|
||||
.get("/api/v1/network/wifi")
|
||||
.header(ContentType::JSON)
|
||||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::JSON));
|
||||
let body = response.into_string().unwrap();
|
||||
assert!(body.contains("Unable to scan for networks. Interface may be deactivated."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_wifi() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let response = client
|
||||
.post("/api/v1/network/wifi")
|
||||
.header(ContentType::JSON)
|
||||
.body(r#"{ "ssid": "Home", "pass": "Password" }"#)
|
||||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::JSON));
|
||||
let body = response.into_string().unwrap();
|
||||
assert!(body.contains("Failed to add WiFi credentials."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_wifi() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let response = client
|
||||
.post("/api/v1/network/wifi/forget")
|
||||
.header(ContentType::JSON)
|
||||
.body(r#"{ "ssid": "Home" }"#)
|
||||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::JSON));
|
||||
let body = response.into_string().unwrap();
|
||||
assert!(body.contains("Failed to remove WiFi network credentials."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_password() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let response = client
|
||||
.post("/api/v1/network/wifi/modify")
|
||||
.header(ContentType::JSON)
|
||||
.body(r#"{ "ssid": "Home", "pass": "Password" }"#)
|
||||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::JSON));
|
||||
let body = response.into_string().unwrap();
|
||||
assert!(body.contains("Failed to update WiFi password."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ping_pong() {
|
||||
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
|
||||
let response = client
|
||||
.get("/api/v1/ping")
|
||||
.header(ContentType::JSON)
|
||||
.dispatch();
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
assert_eq!(response.content_type(), Some(ContentType::JSON));
|
||||
let body = response.into_string().unwrap();
|
||||
assert!(body.contains("pong!"));
|
||||
}
|
||||
|
||||
// HELPER FUNCTION TESTS
|
||||
|
||||
#[test]
|
||||
fn test_build_json_response() {
|
||||
let status = "success".to_string();
|
||||
let data = json!("WiFi credentials added.".to_string());
|
||||
let j: Value = build_json_response(status, Some(data), None);
|
||||
assert_eq!(j["status"], "success");
|
||||
assert_eq!(j["data"], "WiFi credentials added.");
|
||||
assert_eq!(j["msg"], json!(null));
|
||||
}
|
||||
|
||||
// FILE TESTS
|
||||
|
||||
#[test]
|
||||
@ -709,7 +553,7 @@ fn invalid_path() {
|
||||
|
||||
#[test]
|
||||
fn invalid_get_request() {
|
||||
let client = Client::tracked(init_rocket()).unwrap();
|
||||
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).unwrap();
|
||||
|
||||
// try to get a path that doesn't exist
|
||||
let res = client
|
||||
|
@ -3,26 +3,16 @@ pub mod monitor;
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use rocket::response::{Redirect, Responder};
|
||||
use rocket::serde::json::{Value, json};
|
||||
use rocket::serde::{Serialize};
|
||||
use rocket::serde::Serialize;
|
||||
|
||||
// HELPER FUNCTIONS
|
||||
|
||||
pub fn build_json_response(
|
||||
status: String,
|
||||
data: Option<Value>,
|
||||
msg: Option<String>,
|
||||
) -> Value {
|
||||
json!({ "status": status, "data": data, "msg": msg })
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FlashContext {
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
/// A helper enum which allows routes to either return a Template or a Redirect
|
||||
/// from: https://github.com/SergioBenitez/Rocket/issues/253#issuecomment-532356066
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@ -30,4 +20,4 @@ pub struct FlashContext {
|
||||
pub enum TemplateOrRedirect {
|
||||
Template(Template),
|
||||
Redirect(Redirect),
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
use std::convert::TryInto;
|
||||
|
||||
use nest::{Error, Store, Value};
|
||||
use rocket::form::{FromForm};
|
||||
use rocket::form::FromForm;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
|
@ -220,12 +220,18 @@ body {
|
||||
}
|
||||
|
||||
.capsule-container {
|
||||
margin-left: 2rem;
|
||||
margin-right: 2rem;
|
||||
padding-top: 1rem;
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
.capsule-container {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* CARDS
|
||||
*/
|
||||
@ -235,6 +241,7 @@ body {
|
||||
max-height: 90vh;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
@ -248,8 +255,6 @@ body {
|
||||
.card-container {
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
/* padding-top: 1rem; */
|
||||
/* padding-bottom: 1rem; */
|
||||
}
|
||||
|
||||
.form-container {
|
||||
@ -560,6 +565,7 @@ html {
|
||||
font-size: var(--font-size-6);
|
||||
margin-left: 2rem;
|
||||
margin-right: 2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -678,6 +684,7 @@ html {
|
||||
font-family: var(--sans-serif);
|
||||
font-size: var(--font-size-7);
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.label-medium {
|
||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 9.9 KiB |
BIN
peach-web/static/icons/image-file.png
Normal file
BIN
peach-web/static/icons/image-file.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
BIN
peach-web/static/icons/orbits.png
Normal file
BIN
peach-web/static/icons/orbits.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
BIN
peach-web/static/icons/ram.png
Normal file
BIN
peach-web/static/icons/ram.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* behavioural layer for the `change_password.html.tera` template,
|
||||
*/
|
||||
|
||||
// catch click of 'Save' button and make POST request
|
||||
PEACH.add = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.body.addEventListener('submit', function(e) {
|
||||
// prevent redirect on button press (default behavior)
|
||||
e.preventDefault();
|
||||
// capture form data
|
||||
var formElement = document.querySelector("form");
|
||||
// create form data object from the wifiCreds form element
|
||||
var formData = new FormData(formElement);
|
||||
var object = {};
|
||||
// assign values from form
|
||||
formData.forEach(function(value, key){
|
||||
object[key] = value;
|
||||
});
|
||||
// perform json serialization
|
||||
console.log(object);
|
||||
var jsonData = JSON.stringify(object);
|
||||
// write in-progress status message to ui
|
||||
PEACH.flashMsg("info", "Saving new password.");
|
||||
// send add_wifi POST request
|
||||
fetch("/api/v1/admin/change_password", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonData
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
// write json response message to ui
|
||||
PEACH.flashMsg(jsonData.status, jsonData.msg);
|
||||
})
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
|
||||
var addInstance = PEACH;
|
||||
addInstance.add();
|
@ -1,47 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* Common javascript functions shared by multiple pages:
|
||||
* - flashMsg
|
||||
* - logout
|
||||
*
|
||||
*/
|
||||
|
||||
var PEACH = {};
|
||||
|
||||
// display a message by appending a paragraph element
|
||||
PEACH.flashMsg = function(status, msg) {
|
||||
// set the class of the element according to status
|
||||
var elementClass;
|
||||
if (status === "success") {
|
||||
elementClass = "capsule center-text flash-message font-success";
|
||||
} else if (status === "info") {
|
||||
elementClass = "capsule center-text flash-message font-info";
|
||||
} else {
|
||||
elementClass = "capsule center-text flash-message font-failure";
|
||||
};
|
||||
|
||||
var flashElement = document.getElementById("flashMsg");
|
||||
// if flashElement exists, update the class & text
|
||||
if (flashElement) {
|
||||
flashElement.className = elementClass;
|
||||
flashElement.innerText = msg;
|
||||
// if flashElement does not exist, create it, set id, class, text & append
|
||||
} else {
|
||||
// create new div for flash message
|
||||
var flashDiv = document.createElement("DIV");
|
||||
// set div attributes
|
||||
flashDiv.id = "flashMsg";
|
||||
flashDiv.className = elementClass;
|
||||
// add json response message to flash message div
|
||||
var flashMsg = document.createTextNode(msg);
|
||||
flashDiv.appendChild(flashMsg);
|
||||
// insert the flash message div below the button div
|
||||
var buttonDiv = document.getElementById("buttonDiv");
|
||||
// flashDiv will be added to the end since buttonDiv is the last
|
||||
// child within the parent element (card-container div)
|
||||
buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
var addInstance = PEACH;
|
||||
addInstance.add();
|
@ -1,98 +0,0 @@
|
||||
/*
|
||||
|
||||
behavioural layer for the `configure_dns.html.tera` template,
|
||||
corresponding to the web route `/network/dns`
|
||||
|
||||
- intercept button click for add (form submission of credentials)
|
||||
- perform json api call
|
||||
- update the dom
|
||||
|
||||
*/
|
||||
|
||||
var PEACH_DNS = {};
|
||||
|
||||
// catch click of 'Add' button and make POST request
|
||||
PEACH_DNS.add = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.body.addEventListener('submit', function(e) {
|
||||
// prevent redirect on button press (default behavior)
|
||||
e.preventDefault();
|
||||
// capture form data
|
||||
var formElement = document.querySelector("form");
|
||||
// create form data object from the wifiCreds form element
|
||||
var formData = new FormData(formElement);
|
||||
var object = {};
|
||||
// set checkbox to false (the value is only passed to formData if it is "on")
|
||||
object["enable_dyndns"] = false;
|
||||
// assign values from form
|
||||
formData.forEach(function(value, key){
|
||||
// convert checkbox to bool
|
||||
if (key === "enable_dyndns") {
|
||||
value = (value === "on");
|
||||
}
|
||||
object[key] = value;
|
||||
});
|
||||
// perform json serialization
|
||||
console.log(object);
|
||||
var jsonData = JSON.stringify(object);
|
||||
// write in-progress status message to ui
|
||||
PEACH_DNS.flashMsg("info", "Saving new DNS configurations");
|
||||
// send add_wifi POST request
|
||||
fetch("/api/v1/network/dns/configure", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonData
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
// write json response message to ui
|
||||
PEACH_DNS.flashMsg(jsonData.status, jsonData.msg);
|
||||
let statusIndicator = document.getElementById("dyndns-status-indicator");
|
||||
statusIndicator.remove();
|
||||
|
||||
})
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
|
||||
// display a message by appending a paragraph element
|
||||
PEACH_DNS.flashMsg = function(status, msg) {
|
||||
// set the class of the element according to status
|
||||
var elementClass;
|
||||
if (status === "success") {
|
||||
elementClass = "capsule center-text flash-message font-success";
|
||||
} else if (status === "info") {
|
||||
elementClass = "capsule center-text flash-message font-info";
|
||||
} else {
|
||||
elementClass = "capsule center-text flash-message font-failure";
|
||||
};
|
||||
|
||||
var flashElement = document.getElementById("flashMsg");
|
||||
// if flashElement exists, update the class & text
|
||||
if (flashElement) {
|
||||
flashElement.className = elementClass;
|
||||
flashElement.innerText = msg;
|
||||
// if flashElement does not exist, create it, set id, class, text & append
|
||||
} else {
|
||||
// create new div for flash message
|
||||
var flashDiv = document.createElement("DIV");
|
||||
// set div attributes
|
||||
flashDiv.id = "flashMsg";
|
||||
flashDiv.className = elementClass;
|
||||
// add json response message to flash message div
|
||||
var flashMsg = document.createTextNode(msg);
|
||||
flashDiv.appendChild(flashMsg);
|
||||
// insert the flash message div below the button div
|
||||
var buttonDiv = document.getElementById("buttonDiv");
|
||||
// flashDiv will be added to the end since buttonDiv is the last
|
||||
// child within the parent element (card-container div)
|
||||
buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
var addInstance = PEACH_DNS;
|
||||
addInstance.add();
|
@ -1,93 +0,0 @@
|
||||
/*
|
||||
|
||||
behavioural layer for the `network_add.html.tera` template,
|
||||
corresponding to the web route `/network/wifi/add`
|
||||
|
||||
- intercept button click for add (form submission of credentials)
|
||||
- perform json api call
|
||||
- update the dom
|
||||
|
||||
methods:
|
||||
|
||||
PEACH_NETWORK.add();
|
||||
PEACH_NETWORK.flashMsg(status, msg);
|
||||
|
||||
*/
|
||||
|
||||
var PEACH_NETWORK = {};
|
||||
|
||||
// catch click of 'Add' button and make POST request
|
||||
PEACH_NETWORK.add = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.body.addEventListener('submit', function(e) {
|
||||
// prevent redirect on button press (default behavior)
|
||||
e.preventDefault();
|
||||
// capture form data
|
||||
var formElement = document.querySelector("form");
|
||||
// create form data object from the wifiCreds form element
|
||||
var formData = new FormData(formElement);
|
||||
var object = {};
|
||||
// assign ssid and pass from form
|
||||
formData.forEach(function(value, key){
|
||||
object[key] = value;
|
||||
});
|
||||
// perform json serialization
|
||||
var jsonData = JSON.stringify(object);
|
||||
// write in-progress status message to ui
|
||||
PEACH_NETWORK.flashMsg("info", "Adding WiFi credentials...");
|
||||
// send add_wifi POST request
|
||||
fetch("/api/v1/network/wifi", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonData
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
// write json response message to ui
|
||||
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg);
|
||||
})
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
|
||||
// display a message by appending a paragraph element
|
||||
PEACH_NETWORK.flashMsg = function(status, msg) {
|
||||
// set the class of the element according to status
|
||||
var elementClass;
|
||||
if (status === "success") {
|
||||
elementClass = "capsule center-text flash-message font-success";
|
||||
} else if (status === "info") {
|
||||
elementClass = "capsule center-text flash-message font-info";
|
||||
} else {
|
||||
elementClass = "capsule center-text flash-message font-failure";
|
||||
};
|
||||
|
||||
var flashElement = document.getElementById("flashMsg");
|
||||
// if flashElement exists, update the class & text
|
||||
if (flashElement) {
|
||||
flashElement.className = elementClass;
|
||||
flashElement.innerText = msg;
|
||||
// if flashElement does not exist, create it, set id, class, text & append
|
||||
} else {
|
||||
// create new div for flash message
|
||||
var flashDiv = document.createElement("DIV");
|
||||
// set div attributes
|
||||
flashDiv.id = "flashMsg";
|
||||
flashDiv.className = elementClass;
|
||||
// add json response message to flash message div
|
||||
var flashMsg = document.createTextNode(msg);
|
||||
flashDiv.appendChild(flashMsg);
|
||||
// insert the flash message div below the button div
|
||||
var buttonDiv = document.getElementById("buttonDiv");
|
||||
// flashDiv will be added to the end since buttonDiv is the last
|
||||
// child within the parent element (card-container div)
|
||||
buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
var addInstance = PEACH_NETWORK;
|
||||
addInstance.add();
|
@ -1,201 +0,0 @@
|
||||
/*
|
||||
|
||||
behavioural layer for the `network_card.html.tera` template,
|
||||
corresponding to the web route `/network`
|
||||
|
||||
- intercept form submissions
|
||||
- perform json api calls
|
||||
- update the dom
|
||||
|
||||
methods:
|
||||
|
||||
PEACH_NETWORK.activateAp();
|
||||
PEACH_NETWORK.activateClient();
|
||||
PEACH_NETWORK.apOnline();
|
||||
PEACH_NETWORK.clientOffline();
|
||||
PEACH_NETWORK.clientOnline();
|
||||
PEACH_NETWORK.flashMsg(status, msg);
|
||||
|
||||
*/
|
||||
|
||||
var PEACH_NETWORK = {};
|
||||
|
||||
// catch click of 'Deploy Access Point' and make POST request
|
||||
PEACH_NETWORK.activateAp = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var deployAP = document.getElementById('deployAccessPoint');
|
||||
if (deployAP) {
|
||||
deployAP.addEventListener('click', function(e) {
|
||||
// prevent form submission (default behavior)
|
||||
e.preventDefault();
|
||||
// write in-progress status message to ui
|
||||
PEACH_NETWORK.flashMsg("info", "Deploying access point...");
|
||||
// send activate_ap POST request
|
||||
fetch("/api/v1/network/activate_ap", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
console.log(jsonData.msg);
|
||||
// write json response message to ui
|
||||
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg);
|
||||
// if ap activation is successful, update the ui
|
||||
if (jsonData.status === "success") {
|
||||
PEACH_NETWORK.apOnline();
|
||||
}
|
||||
})
|
||||
}, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// catch click of 'Enable WiFi' and make POST request
|
||||
PEACH_NETWORK.activateClient = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var enableWifi = document.getElementById('connectWifi');
|
||||
if (enableWifi) {
|
||||
enableWifi.addEventListener('click', function(e) {
|
||||
// prevent form submission (default behavior)
|
||||
e.preventDefault();
|
||||
// write in-progress status message to ui
|
||||
PEACH_NETWORK.flashMsg("info", "Enabling WiFi client...");
|
||||
// send activate_ap POST request
|
||||
fetch("/api/v1/network/activate_client", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
console.log(jsonData.msg);
|
||||
// write json response message to ui
|
||||
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg);
|
||||
// if client activation is successful, update the ui
|
||||
if (jsonData.status === "success") {
|
||||
PEACH_NETWORK.clientOnline();
|
||||
}
|
||||
})
|
||||
}, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// update ui for access point mode (status: online)
|
||||
PEACH_NETWORK.apOnline = function() {
|
||||
console.log('Activating AP Mode');
|
||||
|
||||
// update network mode and status (icon & label)
|
||||
let i = document.getElementById("netModeIcon");
|
||||
i.className = "center icon icon-active";
|
||||
i.src = "icons/router.svg";
|
||||
let l = document.getElementById("netModeLabel");
|
||||
l.textContent = "ONLINE";
|
||||
|
||||
// create Enable WiFi button and add it to button div
|
||||
var wifiButton = document.createElement("A");
|
||||
wifiButton.className = "button center";
|
||||
wifiButton.href = "/network/wifi/activate";
|
||||
wifiButton.id = "connectWifi";
|
||||
var label = "Enable WiFi";
|
||||
var buttonText = document.createTextNode(label);
|
||||
wifiButton.appendChild(buttonText);
|
||||
|
||||
// append the new button to the buttons div
|
||||
let buttons = document.getElementById("buttons");
|
||||
buttons.appendChild(wifiButton);
|
||||
|
||||
// remove the old 'Activate Access Point' button
|
||||
let apButton = document.getElementById("deployAccessPoint");
|
||||
apButton.style = "display: none;";
|
||||
}
|
||||
|
||||
// update ui for wifi client mode (status: online)
|
||||
PEACH_NETWORK.clientOnline = function() {
|
||||
console.log('Activating Client Mode');
|
||||
|
||||
// update network mode and status (icon & label)
|
||||
let i = document.getElementById("netModeIcon");
|
||||
i.className = "center icon icon-active";
|
||||
i.src = "icons/wifi.svg";
|
||||
let l = document.getElementById("netModeLabel");
|
||||
l.textContent = "ONLINE";
|
||||
|
||||
// TODO: think about updates for buttons (transition from ap mode)
|
||||
}
|
||||
|
||||
// update ui for wifi client mode (status: offline)
|
||||
PEACH_NETWORK.clientOffline = function() {
|
||||
console.log('Activating Client Mode');
|
||||
|
||||
// update network mode and status (icon & label)
|
||||
let i = document.getElementById("netModeIcon");
|
||||
i.className = "center icon icon-inactive";
|
||||
i.src = "icons/wifi.svg";
|
||||
let l = document.getElementById("netModeLabel");
|
||||
l.textContent = "OFFLINE";
|
||||
|
||||
// TODO: think about updates for buttons (transition from ap mode)
|
||||
}
|
||||
|
||||
// display a message by appending a paragraph element
|
||||
PEACH_NETWORK.flashMsg = function(status, msg) {
|
||||
// set the class of the element according to status
|
||||
var elementClass;
|
||||
if (status === "success") {
|
||||
elementClass = "capsule center-text flash-message font-success";
|
||||
} else if (status === "info") {
|
||||
elementClass = "capsule center-text flash-message font-info";
|
||||
} else {
|
||||
elementClass = "capsule center-text flash-message font-failure";
|
||||
};
|
||||
|
||||
var flashElement = document.getElementById("flashMsg");
|
||||
// if flashElement exists, update the class & text
|
||||
if (flashElement) {
|
||||
flashElement.className = elementClass;
|
||||
flashElement.innerText = msg;
|
||||
// if flashElement does not exist, create it, set id, class, text & append
|
||||
} else {
|
||||
// create new div for flash message
|
||||
var flashDiv = document.createElement("DIV");
|
||||
// set div attributes
|
||||
flashDiv.id = "flashMsg";
|
||||
flashDiv.className = elementClass;
|
||||
// add json response message to flash message div
|
||||
var flashMsg = document.createTextNode(msg);
|
||||
flashDiv.appendChild(flashMsg);
|
||||
// insert the flash message div above the three icon grid div
|
||||
var gridDiv = document.getElementById("gridDiv");
|
||||
gridDiv.parentNode.insertBefore(flashDiv, gridDiv);
|
||||
}
|
||||
}
|
||||
|
||||
var networkInstance = PEACH_NETWORK;
|
||||
networkInstance.activateAp();
|
||||
networkInstance.activateClient();
|
||||
|
||||
/*
|
||||
|
||||
async function exampleFetch() {
|
||||
const response = await fetch('/api/v1/network/state');
|
||||
const myJson = await response.json();
|
||||
//const jsonData = JSON.parse(myJson);
|
||||
console.log(myJson.data.wlan0);
|
||||
//var state = document.createElement("P");
|
||||
//state.innerText = ""jsonData.wlan0;
|
||||
//document.body.appendChild(state);
|
||||
}
|
||||
|
||||
exampleFetch()
|
||||
|
||||
*/
|
@ -1,167 +0,0 @@
|
||||
/*
|
||||
|
||||
behavioural layer for the `network_detail.html.tera` template,
|
||||
corresponding to the web route `/network/wifi?<ssid>`
|
||||
|
||||
- intercept button clicks for connect, disconnect and forget
|
||||
- perform json api call
|
||||
- update the dom
|
||||
|
||||
methods:
|
||||
|
||||
PEACH_NETWORK.connect();
|
||||
PEACH_NETWORK.disconnect();
|
||||
PEACH_NETWORK.forget();
|
||||
PEACH_NETWORK.flashMsg(status, msg);
|
||||
|
||||
*/
|
||||
|
||||
var PEACH_NETWORK = {};
|
||||
|
||||
// catch click of 'Connect' button (form) and make POST request
|
||||
PEACH_NETWORK.connect = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var connectWifi = document.getElementById('connectWifi');
|
||||
if (connectWifi) {
|
||||
connectWifi.addEventListener('click', function(e) {
|
||||
// prevent form submission (default behavior)
|
||||
e.preventDefault();
|
||||
// retrieve ssid value and append to form data object
|
||||
var ssid = document.getElementById('connectSsid').value;
|
||||
// create key:value pair
|
||||
var ssidData = { ssid: ssid };
|
||||
// perform json serialization
|
||||
var jsonData = JSON.stringify(ssidData);
|
||||
// write in-progress status message to ui
|
||||
PEACH_NETWORK.flashMsg("info", "Connecting to access point...");
|
||||
// send add_wifi POST request
|
||||
fetch("/api/v1/network/wifi/connect", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonData
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
// write json response message to ui
|
||||
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg);
|
||||
})
|
||||
}, false);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// catch click of 'Disconnect' button and make POST request
|
||||
PEACH_NETWORK.disconnect = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var disconnectWifi = document.getElementById('disconnectWifi');
|
||||
if (disconnectWifi) {
|
||||
disconnectWifi.addEventListener('click', function(e) {
|
||||
// prevent form submission (default behavior)
|
||||
e.preventDefault();
|
||||
// retrieve ssid value and append to form data object
|
||||
var ssid = document.getElementById('disconnectSsid').value;
|
||||
// create key:value pair
|
||||
var ssidData = { ssid: ssid };
|
||||
// perform json serialization
|
||||
var jsonData = JSON.stringify(ssidData);
|
||||
// write in-progress status message to ui
|
||||
PEACH_NETWORK.flashMsg("info", "Disconnecting from access point...");
|
||||
// send disconnect_wifi POST request
|
||||
fetch("/api/v1/network/wifi/disconnect", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonData
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
// write json response message to ui
|
||||
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg);
|
||||
})
|
||||
}, false);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// catch click of 'Forget' button (form) and make POST request
|
||||
PEACH_NETWORK.forget = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var forgetWifi = document.getElementById('forgetWifi');
|
||||
if (forgetWifi) {
|
||||
forgetWifi.addEventListener('click', function(e) {
|
||||
// prevent form submission (default behavior)
|
||||
e.preventDefault();
|
||||
// retrieve ssid value
|
||||
var ssid = document.getElementById('forgetSsid').value;
|
||||
// create key:value pair
|
||||
var ssidData = { ssid: ssid };
|
||||
// perform json serialization
|
||||
var jsonData = JSON.stringify(ssidData);
|
||||
// write in-progress status message to ui
|
||||
PEACH_NETWORK.flashMsg("info", "Removing credentials for access point...");
|
||||
// send forget_ap POST request
|
||||
fetch("/api/v1/network/wifi/forget", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonData
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
// write json response message to ui
|
||||
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg);
|
||||
})
|
||||
}, false);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// display a message by appending a paragraph element
|
||||
PEACH_NETWORK.flashMsg = function(status, msg) {
|
||||
// set the class of the element according to status
|
||||
var elementClass;
|
||||
if (status === "success") {
|
||||
elementClass = "capsule center-text flash-message font-success";
|
||||
} else if (status === "info") {
|
||||
elementClass = "capsule center-text flash-message font-info";
|
||||
} else {
|
||||
elementClass = "capsule center-text flash-message font-failure";
|
||||
};
|
||||
|
||||
var flashElement = document.getElementById("flashMsg");
|
||||
// if flashElement exists, update the class & text
|
||||
if (flashElement) {
|
||||
flashElement.className = elementClass;
|
||||
flashElement.innerText = msg;
|
||||
// if flashElement does not exist, create it, set id, class, text & append
|
||||
} else {
|
||||
// create new div for flash message
|
||||
var flashDiv = document.createElement("DIV");
|
||||
// set div attributes
|
||||
flashDiv.id = "flashMsg";
|
||||
flashDiv.className = elementClass;
|
||||
// add json response message to flash message div
|
||||
var flashMsg = document.createTextNode(msg);
|
||||
flashDiv.appendChild(flashMsg);
|
||||
// insert the flash message div below the button div
|
||||
var buttonDiv = document.getElementById("buttonDiv");
|
||||
// flashDiv will be added to the end since buttonDiv is the last
|
||||
// child within the parent element (card-container div)
|
||||
buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
var detailInstance = PEACH_NETWORK;
|
||||
detailInstance.connect();
|
||||
detailInstance.disconnect();
|
||||
detailInstance.forget();
|
@ -1,92 +0,0 @@
|
||||
/*
|
||||
|
||||
behavioural layer for the `network_modify.html.tera` template
|
||||
|
||||
- intercept button click for modify (form submission of credentials)
|
||||
- perform json api call
|
||||
- update the dom
|
||||
|
||||
methods:
|
||||
|
||||
PEACH_NETWORK.modify();
|
||||
PEACH_NETWORK.flashMsg(status, msg);
|
||||
|
||||
*/
|
||||
|
||||
var PEACH_NETWORK = {};
|
||||
|
||||
// catch click of 'Save' button and make POST request
|
||||
PEACH_NETWORK.modify = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.body.addEventListener('submit', function(e) {
|
||||
// prevent redirect on button press (default behavior)
|
||||
e.preventDefault();
|
||||
// capture form data
|
||||
var formElement = document.querySelector("form");
|
||||
// create form data object from the wifiModify form element
|
||||
var formData = new FormData(formElement);
|
||||
var object = {};
|
||||
// assign ssid and pass from form
|
||||
formData.forEach(function(value, key){
|
||||
object[key] = value;
|
||||
});
|
||||
// perform json serialization
|
||||
var jsonData = JSON.stringify(object);
|
||||
// write in-progress status message to ui
|
||||
PEACH_NETWORK.flashMsg("info", "Updating WiFi password...");
|
||||
// send new_password POST request
|
||||
fetch("/api/v1/network/wifi/modify", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonData
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
// write json response message to ui
|
||||
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg);
|
||||
})
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
|
||||
// display a message by appending a paragraph element
|
||||
PEACH_NETWORK.flashMsg = function(status, msg) {
|
||||
// set the class of the element according to status
|
||||
var elementClass;
|
||||
if (status === "success") {
|
||||
elementClass = "capsule center-text flash-message font-success";
|
||||
} else if (status === "info") {
|
||||
elementClass = "capsule center-text flash-message font-info";
|
||||
} else {
|
||||
elementClass = "capsule center-text flash-message font-failure";
|
||||
};
|
||||
|
||||
var flashElement = document.getElementById("flashMsg");
|
||||
// if flashElement exists, update the class & text
|
||||
if (flashElement) {
|
||||
flashElement.className = elementClass;
|
||||
flashElement.innerText = msg;
|
||||
// if flashElement does not exist, create it, set id, class, text & append
|
||||
} else {
|
||||
// create new div for flash message
|
||||
var flashDiv = document.createElement("DIV");
|
||||
// set div attributes
|
||||
flashDiv.id = "flashMsg";
|
||||
flashDiv.className = elementClass;
|
||||
// add json response message to flash message div
|
||||
var flashMsg = document.createTextNode(msg);
|
||||
flashDiv.appendChild(flashMsg);
|
||||
// insert the flash message div below the button div
|
||||
var buttonDiv = document.getElementById("buttonDiv");
|
||||
// flashDiv will be added to the end since buttonDiv is the last
|
||||
// child within the parent element (card-container div)
|
||||
buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
var modifyInstance = PEACH_NETWORK;
|
||||
modifyInstance.modify();
|
@ -1,173 +0,0 @@
|
||||
/*
|
||||
|
||||
behavioural layer for the `network_usage.html.tera` template,
|
||||
corresponding to the web route `/network/wifi/usage`
|
||||
|
||||
- intercept form submissions
|
||||
- perform json api calls
|
||||
- update the dom
|
||||
|
||||
methods:
|
||||
|
||||
PEACH_NETWORK.updateAlerts();
|
||||
PEACH_NETWORK.resetUsage();
|
||||
PEACH_NETWORK.toggleWarning();
|
||||
PEACH_NETWORK.toggleCutoff();
|
||||
PEACH_NETWORK.flashMsg(status, msg);
|
||||
|
||||
*/
|
||||
|
||||
var PEACH_NETWORK = {};
|
||||
|
||||
// catch click of 'Update' and make POST request
|
||||
PEACH_NETWORK.updateAlerts = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.body.addEventListener('submit', function(e) {
|
||||
// prevent redirect on button press (default behavior)
|
||||
e.preventDefault();
|
||||
// capture form data
|
||||
var formElement = document.querySelector("form");
|
||||
let warn = formElement.elements.warn.value;
|
||||
let cut = formElement.elements.cut.value;
|
||||
let warn_flag = formElement.elements.warn_flag.checked;
|
||||
let cut_flag = formElement.elements.cut_flag.checked;
|
||||
// perform json serialization
|
||||
var jsonData = JSON.stringify({
|
||||
"warn": parseFloat(warn),
|
||||
"cut": parseFloat(cut),
|
||||
"warn_flag": warn_flag,
|
||||
"cut_flag": cut_flag,
|
||||
});
|
||||
// send update_alerts POST request
|
||||
fetch("/api/v1/network/wifi/usage", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonData
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
// write json response message to ui
|
||||
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg);
|
||||
})
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
|
||||
// catch click of 'Reset' and make POST request
|
||||
PEACH_NETWORK.resetUsage = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var resetBtn = document.getElementById('resetTotal');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', function(e) {
|
||||
// prevent form submission (default behavior)
|
||||
e.preventDefault();
|
||||
// send reset_data_usage POST request
|
||||
fetch("/api/v1/network/wifi/usage/reset", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
console.log(jsonData.msg);
|
||||
// write json response message to ui
|
||||
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg);
|
||||
// if reset is successful, update the ui
|
||||
if (jsonData.status === "success") {
|
||||
console.log(jsonData.data);
|
||||
PEACH_NETWORK.updateTotal(jsonData.data);
|
||||
}
|
||||
})
|
||||
}, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// update data usage total in ui
|
||||
PEACH_NETWORK.updateTotal = function(data) {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log(data);
|
||||
let label = document.getElementById("dataTotal");
|
||||
// take usage total as bytes, convert to MB and round to nearest integer
|
||||
label.textContent = (data / 1024 / 1024).round();
|
||||
});
|
||||
};
|
||||
|
||||
// update ui for warning
|
||||
PEACH_NETWORK.toggleWarning = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let i = document.getElementById("warnIcon");
|
||||
let warnCheck = document.getElementById("warnCheck");
|
||||
warnCheck.addEventListener('click', function(e) {
|
||||
console.log('Toggling warning icon state');
|
||||
if (warnCheck.checked) {
|
||||
i.className = "icon";
|
||||
} else {
|
||||
i.className = "icon icon-inactive";
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// update ui for cutoff
|
||||
PEACH_NETWORK.toggleCutoff = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let i = document.getElementById("cutIcon");
|
||||
let cutCheck = document.getElementById("cutCheck");
|
||||
cutCheck.addEventListener('click', function(e) {
|
||||
console.log('Toggling cutoff icon state');
|
||||
if (cutCheck.checked) {
|
||||
i.className = "icon";
|
||||
} else {
|
||||
i.className = "icon icon-inactive";
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// display a message by appending a paragraph element
|
||||
PEACH_NETWORK.flashMsg = function(status, msg) {
|
||||
// set the class of the element according to status
|
||||
var elementClass;
|
||||
if (status === "success") {
|
||||
elementClass = "capsule center-text flash-message font-success";
|
||||
} else if (status === "info") {
|
||||
elementClass = "capsule center-text flash-message font-info";
|
||||
} else {
|
||||
elementClass = "capsule center-text flash-message font-failure";
|
||||
};
|
||||
|
||||
var flashElement = document.getElementById("flashMsg");
|
||||
// if flashElement exists, update the class & text
|
||||
if (flashElement) {
|
||||
flashElement.className = elementClass;
|
||||
flashElement.innerText = msg;
|
||||
// if flashElement does not exist, create it, set id, class, text & append
|
||||
} else {
|
||||
// create new div for flash message
|
||||
var flashDiv = document.createElement("DIV");
|
||||
// set div attributes
|
||||
flashDiv.id = "flashMsg";
|
||||
flashDiv.className = elementClass;
|
||||
// add json response message to flash message div
|
||||
var flashMsg = document.createTextNode(msg);
|
||||
flashDiv.appendChild(flashMsg);
|
||||
// insert the flash message div below the button div
|
||||
var buttonDiv = document.getElementById("buttonDiv");
|
||||
buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
var usageInstance = PEACH_NETWORK;
|
||||
usageInstance.resetUsage();
|
||||
usageInstance.toggleWarning();
|
||||
usageInstance.toggleCutoff();
|
||||
usageInstance.updateAlerts();
|
@ -1,119 +0,0 @@
|
||||
/*
|
||||
|
||||
behavioural layer for the `power.html.tera` template,
|
||||
corresponding to the web route `/power`
|
||||
|
||||
- intercept button clicks for reboot & shutdown
|
||||
- perform json api calls
|
||||
- update the dom
|
||||
|
||||
methods:
|
||||
|
||||
PEACH_DEVICE.reboot();
|
||||
PEACH_DEVICE.shutdown();
|
||||
PEACH_DEVICE.flashMsg(status, msg);
|
||||
|
||||
*/
|
||||
|
||||
var PEACH_DEVICE = {};
|
||||
|
||||
// catch click of 'Reboot' button and make POST request
|
||||
PEACH_DEVICE.reboot = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var rebootDevice = document.getElementById('rebootBtn');
|
||||
if (rebootDevice) {
|
||||
rebootDevice.addEventListener('click', function(e) {
|
||||
// prevent redirect on button press (default behavior)
|
||||
e.preventDefault();
|
||||
// write reboot flash message
|
||||
PEACH_DEVICE.flashMsg("success", "Rebooting the device...");
|
||||
// send reboot_device POST request
|
||||
fetch("/api/v1/admin/reboot", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
console.log(jsonData.msg);
|
||||
// write json response message to ui
|
||||
PEACH_DEVICE.flashMsg(jsonData.status, jsonData.msg);
|
||||
})
|
||||
}, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// catch click of 'Shutdown' button and make POST request
|
||||
PEACH_DEVICE.shutdown = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var shutdownDevice = document.getElementById('shutdownBtn');
|
||||
if (shutdownDevice) {
|
||||
shutdownDevice.addEventListener('click', function(e) {
|
||||
// prevent form submission (default behavior)
|
||||
e.preventDefault();
|
||||
// write shutdown flash message
|
||||
PEACH_DEVICE.flashMsg("success", "Shutting down the device...");
|
||||
// send shutdown_device POST request
|
||||
fetch("/api/v1/shutdown", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
console.log(jsonData.msg);
|
||||
// write json response message to ui
|
||||
PEACH_DEVICE.flashMsg(jsonData.status, jsonData.msg);
|
||||
})
|
||||
}, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// display a message by appending a paragraph element
|
||||
PEACH_DEVICE.flashMsg = function(status, msg) {
|
||||
// set the class of the element according to status
|
||||
var elementClass;
|
||||
if (status === "success") {
|
||||
elementClass = "capsule center-text flash-message font-success";
|
||||
} else if (status === "info") {
|
||||
elementClass = "capsule center-text flash-message font-info";
|
||||
} else {
|
||||
elementClass = "capsule center-text flash-message font-failure";
|
||||
};
|
||||
|
||||
var flashElement = document.getElementById("flashMsg");
|
||||
// if flashElement exists, update the class & text
|
||||
if (flashElement) {
|
||||
flashElement.className = elementClass;
|
||||
flashElement.innerText = msg;
|
||||
// if flashElement does not exist, create it, set id, class, text & append
|
||||
} else {
|
||||
// create new div for flash message
|
||||
var flashDiv = document.createElement("DIV");
|
||||
// set div attributes
|
||||
flashDiv.id = "flashMsg";
|
||||
flashDiv.className = elementClass;
|
||||
// add json response message to flash message div
|
||||
var flashMsg = document.createTextNode(msg);
|
||||
flashDiv.appendChild(flashMsg);
|
||||
// insert the flash message div below the button div
|
||||
var buttonDiv = document.getElementById("buttonDiv");
|
||||
// flashDiv will be added to the end since buttonDiv is the last
|
||||
// child within the parent element (card-container div)
|
||||
buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
var deviceInstance = PEACH_DEVICE;
|
||||
deviceInstance.reboot();
|
||||
deviceInstance.shutdown();
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* behavioural layer for the `reset_password.html.tera` template,
|
||||
*/
|
||||
|
||||
// catch click of 'Save' button and make POST request
|
||||
PEACH.add = function() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.body.addEventListener('submit', function(e) {
|
||||
// prevent redirect on button press (default behavior)
|
||||
e.preventDefault();
|
||||
// capture form data
|
||||
var formElement = document.querySelector("form");
|
||||
// create form data object from the wifiCreds form element
|
||||
var formData = new FormData(formElement);
|
||||
var object = {};
|
||||
// assign values from form
|
||||
formData.forEach(function(value, key){
|
||||
object[key] = value;
|
||||
});
|
||||
// perform json serialization
|
||||
console.log(object);
|
||||
var jsonData = JSON.stringify(object);
|
||||
// write in-progress status message to ui
|
||||
PEACH.flashMsg("info", "Saving new password.");
|
||||
// send add_wifi POST request
|
||||
fetch("/api/v1/admin/reset_password", {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonData
|
||||
})
|
||||
.then( (response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then( (jsonData) => {
|
||||
// write json response message to ui
|
||||
PEACH.flashMsg(jsonData.status, jsonData.msg);
|
||||
})
|
||||
}, false);
|
||||
});
|
||||
}
|
||||
|
||||
var addInstance = PEACH;
|
||||
addInstance.add();
|
@ -13,5 +13,4 @@
|
||||
<body>
|
||||
{% block nav %}{% endblock nav %}
|
||||
</body>
|
||||
<script type="text/javascript" src="/js/common.js"></script>
|
||||
</html>
|
||||
|
@ -1,9 +1,11 @@
|
||||
{%- extends "nav" -%}
|
||||
{%- block card %}
|
||||
<div class="card center">
|
||||
<div class="card-container capsule info-border">
|
||||
<p>No PeachCloud resource exists for this URL. Please ensure that the URL in the address bar is correct.</p>
|
||||
<p>Click the back arrow in the top-left or the PeachCloud logo at the bottom of your screen to return Home.</p>
|
||||
</div>
|
||||
<div class="card center">
|
||||
<div class="capsule-container">
|
||||
<div class="capsule info-border">
|
||||
<p>No PeachCloud resource exists for this URL. Please ensure that the URL in the address bar is correct.</p>
|
||||
<p>Click the back arrow in the top-left or the PeachCloud logo at the bottom of your screen to return Home.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock card -%}
|
||||
|
@ -12,10 +12,6 @@
|
||||
<!-- display error message -->
|
||||
<div class="center-text flash-message font-failure" style="padding-left: 5px;">{{ flash_msg }}.</div>
|
||||
{%- endif %}
|
||||
<!-- share ux information with the user if JS is disabled -->
|
||||
<noscript>
|
||||
<p class="center-text flash-message">This website will be unresponsive while the device shuts down or reboots.</p>
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock card -%}
|
||||
|
@ -24,13 +24,16 @@
|
||||
</div>
|
||||
</a>
|
||||
<!-- middle -->
|
||||
<a class="middle" href="/hello">
|
||||
<div class="circle circle-large">
|
||||
</div>
|
||||
<a class="middle">
|
||||
<div class="circle circle-large"></div>
|
||||
</a>
|
||||
<!-- bottom-left -->
|
||||
<!-- SYSTEM STATUS LINK AND ICON -->
|
||||
{%- if standalone_mode == true -%}
|
||||
<a class="bottom-left" href="/status/scuttlebutt" title="Status">
|
||||
{% else -%}
|
||||
<a class="bottom-left" href="/status" title="Status">
|
||||
{%- endif -%}
|
||||
<div class="circle circle-small">
|
||||
<img class="icon-medium" src="/icons/heart-pulse.svg">
|
||||
</div>
|
||||
|
@ -10,9 +10,6 @@
|
||||
</div>
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
|
||||
{% include "snippets/noscript" %}
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="/js/power_menu.js"></script>
|
||||
{%- endblock card -%}
|
||||
|
@ -5,8 +5,6 @@
|
||||
<div class="card-container">
|
||||
<!-- FLASH MESSAGE -->
|
||||
{% include "snippets/flash_message" %}
|
||||
<!-- NO SCRIPT FOR WHEN JS IS DISABLED -->
|
||||
{% include "snippets/noscript" %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endblock card -%}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user