49 Commits

Author SHA1 Message Date
8032b83c41 resolve merge conflicts 2021-12-13 08:26:25 +02:00
dfa1306b2d update lockfile 2021-12-13 08:20:03 +02:00
2f7c7aac8f bump version and fix wpactrl dep import 2021-12-13 08:19:55 +02:00
46b7c0fc2b update wpactrl tests 2021-12-13 08:19:25 +02:00
f62c8f0b51 merge stats changes 2021-12-13 07:25:30 +02:00
06e48deb3a Merge pull request 'Remove jsonrpc from peach-stats' (#43) from separate_stats_logic into main
Reviewed-on: #43
2021-12-13 05:20:58 +00:00
fb6d0317b6 bump version 2021-12-13 07:19:14 +02:00
cfbf052d27 fix example code 2021-12-13 07:19:05 +02:00
d240741958 update lockfile 2021-12-10 11:02:06 +02:00
33486b4e1d jsonrpc server with peach-stats methods 2021-12-10 11:01:51 +02:00
8c3fecb875 add docs about feature flags 2021-12-09 09:54:53 +02:00
0907fbc474 remove jsonrpc from peach-stats 2021-12-09 09:44:27 +02:00
b747ff6db2 Merge pull request 'Use tuple err source PeachError::JsonRpcClientCore' (#42) from fix_client_error into main
Reviewed-on: #42
2021-12-08 08:10:07 +00:00
220c7fd540 use tuple err source 2021-12-08 10:07:58 +02:00
ed7e172efb Merge pull request 'Fix manifest mess for peach-lib from nanorand PR' (#40) from nanorand_again into main
Reviewed-on: #40
2021-12-07 12:16:47 +00:00
bc0f2d595b fix pr mess for peach-lib 2021-12-07 14:14:51 +02:00
61b33d1613 trying to resolve conflict for nanorand pr 2021-12-07 14:08:21 +02:00
c3fa188400 testing refactored wpactrl 2021-12-07 10:04:58 +02:00
a1444cf478 Merge pull request 'Error refactor for peach-lib' (#38) from lib_error_refactor into main
Reviewed-on: #38
2021-12-07 08:00:51 +00:00
79c94e6af0 replace snafu with custom error type 2021-12-01 14:26:30 +02:00
cd8e5737c4 implement custom error type 2021-11-30 13:48:16 +02:00
2429ea8fdd replace rand with nanorand and bump version 2021-11-25 15:00:19 +02:00
c8d0a2ddf6 Merge pull request 'Add ENV VAR to allow disabling Rocket authentication' (#36) from disable_auth into main
Reviewed-on: #36
2021-11-25 10:10:54 +00:00
adc1a5bd77 update lockfile 2021-11-25 11:33:25 +02:00
d760f9f92c add init_rocket test wrapper 2021-11-25 11:33:09 +02:00
5c4ef4a529 update auth docs 2021-11-25 11:15:39 +02:00
35ff408365 use rocket config to (en|dis)able auth 2021-11-25 11:12:50 +02:00
c2b785f54b remove unnecessary websocket dependency 2021-11-25 11:12:05 +02:00
1dc740eeae Merge pull request 'Custom error implementation for peach-oled' (#34) from lean_refactor into main
Reviewed-on: #34
2021-11-24 09:16:19 +00:00
b3c6138e03 Merge pull request 'Fix broken JS and tweak styling (CSS & HTML)' (#33) from js_and_style_fixes into main
Reviewed-on: #33
2021-11-24 09:15:50 +00:00
b59e62f920 bump crate version 2021-11-23 12:30:34 +02:00
3325706dcb document disable auth flag 2021-11-23 12:30:24 +02:00
bc28a84ad4 disable auth for tests 2021-11-23 12:30:09 +02:00
bb34bdd653 add auth flag check 2021-11-23 12:29:58 +02:00
ac98bde760 lockfile update 2021-11-23 10:53:09 +02:00
361b159299 update version number 2021-11-23 10:52:40 +02:00
7344d6f4e0 minor variable name changes and updated error handling 2021-11-23 10:52:26 +02:00
8d18e712a1 implement custom error 2021-11-23 10:51:54 +02:00
116afe78fd remove snafu and bump deps 2021-11-23 10:51:42 +02:00
7e3c500b1e Merge branch 'main' into js_and_style_fixes 2021-11-22 15:57:44 +02:00
b59eb22082 fix function names and remove flashMsg code duplication 2021-11-22 15:31:39 +02:00
ee1da0599c remove status update logic 2021-11-22 15:30:45 +02:00
e5f9a9be83 remove duplicate flashMsg code 2021-11-22 15:30:18 +02:00
e54ff8829a remove duplicate flashMsg code 2021-11-22 15:29:40 +02:00
554997a5c0 fix context back and title 2021-11-18 13:49:10 +02:00
da51070ccd improve template styling consistency 2021-11-18 11:48:37 +02:00
925051a379 improve not_found template 2021-11-18 11:48:03 +02:00
380ee2683a fix js 2021-11-18 11:47:50 +02:00
bae3b7c2ce tweak styling 2021-11-18 11:47:33 +02:00
50 changed files with 1493 additions and 1733 deletions

610
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,25 @@
[package]
name = "peach-jsonrpc-server"
authors = ["Andrew Reid <glyph@mycelial.technology>"]
version = "0.1.0"
edition = "2021"
description = "JSON-RPC over HTTP for the PeachCloud system. Provides a JSON-RPC wrapper around the stats, network and oled libraries."
homepage = "https://opencollective.com/peachcloud"
repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace"
readme = "README.md"
license = "AGPL-3.0-only"
publish = false
[badges]
maintenance = { status = "actively-developed" }
[dependencies]
env_logger = "0.9"
jsonrpc-core = "18"
jsonrpc-http-server = "18"
log = "0.4"
miniserde = "0.1.15"
peach-stats = { path = "../peach-stats", features = ["miniserde_support"] }
[dev-dependencies]
jsonrpc-test = "18"

View File

@ -0,0 +1,72 @@
# peach-jsonrpc-server
A JSON-RPC server for the PeachCloud system which exposes an API over HTTP.
Currently includes peach-stats capability (system statistics).
## JSON-RPC API
| Method | Description | Returns |
| --- | --- | --- |
| `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` |
| `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` |
| `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` |
| `load_average` | Load average statistics | `one`, `five`, `fifteen` |
| `mem_stats` | Memory statistics | `total`, `free`, `used` |
| `ping` | Microservice status | `success` if running |
| `uptime` | System uptime | `secs` |
## Environment
The JSON-RPC HTTP server is currently hardcoded to run on "127.0.0.1:5110". Address and port configuration settings will later be exposed via CLI arguments and possibly an environment variable.
Logging is made available with `env_logger`:
`export RUST_LOG=info`
Other logging levels include `debug`, `warn` and `error`.
## Setup
Clone the peach-workspace repo:
`git clone https://git.coopcloud.tech/PeachCloud/peach-workspace`
Move into the repo peaach-jsonrpc-server directory and compile a release build:
`cd peach-jsonrpc-server`
`cargo build --release`
Run the binary:
`./peach-workspace/target/release/peach-jsonrpc-server`
## Debian Packaging
TODO.
## Example Usage
**Get CPU Statistics**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "cpu_stats", "id":1 }' 127.0.0.1:5110`
Server responds with:
`{"jsonrpc":"2.0","result":"{\"user\":4661083,\"system\":1240371,\"idle\":326838290,\"nice\":0}","id":1}`
**Get System Uptime**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "uptime", "id":1 }' 127.0.0.1:5110`
Server responds with:
`{"jsonrpc":"2.0","result":"{\"secs\":840968}","id":1}`
### Licensing
AGPL-3.0

View File

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

View File

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

View File

@ -0,0 +1,34 @@
#![warn(missing_docs)]
//! # peach-jsonrpc-server
//!
//! A JSON-RPC server which exposes an over HTTP.
//!
//! Currently includes peach-stats capability (system statistics).
//!
//! ## API
//!
//! | Method | Description | Returns |
//! | --- | --- | --- |
//! | `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` |
//! | `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` |
//! | `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` |
//! | `load_average` | Load average statistics | `one`, `five`, `fifteen` |
//! | `mem_stats` | Memory statistics | `total`, `free`, `used` |
//! | `ping` | Microservice status | `success` if running |
//! | `uptime` | System uptime | `secs` |
use std::process;
use log::error;
fn main() {
// initalize the logger
env_logger::init();
// handle errors returned from `run`
if let Err(e) = peach_jsonrpc_server::run() {
error!("Application error: {}", e);
process::exit(1);
}
}

View File

@ -1,19 +1,19 @@
[package]
name = "peach-lib"
version = "1.3.0"
version = "1.3.1"
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"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
nanorand = "0.6.1"
regex = "1"
rust-crypto = "0.2.36"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.8"
regex = "1"
chrono = "0.4.19"
rand="0.8.4"
fslock="0.1.6"

View File

@ -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 crate::{config_manager, error::PeachError, sbot_client};
@ -68,13 +66,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()?;

View File

@ -1,6 +1,6 @@
[package]
name = "peach-network"
version = "0.2.12"
version = "0.2.13"
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
edition = "2018"
description = "Query and configure network interfaces using JSON-RPC over HTTP."
@ -28,7 +28,6 @@ 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"
@ -36,9 +35,9 @@ log = "0.4"
probes = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
snafu = "0.6"
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"

View File

@ -1,6 +1,6 @@
# peach-network
[![Build Status](https://travis-ci.com/peachcloud/peach-network.svg?branch=master)](https://travis-ci.com/peachcloud/peach-network) ![Generic badge](https://img.shields.io/badge/version-0.2.12-<COLOR>.svg)
[![Build Status](https://travis-ci.com/peachcloud/peach-network.svg?branch=master)](https://travis-ci.com/peachcloud/peach-network) ![Generic badge](https://img.shields.io/badge/version-0.2.13-<COLOR>.svg)
Networking microservice module for PeachCloud. Query and configure device interfaces using [JSON-RPC](https://www.jsonrpc.org/specification) over http.

View File

@ -1,156 +1,302 @@
use std::{error, io, str};
use std::io;
use std::num::ParseIntError;
use jsonrpc_core::{types::error::Error, ErrorCode};
use io::Error as IoError;
use jsonrpc_core::{types::error::Error as JsonRpcError, ErrorCode};
use probes::ProbeError;
use serde_json::error::Error as SerdeError;
use snafu::Snafu;
use regex::Error as RegexError;
use serde_json::Error as SerdeError;
use wpactrl::WpaError;
pub type BoxError = Box<dyn error::Error>;
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
#[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 },
NoState { iface: String, source: IoError },
#[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 },
GenWpaPassphrase { ssid: String, source: IoError },
#[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 },
NoIp { iface: String, source: IoError },
#[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"))]
SavedNetworks,
#[snafu(display("No networks found in range of interface: {}", iface))]
AvailableNetworks { iface: String },
#[snafu(display("Missing expected parameters: {}", e))]
MissingParams { e: Error },
MissingParams(JsonRpcError),
#[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 },
ParseInt(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"))]
Reconfigure,
#[snafu(display("Failed to reconnect with WiFi network for interface: {}", iface))]
Reconnect { iface: String },
#[snafu(display("Regex command failed"))]
Regex { source: regex::Error },
Regex(RegexError),
#[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 },
WlanState(IoError),
#[snafu(display("Failed to retrieve connection state of wlan0 interface: {}", source))]
WlanOperstate { source: io::Error },
WlanOperstate(IoError),
#[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 },
StartAp0(IoError),
#[snafu(display("Failed to start wlan0 service: {}", source))]
StartWlan0 { source: io::Error },
StartWlan0(IoError),
#[snafu(display("JSON serialization failed: {}", source))]
SerdeSerialize { source: SerdeError },
SerdeSerialize(SerdeError),
#[snafu(display("Failed to open control interface for wpasupplicant"))]
WpaCtrlOpen {
#[snafu(source(from(failure::Error, std::convert::Into::into)))]
source: BoxError,
},
#[snafu(display("Request to wpasupplicant via wpactrl failed"))]
WpaCtrlRequest {
#[snafu(source(from(failure::Error, std::convert::Into::into)))]
source: BoxError,
},
WpaCtrl(WpaError),
}
impl From<NetworkError> for Error {
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::MissingParams(ref source) => Some(source),
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 => None,
NetworkError::Connect { .. } => None,
NetworkError::StartWlan0(ref source) => Some(source),
NetworkError::StartAp0(ref source) => Some(source),
NetworkError::SerdeSerialize(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 traffice 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::MissingParams(ref source) => {
write!(f, "Missing expected parameters: {}", source)
}
NetworkError::Modify { ref id, ref iface } => {
write!(
f,
"Failed to set new password for network {} on {}",
id, iface
)
}
NetworkError::Ip { ref iface } => {
write!(f, "No IP found for interface: {}", iface)
}
NetworkError::ParseInt(_) => {
write!(f, "Failed to parse integer from string for RSSI value")
}
NetworkError::NoTraffic { ref iface, .. } => {
write!(
f,
"Failed to retrieve network traffic measurement for {}",
iface
)
}
NetworkError::Reassociate { ref iface } => {
write!(
f,
"Failed to reassociate with WiFi network for interface: {}",
iface
)
}
NetworkError::Reconfigure => {
write!(
f,
"Failed to force reread of wpa_supplicant configuration file"
)
}
NetworkError::Reconnect { ref iface } => {
write!(
f,
"Failed to reconnect with WiFi network for interface: {}",
iface
)
}
NetworkError::Regex(_) => write!(f, "Regex command failed"),
NetworkError::Delete { ref id, ref iface } => {
write!(
f,
"Failed to delete network {} for interface: {}",
id, iface
)
}
NetworkError::WlanState(_) => write!(f, "Failed to retrieve state of wlan0 service"),
NetworkError::WlanOperstate(_) => {
write!(f, "Failed to retrieve connection state of wlan0 interface")
}
NetworkError::Save => write!(f, "Failed to save configuration changes to file"),
NetworkError::Connect { ref id, ref iface } => {
write!(
f,
"Failed to connect to network {} for interface: {}",
id, iface
)
}
NetworkError::StartWlan0(_) => write!(f, "Failed to start ap0 service"),
NetworkError::StartAp0(_) => write!(f, "Failed to start wlan0 service"),
NetworkError::SerdeSerialize(_) => write!(f, "JSON serialization failed"),
NetworkError::WpaCtrl(_) => write!(f, "WpaCtrl command failed"),
}
}
}
impl From<SerdeError> for NetworkError {
fn from(err: SerdeError) -> Self {
NetworkError::SerdeSerialize(err)
}
}
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)
}
}
impl From<NetworkError> for JsonRpcError {
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 {
NetworkError::Add { ssid } => JsonRpcError {
code: ErrorCode::ServerError(-32000),
message: format!("Failed to add network for {}", ssid),
data: None,
},
NetworkError::NoState { iface, source } => Error {
NetworkError::NoState { iface, source } => JsonRpcError {
code: ErrorCode::ServerError(-32022),
message: format!(
"Failed to retrieve interface state for {}: {}",
@ -158,22 +304,22 @@ impl From<NetworkError> for Error {
),
data: None,
},
NetworkError::Disable { id, iface } => Error {
NetworkError::Disable { id, iface } => JsonRpcError {
code: ErrorCode::ServerError(-32029),
message: format!("Failed to disable network {} for {}", id, iface),
data: None,
},
NetworkError::Disconnect { iface } => Error {
NetworkError::Disconnect { iface } => JsonRpcError {
code: ErrorCode::ServerError(-32032),
message: format!("Failed to disconnect {}", iface),
data: None,
},
NetworkError::GenWpaPassphrase { ssid, source } => Error {
NetworkError::GenWpaPassphrase { ssid, source } => JsonRpcError {
code: ErrorCode::ServerError(-32025),
message: format!("Failed to generate wpa passphrase for {}: {}", ssid, source),
data: None,
},
NetworkError::GenWpaPassphraseWarning { ssid, err_msg } => Error {
NetworkError::GenWpaPassphraseWarning { ssid, err_msg } => JsonRpcError {
code: ErrorCode::ServerError(-32036),
message: format!(
"Failed to generate wpa passphrase for {}: {}",
@ -181,17 +327,17 @@ impl From<NetworkError> for Error {
),
data: None,
},
NetworkError::Id { iface, ssid } => Error {
NetworkError::Id { iface, ssid } => JsonRpcError {
code: ErrorCode::ServerError(-32026),
message: format!("No ID found for {} on interface {}", ssid, iface),
data: None,
},
NetworkError::NoIp { iface, source } => Error {
NetworkError::NoIp { iface, source } => JsonRpcError {
code: ErrorCode::ServerError(-32001),
message: format!("Failed to retrieve IP address for {}: {}", iface, source),
data: None,
},
NetworkError::Rssi { iface } => Error {
NetworkError::Rssi { iface } => JsonRpcError {
code: ErrorCode::ServerError(-32002),
message: format!(
"Failed to retrieve RSSI for {}. Interface may not be connected",
@ -199,7 +345,7 @@ impl From<NetworkError> for Error {
),
data: None,
},
NetworkError::RssiPercent { iface } => Error {
NetworkError::RssiPercent { iface } => JsonRpcError {
code: ErrorCode::ServerError(-32034),
message: format!(
"Failed to retrieve signal quality (%) for {}. Interface may not be connected",
@ -207,7 +353,7 @@ impl From<NetworkError> for Error {
),
data: None,
},
NetworkError::Ssid { iface } => Error {
NetworkError::Ssid { iface } => JsonRpcError {
code: ErrorCode::ServerError(-32003),
message: format!(
"Failed to retrieve SSID for {}. Interface may not be connected",
@ -215,17 +361,17 @@ impl From<NetworkError> for Error {
),
data: None,
},
NetworkError::State { iface } => Error {
NetworkError::State { iface } => JsonRpcError {
code: ErrorCode::ServerError(-32023),
message: format!("No state found for {}. Interface may not exist", iface),
data: None,
},
NetworkError::Status { iface } => Error {
NetworkError::Status { iface } => JsonRpcError {
code: ErrorCode::ServerError(-32024),
message: format!("No status found for {}. Interface may not exist", iface),
data: None,
},
NetworkError::Traffic { iface } => Error {
NetworkError::Traffic { iface } => JsonRpcError {
code: ErrorCode::ServerError(-32004),
message: format!(
"No network traffic statistics found for {}. Interface may not exist",
@ -233,28 +379,28 @@ impl From<NetworkError> for Error {
),
data: None,
},
NetworkError::SavedNetworks => Error {
NetworkError::SavedNetworks => JsonRpcError {
code: ErrorCode::ServerError(-32005),
message: "No saved networks found".to_string(),
data: None,
},
NetworkError::AvailableNetworks { iface } => Error {
NetworkError::AvailableNetworks { iface } => JsonRpcError {
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 {
NetworkError::MissingParams(e) => e.clone(),
NetworkError::Modify { id, iface } => JsonRpcError {
code: ErrorCode::ServerError(-32033),
message: format!("Failed to set new password for network {} on {}", id, iface),
data: None,
},
NetworkError::Ip { iface } => Error {
NetworkError::Ip { iface } => JsonRpcError {
code: ErrorCode::ServerError(-32007),
message: format!("No IP address found for {}", iface),
data: None,
},
NetworkError::ParseString { source } => Error {
NetworkError::ParseInt(source) => JsonRpcError {
code: ErrorCode::ServerError(-32035),
message: format!(
"Failed to parse integer from string for RSSI value: {}",
@ -262,7 +408,7 @@ impl From<NetworkError> for Error {
),
data: None,
},
NetworkError::NoTraffic { iface, source } => Error {
NetworkError::NoTraffic { iface, source } => JsonRpcError {
code: ErrorCode::ServerError(-32015),
message: format!(
"Failed to retrieve network traffic statistics for {}: {}",
@ -270,37 +416,37 @@ impl From<NetworkError> for Error {
),
data: None,
},
NetworkError::Reassociate { iface } => Error {
NetworkError::Reassociate { iface } => JsonRpcError {
code: ErrorCode::ServerError(-32008),
message: format!("Failed to reassociate with WiFi network for {}", iface),
data: None,
},
NetworkError::Reconfigure => Error {
NetworkError::Reconfigure => JsonRpcError {
code: ErrorCode::ServerError(-32030),
message: "Failed to force reread of wpa_supplicant configuration file".to_string(),
data: None,
},
NetworkError::Reconnect { iface } => Error {
NetworkError::Reconnect { iface } => JsonRpcError {
code: ErrorCode::ServerError(-32009),
message: format!("Failed to reconnect with WiFi network for {}", iface),
data: None,
},
NetworkError::Regex { source } => Error {
NetworkError::Regex(source) => JsonRpcError {
code: ErrorCode::ServerError(-32010),
message: format!("Regex command error: {}", source),
data: None,
},
NetworkError::Delete { id, iface } => Error {
NetworkError::Delete { id, iface } => JsonRpcError {
code: ErrorCode::ServerError(-32028),
message: format!("Failed to delete network {} for {}", id, iface),
data: None,
},
NetworkError::WlanState { source } => Error {
NetworkError::WlanState(source) => JsonRpcError {
code: ErrorCode::ServerError(-32011),
message: format!("Failed to retrieve state of wlan0 service: {}", source),
data: None,
},
NetworkError::WlanOperstate { source } => Error {
NetworkError::WlanOperstate(source) => JsonRpcError {
code: ErrorCode::ServerError(-32021),
message: format!(
"Failed to retrieve connection state of wlan0 interface: {}",
@ -308,42 +454,34 @@ impl From<NetworkError> for Error {
),
data: None,
},
NetworkError::Save => Error {
NetworkError::Save => JsonRpcError {
code: ErrorCode::ServerError(-32031),
message: "Failed to save configuration changes to file".to_string(),
data: None,
},
NetworkError::Connect { id, iface } => Error {
NetworkError::Connect { id, iface } => JsonRpcError {
code: ErrorCode::ServerError(-32027),
message: format!("Failed to connect to network {} for {}", id, iface),
data: None,
},
NetworkError::StartAp0 { source } => Error {
NetworkError::StartAp0(source) => JsonRpcError {
code: ErrorCode::ServerError(-32016),
message: format!("Failed to start ap0 service: {}", source),
data: None,
},
NetworkError::StartWlan0 { source } => Error {
NetworkError::StartWlan0(source) => JsonRpcError {
code: ErrorCode::ServerError(-32018),
message: format!("Failed to start wlan0 service: {}", source),
data: None,
},
NetworkError::SerdeSerialize { source } => Error {
NetworkError::SerdeSerialize(source) => JsonRpcError {
code: ErrorCode::ServerError(-32012),
message: format!("JSON serialization failed: {}", source),
data: None,
},
NetworkError::WpaCtrlOpen { source } => Error {
NetworkError::WpaCtrl(source) => JsonRpcError {
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),
message: format!("WPA control interface failure: {}", source),
data: None,
},
}

View File

@ -25,11 +25,11 @@ use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBui
use log::info;
use serde_json::json;
use crate::error::{BoxError, NetworkError};
use crate::error::NetworkError;
use crate::network::{Iface, IfaceId, IfaceIdPass, IfaceSsid, WiFi};
/// Create JSON-RPC I/O handler, add RPC methods and launch HTTP server.
pub fn run() -> Result<(), BoxError> {
pub fn run() -> Result<(), NetworkError> {
info!("Starting up.");
info!("Creating JSON-RPC I/O handler.");
@ -47,7 +47,7 @@ pub fn run() -> Result<(), BoxError> {
None => Err(Error::from(NetworkError::AvailableNetworks { iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -62,7 +62,7 @@ pub fn run() -> Result<(), BoxError> {
None => Err(Error::from(NetworkError::Id { iface, ssid })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -76,7 +76,7 @@ pub fn run() -> Result<(), BoxError> {
None => Err(Error::from(NetworkError::Ip { iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -92,7 +92,7 @@ pub fn run() -> Result<(), BoxError> {
None => Err(Error::from(NetworkError::Rssi { iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -106,7 +106,7 @@ pub fn run() -> Result<(), BoxError> {
None => Err(Error::from(NetworkError::RssiPercent { iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -128,7 +128,7 @@ pub fn run() -> Result<(), BoxError> {
None => Err(Error::from(NetworkError::Ssid { iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -142,7 +142,7 @@ pub fn run() -> Result<(), BoxError> {
None => Err(Error::from(NetworkError::State { iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -159,7 +159,7 @@ pub fn run() -> Result<(), BoxError> {
None => Err(Error::from(NetworkError::Status { iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -173,7 +173,7 @@ pub fn run() -> Result<(), BoxError> {
None => Err(Error::from(NetworkError::Traffic { iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -198,7 +198,7 @@ pub fn run() -> Result<(), BoxError> {
Ok(_) => Ok(Value::String("success".to_string())),
Err(e) => Err(Error::from(e)),
},
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -219,7 +219,7 @@ pub fn run() -> Result<(), BoxError> {
Err(_) => Err(Error::from(NetworkError::Delete { id, iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -234,7 +234,7 @@ pub fn run() -> Result<(), BoxError> {
Err(_) => Err(Error::from(NetworkError::Disable { id, iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -248,7 +248,7 @@ pub fn run() -> Result<(), BoxError> {
Err(_) => Err(Error::from(NetworkError::Disconnect { iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -264,7 +264,7 @@ pub fn run() -> Result<(), BoxError> {
Err(_) => Err(Error::from(NetworkError::Modify { iface, id })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -278,7 +278,7 @@ pub fn run() -> Result<(), BoxError> {
Err(_) => Err(Error::from(NetworkError::Reassociate { iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -297,7 +297,7 @@ pub fn run() -> Result<(), BoxError> {
Err(_) => Err(Error::from(NetworkError::Reconnect { iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -317,7 +317,7 @@ pub fn run() -> Result<(), BoxError> {
Err(_) => Err(Error::from(NetworkError::Connect { id, iface })),
}
}
Err(e) => Err(Error::from(NetworkError::MissingParams { e })),
Err(e) => Err(Error::from(NetworkError::MissingParams(e))),
}
});
@ -374,7 +374,7 @@ mod tests {
message: String::from("Parse error"),
data: None,
};
Err(Error::from(NetworkError::MissingParams { e }))
Err(Error::from(NetworkError::MissingParams(e)))
});
test_rpc::Rpc::from(io)
};
@ -689,7 +689,7 @@ mod tests {
),
data: None,
};
Err(Error::from(NetworkError::MissingParams { e }))
Err(Error::from(NetworkError::MissingParams(e)))
});
test_rpc::Rpc::from(io)
};
@ -842,7 +842,7 @@ mod tests {
let mut io = IoHandler::new();
io.add_method("rpc_regex_error", |_| {
let source = regex::Error::Syntax("oh no!".to_string());
Err(Error::from(NetworkError::Regex { source }))
Err(Error::from(NetworkError::Regex(source)))
});
test_rpc::Rpc::from(io)
};
@ -886,7 +886,7 @@ mod tests {
let mut io = IoHandler::new();
io.add_method("rpc_wlanstate_error", |_| {
let source = IoError::new(ErrorKind::PermissionDenied, "oh no!");
Err(Error::from(NetworkError::WlanState { source }))
Err(Error::from(NetworkError::WlanState(source)))
});
test_rpc::Rpc::from(io)
};
@ -907,7 +907,7 @@ mod tests {
let mut io = IoHandler::new();
io.add_method("rpc_wlanoperstate_error", |_| {
let source = IoError::new(ErrorKind::PermissionDenied, "oh no!");
Err(Error::from(NetworkError::WlanOperstate { source }))
Err(Error::from(NetworkError::WlanOperstate(source)))
});
test_rpc::Rpc::from(io)
};
@ -945,9 +945,13 @@ mod tests {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_wpactrlopen_error", |_| {
let fail_err = failure::err_msg("Permission denied (os error 13)").compat();
let source = Box::new(fail_err);
Err(Error::from(NetworkError::WpaCtrlOpen { source }))
let permission_error = IoError::new(
ErrorKind::PermissionDenied,
"Permission denied (os error 13)",
);
Err(Error::from(NetworkError::WpaCtrl(wpactrl::WpaError::Io(
permission_error,
))))
});
test_rpc::Rpc::from(io)
};
@ -956,7 +960,7 @@ mod tests {
rpc.request("rpc_wpactrlopen_error", &()),
r#"{
"code": -32013,
"message": "Failed to open control interface for wpasupplicant: Permission denied (os error 13)"
"message": "WPA control interface failure: Failed to execute the specified command: Permission denied (os error 13)"
}"#
);
}
@ -967,9 +971,10 @@ mod tests {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_wpactrlrequest_error", |_| {
let fail_err = failure::err_msg("oh no!").compat();
let source = Box::new(fail_err);
Err(Error::from(NetworkError::WpaCtrlRequest { source }))
let conn_refused_error = IoError::new(ErrorKind::ConnectionRefused, "oh no!");
Err(Error::from(NetworkError::WpaCtrl(wpactrl::WpaError::Io(
conn_refused_error,
))))
});
test_rpc::Rpc::from(io)
};
@ -977,8 +982,8 @@ mod tests {
assert_eq!(
rpc.request("rpc_wpactrlrequest_error", &()),
r#"{
"code": -32014,
"message": "WPA supplicant request failed: oh no!"
"code": -32013,
"message": "WPA control interface failure: Failed to execute the specified command: oh no!"
}"#
);
}

View File

@ -21,13 +21,9 @@ use std::{
str,
};
use crate::error::{
GenWpaPassphrase, NetworkError, NoIp, NoState, NoTraffic, ParseString, SerdeSerialize,
StartAp0, StartWlan0, WlanState, WpaCtrlOpen, WpaCtrlRequest,
};
use crate::error::NetworkError;
use probes::network;
use serde::{Deserialize, Serialize};
use snafu::ResultExt;
use crate::utils;
@ -122,6 +118,11 @@ pub struct WiFi {
pub pass: String,
}
// TODO: wrap this into a helper function:
//
// let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
// let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
/* GET - Methods for retrieving data */
/// Retrieve list of available wireless access points for a given network
@ -142,12 +143,9 @@ pub struct WiFi {
///
pub fn available_networks(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)?;
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();
@ -178,7 +176,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)?;
let results = serde_json::to_string(&scan)?;
Ok(Some(results))
}
}
@ -201,11 +199,8 @@ pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
///
pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let 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();
@ -239,7 +234,10 @@ pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
///
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)
@ -265,11 +263,8 @@ pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
///
pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let 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() {
@ -297,17 +292,14 @@ pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
///
pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let 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
@ -335,8 +327,8 @@ pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
/// 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)?;
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();
@ -351,7 +343,7 @@ pub fn saved_networks() -> Result<Option<String>, NetworkError> {
if ssids.is_empty() {
Ok(None)
} else {
let results = serde_json::to_string(&ssids).context(SerdeSerialize)?;
let results = serde_json::to_string(&ssids)?;
Ok(Some(results))
}
}
@ -372,11 +364,8 @@ pub fn saved_networks() -> Result<Option<String>, NetworkError> {
///
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)?;
@ -404,7 +393,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();
@ -433,11 +425,8 @@ pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
///
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)?;
@ -495,7 +484,10 @@ pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
/// response is sent to the caller.
///
pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
let network = network::read().context(NoTraffic { iface })?;
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 {
@ -506,7 +498,7 @@ pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
transmitted,
};
// TODO: add test for SerdeSerialize error
let t = serde_json::to_string(&traffic).context(SerdeSerialize)?;
let t = serde_json::to_string(&traffic)?;
return Ok(Some(t));
}
}
@ -531,7 +523,7 @@ pub fn activate_ap() -> Result<(), NetworkError> {
.arg("start")
.arg("wpa_supplicant@ap0.service")
.output()
.context(StartAp0)?;
.map_err(NetworkError::StartAp0)?;
Ok(())
}
@ -551,7 +543,7 @@ pub fn activate_client() -> Result<(), NetworkError> {
.arg("start")
.arg("wpa_supplicant@wlan0.service")
.output()
.context(StartWlan0)?;
.map_err(NetworkError::StartWlan0)?;
Ok(())
}
@ -576,7 +568,10 @@ pub fn add(wifi: &WiFi) -> Result<(), NetworkError> {
.arg(&wifi.pass)
.stdout(Stdio::piped())
.output()
.context(GenWpaPassphrase { ssid: &wifi.ssid })?;
.map_err(|source| NetworkError::GenWpaPassphrase {
ssid: wifi.ssid.to_string(),
source,
})?;
// prepend newline to wpa_details to safeguard against malformed supplicant
let mut wpa_details = "\n".as_bytes().to_vec();
@ -621,7 +616,7 @@ pub fn check_iface() -> Result<(), NetworkError> {
.arg("is-active")
.arg("wpa_supplicant@wlan0.service")
.status()
.context(WlanState)?;
.map_err(NetworkError::WlanState)?;
// returns the current state of the wlan0 interface
let iface_state = state("wlan0")?;
@ -657,12 +652,9 @@ pub fn check_iface() -> Result<(), NetworkError> {
///
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(())
}
@ -682,12 +674,9 @@ pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
///
pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let 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(())
}
@ -706,12 +695,9 @@ pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
///
pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let 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(())
}
@ -729,12 +715,9 @@ pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
///
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(())
}
@ -754,12 +737,9 @@ pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
///
pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let 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(())
}
@ -777,11 +757,8 @@ pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
///
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(())
}
@ -795,8 +772,8 @@ pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
/// caller.
///
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(())
}
@ -814,12 +791,9 @@ pub fn reconfigure() -> Result<(), NetworkError> {
///
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(())
}
@ -832,7 +806,7 @@ pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
/// appropriate JSON RPC response is sent to the caller.
///
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(())
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,109 +1,47 @@
# peach-stats
[![Build Status](https://travis-ci.com/peachcloud/peach-stats.svg?branch=master)](https://travis-ci.com/peachcloud/peach-stats) ![Generic badge](https://img.shields.io/badge/version-0.1.3-<COLOR>.svg)
![Generic badge](https://img.shields.io/badge/version-0.2.0-<COLOR>.svg)
System statistics microservice module for PeachCloud. Provides a JSON-RPC wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates.
System statistics library for PeachCloud. Provides a wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates.
### JSON-RPC API
Currently offers the following statistics and associated data structures:
| Method | Description | Returns |
| --- | --- | --- |
| `cpu_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.

View File

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

View File

@ -1,69 +1,44 @@
use std::{error, fmt, io};
//! Custom error type for `peach-stats`.
use jsonrpc_core::{types::error::Error, ErrorCode};
use probes::ProbeError;
use 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,
},
}
}
}

View File

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

View File

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

View File

@ -1,14 +1,96 @@
//! System statistics retrieval functions and associated data types.
use std::result::Result;
use 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)
}

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "peach-web"
version = "0.4.11"
version = "0.4.12"
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
edition = "2018"
description = "peach-web is a web application which provides a web interface for monitoring and interacting with the PeachCloud device. This allows administration of the single-board computer (ie. Raspberry Pi) running PeachCloud, as well as the ssb-server and related plugins."
@ -38,17 +38,16 @@ maintenance = { status = "actively-developed" }
env_logger = "0.8"
log = "0.4"
nest = "1.0.0"
openssl = { version = "0.10", features = ["vendored"] }
peach-lib = { path = "../peach-lib" }
percent-encoding = "2.1.0"
regex = "1"
rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
snafu = "0.6"
tera = { version = "1.12.1", features = ["builtins"] }
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"

View File

@ -1,6 +1,6 @@
# peach-web
[![Build Status](https://travis-ci.com/peachcloud/peach-web.svg?branch=master)](https://travis-ci.com/peachcloud/peach-web) ![Generic badge](https://img.shields.io/badge/version-0.4.6-<COLOR>.svg)
[![Build Status](https://travis-ci.com/peachcloud/peach-web.svg?branch=master)](https://travis-ci.com/peachcloud/peach-web) ![Generic badge](https://img.shields.io/badge/version-0.4.12-<COLOR>.svg)
## Web Interface for PeachCloud
@ -39,12 +39,22 @@ _Note: Networking functionality requires peach-network microservice to be runnin
### Environment
**Deployment Mode**
The web application deployment mode is 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.
**Authentication**
Authentication is disabled in `development` mode and enabled by default when running the application in `production` mode. It can be disabled by setting the `ROCKET_DISABLE_AUTH` environment variable to `true`:
`export ROCKET_DISABLE_AUTH=true`
**Logging**
Logging is made available with `env_logger`:
`export RUST_LOG=info`

View File

@ -1,5 +1,7 @@
[development]
template_dir = "templates/"
disable_auth = true
[production]
template_dir = "templates/"
disable_auth = false

View File

@ -1,10 +1,13 @@
use log::info;
use rocket::form::{Form, FromForm};
use rocket::request::FlashMessage;
use rocket::http::{Cookie, CookieJar, Status};
use rocket::request::{self, FlashMessage, FromRequest, Request};
use rocket::response::{Flash, Redirect};
use rocket::serde::json::Json;
use rocket::serde::{Deserialize, Serialize};
use rocket::{get, post};
use rocket::serde::{
json::{Json, Value},
Deserialize, Serialize,
};
use rocket::{get, post, Config};
use rocket_dyn_templates::Template;
use peach_lib::error::PeachError;
@ -12,9 +15,6 @@ use peach_lib::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;
// HELPERS AND STRUCTS FOR AUTHENTICATION WITH COOKIES
@ -42,14 +42,27 @@ 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)),
// check for `disable_auth` config value; set to `false` if unset
// can be set via the `ROCKET_DISABLE_AUTH` environment variable
// - env var, if set, takes precedence over value defined in `Rocket.toml`
let authentication_is_disabled: bool = match Config::figment().find_value("disable_auth") {
// deserialize the boolean value; set to `false` if an error is encountered
Ok(value) => value.deserialize().unwrap_or(false),
Err(_) => false,
};
if authentication_is_disabled {
let auth = Authenticated {};
request::Outcome::Success(auth)
} else {
let authenticated = req
.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)),
}
}
}
}

View File

@ -52,7 +52,7 @@ 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 } => {
PeachError::JsonRpcClientCore(source) => {
match source {
Error(ErrorKind::JsonRpcError(err), _state) => match err.code {
ErrorCode::ServerError(-32030) => {

View File

@ -4,17 +4,29 @@ use std::io::Read;
use rocket::http::{ContentType, Status};
use rocket::local::blocking::Client;
use rocket::serde::json::{json, Value};
use rocket::{Build, Config, Rocket};
use crate::utils::build_json_response;
use super::init_rocket;
// define authentication mode
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 +51,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 +66,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 +76,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 +86,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 +95,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 +109,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 +117,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 +128,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 +139,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 +149,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 +161,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 +171,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 +181,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 +191,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 +201,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 +211,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 +221,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 +232,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 +245,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 +257,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 +270,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 +282,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 +308,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 +320,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));
@ -318,7 +330,7 @@ fn network_settings_menu_html() {
#[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);
@ -327,7 +339,7 @@ fn deploy_ap() {
#[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 +353,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 +365,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));
@ -363,7 +375,7 @@ fn ap_details_html() {
#[test]
fn deploy_client() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client.get("/settings/network/wifi/activate").dispatch();
// check for 303 status (redirect)
assert_eq!(response.status(), Status::SeeOther);
@ -372,7 +384,7 @@ fn deploy_client() {
#[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 +398,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 +414,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 +426,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 +438,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 +450,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 +465,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 +484,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 +497,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));
@ -502,7 +514,7 @@ fn network_status_html() {
#[test]
fn activate_ap() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/activate_ap")
.header(ContentType::JSON)
@ -513,7 +525,7 @@ fn activate_ap() {
#[test]
fn activate_client() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/activate_client")
.header(ContentType::JSON)
@ -524,7 +536,7 @@ fn activate_client() {
#[test]
fn return_ip() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/ip")
.header(ContentType::JSON)
@ -538,7 +550,7 @@ fn return_ip() {
#[test]
fn return_rssi() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/rssi")
.header(ContentType::JSON)
@ -551,7 +563,7 @@ fn return_rssi() {
#[test]
fn return_ssid() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/ssid")
.header(ContentType::JSON)
@ -564,7 +576,7 @@ fn return_ssid() {
#[test]
fn return_state() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/state")
.header(ContentType::JSON)
@ -579,7 +591,7 @@ fn return_state() {
#[test]
fn return_status() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/status")
.header(ContentType::JSON)
@ -592,7 +604,7 @@ fn return_status() {
#[test]
fn scan_networks() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/network/wifi")
.header(ContentType::JSON)
@ -605,7 +617,7 @@ fn scan_networks() {
#[test]
fn add_wifi() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/wifi")
.header(ContentType::JSON)
@ -619,7 +631,7 @@ fn add_wifi() {
#[test]
fn remove_wifi() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/wifi/forget")
.header(ContentType::JSON)
@ -633,7 +645,7 @@ fn remove_wifi() {
#[test]
fn new_password() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.post("/api/v1/network/wifi/modify")
.header(ContentType::JSON)
@ -647,7 +659,7 @@ fn new_password() {
#[test]
fn ping_pong() {
let client = Client::tracked(init_rocket()).expect("valid rocket instance");
let client = Client::tracked(init_test_rocket(DISABLE_AUTH)).expect("valid rocket instance");
let response = client
.get("/api/v1/ping")
.header(ContentType::JSON)
@ -709,7 +721,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

View File

@ -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;
}
/*

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
/*
behavioural layer for the `network_card.html.tera` template,
corresponding to the web route `/network`
corresponding to the web route `/settings/network`
- intercept form submissions
- perform json api calls
@ -11,10 +11,8 @@ methods:
PEACH_NETWORK.activateAp();
PEACH_NETWORK.activateClient();
PEACH_NETWORK.apOnline();
PEACH_NETWORK.clientOffline();
PEACH_NETWORK.clientOnline();
PEACH_NETWORK.flashMsg(status, msg);
PEACH_NETWORK.apMode();
PEACH_NETWORK.clientMode();
*/
@ -29,7 +27,7 @@ PEACH_NETWORK.activateAp = function() {
// prevent form submission (default behavior)
e.preventDefault();
// write in-progress status message to ui
PEACH_NETWORK.flashMsg("info", "Deploying access point...");
PEACH.flashMsg("info", "Deploying access point...");
// send activate_ap POST request
fetch("/api/v1/network/activate_ap", {
method: "post",
@ -44,10 +42,10 @@ PEACH_NETWORK.activateAp = function() {
.then( (jsonData) => {
console.log(jsonData.msg);
// write json response message to ui
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg);
PEACH.flashMsg(jsonData.status, jsonData.msg);
// if ap activation is successful, update the ui
if (jsonData.status === "success") {
PEACH_NETWORK.apOnline();
PEACH_NETWORK.apMode();
}
})
}, false);
@ -64,7 +62,7 @@ PEACH_NETWORK.activateClient = function() {
// prevent form submission (default behavior)
e.preventDefault();
// write in-progress status message to ui
PEACH_NETWORK.flashMsg("info", "Enabling WiFi client...");
PEACH.flashMsg("info", "Enabling WiFi client...");
// send activate_ap POST request
fetch("/api/v1/network/activate_client", {
method: "post",
@ -79,10 +77,10 @@ PEACH_NETWORK.activateClient = function() {
.then( (jsonData) => {
console.log(jsonData.msg);
// write json response message to ui
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg);
PEACH.flashMsg(jsonData.status, jsonData.msg);
// if client activation is successful, update the ui
if (jsonData.status === "success") {
PEACH_NETWORK.clientOnline();
PEACH_NETWORK.clientMode();
}
})
}, false);
@ -90,21 +88,12 @@ PEACH_NETWORK.activateClient = function() {
});
}
// 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";
// replace 'Deploy Access Point' button with 'Enable WiFi' button
PEACH_NETWORK.apMode = function() {
// create Enable WiFi button and add it to button div
var wifiButton = document.createElement("A");
wifiButton.className = "button center";
wifiButton.href = "/network/wifi/activate";
wifiButton.href = "/settings/network/wifi/activate";
wifiButton.id = "connectWifi";
var label = "Enable WiFi";
var buttonText = document.createTextNode(label);
@ -114,88 +103,31 @@ PEACH_NETWORK.apOnline = function() {
let buttons = document.getElementById("buttons");
buttons.appendChild(wifiButton);
// remove the old 'Activate Access Point' button
// remove the old 'Deploy Access Point' button
let apButton = document.getElementById("deployAccessPoint");
apButton.style = "display: none;";
apButton.remove();
}
// update ui for wifi client mode (status: online)
PEACH_NETWORK.clientOnline = function() {
console.log('Activating Client Mode');
// replace 'Enable WiFi' button with 'Deploy Access Point' button
PEACH_NETWORK.clientMode = function() {
// create Deploy Access Point button and add it to button div
var apButton = document.createElement("A");
apButton.className = "button center";
apButton.href = "/settings/network/ap/activate";
apButton.id = "deployAccessPoint";
var label = "Deploy Access Point";
var buttonText = document.createTextNode(label);
apButton.appendChild(buttonText);
// 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";
// append the new button to the buttons div
let buttons = document.getElementById("buttons");
buttons.appendChild(apButton);
// 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);
}
// remove the old 'Enable Wifi' button
let wifiButton = document.getElementById("connectWifi");
wifiButton.remove();
}
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()
*/

View File

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

View File

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

View File

@ -1,7 +1,7 @@
/*
behavioural layer for the `network_usage.html.tera` template,
corresponding to the web route `/network/wifi/usage`
corresponding to the web route `/settings/network/wifi/usage`
- intercept form submissions
- perform json api calls
@ -13,7 +13,6 @@ methods:
PEACH_NETWORK.resetUsage();
PEACH_NETWORK.toggleWarning();
PEACH_NETWORK.toggleCutoff();
PEACH_NETWORK.flashMsg(status, msg);
*/
@ -51,7 +50,7 @@ PEACH_NETWORK.updateAlerts = function() {
})
.then( (jsonData) => {
// write json response message to ui
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg);
PEACH.flashMsg(jsonData.status, jsonData.msg);
})
}, false);
});
@ -79,7 +78,7 @@ PEACH_NETWORK.resetUsage = function() {
.then( (jsonData) => {
console.log(jsonData.msg);
// write json response message to ui
PEACH_NETWORK.flashMsg(jsonData.status, jsonData.msg);
PEACH.flashMsg(jsonData.status, jsonData.msg);
// if reset is successful, update the ui
if (jsonData.status === "success") {
console.log(jsonData.data);
@ -133,39 +132,6 @@ PEACH_NETWORK.toggleCutoff = function() {
});
};
// display a message by appending a paragraph element
PEACH_NETWORK.flashMsg = function(status, msg) {
// set the class of the element according to status
var elementClass;
if (status === "success") {
elementClass = "capsule center-text flash-message font-success";
} else if (status === "info") {
elementClass = "capsule center-text flash-message font-info";
} else {
elementClass = "capsule center-text flash-message font-failure";
};
var flashElement = document.getElementById("flashMsg");
// if flashElement exists, update the class & text
if (flashElement) {
flashElement.className = elementClass;
flashElement.innerText = msg;
// if flashElement does not exist, create it, set id, class, text & append
} else {
// create new div for flash message
var flashDiv = document.createElement("DIV");
// set div attributes
flashDiv.id = "flashMsg";
flashDiv.className = elementClass;
// add json response message to flash message div
var flashMsg = document.createTextNode(msg);
flashDiv.appendChild(flashMsg);
// insert the flash message div below the button div
var buttonDiv = document.getElementById("buttonDiv");
buttonDiv.parentNode.insertBefore(flashDiv, buttonDiv.nextSibling);
}
}
var usageInstance = PEACH_NETWORK;
usageInstance.resetUsage();
usageInstance.toggleWarning();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,13 +2,11 @@
{%- block card %}
<!-- SETTINGS MENU -->
<div class="card center">
<div class="card-container">
<!-- BUTTONS -->
<div id="settingsButtons">
<a id="network" class="button button-primary center" href="/settings/network" title="Network Settings">Network</a>
<a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings">Scuttlebutt</a>
<a id="admin" class="button button-primary center" href="/settings/admin" title="Administrator Settings">Administration</a>
</div>
<!-- BUTTONS -->
<div id="settingsButtons">
<a id="network" class="button button-primary center" href="/settings/network" title="Network Settings">Network</a>
<a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings">Scuttlebutt</a>
<a id="admin" class="button button-primary center" href="/settings/admin" title="Administrator Settings">Administration</a>
</div>
</div>
{%- endblock card -%}

View File

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