Compare commits
93 Commits
lib_error_
...
forget_upd
Author | SHA1 | Date | |
---|---|---|---|
32c91dacbe | |||
2c3ee5056e | |||
523c781b46 | |||
da0152a725 | |||
7b953dd929 | |||
42774674e5 | |||
57ed0ab66a | |||
49ad74595c | |||
17d52c771f | |||
6792e4702d | |||
446927f587 | |||
567b0bbc2a | |||
3ab3e65eb7 | |||
a0e80fcda7 | |||
731bc1958b | |||
58f2ddde05 | |||
4b0b2626a4 | |||
a05e67c22f | |||
068d3430d7 | |||
62793f401e | |||
9324b3ec0b | |||
f43fbf19f5 | |||
570f6a679b | |||
399af51ccc | |||
94bac00664 | |||
c41dae8d04 | |||
e34df3b656 | |||
3399a3c80f | |||
1c26cb70fa | |||
c79bd4b19f | |||
7743511923 | |||
10833078fa | |||
244a2132fa | |||
f737236abc | |||
b5ce677a5b | |||
4d6dbd511e | |||
7fe4715014 | |||
dd33fdd47d | |||
1986d31461 | |||
a824be53b9 | |||
287082381e | |||
9f40378fce | |||
4f5eb3aa04 | |||
f4113f0632 | |||
8032b83c41 | |||
dfa1306b2d | |||
2f7c7aac8f | |||
46b7c0fc2b | |||
f62c8f0b51 | |||
06e48deb3a | |||
fb6d0317b6 | |||
cfbf052d27 | |||
d240741958 | |||
33486b4e1d | |||
8c3fecb875 | |||
0907fbc474 | |||
b747ff6db2 | |||
220c7fd540 | |||
ed7e172efb | |||
bc0f2d595b | |||
61b33d1613 | |||
c3fa188400 | |||
a1444cf478 | |||
79c94e6af0 | |||
cd8e5737c4 | |||
2429ea8fdd | |||
c8d0a2ddf6 | |||
adc1a5bd77 | |||
d760f9f92c | |||
5c4ef4a529 | |||
35ff408365 | |||
c2b785f54b | |||
1dc740eeae | |||
b3c6138e03 | |||
b59e62f920 | |||
3325706dcb | |||
bc28a84ad4 | |||
bb34bdd653 | |||
ac98bde760 | |||
361b159299 | |||
7344d6f4e0 | |||
8d18e712a1 | |||
116afe78fd | |||
7e3c500b1e | |||
b59eb22082 | |||
ee1da0599c | |||
e5f9a9be83 | |||
e54ff8829a | |||
554997a5c0 | |||
da51070ccd | |||
925051a379 | |||
380ee2683a | |||
bae3b7c2ce |
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
||||
.idea
|
||||
target
|
||||
*peachdeploy.sh
|
||||
*vpsdeploy.sh
|
||||
|
738
Cargo.lock
generated
@ -10,6 +10,6 @@ members = [
|
||||
"peach-menu",
|
||||
"peach-monitor",
|
||||
"peach-stats",
|
||||
"peach-probe",
|
||||
"peach-jsonrpc-server",
|
||||
"peach-dyndns-updater"
|
||||
]
|
||||
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "peach-config"
|
||||
version = "0.1.10"
|
||||
version = "0.1.15"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
|
||||
edition = "2018"
|
||||
description = "Command line tool for installing, updating and configuring PeachCloud"
|
||||
@ -35,3 +35,5 @@ structopt = "0.3.13"
|
||||
clap = "2.33.3"
|
||||
log = "0.4"
|
||||
lazy_static = "1.4.0"
|
||||
peach-lib = { path = "../peach-lib" }
|
||||
rpassword = "5.0"
|
||||
|
@ -8,7 +8,7 @@ dtparam=i2c_arm=on
|
||||
# Apply device tree overlay to enable pull-up resistors for buttons
|
||||
device_tree_overlay=overlays/mygpio.dtbo
|
||||
|
||||
kernel=vmlinuz-4.19.0-17-arm64
|
||||
kernel=vmlinuz-4.19.0-18-arm64
|
||||
# For details on the initramfs directive, see
|
||||
# https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=10532
|
||||
initramfs initrd.img-4.19.0-17-arm64
|
||||
initramfs initrd.img-4.19.0-18-arm64
|
||||
|
35
peach-config/src/change_password.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use crate::error::PeachConfigError;
|
||||
use crate::ChangePasswordOpts;
|
||||
use peach_lib::password_utils::set_new_password;
|
||||
|
||||
/// Utility function to set the admin password for peach-web from the command-line.
|
||||
pub fn set_peach_web_password(opts: ChangePasswordOpts) -> Result<(), PeachConfigError> {
|
||||
match opts.password {
|
||||
// read password from CLI arg
|
||||
Some(password) => {
|
||||
set_new_password(&password)
|
||||
.map_err(|err| PeachConfigError::ChangePasswordError { source: err })?;
|
||||
println!(
|
||||
"Your new password has been set for peach-web. You can login through the \
|
||||
web interface with username admin."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
// read password from tty
|
||||
None => {
|
||||
let pass1 = rpassword::read_password_from_tty(Some("New password: "))?;
|
||||
let pass2 = rpassword::read_password_from_tty(Some("Confirm password: "))?;
|
||||
if pass1 != pass2 {
|
||||
Err(PeachConfigError::InvalidPassword)
|
||||
} else {
|
||||
set_new_password(&pass1)
|
||||
.map_err(|err| PeachConfigError::ChangePasswordError { source: err })?;
|
||||
println!(
|
||||
"Your new password has been set for peach-web. You can login through the \
|
||||
web interface with username admin."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
#![allow(clippy::nonstandard_macro_braces)]
|
||||
use peach_lib::error::PeachError;
|
||||
pub use snafu::ResultExt;
|
||||
use snafu::Snafu;
|
||||
|
||||
@ -30,6 +31,10 @@ pub enum PeachConfigError {
|
||||
},
|
||||
#[snafu(display("Error serializing json: {}", source))]
|
||||
SerdeError { source: serde_json::Error },
|
||||
#[snafu(display("Error changing password: {}", source))]
|
||||
ChangePasswordError { source: PeachError },
|
||||
#[snafu(display("Entered passwords did not match. Please try again."))]
|
||||
InvalidPassword,
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for PeachConfigError {
|
||||
|
@ -1,6 +1,8 @@
|
||||
mod change_password;
|
||||
mod constants;
|
||||
mod error;
|
||||
mod generate_manifest;
|
||||
mod set_permissions;
|
||||
mod setup_networking;
|
||||
mod setup_peach;
|
||||
mod setup_peach_deb;
|
||||
@ -12,10 +14,6 @@ use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use structopt::StructOpt;
|
||||
|
||||
use crate::generate_manifest::generate_manifest;
|
||||
use crate::setup_peach::setup_peach;
|
||||
use crate::update::update;
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(
|
||||
name = "peach-config",
|
||||
@ -44,6 +42,14 @@ enum PeachConfig {
|
||||
/// Updates all PeachCloud microservices
|
||||
#[structopt(name = "update")]
|
||||
Update(UpdateOpts),
|
||||
|
||||
/// Changes the password for the peach-web interface
|
||||
#[structopt(name = "changepassword")]
|
||||
ChangePassword(ChangePasswordOpts),
|
||||
|
||||
/// Updates file permissions on PeachCloud device
|
||||
#[structopt(name = "permissions")]
|
||||
SetPermissions,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
@ -76,6 +82,14 @@ pub struct UpdateOpts {
|
||||
list: bool,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub struct ChangePasswordOpts {
|
||||
/// Optional argument to specify password as CLI argument
|
||||
/// if not specified, this command asks for user input for the passwords
|
||||
#[structopt(short, long)]
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
arg_enum! {
|
||||
/// enum options for real-time clock choices
|
||||
#[derive(Debug)]
|
||||
@ -99,28 +113,48 @@ fn main() {
|
||||
if let Some(subcommand) = opt.commands {
|
||||
match subcommand {
|
||||
PeachConfig::Setup(cfg) => {
|
||||
match setup_peach(cfg.no_input, cfg.default_locale, cfg.i2c, cfg.rtc) {
|
||||
match setup_peach::setup_peach(cfg.no_input, cfg.default_locale, cfg.i2c, cfg.rtc) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!("peach-config encountered an error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
PeachConfig::Manifest => match generate_manifest() {
|
||||
PeachConfig::Manifest => match generate_manifest::generate_manifest() {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"peach-config countered an error generating manifest: {}",
|
||||
"peach-config encountered an error generating manifest: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
},
|
||||
PeachConfig::Update(opts) => match update(opts) {
|
||||
PeachConfig::Update(opts) => match update::update(opts) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!("peach-config encountered an error during update: {}", err)
|
||||
}
|
||||
},
|
||||
PeachConfig::ChangePassword(opts) => {
|
||||
match change_password::set_peach_web_password(opts) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"peach-config encountered an error during password update: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
PeachConfig::SetPermissions => match set_permissions::set_permissions() {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"peach-config ecountered an error updating file permissions: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
peach-config/src/set_permissions.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use crate::error::PeachConfigError;
|
||||
use crate::utils::cmd;
|
||||
|
||||
/// All configs are stored in this folder, and should be read/writeable by peach group
|
||||
/// so they can be read and written by all PeachCloud services.
|
||||
pub const CONFIGS_DIR: &str = "/var/lib/peachcloud";
|
||||
pub const PEACH_WEB_DIR: &str = "/usr/share/peach-web";
|
||||
|
||||
/// Utility function to set correct file permissions on the PeachCloud device.
|
||||
/// Accidentally changing file permissions is a fairly common thing to happen,
|
||||
/// so this is a useful CLI function for quickly correcting anything that may be out of order.
|
||||
pub fn set_permissions() -> Result<(), PeachConfigError> {
|
||||
println!("[ UPDATING FILE PERMISSIONS ON PEACHCLOUD DEVICE ]");
|
||||
cmd(&["chmod", "-R", "u+rwX,g+rwX", CONFIGS_DIR])?;
|
||||
cmd(&["chown", "-R", "peach", CONFIGS_DIR])?;
|
||||
cmd(&["chgrp", "-R", "peach", CONFIGS_DIR])?;
|
||||
cmd(&["chmod", "-R", "u+rwX,g+rwX", PEACH_WEB_DIR])?;
|
||||
cmd(&["chown", "-R", "peach-web:peach", PEACH_WEB_DIR])?;
|
||||
println!("[ PERMISSIONS SUCCESSFULLY UPDATED ]");
|
||||
Ok(())
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
29
peach-dyndns-updater/bindeploy.sh
Executable file
@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# exit when any command fails
|
||||
set -e
|
||||
|
||||
KEYFILE=/Users/notplants/.ssh/id_rsa
|
||||
SERVICE=peach-dyndns-updater
|
||||
|
||||
# deploy
|
||||
rsync -avzh --exclude target --exclude .idea --exclude .git -e "ssh -i $KEYFILE" . rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/$SERVICE/
|
||||
rsync -avzh --exclude target --exclude .idea --exclude .git -e "ssh -i $KEYFILE" ~/computer/projects/peachcloud/peach-workspace/peach-lib/ rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/peach-lib/
|
||||
|
||||
echo "++ cross compiling on vps"
|
||||
BIN_PATH=$(ssh -i $KEYFILE rust@167.99.136.83 'cd /srv/peachcloud/automation/peach-workspace/peach-dyndns-updater; /home/rust/.cargo/bin/cargo clean -p peach-lib; /home/rust/.cargo/bin/cargo build --release --target=aarch64-unknown-linux-gnu')
|
||||
|
||||
echo "++ copying ${BIN_PATH} to local"
|
||||
rm -f target/$SERVICE
|
||||
scp -i $KEYFILE rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/target/aarch64-unknown-linux-gnu/release/peach-dyndns-updater ../target/vps-bin-$SERVICE
|
||||
|
||||
#echo "++ cross compiling"
|
||||
BINFILE="../target/vps-bin-$SERVICE"
|
||||
echo $BINFILE
|
||||
|
||||
|
||||
echo "++ build successful"
|
||||
|
||||
echo "++ copying to pi"
|
||||
ssh -t -i $KEYFILE peach@peach.link 'mkdir -p /srv/dev/bins'
|
||||
scp -i $KEYFILE $BINFILE peach@peach.link:/srv/dev/bins/$SERVICE
|
||||
|
25
peach-jsonrpc-server/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "peach-jsonrpc-server"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "JSON-RPC over HTTP for the PeachCloud system. Provides a JSON-RPC wrapper around the stats, network and oled libraries."
|
||||
homepage = "https://opencollective.com/peachcloud"
|
||||
repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace"
|
||||
readme = "README.md"
|
||||
license = "AGPL-3.0-only"
|
||||
publish = false
|
||||
|
||||
[badges]
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.9"
|
||||
jsonrpc-core = "18"
|
||||
jsonrpc-http-server = "18"
|
||||
log = "0.4"
|
||||
miniserde = "0.1.15"
|
||||
peach-stats = { path = "../peach-stats", features = ["miniserde_support"] }
|
||||
|
||||
[dev-dependencies]
|
||||
jsonrpc-test = "18"
|
72
peach-jsonrpc-server/README.md
Normal file
@ -0,0 +1,72 @@
|
||||
# peach-jsonrpc-server
|
||||
|
||||
A JSON-RPC server for the PeachCloud system which exposes an API over HTTP.
|
||||
|
||||
Currently includes peach-stats capability (system statistics).
|
||||
|
||||
## JSON-RPC API
|
||||
|
||||
| Method | Description | Returns |
|
||||
| --- | --- | --- |
|
||||
| `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` |
|
||||
| `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` |
|
||||
| `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` |
|
||||
| `load_average` | Load average statistics | `one`, `five`, `fifteen` |
|
||||
| `mem_stats` | Memory statistics | `total`, `free`, `used` |
|
||||
| `ping` | Microservice status | `success` if running |
|
||||
| `uptime` | System uptime | `secs` |
|
||||
|
||||
## Environment
|
||||
|
||||
The JSON-RPC HTTP server is currently hardcoded to run on "127.0.0.1:5110". Address and port configuration settings will later be exposed via CLI arguments and possibly an environment variable.
|
||||
|
||||
Logging is made available with `env_logger`:
|
||||
|
||||
`export RUST_LOG=info`
|
||||
|
||||
Other logging levels include `debug`, `warn` and `error`.
|
||||
|
||||
## Setup
|
||||
|
||||
Clone the peach-workspace repo:
|
||||
|
||||
`git clone https://git.coopcloud.tech/PeachCloud/peach-workspace`
|
||||
|
||||
Move into the repo peaach-jsonrpc-server directory and compile a release build:
|
||||
|
||||
`cd peach-jsonrpc-server`
|
||||
`cargo build --release`
|
||||
|
||||
Run the binary:
|
||||
|
||||
`./peach-workspace/target/release/peach-jsonrpc-server`
|
||||
|
||||
## Debian Packaging
|
||||
|
||||
TODO.
|
||||
|
||||
## Example Usage
|
||||
|
||||
**Get CPU Statistics**
|
||||
|
||||
With microservice running, open a second terminal window and use `curl` to call server methods:
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "cpu_stats", "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server responds with:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"{\"user\":4661083,\"system\":1240371,\"idle\":326838290,\"nice\":0}","id":1}`
|
||||
|
||||
**Get System Uptime**
|
||||
|
||||
With microservice running, open a second terminal window and use `curl` to call server methods:
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "uptime", "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server responds with:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"{\"secs\":840968}","id":1}`
|
||||
|
||||
### Licensing
|
||||
|
||||
AGPL-3.0
|
46
peach-jsonrpc-server/src/error.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
141
peach-jsonrpc-server/src/lib.rs
Normal 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"
|
||||
}"#
|
||||
);
|
||||
}
|
||||
}
|
34
peach-jsonrpc-server/src/main.rs
Normal file
@ -0,0 +1,34 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
//! # peach-jsonrpc-server
|
||||
//!
|
||||
//! A JSON-RPC server which exposes an over HTTP.
|
||||
//!
|
||||
//! Currently includes peach-stats capability (system statistics).
|
||||
//!
|
||||
//! ## API
|
||||
//!
|
||||
//! | Method | Description | Returns |
|
||||
//! | --- | --- | --- |
|
||||
//! | `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` |
|
||||
//! | `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` |
|
||||
//! | `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` |
|
||||
//! | `load_average` | Load average statistics | `one`, `five`, `fifteen` |
|
||||
//! | `mem_stats` | Memory statistics | `total`, `free`, `used` |
|
||||
//! | `ping` | Microservice status | `success` if running |
|
||||
//! | `uptime` | System uptime | `secs` |
|
||||
|
||||
use std::process;
|
||||
|
||||
use log::error;
|
||||
|
||||
fn main() {
|
||||
// initalize the logger
|
||||
env_logger::init();
|
||||
|
||||
// handle errors returned from `run`
|
||||
if let Err(e) = peach_jsonrpc_server::run() {
|
||||
error!("Application error: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
@ -1,19 +1,19 @@
|
||||
[package]
|
||||
name = "peach-lib"
|
||||
version = "1.3.0"
|
||||
version = "1.3.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"
|
||||
log = "0.4"
|
||||
nanorand = "0.6.1"
|
||||
regex = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
rust-crypto = "0.2.36"
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.8"
|
||||
regex = "1"
|
||||
chrono = "0.4.19"
|
||||
rand="0.8.4"
|
||||
fslock="0.1.6"
|
||||
sha3 = "0.10.0"
|
||||
|
@ -1,7 +1,5 @@
|
||||
use std::iter;
|
||||
|
||||
use crypto::{digest::Digest, sha3::Sha3};
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use nanorand::{Rng, WyRand};
|
||||
use sha3::{Digest, Sha3_256};
|
||||
|
||||
use crate::{config_manager, error::PeachError, sbot_client};
|
||||
|
||||
@ -39,9 +37,13 @@ pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
|
||||
|
||||
/// Creates a hash from a password string
|
||||
pub fn hash_password(password: &str) -> String {
|
||||
let mut hasher = Sha3::sha3_256();
|
||||
hasher.input_str(password);
|
||||
hasher.result_str()
|
||||
let mut hasher = Sha3_256::new();
|
||||
// write input message
|
||||
hasher.update(password);
|
||||
// read hash digest
|
||||
let result = hasher.finalize();
|
||||
// convert `u8` to `String`
|
||||
result[0].to_string()
|
||||
}
|
||||
|
||||
/// Sets a new temporary password for the admin user
|
||||
@ -68,13 +70,10 @@ pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> {
|
||||
/// Generates a temporary password and sends it via ssb dm
|
||||
/// to the ssb id configured to be the admin of the peachcloud device
|
||||
pub fn send_password_reset() -> Result<(), PeachError> {
|
||||
// first generate a new random password of ascii characters
|
||||
let mut rng = thread_rng();
|
||||
let temporary_password: String = iter::repeat(())
|
||||
.map(|()| rng.sample(Alphanumeric))
|
||||
.map(char::from)
|
||||
.take(10)
|
||||
.collect();
|
||||
// initialise random number generator
|
||||
let mut rng = WyRand::new();
|
||||
// generate a new password of random numbers
|
||||
let temporary_password = rng.generate::<u64>().to_string();
|
||||
// save this string as a new temporary password
|
||||
set_new_temporary_password(&temporary_password)?;
|
||||
let domain = config_manager::get_peachcloud_domain()?;
|
||||
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,44 +1,32 @@
|
||||
[package]
|
||||
name = "peach-network"
|
||||
version = "0.2.12"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
|
||||
edition = "2018"
|
||||
description = "Query and configure network interfaces using JSON-RPC over HTTP."
|
||||
version = "0.4.1"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
edition = "2021"
|
||||
description = "Query and configure network interfaces."
|
||||
homepage = "https://opencollective.com/peachcloud"
|
||||
repository = "https://github.com/peachcloud/peach-network"
|
||||
repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-network"
|
||||
readme = "README.md"
|
||||
license = "AGPL-3.0-only"
|
||||
license = "LGPL-3.0-only"
|
||||
publish = false
|
||||
|
||||
[package.metadata.deb]
|
||||
depends = "$auto"
|
||||
extended-description = """\
|
||||
peach-network is a microservice to query and configure network interfaces \
|
||||
using JSON-RPC over HTTP."""
|
||||
maintainer-scripts="debian"
|
||||
systemd-units = { unit-name = "peach-network" }
|
||||
assets = [
|
||||
["target/release/peach-network", "usr/bin/", "755"],
|
||||
["README.md", "usr/share/doc/peach-network/README", "644"],
|
||||
]
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "peachcloud/peach-network", branch = "master" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.6"
|
||||
failure = "0.1"
|
||||
get_if_addrs = "0.5.3"
|
||||
jsonrpc-core = "11"
|
||||
jsonrpc-http-server = "11"
|
||||
log = "0.4"
|
||||
probes = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
snafu = "0.6"
|
||||
miniserde = { version = "0.1.15", optional = true }
|
||||
probes = "0.4.1"
|
||||
serde = { version = "1.0.130", features = ["derive"], optional = true }
|
||||
regex = "1"
|
||||
wpactrl = "0.3.1"
|
||||
# replace this with crate import once latest changes have been published
|
||||
wpactrl = { git = "https://github.com/sauyon/wpa-ctrl-rs.git", branch = "master" }
|
||||
|
||||
[dev-dependencies]
|
||||
jsonrpc-test = "11"
|
||||
[features]
|
||||
default = []
|
||||
|
||||
# Provide `Serialize` and `Deserialize` traits for library structs using `miniserde`
|
||||
miniserde_support = ["miniserde"]
|
||||
|
||||
# Provide `Serialize` and `Deserialize` traits for library structs using `serde`
|
||||
serde_support = ["serde"]
|
||||
|
@ -1,178 +1,46 @@
|
||||
# peach-network
|
||||
|
||||
[](https://travis-ci.com/peachcloud/peach-network) 
|
||||

|
||||
|
||||
Networking microservice module for PeachCloud. Query and configure device interfaces using [JSON-RPC](https://www.jsonrpc.org/specification) over http.
|
||||
Network interface state query and modification library.
|
||||
|
||||
Interaction with wireless interfaces occurs primarily through the [wpactrl crate](https://docs.rs/wpactrl/0.3.1/wpactrl/) which provides "a pure-Rust lowlevel library for controlling wpasupplicant remotely". This approach is akin to using `wpa_cli` (a WPA command line client).
|
||||
|
||||
_Note: This module is a work-in-progress._
|
||||
## API Documentation
|
||||
|
||||
### JSON-RPC API
|
||||
API documentation can be built and served with `cargo doc --no-deps --open`. The full set of available data structures and functions is listed in the `peach_network::network` module. A custom error type (`NetworkError`) is also publically exposed for library users; it encapsulates all possible error variants.
|
||||
|
||||
Methods for **retrieving data**:
|
||||
## Example Usage
|
||||
|
||||
| Method | Parameters | Description |
|
||||
| --- | --- | --- |
|
||||
| `available_networks` | `iface` | List SSID, flags (security), frequency and signal level for all networks in range of given interface |
|
||||
| `id` | `iface`, `ssid` | Return ID of given SSID |
|
||||
| `ip` | `iface` | Return IP of given network interface |
|
||||
| `ping` | | Respond with `success` if microservice is running |
|
||||
| `rssi` | `iface` | Return average signal strength (dBm) for given interface |
|
||||
| `rssi_percent` | `iface` | Return average signal strength (%) for given interface |
|
||||
| `saved_networks` | | List all networks saved in wpasupplicant config |
|
||||
| `ssid` | `iface` | Return SSID of currently-connected network for given interface |
|
||||
| `state` | `iface` | Return state of given interface |
|
||||
| `status` | `iface` | Return status parameters for given interface |
|
||||
| `traffic` | `iface` | Return network traffic for given interface |
|
||||
```rust
|
||||
use peach_network::{network, NetworkError};
|
||||
|
||||
Methods for **modifying state**:
|
||||
fn main() -> Result<(), NetworkError> {
|
||||
let wlan_iface = "wlan0";
|
||||
|
||||
| Method | Parameters | Description |
|
||||
| --- | --- | --- |
|
||||
| `activate_ap` | | Activate WiFi access point (start `wpa_supplicant@ap0.service`) |
|
||||
| `activate_client` | | Activate WiFi client connection (start `wpa_supplicant@wlan0.service`) |
|
||||
| `add` | `ssid`, `pass` | Add WiFi credentials to `wpa_supplicant-wlan0.conf` |
|
||||
| `check_iface` | | Activate WiFi access point if client mode is active without a connection |
|
||||
| `connect` | `id`, `iface` | Disable other networks and attempt connection with AP represented by given id |
|
||||
| `delete` | `id`, `iface` | Remove WiFi credentials for given network id and interface |
|
||||
| `disable` | `id`, `iface` | Disable connection with AP represented by given id |
|
||||
| `disconnect` | `iface` | Disconnect given interface |
|
||||
| `modify` | `id`, `iface`, `password` | Set a new password for given network id and interface |
|
||||
| `reassociate` | `iface` | Reassociate with current AP for given interface |
|
||||
| `reconfigure` | | Force wpa_supplicant to re-read its configuration file |
|
||||
| `reconnect` | `iface` | Disconnect and reconnect given interface |
|
||||
| `save` | | Save configuration changes to `wpa_supplicant-wlan0.conf` |
|
||||
let wlan_ip = network::ip(wlan_iface)?;
|
||||
let wlan_ssid = network::ssid(wlan_iface)?;
|
||||
|
||||
### API Documentation
|
||||
let ssid = "Home";
|
||||
let pass = "SuperSecret";
|
||||
|
||||
API documentation can be built and served with `cargo doc --no-deps --open`. This set of documentation is intended for developers who wish to work on the project or better understand the API of the `src/network.rs` module.
|
||||
network::add(&wlan_iface, &ssid, &pass)?;
|
||||
network::save()?;
|
||||
|
||||
### Environment
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
The JSON-RPC HTTP server address and port can be configured with the `PEACH_NETWORK_SERVER` environment variable:
|
||||
## Feature Flags
|
||||
|
||||
`export PEACH_NETWORK_SERVER=127.0.0.1:5000`
|
||||
Feature flags are used to offer `Serialize` and `Deserialize` implementations for all `struct` data types provided by this library. These traits are not provided by default. A choice of `miniserde` and `serde` is provided.
|
||||
|
||||
When not set, the value defaults to `127.0.0.1:5110`.
|
||||
Define the desired feature in the `Cargo.toml` manifest of your project:
|
||||
|
||||
Logging is made available with `env_logger`:
|
||||
```toml
|
||||
peach-network = { version = "0.3.0", features = ["miniserde_support"] }
|
||||
```
|
||||
|
||||
`export RUST_LOG=info`
|
||||
## License
|
||||
|
||||
Other logging levels include `debug`, `warn` and `error`.
|
||||
|
||||
### Setup
|
||||
|
||||
Clone this repo:
|
||||
|
||||
`git clone https://github.com/peachcloud/peach-network.git`
|
||||
|
||||
Move into the repo and compile:
|
||||
|
||||
`cd peach-network`
|
||||
`cargo build --release`
|
||||
|
||||
Run the binary (sudo needed to satisfy permission requirements):
|
||||
|
||||
`sudo ./target/release/peach-network`
|
||||
|
||||
### Debian Packaging
|
||||
|
||||
A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-network` to be easily bundled as a Debian package (`.deb`). The `cargo-deb` [crate](https://crates.io/crates/cargo-deb) can be used to achieve this.
|
||||
|
||||
Install `cargo-deb`:
|
||||
|
||||
`cargo install cargo-deb`
|
||||
|
||||
Move into the repo:
|
||||
|
||||
`cd peach-network`
|
||||
|
||||
Build the package:
|
||||
|
||||
`cargo deb`
|
||||
|
||||
The output will be written to `target/debian/peach-network_0.2.4_arm64.deb` (or similar).
|
||||
|
||||
Build the package (aarch64):
|
||||
|
||||
`cargo deb --target aarch64-unknown-linux-gnu`
|
||||
|
||||
Install the package as follows:
|
||||
|
||||
`sudo dpkg -i target/debian/peach-network_0.2.4_arm64.deb`
|
||||
|
||||
The service will be automatically enabled and started.
|
||||
|
||||
Uninstall the service:
|
||||
|
||||
`sudo apt-get remove peach-network`
|
||||
|
||||
Remove configuration files (not removed with `apt-get remove`):
|
||||
|
||||
`sudo apt-get purge peach-network`
|
||||
|
||||
### Example Usage
|
||||
|
||||
**Retrieve IP address for wlan0**
|
||||
|
||||
With microservice running, open a second terminal window and use `curl` to call server methods:
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "ip", "params" : {"iface": "wlan0" }, "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server responds with:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"192.168.1.21","id":1}`
|
||||
|
||||
**Retrieve SSID of connected access point for wlan1**
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "ssid", "params" : {"iface": "wlan1" }, "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server response when interface is connected:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"Home","id":1}`
|
||||
|
||||
Server response when interface is not connected:
|
||||
|
||||
`{"jsonrpc":"2.0","error":{"code":-32003,"message":"Failed to retrieve SSID for wlan1. Interface may not be connected."},"id":1}`
|
||||
|
||||
**Retrieve list of SSIDs for all networks in range of wlan0**
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "available_networks", "params" : {"iface": "wlan0" }, "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server response when interface is connected:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"[{\"frequency\":\"2412\",\"signal_level\":\"-72\",\"ssid\":\"Home\",\"flags\":\"[WPA2-PSK-CCMP][ESS]\"},{\"frequency\":\"2472\",\"signal_level\":\"-56\",\"ssid\":\"podetium\",\"flags\":\"[WPA2-PSK-CCMP+TKIP][ESS]\"}]","id":1}`
|
||||
|
||||
Server response when interface is not connected:
|
||||
|
||||
`{"jsonrpc":"2.0","error":{"code":-32006,"message":"No networks found in range of wlan0"},"id":1}`
|
||||
|
||||
**Retrieve network traffic statistics for wlan1**
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "traffic", "params" : {"iface": "wlan1" }, "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server response if interface exists:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"{\"received\":26396361,\"transmitted\":22352530}","id":1}`
|
||||
|
||||
Server response when interface is not found:
|
||||
|
||||
`{"jsonrpc":"2.0","error":{"code":-32004,"message":"Failed to retrieve network traffic for wlan3. Interface may not be connected"},"id":1}`
|
||||
|
||||
**Retrieve status information for wlan0**
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "status", "params" : {"iface": "wlan0" }, "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server response if interface exists:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"{\"address\":\"b8:27:eb:9b:5d:5f\",\"bssid\":\"f4:8c:eb:cd:31:81\",\"freq\":\"2412\",\"group_cipher\":\"CCMP\",\"id\":\"0\",\"ip_address\":\"192.168.0.162\",\"key_mgmt\":\"WPA2-PSK\",\"mode\":\"station\",\"pairwise_cipher\":\"CCMP\",\"ssid\":\"Home\",\"wpa_state\":\"COMPLETED\"}","id":1}`
|
||||
|
||||
Server response when interface is not found:
|
||||
|
||||
`{"jsonrpc":"2.0","error":{"code":-32013,"message":"Failed to open control interface for wpasupplicant: No such file or directory (os error 2)"},"id":1}`
|
||||
|
||||
### Licensing
|
||||
|
||||
AGPL-3.0
|
||||
LGPL-3.0.
|
||||
|
@ -1,13 +0,0 @@
|
||||
[Unit]
|
||||
Description=Query and configure network interfaces using JSON-RPC over HTTP.
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=netdev
|
||||
Environment="RUST_LOG=error"
|
||||
ExecStart=/usr/bin/peach-network
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -1,351 +1,367 @@
|
||||
use std::{error, io, str};
|
||||
//! Custom error type for `peach-network`.
|
||||
|
||||
use jsonrpc_core::{types::error::Error, ErrorCode};
|
||||
use std::io;
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use io::Error as IoError;
|
||||
use probes::ProbeError;
|
||||
use serde_json::error::Error as SerdeError;
|
||||
use snafu::Snafu;
|
||||
use regex::Error as RegexError;
|
||||
use wpactrl::WpaError;
|
||||
|
||||
pub type BoxError = Box<dyn error::Error>;
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
#[snafu(visibility(pub(crate)))]
|
||||
/// Custom error type encapsulating all possible errors when querying
|
||||
/// network interfaces and modifying their state.
|
||||
#[derive(Debug)]
|
||||
pub enum NetworkError {
|
||||
#[snafu(display("{}", err_msg))]
|
||||
ActivateAp { err_msg: String },
|
||||
|
||||
#[snafu(display("{}", err_msg))]
|
||||
ActivateClient { err_msg: String },
|
||||
|
||||
#[snafu(display("Failed to add network for {}", ssid))]
|
||||
Add { ssid: String },
|
||||
|
||||
#[snafu(display("Failed to retrieve state for interface: {}", iface))]
|
||||
NoState { iface: String, source: io::Error },
|
||||
|
||||
#[snafu(display("Failed to disable network {} for interface: {}", id, iface))]
|
||||
Disable { id: String, iface: String },
|
||||
|
||||
#[snafu(display("Failed to disconnect {}", iface))]
|
||||
Disconnect { iface: String },
|
||||
|
||||
#[snafu(display("Failed to generate wpa passphrase for {}: {}", ssid, source))]
|
||||
GenWpaPassphrase { ssid: String, source: io::Error },
|
||||
|
||||
#[snafu(display("Failed to generate wpa passphrase for {}: {}", ssid, err_msg))]
|
||||
GenWpaPassphraseWarning { ssid: String, err_msg: String },
|
||||
|
||||
#[snafu(display("No ID found for {} on interface: {}", ssid, iface))]
|
||||
Id { ssid: String, iface: String },
|
||||
|
||||
#[snafu(display("Could not access IP address for interface: {}", iface))]
|
||||
NoIp { iface: String, source: io::Error },
|
||||
|
||||
#[snafu(display("Could not find RSSI for interface: {}", iface))]
|
||||
Rssi { iface: String },
|
||||
|
||||
#[snafu(display("Could not find signal quality (%) for interface: {}", iface))]
|
||||
RssiPercent { iface: String },
|
||||
|
||||
#[snafu(display("Could not find SSID for interface: {}", iface))]
|
||||
Ssid { iface: String },
|
||||
|
||||
#[snafu(display("No state found for interface: {}", iface))]
|
||||
State { iface: String },
|
||||
|
||||
#[snafu(display("No status found for interface: {}", iface))]
|
||||
Status { iface: String },
|
||||
|
||||
#[snafu(display("Could not find network traffic for interface: {}", iface))]
|
||||
Traffic { iface: String },
|
||||
|
||||
#[snafu(display("No saved networks found for default interface"))]
|
||||
/// Failed to add network.
|
||||
Add {
|
||||
/// SSID.
|
||||
ssid: String,
|
||||
},
|
||||
/// Failed to retrieve network state.
|
||||
NoState {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
/// Underlying error source.
|
||||
source: IoError,
|
||||
},
|
||||
/// Failed to disable network.
|
||||
Disable {
|
||||
/// ID.
|
||||
id: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to disconnect interface.
|
||||
Disconnect {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to execute wpa_passphrase command.
|
||||
GenWpaPassphrase {
|
||||
/// SSID.
|
||||
ssid: String,
|
||||
/// Underlying error source.
|
||||
source: IoError,
|
||||
},
|
||||
/// Failed to successfully generate wpa passphrase.
|
||||
GenWpaPassphraseWarning {
|
||||
/// SSID.
|
||||
ssid: String,
|
||||
/// Error message describing context.
|
||||
err_msg: String,
|
||||
},
|
||||
/// Failed to retrieve ID for the given SSID and interface.
|
||||
Id {
|
||||
/// SSID.
|
||||
ssid: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve IP address.
|
||||
NoIp {
|
||||
/// Inteface.
|
||||
iface: String,
|
||||
/// Underlying error source.
|
||||
source: IoError,
|
||||
},
|
||||
/// Failed to retrieve RSSI.
|
||||
Rssi {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve signal quality (%).
|
||||
RssiPercent {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve SSID.
|
||||
Ssid {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve state.
|
||||
State {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve status.
|
||||
Status {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retieve network traffic.
|
||||
Traffic {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// No saved network found for the default interface.
|
||||
SavedNetworks,
|
||||
|
||||
#[snafu(display("No networks found in range of interface: {}", iface))]
|
||||
AvailableNetworks { iface: String },
|
||||
|
||||
#[snafu(display("Missing expected parameters: {}", e))]
|
||||
MissingParams { e: Error },
|
||||
|
||||
#[snafu(display("Failed to set new password for network {} on {}", id, iface))]
|
||||
Modify { id: String, iface: String },
|
||||
|
||||
#[snafu(display("No IP found for interface: {}", iface))]
|
||||
Ip { iface: String },
|
||||
|
||||
#[snafu(display("Failed to parse integer from string for RSSI value: {}", source))]
|
||||
ParseString { source: std::num::ParseIntError },
|
||||
|
||||
#[snafu(display(
|
||||
"Failed to retrieve network traffic measurement for {}: {}",
|
||||
iface,
|
||||
source
|
||||
))]
|
||||
NoTraffic { iface: String, source: ProbeError },
|
||||
|
||||
#[snafu(display("Failed to reassociate with WiFi network for interface: {}", iface))]
|
||||
Reassociate { iface: String },
|
||||
|
||||
#[snafu(display("Failed to force reread of wpa_supplicant configuration file"))]
|
||||
/// No networks found in range.
|
||||
AvailableNetworks {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to set new password.
|
||||
Modify {
|
||||
/// ID.
|
||||
id: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve IP address.
|
||||
Ip {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to parse integer from string.
|
||||
ParseInt(ParseIntError),
|
||||
/// Failed to retrieve network traffic measurement.
|
||||
NoTraffic {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
/// Underlying error source.
|
||||
source: ProbeError,
|
||||
},
|
||||
/// Failed to reassociate with WiFi network.
|
||||
Reassociate {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to force reread of wpa_supplicant configuration file.
|
||||
Reconfigure,
|
||||
|
||||
#[snafu(display("Failed to reconnect with WiFi network for interface: {}", iface))]
|
||||
Reconnect { iface: String },
|
||||
|
||||
#[snafu(display("Regex command failed"))]
|
||||
Regex { source: regex::Error },
|
||||
|
||||
#[snafu(display("Failed to delete network {} for interface: {}", id, iface))]
|
||||
Delete { id: String, iface: String },
|
||||
|
||||
#[snafu(display("Failed to retrieve state of wlan0 service: {}", source))]
|
||||
WlanState { source: io::Error },
|
||||
|
||||
#[snafu(display("Failed to retrieve connection state of wlan0 interface: {}", source))]
|
||||
WlanOperstate { source: io::Error },
|
||||
|
||||
#[snafu(display("Failed to save configuration changes to file"))]
|
||||
Save,
|
||||
|
||||
#[snafu(display("Failed to connect to network {} for interface: {}", id, iface))]
|
||||
Connect { id: String, iface: String },
|
||||
|
||||
#[snafu(display("Failed to start ap0 service: {}", source))]
|
||||
StartAp0 { source: io::Error },
|
||||
|
||||
#[snafu(display("Failed to start wlan0 service: {}", source))]
|
||||
StartWlan0 { source: io::Error },
|
||||
|
||||
#[snafu(display("JSON serialization failed: {}", source))]
|
||||
SerdeSerialize { source: SerdeError },
|
||||
|
||||
#[snafu(display("Failed to open control interface for wpasupplicant"))]
|
||||
WpaCtrlOpen {
|
||||
#[snafu(source(from(failure::Error, std::convert::Into::into)))]
|
||||
source: BoxError,
|
||||
/// Failed to reconnect with WiFi network.
|
||||
Reconnect {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
|
||||
#[snafu(display("Request to wpasupplicant via wpactrl failed"))]
|
||||
WpaCtrlRequest {
|
||||
#[snafu(source(from(failure::Error, std::convert::Into::into)))]
|
||||
source: BoxError,
|
||||
/// Failed to execute Regex command.
|
||||
Regex(RegexError),
|
||||
/// Failed to delete network.
|
||||
Delete {
|
||||
/// ID.
|
||||
id: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve state of wlan0 service.
|
||||
WlanState(IoError),
|
||||
/// Failed to retrieve connection state of wlan0 interface.
|
||||
WlanOperstate(IoError),
|
||||
/// Failed to save wpa_supplicant configuration changes to file.
|
||||
Save(IoError),
|
||||
/// Failed to connect to network.
|
||||
Connect {
|
||||
/// ID.
|
||||
id: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to start systemctl service for a network interface.
|
||||
StartInterface {
|
||||
/// Underlying error source.
|
||||
source: IoError,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to execute wpa-ctrl command.
|
||||
WpaCtrl(WpaError),
|
||||
}
|
||||
|
||||
impl From<NetworkError> for Error {
|
||||
fn from(err: NetworkError) -> Self {
|
||||
match &err {
|
||||
NetworkError::ActivateAp { err_msg } => Error {
|
||||
code: ErrorCode::ServerError(-32015),
|
||||
message: err_msg.to_string(),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::ActivateClient { err_msg } => Error {
|
||||
code: ErrorCode::ServerError(-32017),
|
||||
message: err_msg.to_string(),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Add { ssid } => Error {
|
||||
code: ErrorCode::ServerError(-32000),
|
||||
message: format!("Failed to add network for {}", ssid),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::NoState { iface, source } => Error {
|
||||
code: ErrorCode::ServerError(-32022),
|
||||
message: format!(
|
||||
"Failed to retrieve interface state for {}: {}",
|
||||
iface, source
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Disable { id, iface } => Error {
|
||||
code: ErrorCode::ServerError(-32029),
|
||||
message: format!("Failed to disable network {} for {}", id, iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Disconnect { iface } => Error {
|
||||
code: ErrorCode::ServerError(-32032),
|
||||
message: format!("Failed to disconnect {}", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::GenWpaPassphrase { ssid, source } => Error {
|
||||
code: ErrorCode::ServerError(-32025),
|
||||
message: format!("Failed to generate wpa passphrase for {}: {}", ssid, source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::GenWpaPassphraseWarning { ssid, err_msg } => Error {
|
||||
code: ErrorCode::ServerError(-32036),
|
||||
message: format!(
|
||||
"Failed to generate wpa passphrase for {}: {}",
|
||||
ssid, err_msg
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Id { iface, ssid } => Error {
|
||||
code: ErrorCode::ServerError(-32026),
|
||||
message: format!("No ID found for {} on interface {}", ssid, iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::NoIp { iface, source } => Error {
|
||||
code: ErrorCode::ServerError(-32001),
|
||||
message: format!("Failed to retrieve IP address for {}: {}", iface, source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Rssi { iface } => Error {
|
||||
code: ErrorCode::ServerError(-32002),
|
||||
message: format!(
|
||||
"Failed to retrieve RSSI for {}. Interface may not be connected",
|
||||
iface
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::RssiPercent { iface } => Error {
|
||||
code: ErrorCode::ServerError(-32034),
|
||||
message: format!(
|
||||
"Failed to retrieve signal quality (%) for {}. Interface may not be connected",
|
||||
iface
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Ssid { iface } => Error {
|
||||
code: ErrorCode::ServerError(-32003),
|
||||
message: format!(
|
||||
"Failed to retrieve SSID for {}. Interface may not be connected",
|
||||
iface
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::State { iface } => Error {
|
||||
code: ErrorCode::ServerError(-32023),
|
||||
message: format!("No state found for {}. Interface may not exist", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Status { iface } => Error {
|
||||
code: ErrorCode::ServerError(-32024),
|
||||
message: format!("No status found for {}. Interface may not exist", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Traffic { iface } => Error {
|
||||
code: ErrorCode::ServerError(-32004),
|
||||
message: format!(
|
||||
"No network traffic statistics found for {}. Interface may not exist",
|
||||
iface
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::SavedNetworks => Error {
|
||||
code: ErrorCode::ServerError(-32005),
|
||||
message: "No saved networks found".to_string(),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::AvailableNetworks { iface } => Error {
|
||||
code: ErrorCode::ServerError(-32006),
|
||||
message: format!("No networks found in range of {}", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::MissingParams { e } => e.clone(),
|
||||
NetworkError::Modify { id, iface } => Error {
|
||||
code: ErrorCode::ServerError(-32033),
|
||||
message: format!("Failed to set new password for network {} on {}", id, iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Ip { iface } => Error {
|
||||
code: ErrorCode::ServerError(-32007),
|
||||
message: format!("No IP address found for {}", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::ParseString { source } => Error {
|
||||
code: ErrorCode::ServerError(-32035),
|
||||
message: format!(
|
||||
"Failed to parse integer from string for RSSI value: {}",
|
||||
source
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::NoTraffic { iface, source } => Error {
|
||||
code: ErrorCode::ServerError(-32015),
|
||||
message: format!(
|
||||
"Failed to retrieve network traffic statistics for {}: {}",
|
||||
iface, source
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Reassociate { iface } => Error {
|
||||
code: ErrorCode::ServerError(-32008),
|
||||
message: format!("Failed to reassociate with WiFi network for {}", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Reconfigure => Error {
|
||||
code: ErrorCode::ServerError(-32030),
|
||||
message: "Failed to force reread of wpa_supplicant configuration file".to_string(),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Reconnect { iface } => Error {
|
||||
code: ErrorCode::ServerError(-32009),
|
||||
message: format!("Failed to reconnect with WiFi network for {}", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Regex { source } => Error {
|
||||
code: ErrorCode::ServerError(-32010),
|
||||
message: format!("Regex command error: {}", source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Delete { id, iface } => Error {
|
||||
code: ErrorCode::ServerError(-32028),
|
||||
message: format!("Failed to delete network {} for {}", id, iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::WlanState { source } => Error {
|
||||
code: ErrorCode::ServerError(-32011),
|
||||
message: format!("Failed to retrieve state of wlan0 service: {}", source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::WlanOperstate { source } => Error {
|
||||
code: ErrorCode::ServerError(-32021),
|
||||
message: format!(
|
||||
"Failed to retrieve connection state of wlan0 interface: {}",
|
||||
source
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Save => Error {
|
||||
code: ErrorCode::ServerError(-32031),
|
||||
message: "Failed to save configuration changes to file".to_string(),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Connect { id, iface } => Error {
|
||||
code: ErrorCode::ServerError(-32027),
|
||||
message: format!("Failed to connect to network {} for {}", id, iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::StartAp0 { source } => Error {
|
||||
code: ErrorCode::ServerError(-32016),
|
||||
message: format!("Failed to start ap0 service: {}", source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::StartWlan0 { source } => Error {
|
||||
code: ErrorCode::ServerError(-32018),
|
||||
message: format!("Failed to start wlan0 service: {}", source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::SerdeSerialize { source } => Error {
|
||||
code: ErrorCode::ServerError(-32012),
|
||||
message: format!("JSON serialization failed: {}", source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::WpaCtrlOpen { source } => Error {
|
||||
code: ErrorCode::ServerError(-32013),
|
||||
message: format!(
|
||||
"Failed to open control interface for wpasupplicant: {}",
|
||||
source
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::WpaCtrlRequest { source } => Error {
|
||||
code: ErrorCode::ServerError(-32014),
|
||||
message: format!("WPA supplicant request failed: {}", source),
|
||||
data: None,
|
||||
},
|
||||
impl std::error::Error for NetworkError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match *self {
|
||||
NetworkError::Add { .. } => None,
|
||||
NetworkError::NoState { ref source, .. } => Some(source),
|
||||
NetworkError::Disable { .. } => None,
|
||||
NetworkError::Disconnect { .. } => None,
|
||||
NetworkError::GenWpaPassphrase { ref source, .. } => Some(source),
|
||||
NetworkError::GenWpaPassphraseWarning { .. } => None,
|
||||
NetworkError::Id { .. } => None,
|
||||
NetworkError::NoIp { ref source, .. } => Some(source),
|
||||
NetworkError::Rssi { .. } => None,
|
||||
NetworkError::RssiPercent { .. } => None,
|
||||
NetworkError::Ssid { .. } => None,
|
||||
NetworkError::State { .. } => None,
|
||||
NetworkError::Status { .. } => None,
|
||||
NetworkError::Traffic { .. } => None,
|
||||
NetworkError::SavedNetworks => None,
|
||||
NetworkError::AvailableNetworks { .. } => None,
|
||||
NetworkError::Modify { .. } => None,
|
||||
NetworkError::Ip { .. } => None,
|
||||
NetworkError::ParseInt(ref source) => Some(source),
|
||||
NetworkError::NoTraffic { ref source, .. } => Some(source),
|
||||
NetworkError::Reassociate { .. } => None,
|
||||
NetworkError::Reconfigure { .. } => None,
|
||||
NetworkError::Reconnect { .. } => None,
|
||||
NetworkError::Regex(ref source) => Some(source),
|
||||
NetworkError::Delete { .. } => None,
|
||||
NetworkError::WlanState(ref source) => Some(source),
|
||||
NetworkError::WlanOperstate(ref source) => Some(source),
|
||||
NetworkError::Save(ref source) => Some(source),
|
||||
NetworkError::Connect { .. } => None,
|
||||
NetworkError::StartInterface { ref source, .. } => Some(source),
|
||||
NetworkError::WpaCtrl(ref source) => Some(source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NetworkError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match *self {
|
||||
NetworkError::Add { ref ssid } => {
|
||||
write!(f, "Failed to add network for {}", ssid)
|
||||
}
|
||||
NetworkError::NoState { ref iface, .. } => {
|
||||
write!(f, "Failed to retrieve state for interface: {}", iface)
|
||||
}
|
||||
NetworkError::Disable { ref id, ref iface } => {
|
||||
write!(
|
||||
f,
|
||||
"Failed to disable network {} for interface: {}",
|
||||
id, iface
|
||||
)
|
||||
}
|
||||
NetworkError::Disconnect { ref iface } => {
|
||||
write!(f, "Failed to disconnect {}", iface)
|
||||
}
|
||||
NetworkError::GenWpaPassphrase { ref ssid, .. } => {
|
||||
write!(f, "Failed to generate wpa passphrase for {}", ssid)
|
||||
}
|
||||
NetworkError::GenWpaPassphraseWarning {
|
||||
ref ssid,
|
||||
ref err_msg,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"Failed to generate wpa passphrase for {}: {}",
|
||||
ssid, err_msg
|
||||
)
|
||||
}
|
||||
NetworkError::Id {
|
||||
ref ssid,
|
||||
ref iface,
|
||||
} => {
|
||||
write!(f, "No ID found for {} on interface: {}", ssid, iface)
|
||||
}
|
||||
NetworkError::NoIp { ref iface, .. } => {
|
||||
write!(f, "Could not access IP address for interface: {}", iface)
|
||||
}
|
||||
NetworkError::Rssi { ref iface } => {
|
||||
write!(f, "Could not find RSSI for interface: {}", iface)
|
||||
}
|
||||
NetworkError::RssiPercent { ref iface } => {
|
||||
write!(
|
||||
f,
|
||||
"Could not find signal quality (%) for interface: {}",
|
||||
iface
|
||||
)
|
||||
}
|
||||
NetworkError::Ssid { ref iface } => {
|
||||
write!(f, "Could not find SSID for interface: {}", iface)
|
||||
}
|
||||
NetworkError::State { ref iface } => {
|
||||
write!(f, "No state found for interface: {}", iface)
|
||||
}
|
||||
NetworkError::Status { ref iface } => {
|
||||
write!(f, "No status found for interface: {}", iface)
|
||||
}
|
||||
NetworkError::Traffic { ref iface } => {
|
||||
write!(f, "Could not find network traffic for interface: {}", iface)
|
||||
}
|
||||
NetworkError::SavedNetworks => {
|
||||
write!(f, "No saved networks found for default interface")
|
||||
}
|
||||
NetworkError::AvailableNetworks { ref iface } => {
|
||||
write!(f, "No networks found in range of interface: {}", iface)
|
||||
}
|
||||
NetworkError::Modify { ref id, ref iface } => {
|
||||
write!(
|
||||
f,
|
||||
"Failed to set new password for network {} on {}",
|
||||
id, iface
|
||||
)
|
||||
}
|
||||
NetworkError::Ip { ref iface } => {
|
||||
write!(f, "No IP found for interface: {}", iface)
|
||||
}
|
||||
NetworkError::ParseInt(_) => {
|
||||
write!(f, "Failed to parse integer from string for RSSI value")
|
||||
}
|
||||
NetworkError::NoTraffic { ref iface, .. } => {
|
||||
write!(
|
||||
f,
|
||||
"Failed to retrieve network traffic measurement for {}",
|
||||
iface
|
||||
)
|
||||
}
|
||||
NetworkError::Reassociate { ref iface } => {
|
||||
write!(
|
||||
f,
|
||||
"Failed to reassociate with WiFi network for interface: {}",
|
||||
iface
|
||||
)
|
||||
}
|
||||
NetworkError::Reconfigure => {
|
||||
write!(
|
||||
f,
|
||||
"Failed to force reread of wpa_supplicant configuration file"
|
||||
)
|
||||
}
|
||||
NetworkError::Reconnect { ref iface } => {
|
||||
write!(
|
||||
f,
|
||||
"Failed to reconnect with WiFi network for interface: {}",
|
||||
iface
|
||||
)
|
||||
}
|
||||
NetworkError::Regex(_) => write!(f, "Regex command failed"),
|
||||
NetworkError::Delete { ref id, ref iface } => {
|
||||
write!(
|
||||
f,
|
||||
"Failed to delete network {} for interface: {}",
|
||||
id, iface
|
||||
)
|
||||
}
|
||||
NetworkError::WlanState(_) => write!(f, "Failed to retrieve state of wlan0 service"),
|
||||
NetworkError::WlanOperstate(_) => {
|
||||
write!(f, "Failed to retrieve connection state of wlan0 interface")
|
||||
}
|
||||
NetworkError::Save(ref source) => write!(
|
||||
f,
|
||||
"Failed to save configuration changes to file: {}",
|
||||
source
|
||||
),
|
||||
NetworkError::Connect { ref id, ref iface } => {
|
||||
write!(
|
||||
f,
|
||||
"Failed to connect to network {} for interface: {}",
|
||||
id, iface
|
||||
)
|
||||
}
|
||||
NetworkError::StartInterface { ref iface, .. } => write!(
|
||||
f,
|
||||
"Failed to start systemctl service for {} interface",
|
||||
iface
|
||||
),
|
||||
NetworkError::WpaCtrl(_) => write!(f, "WpaCtrl command failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WpaError> for NetworkError {
|
||||
fn from(err: WpaError) -> Self {
|
||||
NetworkError::WpaCtrl(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseIntError> for NetworkError {
|
||||
fn from(err: ParseIntError) -> Self {
|
||||
NetworkError::ParseInt(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RegexError> for NetworkError {
|
||||
fn from(err: RegexError) -> Self {
|
||||
NetworkError::Regex(err)
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
use std::process;
|
||||
|
||||
use log::error;
|
||||
|
||||
fn main() {
|
||||
// initalize the logger
|
||||
env_logger::init();
|
||||
|
||||
// handle errors returned from `run`
|
||||
if let Err(e) = peach_network::run() {
|
||||
error!("Application error: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
//! Retrieve network data and modify interface state.
|
||||
//!
|
||||
//! This module contains the core logic of the `peach-network` microservice and
|
||||
//! This module contains the core logic of the `peach-network` and
|
||||
//! provides convenience wrappers for a range of `wpasupplicant` commands,
|
||||
//! many of which are ordinarily executed using `wpa_cli` (a WPA command line
|
||||
//! client).
|
||||
@ -11,8 +11,8 @@
|
||||
//! Switching between client mode and access point mode is achieved by making
|
||||
//! system calls to systemd (via `systemctl`). Further networking functionality
|
||||
//! is provided by making system calls to retrieve interface state and write
|
||||
//! access point credentials to `wpa_supplicant-wlan0.conf`.
|
||||
//!
|
||||
//! access point credentials to `wpa_supplicant-<wlan_iface>.conf`.
|
||||
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
io::prelude::*,
|
||||
@ -21,72 +21,58 @@ use std::{
|
||||
str,
|
||||
};
|
||||
|
||||
use crate::error::{
|
||||
GenWpaPassphrase, NetworkError, NoIp, NoState, NoTraffic, ParseString, SerdeSerialize,
|
||||
StartAp0, StartWlan0, WlanState, WpaCtrlOpen, WpaCtrlRequest,
|
||||
};
|
||||
use probes::network;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use snafu::ResultExt;
|
||||
|
||||
#[cfg(feature = "miniserde_support")]
|
||||
use miniserde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "serde_support")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::NetworkError;
|
||||
use crate::utils;
|
||||
|
||||
/// Network interface name.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Iface {
|
||||
pub iface: String,
|
||||
}
|
||||
|
||||
/// Network interface name and network identifier.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IfaceId {
|
||||
pub iface: String,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
/// Network interface name, network identifier and password.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IfaceIdPass {
|
||||
pub iface: String,
|
||||
pub id: String,
|
||||
pub pass: String,
|
||||
}
|
||||
|
||||
/// Network interface name and network SSID.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IfaceSsid {
|
||||
pub iface: String,
|
||||
pub ssid: String,
|
||||
}
|
||||
|
||||
/// Network SSID.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Network {
|
||||
pub ssid: String,
|
||||
}
|
||||
|
||||
/// Access point data retrieved via scan.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct Scan {
|
||||
/// Frequency.
|
||||
pub frequency: String,
|
||||
/// Protocol.
|
||||
pub protocol: String,
|
||||
/// Signal strength.
|
||||
pub signal_level: String,
|
||||
/// SSID.
|
||||
pub ssid: String,
|
||||
}
|
||||
|
||||
/// Status data for a network interface.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct Status {
|
||||
/// MAC address.
|
||||
pub address: Option<String>,
|
||||
/// Basic Service Set Identifier (BSSID).
|
||||
pub bssid: Option<String>,
|
||||
/// Frequency.
|
||||
pub freq: Option<String>,
|
||||
/// Group cipher.
|
||||
pub group_cipher: Option<String>,
|
||||
/// Local ID.
|
||||
pub id: Option<String>,
|
||||
/// IP address.
|
||||
pub ip_address: Option<String>,
|
||||
/// Key management.
|
||||
pub key_mgmt: Option<String>,
|
||||
/// Mode.
|
||||
pub mode: Option<String>,
|
||||
/// Pairwise cipher.
|
||||
pub pairwise_cipher: Option<String>,
|
||||
/// SSID.
|
||||
pub ssid: Option<String>,
|
||||
/// WPA state.
|
||||
pub wpa_state: Option<String>,
|
||||
}
|
||||
|
||||
@ -109,19 +95,16 @@ impl Status {
|
||||
}
|
||||
|
||||
/// Received and transmitted network traffic (bytes).
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct Traffic {
|
||||
/// Total bytes received.
|
||||
pub received: u64,
|
||||
/// Total bytes transmitted.
|
||||
pub transmitted: u64,
|
||||
}
|
||||
|
||||
/// SSID and password for a wireless access point.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WiFi {
|
||||
pub ssid: String,
|
||||
pub pass: String,
|
||||
}
|
||||
|
||||
/* GET - Methods for retrieving data */
|
||||
|
||||
/// Retrieve list of available wireless access points for a given network
|
||||
@ -132,22 +115,15 @@ pub struct WiFi {
|
||||
/// * `iface` - A string slice holding the name of a wireless network interface
|
||||
///
|
||||
/// If the scan results include one or more access points for the given network
|
||||
/// interface, an `Ok` `Result` type is returned containing `Some(String)` -
|
||||
/// where `String` is a serialized vector of `Scan` structs containing
|
||||
/// data for the in-range access points. If no access points are found,
|
||||
/// a `None` type is returned in the `Result`. In the event of an error, a
|
||||
/// `NetworkError` is returned in the `Result`. The `NetworkError` is then
|
||||
/// enumerated to a specific error type and an appropriate JSON RPC response is
|
||||
/// sent to the caller.
|
||||
///
|
||||
pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// interface, an `Ok` `Result` type is returned containing `Some(Vec<Scan>)`.
|
||||
/// The vector of `Scan` structs contains data for the in-range access points.
|
||||
/// If no access points are found, a `None` type is returned in the `Result`.
|
||||
/// In the event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
wpa.request("SCAN").context(WpaCtrlRequest)?;
|
||||
let networks = wpa.request("SCAN_RESULTS").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
wpa.request("SCAN")?;
|
||||
let networks = wpa.request("SCAN_RESULTS")?;
|
||||
let mut scan = Vec::new();
|
||||
for network in networks.lines() {
|
||||
let v: Vec<&str> = network.split('\t').collect();
|
||||
@ -178,8 +154,7 @@ pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
if scan.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let results = serde_json::to_string(&scan).context(SerdeSerialize)?;
|
||||
Ok(Some(results))
|
||||
Ok(Some(scan))
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,17 +170,11 @@ pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// found in the list of saved networks, an `Ok` `Result` type is returned
|
||||
/// containing `Some(String)` - where `String` is the network identifier.
|
||||
/// If no match is found, a `None` type is returned in the `Result`. In the
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`. The
|
||||
/// `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let networks = wpa.request("LIST_NETWORKS").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let networks = wpa.request("LIST_NETWORKS")?;
|
||||
let mut id = Vec::new();
|
||||
for network in networks.lines() {
|
||||
let v: Vec<&str> = network.split('\t').collect();
|
||||
@ -233,13 +202,13 @@ pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// an `Ok` `Result` type is returned containing `Some(String)` - where `String`
|
||||
/// is the IP address of the interface. If no match is found, a `None` type is
|
||||
/// returned in the `Result`. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`. The `NetworkError` is then enumerated to a
|
||||
/// specific error type and an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
/// returned in the `Result`.
|
||||
pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let net_if: String = iface.to_string();
|
||||
let ifaces = get_if_addrs::get_if_addrs().context(NoIp { iface: net_if })?;
|
||||
let ifaces = get_if_addrs::get_if_addrs().map_err(|source| NetworkError::NoIp {
|
||||
iface: net_if,
|
||||
source,
|
||||
})?;
|
||||
let ip = ifaces
|
||||
.iter()
|
||||
.find(|&i| i.name == iface)
|
||||
@ -260,16 +229,11 @@ pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// is the RSSI (Received Signal Strength Indicator) of the connection measured
|
||||
/// in dBm. If signal strength is not found, a `None` type is returned in the
|
||||
/// `Result`. In the event of an error, a `NetworkError` is returned in the
|
||||
/// `Result`. The `NetworkError` is then enumerated to a specific error type and
|
||||
/// an appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// `Result`.
|
||||
pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let status = wpa.request("SIGNAL_POLL").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let status = wpa.request("SIGNAL_POLL")?;
|
||||
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
|
||||
|
||||
if rssi.is_none() {
|
||||
@ -292,22 +256,17 @@ pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// is the RSSI (Received Signal Strength Indicator) of the connection measured
|
||||
/// as a percentage. If signal strength is not found, a `None` type is returned
|
||||
/// in the `Result`. In the event of an error, a `NetworkError` is returned in
|
||||
/// the `Result`. The `NetworkError` is then enumerated to a specific error type
|
||||
/// and an appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// the `Result`.
|
||||
pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let status = wpa.request("SIGNAL_POLL").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let status = wpa.request("SIGNAL_POLL")?;
|
||||
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
|
||||
|
||||
match rssi {
|
||||
Some(rssi) => {
|
||||
// parse the string to a signed integer (for math)
|
||||
let rssi_parsed = rssi.parse::<i32>().context(ParseString)?;
|
||||
let rssi_parsed = rssi.parse::<i32>()?;
|
||||
// perform rssi (dBm) to quality (%) conversion
|
||||
let quality_percent = 2 * (rssi_parsed + 100);
|
||||
// convert signal quality integer to string
|
||||
@ -327,32 +286,27 @@ pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
///
|
||||
/// If the wpasupplicant configuration file contains credentials for one or
|
||||
/// more access points, an `Ok` `Result` type is returned containing
|
||||
/// `Some(String)` - where `String` is a serialized vector of `Network` structs
|
||||
/// containing the SSIDs of all saved networks. If no network credentials are
|
||||
/// found, a `None` type is returned in the `Result`. In the event of an error,
|
||||
/// a `NetworkError` is returned in the `Result`. The `NetworkError` is then
|
||||
/// enumerated to a specific error type and an appropriate JSON RPC response is
|
||||
/// sent to the caller.
|
||||
///
|
||||
pub fn saved_networks() -> Result<Option<String>, NetworkError> {
|
||||
let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?;
|
||||
let networks = wpa.request("LIST_NETWORKS").context(WpaCtrlRequest)?;
|
||||
/// `Some(Vec<Network>)`. The vector of `Network` structs contains the SSIDs
|
||||
/// of all saved networks. If no network credentials are found, a `None` type
|
||||
/// is returned in the `Result`. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`.
|
||||
pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
||||
let networks = wpa.request("LIST_NETWORKS")?;
|
||||
let mut ssids = Vec::new();
|
||||
for network in networks.lines() {
|
||||
let v: Vec<&str> = network.split('\t').collect();
|
||||
let len = v.len();
|
||||
if len > 1 {
|
||||
let ssid = v[1].trim().to_string();
|
||||
let response = Network { ssid };
|
||||
ssids.push(response)
|
||||
ssids.push(ssid)
|
||||
}
|
||||
}
|
||||
|
||||
if ssids.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let results = serde_json::to_string(&ssids).context(SerdeSerialize)?;
|
||||
Ok(Some(results))
|
||||
Ok(Some(ssids))
|
||||
}
|
||||
}
|
||||
|
||||
@ -366,17 +320,11 @@ pub fn saved_networks() -> Result<Option<String>, NetworkError> {
|
||||
/// an `Ok` `Result` type is returned containing `Some(String)` - where `String`
|
||||
/// is the SSID of the associated network. If SSID is not found, a `None` type
|
||||
/// is returned in the `Result`. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`. The `NetworkError` is then enumerated to a
|
||||
/// specific error type and an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
/// returned in the `Result`.
|
||||
pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let status = wpa.request("STATUS").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let status = wpa.request("STATUS")?;
|
||||
|
||||
// pass the regex pattern and status output to the regex finder
|
||||
let ssid = utils::regex_finder(r"\nssid=(.*)\n", &status)?;
|
||||
@ -394,9 +342,7 @@ pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// returned containing `Some(String)` - where `String` is the state of the
|
||||
/// network interface. If state is not found, a `None` type is returned in the
|
||||
/// `Result`. In the event of an error, a `NetworkError` is returned in the
|
||||
/// `Result`. The `NetworkError` is then enumerated to a specific error type and
|
||||
/// an appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// `Result`.
|
||||
pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
// construct the interface operstate path
|
||||
let iface_path: String = format!("/sys/class/net/{}/operstate", iface);
|
||||
@ -404,7 +350,10 @@ pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let output = Command::new("cat")
|
||||
.arg(iface_path)
|
||||
.output()
|
||||
.context(NoState { iface })?;
|
||||
.map_err(|source| NetworkError::NoState {
|
||||
iface: iface.to_string(),
|
||||
source,
|
||||
})?;
|
||||
if !output.stdout.is_empty() {
|
||||
// unwrap the command result and convert to String
|
||||
let mut state = String::from_utf8(output.stdout).unwrap();
|
||||
@ -427,17 +376,11 @@ pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// returned containing `Some(Status)` - where `Status` is a `struct`
|
||||
/// containing the aggregated interface data in named fields. If status is not
|
||||
/// found, a `None` type is returned in the `Result`. In the event of an error,
|
||||
/// a `NetworkError` is returned in the `Result`. The `NetworkError` is then
|
||||
/// enumerated to a specific error type and an appropriate JSON RPC response is
|
||||
/// sent to the caller.
|
||||
///
|
||||
/// a `NetworkError` is returned in the `Result`.
|
||||
pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let wpa_status = wpa.request("STATUS").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let wpa_status = wpa.request("STATUS")?;
|
||||
|
||||
// pass the regex pattern and status output to the regex finder
|
||||
let state = utils::regex_finder(r"wpa_state=(.*)\n", &wpa_status)?;
|
||||
@ -486,16 +429,16 @@ pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
|
||||
/// * `iface` - A string slice holding the name of a wireless network interface
|
||||
///
|
||||
/// If the network traffic statistics are found for the given interface, an `Ok`
|
||||
/// `Result` type is returned containing `Some(String)` - where `String` is a
|
||||
/// serialized `Traffic` `struct` with fields for received and transmitted
|
||||
/// network data statistics. If network traffic statistics are not found for the
|
||||
/// given interface, a `None` type is returned in the `Result`. In the event of
|
||||
/// an error, a `NetworkError` is returned in the `Result`. The `NetworkError`
|
||||
/// is then enumerated to a specific error type and an appropriate JSON RPC
|
||||
/// response is sent to the caller.
|
||||
///
|
||||
pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let network = network::read().context(NoTraffic { iface })?;
|
||||
/// `Result` type is returned containing `Some(Traffic)`. The `Traffic` `struct`
|
||||
/// includes fields for received and transmitted network data statistics. If
|
||||
/// network traffic statistics are not found for the given interface, a `None`
|
||||
/// type is returned in the `Result`. In the event of an error, a `NetworkError`
|
||||
/// is returned in the `Result`.
|
||||
pub fn traffic(iface: &str) -> Result<Option<Traffic>, NetworkError> {
|
||||
let network = network::read().map_err(|source| NetworkError::NoTraffic {
|
||||
iface: iface.to_string(),
|
||||
source,
|
||||
})?;
|
||||
// iterate through interfaces returned in network data
|
||||
for (interface, traffic) in network.interfaces {
|
||||
if interface == iface {
|
||||
@ -505,9 +448,7 @@ pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
received,
|
||||
transmitted,
|
||||
};
|
||||
// TODO: add test for SerdeSerialize error
|
||||
let t = serde_json::to_string(&traffic).context(SerdeSerialize)?;
|
||||
return Ok(Some(t));
|
||||
return Ok(Some(traffic));
|
||||
}
|
||||
}
|
||||
|
||||
@ -516,42 +457,25 @@ pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
|
||||
/* SET - Methods for modifying state */
|
||||
|
||||
/// Activate wireless access point.
|
||||
/// Start network interface service.
|
||||
///
|
||||
/// A `systemctl `command is invoked which starts the `ap0` interface service.
|
||||
/// If the command executes successfully, an `Ok` `Result` type is returned.
|
||||
/// In the event of an error, a `NetworkError` is returned in the `Result`.
|
||||
/// The `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
pub fn activate_ap() -> Result<(), NetworkError> {
|
||||
// start the ap0 interface service
|
||||
/// A `systemctl `command is invoked which starts the service for the given
|
||||
/// network interface. If the command executes successfully, an `Ok` `Result`
|
||||
/// type is returned. In the event of an error, a `NetworkError` is returned
|
||||
/// in the `Result`.
|
||||
pub fn start_iface_service(iface: &str) -> Result<(), NetworkError> {
|
||||
let iface_service = format!("wpa_supplicant@{}.service", &iface);
|
||||
|
||||
// start the interface service
|
||||
Command::new("sudo")
|
||||
.arg("/usr/bin/systemctl")
|
||||
.arg("start")
|
||||
.arg("wpa_supplicant@ap0.service")
|
||||
.arg(iface_service)
|
||||
.output()
|
||||
.context(StartAp0)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Activate wireless client.
|
||||
///
|
||||
/// A `systemctl` command is invoked which starts the `wlan0` interface service.
|
||||
/// If the command executes successfully, an `Ok` `Result` type is returned.
|
||||
/// In the event of an error, a `NetworkError` is returned in the `Result`.
|
||||
/// The `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
pub fn activate_client() -> Result<(), NetworkError> {
|
||||
// start the wlan0 interface service
|
||||
Command::new("sudo")
|
||||
.arg("/usr/bin/systemctl")
|
||||
.arg("start")
|
||||
.arg("wpa_supplicant@wlan0.service")
|
||||
.output()
|
||||
.context(StartWlan0)?;
|
||||
.map_err(|source| NetworkError::StartInterface {
|
||||
source,
|
||||
iface: iface.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -560,81 +484,82 @@ pub fn activate_client() -> Result<(), NetworkError> {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `wlan_iface` - A local wireless interface.
|
||||
/// * `wifi` - An instance of the `WiFi` `struct` with fields `ssid` and `pass`
|
||||
///
|
||||
/// If configuration parameters are successfully generated from the provided
|
||||
/// SSID and password and appended to `wpa_supplicant-wlan0.conf`, an `Ok`
|
||||
/// `Result` type is returned. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`. The `NetworkError` is then enumerated to a
|
||||
/// specific error type and an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
pub fn add(wifi: &WiFi) -> Result<(), NetworkError> {
|
||||
/// SSID and password and appended to `wpa_supplicant-<wlan_iface>.conf` (where
|
||||
/// `<wlan_iface>` is the provided interface parameter), an `Ok` `Result` type
|
||||
/// is returned. In the event of an error, a `NetworkError` is returned in the
|
||||
/// `Result`.
|
||||
pub fn add(wlan_iface: &str, ssid: &str, pass: &str) -> Result<(), NetworkError> {
|
||||
// generate configuration based on provided ssid & password
|
||||
let output = Command::new("wpa_passphrase")
|
||||
.arg(&wifi.ssid)
|
||||
.arg(&wifi.pass)
|
||||
.arg(&ssid)
|
||||
.arg(&pass)
|
||||
.stdout(Stdio::piped())
|
||||
.output()
|
||||
.context(GenWpaPassphrase { ssid: &wifi.ssid })?;
|
||||
.map_err(|source| NetworkError::GenWpaPassphrase {
|
||||
ssid: ssid.to_string(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
// prepend newline to wpa_details to safeguard against malformed supplicant
|
||||
let mut wpa_details = "\n".as_bytes().to_vec();
|
||||
wpa_details.extend(&*(output.stdout));
|
||||
|
||||
// append wpa_passphrase output to wpa_supplicant-wlan0.conf if successful
|
||||
let wlan_config = format!("/etc/wpa_supplicant/wpa_supplicant-{}.conf", wlan_iface);
|
||||
|
||||
// append wpa_passphrase output to wpa_supplicant-<wlan_iface>.conf if successful
|
||||
if output.status.success() {
|
||||
// open file in append mode
|
||||
let file = OpenOptions::new()
|
||||
let mut file = OpenOptions::new()
|
||||
.append(true)
|
||||
.open("/etc/wpa_supplicant/wpa_supplicant-wlan0.conf");
|
||||
.open(wlan_config)
|
||||
// TODO: create the file if it doesn't exist
|
||||
.map_err(NetworkError::Save)?;
|
||||
|
||||
file.write(&wpa_details).map_err(NetworkError::Save)?;
|
||||
|
||||
let _file = match file {
|
||||
// if file exists & open succeeds, write wifi configuration
|
||||
Ok(mut f) => f.write(&wpa_details),
|
||||
// TODO: handle this better: create file if not found
|
||||
// & seed with 'ctrl_interace' & 'update_config' settings
|
||||
// config file could also be copied from peach/config fs location
|
||||
Err(e) => panic!("Failed to write to file: {}", e),
|
||||
};
|
||||
Ok(())
|
||||
} else {
|
||||
let err_msg = String::from_utf8_lossy(&output.stdout);
|
||||
Err(NetworkError::GenWpaPassphraseWarning {
|
||||
ssid: wifi.ssid.to_string(),
|
||||
ssid: ssid.to_string(),
|
||||
err_msg: err_msg.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Deploy the access point if the `wlan0` interface is `up` without an active
|
||||
/// Deploy an access point if the wireless interface is `up` without an active
|
||||
/// connection.
|
||||
///
|
||||
/// The status of the `wlan0` service and the state of the `wlan0` interface
|
||||
/// The status of the wireless service and the state of the wireless interface
|
||||
/// are checked. If the service is active but the interface is down (ie. not
|
||||
/// currently connected to an access point), then the access point is activated
|
||||
/// by calling the `activate_ap()` function.
|
||||
///
|
||||
pub fn check_iface() -> Result<(), NetworkError> {
|
||||
// returns 0 if the service is currently active
|
||||
let wlan0_status = Command::new("/usr/bin/systemctl")
|
||||
.arg("is-active")
|
||||
.arg("wpa_supplicant@wlan0.service")
|
||||
.status()
|
||||
.context(WlanState)?;
|
||||
pub fn check_iface(wlan_iface: &str, ap_iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_service = format!("wpa_supplicant@{}.service", &wlan_iface);
|
||||
|
||||
// returns the current state of the wlan0 interface
|
||||
let iface_state = state("wlan0")?;
|
||||
// returns 0 if the service is currently active
|
||||
let wlan_status = Command::new("/usr/bin/systemctl")
|
||||
.arg("is-active")
|
||||
.arg(wpa_service)
|
||||
.status()
|
||||
.map_err(NetworkError::WlanState)?;
|
||||
|
||||
// returns the current state of the wlan interface
|
||||
let iface_state = state(wlan_iface)?;
|
||||
|
||||
// returns down if the interface is not currently connected to an ap
|
||||
let wlan0_state = match iface_state {
|
||||
let wlan_state = match iface_state {
|
||||
Some(state) => state,
|
||||
None => "error".to_string(),
|
||||
};
|
||||
|
||||
// if wlan0 is active but not connected, start the ap0 service
|
||||
if wlan0_status.success() && wlan0_state == "down" {
|
||||
activate_ap()?
|
||||
// if wlan is active but not connected, start the ap service
|
||||
if wlan_status.success() && wlan_state == "down" {
|
||||
start_iface_service(ap_iface)?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -651,18 +576,12 @@ pub fn check_iface() -> Result<(), NetworkError> {
|
||||
/// If the network connection is successfully activated for the access point
|
||||
/// represented by the given network identifier on the given wireless interface,
|
||||
/// an `Ok` `Result`type is returned. In the event of an error, a `NetworkError`
|
||||
/// is returned in the `Result`. The `NetworkError` is then enumerated to a
|
||||
/// specific error type and an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
/// is returned in the `Result`.
|
||||
pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let select = format!("SELECT {}", id);
|
||||
wpa.request(&select).context(WpaCtrlRequest)?;
|
||||
wpa.request(&select)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -676,18 +595,12 @@ pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
/// If the network configuration parameters are successfully deleted for
|
||||
/// the access point represented by the given network identifier, an `Ok`
|
||||
/// `Result`type is returned. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`. The `NetworkError` is then enumerated to a
|
||||
/// specific error type and an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
/// returned in the `Result`.
|
||||
pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let remove = format!("REMOVE_NETWORK {}", id);
|
||||
wpa.request(&remove).context(WpaCtrlRequest)?;
|
||||
wpa.request(&remove)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -701,17 +614,12 @@ pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
/// If the network connection is successfully disabled for the access point
|
||||
/// represented by the given network identifier, an `Ok` `Result`type is
|
||||
/// returned. In the event of an error, a `NetworkError` is returned in the
|
||||
/// `Result`. The `NetworkError` is then enumerated to a specific error type and
|
||||
/// an appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// `Result`.
|
||||
pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let disable = format!("DISABLE_NETWORK {}", id);
|
||||
wpa.request(&disable).context(WpaCtrlRequest)?;
|
||||
wpa.request(&disable)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -723,18 +631,44 @@ pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
///
|
||||
/// If the network connection is successfully disconnected for the given
|
||||
/// wireless interface, an `Ok` `Result` type is returned. In the event of an
|
||||
/// error, a `NetworkError` is returned in the `Result`. The `NetworkError` is
|
||||
/// then enumerated to a specific error type and an appropriate JSON RPC
|
||||
/// response is sent to the caller.
|
||||
///
|
||||
/// error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let disconnect = "DISCONNECT".to_string();
|
||||
wpa.request(&disconnect).context(WpaCtrlRequest)?;
|
||||
wpa.request(&disconnect)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Forget credentials for the given network SSID and interface.
|
||||
/// Look up the network identified for the given SSID, delete the credentials
|
||||
/// and then save.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `iface` - A string slice holding the name of a wireless network interface
|
||||
/// * `ssid` - A string slice holding the SSID for a wireless access point
|
||||
///
|
||||
/// If the credentials are successfully deleted and saved, an `Ok` `Result`
|
||||
/// type is returned. In the event of an error, a `NetworkError` is returned
|
||||
/// in the `Result`.
|
||||
pub fn forget(iface: &str, ssid: &str) -> Result<(), NetworkError> {
|
||||
// get the id of the network
|
||||
let id_opt = id(iface, ssid)?;
|
||||
let id = id_opt.ok_or(NetworkError::Id {
|
||||
ssid: ssid.to_string(),
|
||||
iface: iface.to_string(),
|
||||
})?;
|
||||
// delete the old credentials
|
||||
// TODO: i've switched these back to the "correct" order
|
||||
// WEIRD BUG: the parameters below are technically in the wrong order:
|
||||
// it should be id first and then iface, but somehow they get twisted.
|
||||
// i don't understand computers.
|
||||
//delete(&iface, &id)?;
|
||||
delete(&id, iface)?;
|
||||
// save the updates to wpa_supplicant.conf
|
||||
save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -748,18 +682,12 @@ pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
|
||||
///
|
||||
/// If the password is successfully updated for the access point represented by
|
||||
/// the given network identifier, an `Ok` `Result` type is returned. In the
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`. The
|
||||
/// `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
let new_pass = format!("NEW_PASSWORD {} {}", id, pass);
|
||||
wpa.request(&new_pass).context(WpaCtrlRequest)?;
|
||||
wpa.request(&new_pass)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -771,17 +699,11 @@ pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
|
||||
///
|
||||
/// If the network connection is successfully reassociated for the given
|
||||
/// wireless interface, an `Ok` `Result` type is returned. In the event of an
|
||||
/// error, a `NetworkError` is returned in the `Result`. The `NetworkError` is
|
||||
/// then enumerated to a specific error type and an appropriate JSON RPC
|
||||
/// response is sent to the caller.
|
||||
///
|
||||
/// error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
wpa.request("REASSOCIATE").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
wpa.request("REASSOCIATE")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -790,13 +712,10 @@ pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
|
||||
/// If the reconfigure command is successfully executed, indicating a reread
|
||||
/// of the `wpa_supplicant.conf` file by the `wpa_supplicant` process, an `Ok`
|
||||
/// `Result` type is returned. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`. The `NetworkError` is then enumerated to a
|
||||
/// specific error type and an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
/// returned in the `Result`.
|
||||
pub fn reconfigure() -> Result<(), NetworkError> {
|
||||
let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?;
|
||||
wpa.request("RECONFIGURE").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
||||
wpa.request("RECONFIGURE")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -808,31 +727,37 @@ pub fn reconfigure() -> Result<(), NetworkError> {
|
||||
///
|
||||
/// If the network connection is successfully disconnected and reconnected for
|
||||
/// the given wireless interface, an `Ok` `Result` type is returned. In the
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`. The
|
||||
/// `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::new()
|
||||
.ctrl_path(wpa_path)
|
||||
.open()
|
||||
.context(WpaCtrlOpen)?;
|
||||
wpa.request("DISCONNECT").context(WpaCtrlRequest)?;
|
||||
wpa.request("RECONNECT").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
wpa.request("DISCONNECT")?;
|
||||
wpa.request("RECONNECT")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save configuration updates to the `wpa_supplicant` configuration file.
|
||||
///
|
||||
/// If wireless network configuration updates are successfully save to the
|
||||
/// If wireless network configuration updates are successfully saved to the
|
||||
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`. The
|
||||
/// `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn save() -> Result<(), NetworkError> {
|
||||
let mut wpa = wpactrl::WpaCtrl::new().open().context(WpaCtrlOpen)?;
|
||||
wpa.request("SAVE_CONFIG").context(WpaCtrlRequest)?;
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
||||
wpa.request("SAVE_CONFIG")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update password for an access point and save configuration updates to the
|
||||
/// `wpa_supplicant` configuration file.
|
||||
///
|
||||
/// If wireless network configuration updates are successfully saved to the
|
||||
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn update(iface: &str, ssid: &str, pass: &str) -> Result<(), NetworkError> {
|
||||
// delete the old credentials and save the changes
|
||||
forget(iface, ssid)?;
|
||||
// add the new credentials
|
||||
add(iface, ssid, pass)?;
|
||||
reconfigure()?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
use regex::Regex;
|
||||
use snafu::ResultExt;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::error::NetworkError;
|
||||
|
||||
/// Return matches for a given Regex pattern and text
|
||||
///
|
||||
@ -11,7 +10,7 @@ use crate::error::*;
|
||||
/// * `text` - A string slice containing the text to be matched on
|
||||
///
|
||||
pub fn regex_finder(pattern: &str, text: &str) -> Result<Option<String>, NetworkError> {
|
||||
let re = Regex::new(pattern).context(Regex)?;
|
||||
let re = Regex::new(pattern)?;
|
||||
let caps = re.captures(text);
|
||||
let result = caps.map(|caps| caps[1].to_string());
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "peach-oled"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
|
||||
edition = "2018"
|
||||
description = "Write and draw to OLED display using JSON-RPC over HTTP."
|
||||
@ -27,18 +27,16 @@ travis-ci = { repository = "peachcloud/peach-oled", branch = "master" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
jsonrpc-core = "11.0.0"
|
||||
jsonrpc-http-server = "11.0.0"
|
||||
linux-embedded-hal = "0.2.2"
|
||||
embedded-graphics = "0.4.7"
|
||||
tinybmp = "0.1.0"
|
||||
ssd1306 = "0.2.6"
|
||||
serde = { version = "1.0.87", features = ["derive"] }
|
||||
serde_json = "1.0.39"
|
||||
log = "0.4.0"
|
||||
env_logger = "0.6.1"
|
||||
snafu = "0.4.1"
|
||||
env_logger = "0.9"
|
||||
jsonrpc-core = "18"
|
||||
jsonrpc-http-server = "18"
|
||||
linux-embedded-hal = "0.2.2"
|
||||
log = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
nix="0.11"
|
||||
ssd1306 = "0.2.6"
|
||||
tinybmp = "0.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
jsonrpc-test = "11.0.0"
|
||||
jsonrpc-test = "18"
|
||||
|
@ -1,6 +1,6 @@
|
||||
# peach-oled
|
||||
|
||||
[](https://travis-ci.com/peachcloud/peach-oled) 
|
||||
[](https://travis-ci.com/peachcloud/peach-oled) 
|
||||
|
||||
OLED microservice module for PeachCloud. Write to a 128x64 OLED display with SDD1306 driver (I2C) using [JSON-RPC](https://www.jsonrpc.org/specification) over http.
|
||||
|
||||
|
@ -1,44 +1,68 @@
|
||||
use std::error;
|
||||
use std::{error, fmt};
|
||||
|
||||
use jsonrpc_core::{types::error::Error, ErrorCode};
|
||||
use linux_embedded_hal as hal;
|
||||
use snafu::Snafu;
|
||||
use jsonrpc_core::types::error::Error as JsonRpcError;
|
||||
use jsonrpc_core::ErrorCode;
|
||||
use linux_embedded_hal::i2cdev::linux::LinuxI2CError;
|
||||
|
||||
pub type BoxError = Box<dyn error::Error>;
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
#[snafu(visibility(pub(crate)))]
|
||||
#[derive(Debug)]
|
||||
pub enum OledError {
|
||||
#[snafu(display("Failed to create interface for I2C device: {}", source))]
|
||||
I2CError {
|
||||
source: hal::i2cdev::linux::LinuxI2CError,
|
||||
source: LinuxI2CError,
|
||||
},
|
||||
|
||||
#[snafu(display("Coordinate {} out of range {}: {}", coord, range, value))]
|
||||
InvalidCoordinate {
|
||||
coord: String,
|
||||
range: String,
|
||||
value: i32,
|
||||
},
|
||||
|
||||
// TODO: implement for validate() in src/lib.rs
|
||||
#[snafu(display("Font size invalid: {}", font))]
|
||||
InvalidFontSize { font: String },
|
||||
|
||||
#[snafu(display("String length out of range 0-21: {}", len))]
|
||||
InvalidString { len: usize },
|
||||
|
||||
#[snafu(display("Missing expected parameter: {}", e))]
|
||||
MissingParameter { e: Error },
|
||||
|
||||
#[snafu(display("Failed to parse parameter: {}", e))]
|
||||
ParseError { e: Error },
|
||||
InvalidFontSize {
|
||||
font: String,
|
||||
},
|
||||
InvalidString {
|
||||
len: usize,
|
||||
},
|
||||
MissingParameter {
|
||||
source: JsonRpcError,
|
||||
},
|
||||
ParseError {
|
||||
source: JsonRpcError,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<OledError> for Error {
|
||||
impl error::Error for OledError {}
|
||||
|
||||
impl fmt::Display for OledError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
OledError::ParseError { ref source } => {
|
||||
write!(f, "Failed to parse parameter: {}", source)
|
||||
}
|
||||
OledError::MissingParameter { ref source } => {
|
||||
write!(f, "Missing expected parameter: {}", source)
|
||||
}
|
||||
OledError::InvalidString { len } => {
|
||||
write!(f, "String length out of range 0-21: {}", len)
|
||||
}
|
||||
OledError::InvalidFontSize { ref font } => {
|
||||
write!(f, "Invalid font size: {}", font)
|
||||
}
|
||||
OledError::InvalidCoordinate {
|
||||
ref coord,
|
||||
ref range,
|
||||
value,
|
||||
} => {
|
||||
write!(f, "Coordinate {} out of range {}: {}", coord, range, value)
|
||||
}
|
||||
OledError::I2CError { ref source } => {
|
||||
write!(f, "Failed to create interface for I2C device: {}", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OledError> for JsonRpcError {
|
||||
fn from(err: OledError) -> Self {
|
||||
match &err {
|
||||
OledError::I2CError { source } => Error {
|
||||
OledError::I2CError { source } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32000),
|
||||
message: format!("Failed to create interface for I2C device: {}", source),
|
||||
data: None,
|
||||
@ -47,7 +71,7 @@ impl From<OledError> for Error {
|
||||
coord,
|
||||
value,
|
||||
range,
|
||||
} => Error {
|
||||
} => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32001),
|
||||
message: format!(
|
||||
"Validation error: coordinate {} out of range {}: {}",
|
||||
@ -55,18 +79,18 @@ impl From<OledError> for Error {
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
OledError::InvalidFontSize { font } => Error {
|
||||
OledError::InvalidFontSize { font } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32002),
|
||||
message: format!("Validation error: {} is not an accepted font size. Use 6x8, 6x12, 8x16 or 12x16 instead", font),
|
||||
data: None,
|
||||
},
|
||||
OledError::InvalidString { len } => Error {
|
||||
OledError::InvalidString { len } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32003),
|
||||
message: format!("Validation error: string length {} out of range 0-21", len),
|
||||
data: None,
|
||||
},
|
||||
OledError::MissingParameter { e } => e.clone(),
|
||||
OledError::ParseError { e } => e.clone(),
|
||||
OledError::MissingParameter { source } => source.clone(),
|
||||
OledError::ParseError { source } => source.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,23 +6,23 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use embedded_graphics::coord::Coord;
|
||||
use embedded_graphics::fonts::{Font12x16, Font6x12, Font6x8, Font8x16};
|
||||
use embedded_graphics::image::Image1BPP;
|
||||
use embedded_graphics::prelude::*;
|
||||
use embedded_graphics::{
|
||||
coord::Coord,
|
||||
fonts::{Font12x16, Font6x12, Font6x8, Font8x16},
|
||||
image::Image1BPP,
|
||||
prelude::*,
|
||||
};
|
||||
use hal::I2cdev;
|
||||
use jsonrpc_core::{types::error::Error, IoHandler, Params, Value};
|
||||
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
|
||||
use linux_embedded_hal as hal;
|
||||
use log::{debug, error, info};
|
||||
use serde::Deserialize;
|
||||
use snafu::{ensure, ResultExt};
|
||||
use ssd1306::prelude::*;
|
||||
use ssd1306::Builder;
|
||||
use ssd1306::{prelude::*, Builder};
|
||||
|
||||
use crate::error::{BoxError, I2CError, InvalidCoordinate, InvalidString, OledError};
|
||||
use crate::error::OledError;
|
||||
|
||||
//define the Graphic struct for receiving draw commands
|
||||
// define the Graphic struct for receiving draw commands
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Graphic {
|
||||
bytes: Vec<u8>,
|
||||
@ -32,7 +32,7 @@ pub struct Graphic {
|
||||
y_coord: i32,
|
||||
}
|
||||
|
||||
//define the Msg struct for receiving write commands
|
||||
// define the Msg struct for receiving write commands
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Msg {
|
||||
x_coord: i32,
|
||||
@ -41,86 +41,61 @@ pub struct Msg {
|
||||
font_size: String,
|
||||
}
|
||||
|
||||
//definte the On struct for receiving power on/off commands
|
||||
// definte the On struct for receiving power on/off commands
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct On {
|
||||
on: bool,
|
||||
}
|
||||
|
||||
fn validate(m: &Msg) -> Result<(), OledError> {
|
||||
ensure!(
|
||||
m.string.len() <= 21,
|
||||
InvalidString {
|
||||
len: m.string.len()
|
||||
}
|
||||
);
|
||||
|
||||
ensure!(
|
||||
m.x_coord >= 0,
|
||||
InvalidCoordinate {
|
||||
fn validate(msg: &Msg) -> Result<(), OledError> {
|
||||
if msg.string.len() > 21 {
|
||||
Err(OledError::InvalidString {
|
||||
len: msg.string.len(),
|
||||
})
|
||||
} else if msg.x_coord < 0 || msg.x_coord > 128 {
|
||||
Err(OledError::InvalidCoordinate {
|
||||
coord: "x".to_string(),
|
||||
range: "0-128".to_string(),
|
||||
value: m.x_coord,
|
||||
}
|
||||
);
|
||||
|
||||
ensure!(
|
||||
m.x_coord < 129,
|
||||
InvalidCoordinate {
|
||||
coord: "x".to_string(),
|
||||
range: "0-128".to_string(),
|
||||
value: m.x_coord,
|
||||
}
|
||||
);
|
||||
|
||||
ensure!(
|
||||
m.y_coord >= 0,
|
||||
InvalidCoordinate {
|
||||
value: msg.x_coord,
|
||||
})
|
||||
} else if msg.y_coord < 0 || msg.y_coord > 147 {
|
||||
Err(OledError::InvalidCoordinate {
|
||||
coord: "y".to_string(),
|
||||
range: "0-47".to_string(),
|
||||
value: m.y_coord,
|
||||
}
|
||||
);
|
||||
|
||||
ensure!(
|
||||
m.y_coord < 148,
|
||||
InvalidCoordinate {
|
||||
coord: "y".to_string(),
|
||||
range: "0-47".to_string(),
|
||||
value: m.y_coord,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
value: msg.y_coord,
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), BoxError> {
|
||||
pub fn run() -> Result<(), OledError> {
|
||||
info!("Starting up.");
|
||||
|
||||
debug!("Creating interface for I2C device.");
|
||||
let i2c = I2cdev::new("/dev/i2c-1").context(I2CError)?;
|
||||
let i2c = I2cdev::new("/dev/i2c-1").map_err(|source| OledError::I2CError { source })?;
|
||||
|
||||
let mut disp: GraphicsMode<_> = Builder::new().connect_i2c(i2c).into();
|
||||
let mut display: GraphicsMode<_> = Builder::new().connect_i2c(i2c).into();
|
||||
|
||||
info!("Initializing the display.");
|
||||
disp.init().unwrap_or_else(|_| {
|
||||
display.init().unwrap_or_else(|_| {
|
||||
error!("Problem initializing the OLED display.");
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
debug!("Flushing the display.");
|
||||
disp.flush().unwrap_or_else(|_| {
|
||||
display.flush().unwrap_or_else(|_| {
|
||||
error!("Problem flushing the OLED display.");
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
let oled = Arc::new(Mutex::new(disp));
|
||||
let oled = Arc::new(Mutex::new(display));
|
||||
let oled_clone = Arc::clone(&oled);
|
||||
|
||||
info!("Creating JSON-RPC I/O handler.");
|
||||
let mut io = IoHandler::default();
|
||||
|
||||
io.add_method("clear", move |_| {
|
||||
io.add_sync_method("clear", move |_| {
|
||||
let mut oled = oled_clone.lock().unwrap();
|
||||
info!("Clearing the display.");
|
||||
oled.clear();
|
||||
@ -134,21 +109,20 @@ pub fn run() -> Result<(), BoxError> {
|
||||
|
||||
let oled_clone = Arc::clone(&oled);
|
||||
|
||||
io.add_method("draw", move |params: Params| {
|
||||
let g: Result<Graphic, Error> = params.parse();
|
||||
let g: Graphic = g?;
|
||||
io.add_sync_method("draw", move |params: Params| {
|
||||
let graphic: Graphic = params.parse()?;
|
||||
// TODO: add simple byte validation function
|
||||
let mut oled = oled_clone.lock().unwrap();
|
||||
info!("Drawing image to the display.");
|
||||
let im =
|
||||
Image1BPP::new(&g.bytes, g.width, g.height).translate(Coord::new(g.x_coord, g.y_coord));
|
||||
oled.draw(im.into_iter());
|
||||
let image = Image1BPP::new(&graphic.bytes, graphic.width, graphic.height)
|
||||
.translate(Coord::new(graphic.x_coord, graphic.y_coord));
|
||||
oled.draw(image.into_iter());
|
||||
Ok(Value::String("success".into()))
|
||||
});
|
||||
|
||||
let oled_clone = Arc::clone(&oled);
|
||||
|
||||
io.add_method("flush", move |_| {
|
||||
io.add_sync_method("flush", move |_| {
|
||||
let mut oled = oled_clone.lock().unwrap();
|
||||
info!("Flushing the display.");
|
||||
oled.flush().unwrap_or_else(|_| {
|
||||
@ -160,9 +134,9 @@ pub fn run() -> Result<(), BoxError> {
|
||||
|
||||
let oled_clone = Arc::clone(&oled);
|
||||
|
||||
io.add_method("ping", |_| Ok(Value::String("success".to_string())));
|
||||
io.add_sync_method("ping", |_| Ok(Value::String("success".to_string())));
|
||||
|
||||
io.add_method("power", move |params: Params| {
|
||||
io.add_sync_method("power", move |params: Params| {
|
||||
let o: Result<On, Error> = params.parse();
|
||||
let o: On = o?;
|
||||
let mut oled = oled_clone.lock().unwrap();
|
||||
@ -180,37 +154,36 @@ pub fn run() -> Result<(), BoxError> {
|
||||
|
||||
let oled_clone = Arc::clone(&oled);
|
||||
|
||||
io.add_method("write", move |params: Params| {
|
||||
io.add_sync_method("write", move |params: Params| {
|
||||
info!("Received a 'write' request.");
|
||||
let m: Result<Msg, Error> = params.parse();
|
||||
let m: Msg = m?;
|
||||
validate(&m)?;
|
||||
let msg = params.parse()?;
|
||||
validate(&msg)?;
|
||||
|
||||
let mut oled = oled_clone.lock().unwrap();
|
||||
|
||||
info!("Writing to the display.");
|
||||
if m.font_size == "6x8" {
|
||||
if msg.font_size == "6x8" {
|
||||
oled.draw(
|
||||
Font6x8::render_str(&m.string)
|
||||
.translate(Coord::new(m.x_coord, m.y_coord))
|
||||
Font6x8::render_str(&msg.string)
|
||||
.translate(Coord::new(msg.x_coord, msg.y_coord))
|
||||
.into_iter(),
|
||||
);
|
||||
} else if m.font_size == "6x12" {
|
||||
} else if msg.font_size == "6x12" {
|
||||
oled.draw(
|
||||
Font6x12::render_str(&m.string)
|
||||
.translate(Coord::new(m.x_coord, m.y_coord))
|
||||
Font6x12::render_str(&msg.string)
|
||||
.translate(Coord::new(msg.x_coord, msg.y_coord))
|
||||
.into_iter(),
|
||||
);
|
||||
} else if m.font_size == "8x16" {
|
||||
} else if msg.font_size == "8x16" {
|
||||
oled.draw(
|
||||
Font8x16::render_str(&m.string)
|
||||
.translate(Coord::new(m.x_coord, m.y_coord))
|
||||
Font8x16::render_str(&msg.string)
|
||||
.translate(Coord::new(msg.x_coord, msg.y_coord))
|
||||
.into_iter(),
|
||||
);
|
||||
} else if m.font_size == "12x16" {
|
||||
} else if msg.font_size == "12x16" {
|
||||
oled.draw(
|
||||
Font12x16::render_str(&m.string)
|
||||
.translate(Coord::new(m.x_coord, m.y_coord))
|
||||
Font12x16::render_str(&msg.string)
|
||||
.translate(Coord::new(msg.x_coord, msg.y_coord))
|
||||
.into_iter(),
|
||||
);
|
||||
}
|
||||
@ -255,7 +228,7 @@ mod tests {
|
||||
fn rpc_success() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_success_response", |_| {
|
||||
io.add_sync_method("rpc_success_response", |_| {
|
||||
Ok(Value::String("success".into()))
|
||||
});
|
||||
test_rpc::Rpc::from(io)
|
||||
@ -269,7 +242,7 @@ mod tests {
|
||||
fn rpc_internal_error() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_internal_error", |_| Err(Error::internal_error()));
|
||||
io.add_sync_method("rpc_internal_error", |_| Err(Error::internal_error()));
|
||||
test_rpc::Rpc::from(io)
|
||||
};
|
||||
|
||||
@ -287,7 +260,7 @@ mod tests {
|
||||
fn rpc_i2c_io_error() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_i2c_io_error", |_| {
|
||||
io.add_sync_method("rpc_i2c_io_error", |_| {
|
||||
let io_err = IoError::new(ErrorKind::PermissionDenied, "oh no!");
|
||||
let source = LinuxI2CError::Io(io_err);
|
||||
Err(Error::from(OledError::I2CError { source }))
|
||||
@ -310,7 +283,7 @@ mod tests {
|
||||
fn rpc_i2c_nix_error() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_i2c_nix_error", |_| {
|
||||
io.add_sync_method("rpc_i2c_nix_error", |_| {
|
||||
let nix_err = NixError::InvalidPath;
|
||||
let source = LinuxI2CError::Nix(nix_err);
|
||||
Err(Error::from(OledError::I2CError { source }))
|
||||
@ -326,14 +299,14 @@ mod tests {
|
||||
}"#
|
||||
);
|
||||
}
|
||||
*/
|
||||
*/
|
||||
|
||||
// test to ensure correct InvalidCoordinate error response
|
||||
#[test]
|
||||
fn rpc_invalid_coord() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_invalid_coord", |_| {
|
||||
io.add_sync_method("rpc_invalid_coord", |_| {
|
||||
Err(Error::from(OledError::InvalidCoordinate {
|
||||
coord: "x".to_string(),
|
||||
range: "0-128".to_string(),
|
||||
@ -357,7 +330,7 @@ mod tests {
|
||||
fn rpc_invalid_fontsize() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_invalid_fontsize", |_| {
|
||||
io.add_sync_method("rpc_invalid_fontsize", |_| {
|
||||
Err(Error::from(OledError::InvalidFontSize {
|
||||
font: "24x32".to_string(),
|
||||
}))
|
||||
@ -379,7 +352,7 @@ mod tests {
|
||||
fn rpc_invalid_string() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_invalid_string", |_| {
|
||||
io.add_sync_method("rpc_invalid_string", |_| {
|
||||
Err(Error::from(OledError::InvalidString { len: 22 }))
|
||||
});
|
||||
test_rpc::Rpc::from(io)
|
||||
@ -399,15 +372,15 @@ mod tests {
|
||||
fn rpc_invalid_params() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_invalid_params", |_| {
|
||||
let e = Error {
|
||||
io.add_sync_method("rpc_invalid_params", |_| {
|
||||
let source = Error {
|
||||
code: ErrorCode::InvalidParams,
|
||||
message: String::from("invalid params"),
|
||||
data: Some(Value::String(
|
||||
"Invalid params: invalid type: null, expected struct Msg.".into(),
|
||||
)),
|
||||
};
|
||||
Err(Error::from(OledError::MissingParameter { e }))
|
||||
Err(Error::from(OledError::MissingParameter { source }))
|
||||
});
|
||||
test_rpc::Rpc::from(io)
|
||||
};
|
||||
@ -427,13 +400,13 @@ mod tests {
|
||||
fn rpc_parse_error() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_parse_error", |_| {
|
||||
let e = Error {
|
||||
io.add_sync_method("rpc_parse_error", |_| {
|
||||
let source = Error {
|
||||
code: ErrorCode::ParseError,
|
||||
message: String::from("Parse error"),
|
||||
data: None,
|
||||
};
|
||||
Err(Error::from(OledError::ParseError { e }))
|
||||
Err(Error::from(OledError::ParseError { source }))
|
||||
});
|
||||
test_rpc::Rpc::from(io)
|
||||
};
|
||||
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,40 +1,31 @@
|
||||
[package]
|
||||
name = "peach-stats"
|
||||
version = "0.1.3"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
|
||||
version = "0.2.0"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
edition = "2018"
|
||||
description = "Query system statistics using JSON-RPC over HTTP. Provides a JSON-RPC wrapper around the probes and systemstat crates."
|
||||
description = "Query system statistics. Provides a wrapper around the probes and systemstat crates."
|
||||
keywords = ["peachcloud", "system stats", "system statistics", "disk", "memory"]
|
||||
homepage = "https://opencollective.com/peachcloud"
|
||||
repository = "https://github.com/peachcloud/peach-stats"
|
||||
repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-stats"
|
||||
readme = "README.md"
|
||||
license = "AGPL-3.0-only"
|
||||
license = "LGPL-3.0-only"
|
||||
publish = false
|
||||
|
||||
[package.metadata.deb]
|
||||
depends = "$auto"
|
||||
extended-description = """\
|
||||
peach-stats is a system statistics microservice module for PeachCloud. \
|
||||
Query system statistics using JSON-RPC over HTTP. Provides a JSON-RPC \
|
||||
wrapper around the probes and systemstat crates."""
|
||||
maintainer-scripts="debian"
|
||||
systemd-units = { unit-name = "peach-stats" }
|
||||
assets = [
|
||||
["target/release/peach-stats", "usr/bin/", "755"],
|
||||
["README.md", "usr/share/doc/peach-stats/README", "644"],
|
||||
]
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "peachcloud/peach-stats", branch = "master" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.9"
|
||||
jsonrpc-core = "18"
|
||||
jsonrpc-http-server = "18"
|
||||
log = "0.4"
|
||||
miniserde = "0.1.15"
|
||||
miniserde = { version = "0.1.15", optional = true }
|
||||
probes = "0.4.1"
|
||||
serde = { version = "1.0.130", features = ["derive"], optional = true }
|
||||
systemstat = "0.1.10"
|
||||
|
||||
[dev-dependencies]
|
||||
jsonrpc-test = "18"
|
||||
[features]
|
||||
default = []
|
||||
|
||||
# Provide `Serialize` and `Deserialize` traits for library structs using `miniserde`
|
||||
miniserde_support = ["miniserde"]
|
||||
|
||||
# Provide `Serialize` and `Deserialize` traits for library structs using `serde`
|
||||
serde_support = ["serde"]
|
||||
|
@ -1,109 +1,47 @@
|
||||
# peach-stats
|
||||
|
||||
[](https://travis-ci.com/peachcloud/peach-stats) 
|
||||

|
||||
|
||||
System statistics microservice module for PeachCloud. Provides a JSON-RPC wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates.
|
||||
System statistics library for PeachCloud. Provides a wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates.
|
||||
|
||||
### JSON-RPC API
|
||||
Currently offers the following statistics and associated data structures:
|
||||
|
||||
| Method | Description | Returns |
|
||||
| --- | --- | --- |
|
||||
| `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` |
|
||||
| `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` |
|
||||
| `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` |
|
||||
| `load_average` | Load average statistics | `one`, `five`, `fifteen` |
|
||||
| `mem_stats` | Memory statistics | `total`, `free`, `used` |
|
||||
| `ping` | Microservice status | `success` if running |
|
||||
| `uptime` | System uptime | `secs` |
|
||||
- CPU: `user`, `system`, `nice`, `idle` (as values or percentages)
|
||||
- Disk usage: `filesystem`, `one_k_blocks`, `one_k_blocks_used`,
|
||||
`one_k_blocks_free`, `used_percentage`, `mountpoint`
|
||||
- Load average: `one`, `five`, `fifteen`
|
||||
- Memory: `total`, `free`, `used`
|
||||
- Uptime: `seconds`
|
||||
|
||||
### Environment
|
||||
## Example Usage
|
||||
|
||||
The JSON-RPC HTTP server address and port can be configured with the `PEACH_STATS_SERVER` environment variable:
|
||||
```rust
|
||||
use peach_stats::{stats, StatsError};
|
||||
|
||||
`export PEACH_STATS_SERVER=127.0.0.1:5000`
|
||||
fn main() -> Result<(), StatsError> {
|
||||
let cpu = stats::cpu_stats()?;
|
||||
let cpu_percentages = stats::cpu_stats_percent()?;
|
||||
let disks = stats::disk_usage()?;
|
||||
let load = stats::load_average()?;
|
||||
let mem = stats::mem_stats()?;
|
||||
let uptime = stats::uptime()?;
|
||||
|
||||
When not set, the value defaults to `127.0.0.1:5113`.
|
||||
// do things with the retrieved values...
|
||||
|
||||
Logging is made available with `env_logger`:
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
`export RUST_LOG=info`
|
||||
## Feature Flags
|
||||
|
||||
Other logging levels include `debug`, `warn` and `error`.
|
||||
Feature flags are used to offer `Serialize` and `Deserialize` implementations for all `struct` data types provided by this library. These traits are not provided by default. A choice of `miniserde` and `serde` is provided.
|
||||
|
||||
### Setup
|
||||
Define the desired feature in the `Cargo.toml` manifest of your project:
|
||||
|
||||
Clone this repo:
|
||||
```toml
|
||||
peach-stats = { version = "0.1.0", features = ["miniserde_support"] }
|
||||
```
|
||||
|
||||
`git clone https://github.com/peachcloud/peach-stats.git`
|
||||
|
||||
Move into the repo and compile a release build:
|
||||
|
||||
`cd peach-stats`
|
||||
`cargo build --release`
|
||||
|
||||
Run the binary:
|
||||
|
||||
`./target/release/peach-stats`
|
||||
|
||||
### Debian Packaging
|
||||
|
||||
A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-stats` to be easily bundled as a Debian package (`.deb`). The `cargo-deb` [crate](https://crates.io/crates/cargo-deb) can be used to achieve this.
|
||||
|
||||
Install `cargo-deb`:
|
||||
|
||||
`cargo install cargo-deb`
|
||||
|
||||
Move into the repo:
|
||||
|
||||
`cd peach-stats`
|
||||
|
||||
Build the package:
|
||||
|
||||
`cargo deb`
|
||||
|
||||
The output will be written to `target/debian/peach-stats_0.1.0_arm64.deb` (or similar).
|
||||
|
||||
Build the package (aarch64):
|
||||
|
||||
`cargo deb --target aarch64-unknown-linux-gnu`
|
||||
|
||||
Install the package as follows:
|
||||
|
||||
`sudo dpkg -i target/debian/peach-stats_0.1.0_arm64.deb`
|
||||
|
||||
The service will be automatically enabled and started.
|
||||
|
||||
Uninstall the service:
|
||||
|
||||
`sudo apt-get remove peach-stats`
|
||||
|
||||
Remove configuration files (not removed with `apt-get remove`):
|
||||
|
||||
`sudo apt-get purge peach-stats`
|
||||
|
||||
### Example Usage
|
||||
|
||||
**Get CPU Statistics**
|
||||
|
||||
With microservice running, open a second terminal window and use `curl` to call server methods:
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "cpu_stats", "id":1 }' 127.0.0.1:5113`
|
||||
|
||||
Server responds with:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"{\"user\":4661083,\"system\":1240371,\"idle\":326838290,\"nice\":0}","id":1}`
|
||||
|
||||
**Get System Uptime**
|
||||
|
||||
With microservice running, open a second terminal window and use `curl` to call server methods:
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "uptime", "id":1 }' 127.0.0.1:5113`
|
||||
|
||||
Server responds with:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"{\"secs\":840968}","id":1}`
|
||||
|
||||
### Licensing
|
||||
|
||||
AGPL-3.0
|
||||
## License
|
||||
|
||||
LGPL-3.0.
|
||||
|
@ -1,27 +0,0 @@
|
||||
[Unit]
|
||||
Description=Query system statistics using JSON-RPC over HTTP.
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=peach-stats
|
||||
Environment="RUST_LOG=error"
|
||||
ExecStart=/usr/bin/peach-stats
|
||||
Restart=always
|
||||
CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYS_BOOT CAP_SYS_TIME CAP_KILL CAP_WAKE_ALARM CAP_LINUX_IMMUTABLE CAP_BLOCK_SUSPEND CAP_LEASE CAP_SYS_NICE CAP_SYS_RESOURCE CAP_RAWIO CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_* CAP_FOWNER CAP_IPC_OWNER CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_AUDIT_*
|
||||
InaccessibleDirectories=/home
|
||||
LockPersonality=yes
|
||||
NoNewPrivileges=yes
|
||||
PrivateDevices=yes
|
||||
PrivateTmp=yes
|
||||
PrivateUsers=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectHome=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectSystem=yes
|
||||
ReadOnlyDirectories=/var
|
||||
RestrictAddressFamilies=~AF_INET6 AF_UNIX
|
||||
SystemCallFilter=~@reboot @clock @debug @module @mount @swap @resources @privileged
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -1,69 +1,44 @@
|
||||
use std::{error, fmt, io};
|
||||
//! Custom error type for `peach-stats`.
|
||||
|
||||
use jsonrpc_core::{types::error::Error, ErrorCode};
|
||||
use probes::ProbeError;
|
||||
use std::{error, fmt, io::Error as IoError};
|
||||
|
||||
/// Custom error type encapsulating all possible errors when retrieving system
|
||||
/// statistics.
|
||||
#[derive(Debug)]
|
||||
pub enum StatError {
|
||||
CpuStat { source: ProbeError },
|
||||
DiskUsage { source: ProbeError },
|
||||
LoadAvg { source: ProbeError },
|
||||
MemStat { source: ProbeError },
|
||||
Uptime { source: io::Error },
|
||||
pub enum StatsError {
|
||||
/// Failed to retrieve CPU statistics.
|
||||
CpuStat(ProbeError),
|
||||
/// Failed to retrieve disk usage statistics.
|
||||
DiskUsage(ProbeError),
|
||||
/// Failed to retrieve load average statistics.
|
||||
LoadAvg(ProbeError),
|
||||
/// Failed to retrieve memory usage statistics.
|
||||
MemStat(ProbeError),
|
||||
/// Failed to retrieve system uptime.
|
||||
Uptime(IoError),
|
||||
}
|
||||
|
||||
impl error::Error for StatError {}
|
||||
impl error::Error for StatsError {}
|
||||
|
||||
impl fmt::Display for StatError {
|
||||
impl fmt::Display for StatsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
StatError::CpuStat { ref source } => {
|
||||
StatsError::CpuStat(ref source) => {
|
||||
write!(f, "Failed to retrieve CPU statistics: {}", source)
|
||||
}
|
||||
StatError::DiskUsage { ref source } => {
|
||||
StatsError::DiskUsage(ref source) => {
|
||||
write!(f, "Failed to retrieve disk usage statistics: {}", source)
|
||||
}
|
||||
StatError::LoadAvg { ref source } => {
|
||||
StatsError::LoadAvg(ref source) => {
|
||||
write!(f, "Failed to retrieve load average statistics: {}", source)
|
||||
}
|
||||
StatError::MemStat { ref source } => {
|
||||
StatsError::MemStat(ref source) => {
|
||||
write!(f, "Failed to retrieve memory statistics: {}", source)
|
||||
}
|
||||
StatError::Uptime { ref source } => {
|
||||
StatsError::Uptime(ref source) => {
|
||||
write!(f, "Failed to retrieve system uptime: {}", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StatError> for Error {
|
||||
fn from(err: StatError) -> Self {
|
||||
match &err {
|
||||
StatError::CpuStat { source } => Error {
|
||||
code: ErrorCode::ServerError(-32001),
|
||||
message: format!("Failed to retrieve CPU statistics: {}", source),
|
||||
data: None,
|
||||
},
|
||||
StatError::DiskUsage { source } => Error {
|
||||
code: ErrorCode::ServerError(-32001),
|
||||
message: format!("Failed to retrieve disk usage statistics: {}", source),
|
||||
data: None,
|
||||
},
|
||||
StatError::LoadAvg { source } => Error {
|
||||
code: ErrorCode::ServerError(-32001),
|
||||
message: format!("Failed to retrieve load average statistics: {}", source),
|
||||
data: None,
|
||||
},
|
||||
StatError::MemStat { source } => Error {
|
||||
code: ErrorCode::ServerError(-32001),
|
||||
message: format!("Failed to retrieve memory statistics: {}", source),
|
||||
data: None,
|
||||
},
|
||||
StatError::Uptime { source } => Error {
|
||||
code: ErrorCode::ServerError(-32001),
|
||||
message: format!("Failed to retrieve system uptime: {}", source),
|
||||
data: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,103 +1,48 @@
|
||||
mod error;
|
||||
mod stats;
|
||||
mod structs;
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use std::{env, result::Result};
|
||||
//! # peach-stats
|
||||
//!
|
||||
//! System statistics retrieval library; designed for use with the PeachCloud platform.
|
||||
//!
|
||||
//! Currently offers the following statistics and associated data structures:
|
||||
//!
|
||||
//! - CPU: `user`, `system`, `nice`, `idle` (as values or percentages)
|
||||
//! - Disk usage: `filesystem`, `one_k_blocks`, `one_k_blocks_used`,
|
||||
//! `one_k_blocks_free`, `used_percentage`, `mountpoint`
|
||||
//! - Load average: `one`, `five`, `fifteen`
|
||||
//! - Memory: `total`, `free`, `used`
|
||||
//! - Uptime: `seconds`
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! ```rust
|
||||
//! use peach_stats::{stats, StatsError};
|
||||
//!
|
||||
//! fn main() -> Result<(), StatsError> {
|
||||
//! let cpu = stats::cpu_stats()?;
|
||||
//! let cpu_percentages = stats::cpu_stats_percent()?;
|
||||
//! let disks = stats::disk_usage()?;
|
||||
//! let load = stats::load_average()?;
|
||||
//! let mem = stats::mem_stats()?;
|
||||
//! let uptime = stats::uptime()?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Feature Flags
|
||||
//!
|
||||
//! Feature flags are used to offer `Serialize` and `Deserialize` implementations
|
||||
//! for all `struct` data types provided by this library. These traits are not
|
||||
//! provided by default. A choice of `miniserde` and `serde` is provided.
|
||||
//!
|
||||
//! Define the desired feature in the `Cargo.toml` manifest of your project:
|
||||
//!
|
||||
//! ```toml
|
||||
//! peach-stats = { version = "0.1.0", features = ["miniserde_support"] }
|
||||
//! ```
|
||||
|
||||
use jsonrpc_core::{IoHandler, Value};
|
||||
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
|
||||
use log::info;
|
||||
pub mod error;
|
||||
pub mod stats;
|
||||
|
||||
use crate::error::StatError;
|
||||
|
||||
pub fn run() -> Result<(), StatError> {
|
||||
info!("Starting up.");
|
||||
|
||||
info!("Creating JSON-RPC I/O handler.");
|
||||
let mut io = IoHandler::default();
|
||||
|
||||
io.add_method("cpu_stats", move |_| async {
|
||||
info!("Fetching CPU statistics.");
|
||||
let stats = stats::cpu_stats()?;
|
||||
|
||||
Ok(Value::String(stats))
|
||||
});
|
||||
|
||||
io.add_method("cpu_stats_percent", move |_| async {
|
||||
info!("Fetching CPU statistics as percentages.");
|
||||
let stats = stats::cpu_stats_percent()?;
|
||||
|
||||
Ok(Value::String(stats))
|
||||
});
|
||||
|
||||
io.add_method("disk_usage", move |_| async {
|
||||
info!("Fetching disk usage statistics.");
|
||||
let disks = stats::disk_usage()?;
|
||||
|
||||
Ok(Value::String(disks))
|
||||
});
|
||||
|
||||
io.add_method("load_average", move |_| async {
|
||||
info!("Fetching system load average statistics.");
|
||||
let avg = stats::load_average()?;
|
||||
|
||||
Ok(Value::String(avg))
|
||||
});
|
||||
|
||||
io.add_method("mem_stats", move |_| async {
|
||||
info!("Fetching current memory statistics.");
|
||||
let mem = stats::mem_stats()?;
|
||||
|
||||
Ok(Value::String(mem))
|
||||
});
|
||||
|
||||
io.add_method("ping", |_| async {
|
||||
Ok(Value::String("success".to_string()))
|
||||
});
|
||||
|
||||
io.add_method("uptime", move |_| async {
|
||||
info!("Fetching system uptime.");
|
||||
let uptime = stats::uptime()?;
|
||||
|
||||
Ok(Value::String(uptime))
|
||||
});
|
||||
|
||||
let http_server = env::var("PEACH_OLED_STATS").unwrap_or_else(|_| "127.0.0.1:5113".to_string());
|
||||
|
||||
info!("Starting JSON-RPC server on {}.", http_server);
|
||||
let server = ServerBuilder::new(io)
|
||||
.cors(DomainsValidation::AllowOnly(vec![
|
||||
AccessControlAllowOrigin::Null,
|
||||
]))
|
||||
.start_http(
|
||||
&http_server
|
||||
.parse()
|
||||
.expect("Invalid HTTP address and port combination"),
|
||||
)
|
||||
.expect("Unable to start RPC server");
|
||||
|
||||
info!("Listening for requests.");
|
||||
server.wait();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use jsonrpc_test as test_rpc;
|
||||
|
||||
// test to ensure correct success response
|
||||
#[test]
|
||||
fn rpc_success() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_method("rpc_success_response", |_| async {
|
||||
Ok(Value::String("success".into()))
|
||||
});
|
||||
test_rpc::Rpc::from(io)
|
||||
};
|
||||
|
||||
assert_eq!(rpc.request("rpc_success_response", &()), r#""success""#);
|
||||
}
|
||||
}
|
||||
pub use crate::error::StatsError;
|
||||
|
@ -1,14 +0,0 @@
|
||||
use std::process;
|
||||
|
||||
use log::error;
|
||||
|
||||
fn main() {
|
||||
// initialize the logger
|
||||
env_logger::init();
|
||||
|
||||
// handle errors returned from `run`
|
||||
if let Err(e) = peach_stats::run() {
|
||||
error!("Application error: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
@ -1,14 +1,96 @@
|
||||
//! System statistics retrieval functions and associated data types.
|
||||
|
||||
use std::result::Result;
|
||||
|
||||
use miniserde::json;
|
||||
#[cfg(feature = "miniserde_support")]
|
||||
use miniserde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "serde_support")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use probes::{cpu, disk_usage, load, memory};
|
||||
use systemstat::{Platform, System};
|
||||
|
||||
use crate::error::StatError;
|
||||
use crate::structs::{CpuStat, CpuStatPercentages, DiskUsage, LoadAverage, MemStat};
|
||||
use crate::error::StatsError;
|
||||
|
||||
pub fn cpu_stats() -> Result<String, StatError> {
|
||||
let cpu_stats = cpu::proc::read().map_err(|source| StatError::CpuStat { source })?;
|
||||
/// CPU statistics.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct CpuStat {
|
||||
/// Time spent running user space (application) code.
|
||||
pub user: u64,
|
||||
/// Time spent running kernel code.
|
||||
pub system: u64,
|
||||
/// Time spent doing nothing.
|
||||
pub idle: u64,
|
||||
/// Time spent running user space processes which have been niced.
|
||||
pub nice: u64,
|
||||
}
|
||||
|
||||
/// CPU statistics as percentages.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct CpuStatPercentages {
|
||||
/// Time spent running user space (application) code.
|
||||
pub user: f32,
|
||||
/// Time spent running kernel code.
|
||||
pub system: f32,
|
||||
/// Time spent doing nothing.
|
||||
pub idle: f32,
|
||||
/// Time spent running user space processes which have been niced.
|
||||
pub nice: f32,
|
||||
}
|
||||
|
||||
/// Disk usage statistics.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct DiskUsage {
|
||||
/// Filesystem device path.
|
||||
pub filesystem: Option<String>,
|
||||
/// Total amount of disk space as a number of 1,000 kilobyte blocks.
|
||||
pub one_k_blocks: u64,
|
||||
/// Total amount of used disk space as a number of 1,000 kilobyte blocks.
|
||||
pub one_k_blocks_used: u64,
|
||||
/// Total amount of free / available disk space as a number of 1,000 kilobyte blocks.
|
||||
pub one_k_blocks_free: u64,
|
||||
/// Total amount of used disk space as a percentage.
|
||||
pub used_percentage: u32,
|
||||
/// Mountpoint of the disk / partition.
|
||||
pub mountpoint: String,
|
||||
}
|
||||
|
||||
/// Load average statistics.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct LoadAverage {
|
||||
/// Average computational work performed over the past minute.
|
||||
pub one: f32,
|
||||
/// Average computational work performed over the past five minutes.
|
||||
pub five: f32,
|
||||
/// Average computational work performed over the past fifteen minutes.
|
||||
pub fifteen: f32,
|
||||
}
|
||||
|
||||
/// Memory statistics.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct MemStat {
|
||||
/// Total amount of physical memory in kilobytes.
|
||||
pub total: u64,
|
||||
/// Total amount of free / available physical memory in kilobytes.
|
||||
pub free: u64,
|
||||
/// Total amount of used physical memory in kilobytes.
|
||||
pub used: u64,
|
||||
}
|
||||
|
||||
/// Retrieve the current CPU statistics.
|
||||
pub fn cpu_stats() -> Result<CpuStat, StatsError> {
|
||||
let cpu_stats = cpu::proc::read().map_err(StatsError::CpuStat)?;
|
||||
let s = cpu_stats.stat;
|
||||
let cpu = CpuStat {
|
||||
user: s.user,
|
||||
@ -16,13 +98,13 @@ pub fn cpu_stats() -> Result<String, StatError> {
|
||||
nice: s.nice,
|
||||
idle: s.idle,
|
||||
};
|
||||
let json_cpu = json::to_string(&cpu);
|
||||
|
||||
Ok(json_cpu)
|
||||
Ok(cpu)
|
||||
}
|
||||
|
||||
pub fn cpu_stats_percent() -> Result<String, StatError> {
|
||||
let cpu_stats = cpu::proc::read().map_err(|source| StatError::CpuStat { source })?;
|
||||
/// Retrieve the current CPU statistics as percentages.
|
||||
pub fn cpu_stats_percent() -> Result<CpuStatPercentages, StatsError> {
|
||||
let cpu_stats = cpu::proc::read().map_err(StatsError::CpuStat)?;
|
||||
let s = cpu_stats.stat.in_percentages();
|
||||
let cpu = CpuStatPercentages {
|
||||
user: s.user,
|
||||
@ -30,13 +112,13 @@ pub fn cpu_stats_percent() -> Result<String, StatError> {
|
||||
nice: s.nice,
|
||||
idle: s.idle,
|
||||
};
|
||||
let json_cpu = json::to_string(&cpu);
|
||||
|
||||
Ok(json_cpu)
|
||||
Ok(cpu)
|
||||
}
|
||||
|
||||
pub fn disk_usage() -> Result<String, StatError> {
|
||||
let disks = disk_usage::read().map_err(|source| StatError::DiskUsage { source })?;
|
||||
/// Retrieve the current disk usage statistics for each available disk / partition.
|
||||
pub fn disk_usage() -> Result<Vec<DiskUsage>, StatsError> {
|
||||
let disks = disk_usage::read().map_err(StatsError::DiskUsage)?;
|
||||
let mut disk_usages = Vec::new();
|
||||
for d in disks {
|
||||
let disk = DiskUsage {
|
||||
@ -49,42 +131,39 @@ pub fn disk_usage() -> Result<String, StatError> {
|
||||
};
|
||||
disk_usages.push(disk);
|
||||
}
|
||||
let json_disks = json::to_string(&disk_usages);
|
||||
|
||||
Ok(json_disks)
|
||||
Ok(disk_usages)
|
||||
}
|
||||
|
||||
pub fn load_average() -> Result<String, StatError> {
|
||||
let l = load::read().map_err(|source| StatError::LoadAvg { source })?;
|
||||
/// Retrieve the current load average statistics.
|
||||
pub fn load_average() -> Result<LoadAverage, StatsError> {
|
||||
let l = load::read().map_err(StatsError::LoadAvg)?;
|
||||
let load_avg = LoadAverage {
|
||||
one: l.one,
|
||||
five: l.five,
|
||||
fifteen: l.fifteen,
|
||||
};
|
||||
let json_load_avg = json::to_string(&load_avg);
|
||||
|
||||
Ok(json_load_avg)
|
||||
Ok(load_avg)
|
||||
}
|
||||
|
||||
pub fn mem_stats() -> Result<String, StatError> {
|
||||
let m = memory::read().map_err(|source| StatError::MemStat { source })?;
|
||||
/// Retrieve the current memory usage statistics.
|
||||
pub fn mem_stats() -> Result<MemStat, StatsError> {
|
||||
let m = memory::read().map_err(StatsError::MemStat)?;
|
||||
let mem = MemStat {
|
||||
total: m.total(),
|
||||
free: m.free(),
|
||||
used: m.used(),
|
||||
};
|
||||
let json_mem = json::to_string(&mem);
|
||||
|
||||
Ok(json_mem)
|
||||
Ok(mem)
|
||||
}
|
||||
|
||||
pub fn uptime() -> Result<String, StatError> {
|
||||
/// Retrieve the system uptime in seconds.
|
||||
pub fn uptime() -> Result<u64, StatsError> {
|
||||
let sys = System::new();
|
||||
let uptime = sys
|
||||
.uptime()
|
||||
.map_err(|source| StatError::Uptime { source })?;
|
||||
let uptime = sys.uptime().map_err(StatsError::Uptime)?;
|
||||
let uptime_secs = uptime.as_secs();
|
||||
let json_uptime = json::to_string(&uptime_secs);
|
||||
|
||||
Ok(json_uptime)
|
||||
Ok(uptime_secs)
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
use miniserde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CpuStat {
|
||||
pub user: u64,
|
||||
pub system: u64,
|
||||
pub idle: u64,
|
||||
pub nice: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CpuStatPercentages {
|
||||
pub user: f32,
|
||||
pub system: f32,
|
||||
pub idle: f32,
|
||||
pub nice: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DiskUsage {
|
||||
pub filesystem: Option<String>,
|
||||
pub one_k_blocks: u64,
|
||||
pub one_k_blocks_used: u64,
|
||||
pub one_k_blocks_free: u64,
|
||||
pub used_percentage: u32,
|
||||
pub mountpoint: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct LoadAverage {
|
||||
pub one: f32,
|
||||
pub five: f32,
|
||||
pub fifteen: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MemStat {
|
||||
pub total: u64,
|
||||
pub free: u64,
|
||||
pub used: u64,
|
||||
}
|
15
peach-web-lite/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "peach-web-lite"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.9.0"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.14"
|
||||
maud = "0.23.0"
|
||||
peach-lib = { path = "../peach-lib" }
|
||||
peach-network = { path = "../peach-network" }
|
||||
peach-stats = { path = "../peach-stats" }
|
||||
rouille = "3.5.0"
|
||||
golgi = { path = "/home/glyph/Projects/playground/rust/golgi" }
|
24
peach-web-lite/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# peach-web (lite)
|
||||
|
||||
A web interface for managing a Scuttlebutt pub.
|
||||
|
||||
## Application Structure
|
||||
|
||||
The application is divided between the route handlers (`src/routes`), context-builders (`src/context`), templates (`src/templates`) and authentication helper functions. The web server itself is initialised in `src/main.rs`. The context-builders are responsible for retrieving data which is required by the templates. This allows separation of data retrieval and data representation (the job of the templates).
|
||||
|
||||
## Built With
|
||||
|
||||
[Rouille](https://crates.io/crates/rouille): "a Rust web micro-framework".
|
||||
[maud](https://crates.io/crates/maud): "Compile-time HTML templates".
|
||||
[golgi](https://git.coopcloud.tech/golgi-ssb/golgi): "an experimental Scuttlebutt client library".
|
||||
[peach-lib](https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-lib).
|
||||
[peach-network](https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-network).
|
||||
[peach-stats](https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-stats)
|
||||
|
||||
## Design Goals
|
||||
|
||||
Be lean and fast.
|
||||
|
||||
## Licensing
|
||||
|
||||
AGPL-3.0
|
68
peach-web-lite/ap_card
Normal file
@ -0,0 +1,68 @@
|
||||
{%- if ap_state == "up" %}
|
||||
<!-- NETWORK CARD -->
|
||||
<div class="card center">
|
||||
<!-- NETWORK INFO BOX -->
|
||||
<div class="capsule capsule-container success-border">
|
||||
<!-- NETWORK STATUS GRID -->
|
||||
<div class="two-grid" title="PeachCloud network mode and status">
|
||||
<!-- top-right config icon -->
|
||||
<a class="link two-grid-top-right" href="/settings/network" title="Configure network settings">
|
||||
<img id="configureNetworking" class="icon-small" src="/icons/cog.svg" alt="Configure">
|
||||
</a>
|
||||
<!-- left column -->
|
||||
<!-- network mode icon with label -->
|
||||
<div class="grid-column-1">
|
||||
<img id="netModeIcon" class="center icon icon-active" src="/icons/router.svg" alt="WiFi router">
|
||||
<label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="Access Point Online">ONLINE</label>
|
||||
</div>
|
||||
<!-- right column -->
|
||||
<!-- network mode, ssid & ip with labels -->
|
||||
<div class="grid-column-2">
|
||||
<label class="label-small font-gray" for="netMode" title="Network Mode">MODE</label>
|
||||
<p id="netMode" class="card-text" title="Network Mode">Access Point</p>
|
||||
<label class="label-small font-gray" for="netSsid" title="Access Point SSID">SSID</label>
|
||||
<p id="netSsid" class="card-text" title="SSID">peach</p>
|
||||
<label class="label-small font-gray" for="netIp" title="Access Point IP Address">IP</label>
|
||||
<p id="netIp" class="card-text" title="IP">{{ ap_ip }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- horizontal dividing line -->
|
||||
<hr>
|
||||
<!-- DEVICES AND TRAFFIC GRID -->
|
||||
<div class="three-grid card-container">
|
||||
<div class="stack">
|
||||
<img id="devices" class="icon icon-medium" title="Connected devices" src="/icons/devices.svg" alt="Digital devices">
|
||||
<div class="flex-grid" style="padding-top: 0.5rem;">
|
||||
<label class="label-medium" for="devices" style="padding-right: 3px;" title="Number of connected devices"></label>
|
||||
</div>
|
||||
<label class="label-small font-gray">DEVICES</label>
|
||||
</div>
|
||||
<div class="stack">
|
||||
<img id="dataDownload" class="icon icon-medium" title="Download" src="/icons/down-arrow.svg" alt="Download">
|
||||
<div class="flex-grid" style="padding-top: 0.5rem;">
|
||||
{%- if ap_traffic -%}
|
||||
<label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in {{ ap_traffic.rx_unit }}">{{ ap_traffic.received }}</label>
|
||||
<label class="label-small font-near-black">{{ ap_traffic.rx_unit }}</label>
|
||||
{%- else -%}
|
||||
<label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total"></label>
|
||||
<label class="label-small font-near-black"></label>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<label class="label-small font-gray">DOWNLOAD</label>
|
||||
</div>
|
||||
<div class="stack">
|
||||
<img id="dataUpload" class="icon icon-medium" title="Upload" src="/icons/up-arrow.svg" alt="Upload">
|
||||
<div class="flex-grid" style="padding-top: 0.5rem;">
|
||||
{%- if ap_traffic -%}
|
||||
<label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in {{ ap_traffic.tx_unit }}">{{ ap_traffic.transmitted }}</label>
|
||||
<label class="label-small font-near-black">{{ ap_traffic.tx_unit }}</label>
|
||||
{%- else -%}
|
||||
<label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total"></label>
|
||||
<label class="label-small font-near-black"></label>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
<label class="label-small font-gray">UPLOAD</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
12
peach-web-lite/compilation_comparions
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
[ peach-web-lite ]
|
||||
|
||||
230 total dependencies
|
||||
|
||||
Finished release [optimized] target(s) in 35.12s
|
||||
|
||||
[ peach-web ]
|
||||
|
||||
522 total dependencies
|
||||
|
||||
Finished release [optimized] target(s) in 1m 33s
|
23
peach-web-lite/src/auth.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use log::debug;
|
||||
use peach_lib::password_utils;
|
||||
|
||||
use crate::error::PeachWebError;
|
||||
|
||||
/// Password save form request handler. This function is for use by a user who is already logged in to change their password.
|
||||
pub fn save_password_form(
|
||||
current_password: String,
|
||||
new_password1: String,
|
||||
new_password2: String,
|
||||
) -> Result<(), PeachWebError> {
|
||||
debug!("attempting to change password");
|
||||
|
||||
password_utils::verify_password(¤t_password)?;
|
||||
|
||||
// if the previous line did not throw an error, then the old password is correct
|
||||
password_utils::validate_new_passwords(&new_password1, &new_password2)?;
|
||||
|
||||
// if the previous line did not throw an error, then the new password is valid
|
||||
password_utils::set_new_password(&new_password1)?;
|
||||
|
||||
Ok(())
|
||||
}
|
8
peach-web-lite/src/context/admin.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use peach_lib::config_manager;
|
||||
|
||||
use crate::error::PeachWebError;
|
||||
|
||||
pub fn ssb_admin_ids() -> Result<Vec<String>, PeachWebError> {
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
Ok(peach_config.ssb_admin_ids)
|
||||
}
|
4
peach-web-lite/src/context/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod admin;
|
||||
pub mod network;
|
||||
pub mod status;
|
||||
pub mod test;
|
402
peach-web-lite/src/context/network.rs
Normal file
@ -0,0 +1,402 @@
|
||||
//! Data retrieval for the purpose of hydrating HTML templates.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use log::info;
|
||||
use peach_lib::{
|
||||
config_manager, dyndns_client,
|
||||
error::PeachError,
|
||||
jsonrpc_client_core::{Error, ErrorKind},
|
||||
jsonrpc_core::types::error::ErrorCode,
|
||||
};
|
||||
use peach_network::{
|
||||
network,
|
||||
network::{Scan, Status, Traffic},
|
||||
};
|
||||
|
||||
use crate::error::PeachWebError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AccessPoint {
|
||||
pub detail: Option<Scan>,
|
||||
pub signal: Option<i32>,
|
||||
pub state: String,
|
||||
}
|
||||
|
||||
pub fn ap_state() -> String {
|
||||
match network::state("ap0") {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_traffic(traffic: Traffic) -> Option<IfaceTraffic> {
|
||||
// modify traffic values & assign measurement unit
|
||||
// based on received and transmitted values
|
||||
let (rx, rx_unit) = if traffic.received > 1_047_527_424 {
|
||||
// convert to GB
|
||||
(traffic.received / 1_073_741_824, "GB".to_string())
|
||||
} else if traffic.received > 0 {
|
||||
// otherwise, convert it to MB
|
||||
((traffic.received / 1024) / 1024, "MB".to_string())
|
||||
} else {
|
||||
(0, "MB".to_string())
|
||||
};
|
||||
|
||||
let (tx, tx_unit) = if traffic.transmitted > 1_047_527_424 {
|
||||
// convert to GB
|
||||
(traffic.transmitted / 1_073_741_824, "GB".to_string())
|
||||
} else if traffic.transmitted > 0 {
|
||||
((traffic.transmitted / 1024) / 1024, "MB".to_string())
|
||||
} else {
|
||||
(0, "MB".to_string())
|
||||
};
|
||||
|
||||
Some(IfaceTraffic {
|
||||
rx,
|
||||
rx_unit,
|
||||
tx,
|
||||
tx_unit,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConfigureDNSContext {
|
||||
pub external_domain: String,
|
||||
pub dyndns_subdomain: String,
|
||||
pub enable_dyndns: bool,
|
||||
pub is_dyndns_online: bool,
|
||||
}
|
||||
|
||||
impl ConfigureDNSContext {
|
||||
pub fn build() -> ConfigureDNSContext {
|
||||
let peach_config = config_manager::load_peach_config().unwrap();
|
||||
let dyndns_fulldomain = peach_config.dyn_domain;
|
||||
let is_dyndns_online = dyndns_client::is_dns_updater_online().unwrap();
|
||||
let dyndns_subdomain =
|
||||
dyndns_client::get_dyndns_subdomain(&dyndns_fulldomain).unwrap_or(dyndns_fulldomain);
|
||||
ConfigureDNSContext {
|
||||
external_domain: peach_config.external_domain,
|
||||
dyndns_subdomain,
|
||||
enable_dyndns: peach_config.dyn_enabled,
|
||||
is_dyndns_online,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this should probably rather go into the appropriate `routes` file
|
||||
pub fn save_dns_configuration(
|
||||
external_domain: String,
|
||||
enable_dyndns: bool,
|
||||
dynamic_domain: String,
|
||||
) -> Result<(), PeachWebError> {
|
||||
// first save local configurations
|
||||
config_manager::set_external_domain(&external_domain)?;
|
||||
config_manager::set_dyndns_enabled_value(enable_dyndns)?;
|
||||
|
||||
// if dynamic dns is enabled and this is a new domain name, then register it
|
||||
if enable_dyndns {
|
||||
let full_dynamic_domain = dyndns_client::get_full_dynamic_domain(&dynamic_domain);
|
||||
// check if this is a new domain or if its already registered
|
||||
let is_new_domain = dyndns_client::check_is_new_dyndns_domain(&full_dynamic_domain);
|
||||
if is_new_domain {
|
||||
match dyndns_client::register_domain(&full_dynamic_domain) {
|
||||
Ok(_) => {
|
||||
info!("Registered new dyndns domain");
|
||||
// successful update
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
info!("Failed to register dyndns domain: {:?}", err);
|
||||
// json response for failed update
|
||||
let msg: String = match err {
|
||||
// TODO: make this a nest high-level match
|
||||
// PeachError::JsonRpcClientCore(Error(ErrorKind::JsonRpcError(err), _))
|
||||
PeachError::JsonRpcClientCore(source) => {
|
||||
match source {
|
||||
Error(ErrorKind::JsonRpcError(err), _state) => match err.code {
|
||||
ErrorCode::ServerError(-32030) => {
|
||||
format!("Error registering domain: {} was previously registered", full_dynamic_domain)
|
||||
}
|
||||
_ => {
|
||||
format!("Failed to register dyndns domain {:?}", err)
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
format!("Failed to register dyndns domain: {:?}", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => "Failed to register dyndns domain".to_string(),
|
||||
};
|
||||
Err(PeachWebError::FailedToRegisterDynDomain(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
// if the domain is already registered, then dont re-register, and just return success
|
||||
else {
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IfaceTraffic {
|
||||
pub rx: u64,
|
||||
pub rx_unit: String,
|
||||
pub tx: u64,
|
||||
pub tx_unit: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NetworkDetailContext {
|
||||
pub saved_aps: Vec<String>,
|
||||
pub wlan_ip: String,
|
||||
pub wlan_networks: HashMap<String, AccessPoint>,
|
||||
pub wlan_rssi: Option<String>,
|
||||
pub wlan_ssid: String,
|
||||
pub wlan_state: String,
|
||||
pub wlan_status: Option<Status>,
|
||||
pub wlan_traffic: Option<IfaceTraffic>,
|
||||
}
|
||||
|
||||
impl NetworkDetailContext {
|
||||
pub fn build() -> NetworkDetailContext {
|
||||
// TODO: read this value from the config file
|
||||
let wlan_iface = "wlan0".to_string();
|
||||
|
||||
let wlan_ip = match network::ip(&wlan_iface) {
|
||||
Ok(Some(ip)) => ip,
|
||||
_ => "x.x.x.x".to_string(),
|
||||
};
|
||||
|
||||
// list of networks saved in wpa_supplicant.conf
|
||||
let wlan_list = match network::saved_networks() {
|
||||
Ok(Some(ssids)) => ssids,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
// list of networks saved in wpa_supplicant.conf
|
||||
let saved_aps = wlan_list.clone();
|
||||
|
||||
let wlan_rssi = match network::rssi_percent(&wlan_iface) {
|
||||
Ok(rssi) => rssi,
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
// list of networks currently in range (online & accessible)
|
||||
let wlan_scan = match network::available_networks(&wlan_iface) {
|
||||
Ok(Some(networks)) => networks,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
let wlan_ssid = match network::ssid(&wlan_iface) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not connected".to_string(),
|
||||
};
|
||||
|
||||
let wlan_state = match network::state(&wlan_iface) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
|
||||
let wlan_status = match network::status(&wlan_iface) {
|
||||
Ok(status) => status,
|
||||
// interface unavailable
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let wlan_traffic = match network::traffic(&wlan_iface) {
|
||||
// convert bytes to mb or gb and add appropriate units
|
||||
Ok(Some(traffic)) => convert_traffic(traffic),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// create a hashmap to combine wlan_list & wlan_scan without repetition
|
||||
let mut wlan_networks = HashMap::new();
|
||||
|
||||
for ap in wlan_scan {
|
||||
let ssid = ap.ssid.clone();
|
||||
let rssi = ap.signal_level.clone();
|
||||
// parse the string to a signed integer (for math)
|
||||
let rssi_parsed = rssi.parse::<i32>().unwrap();
|
||||
// perform rssi (dBm) to quality (%) conversion
|
||||
let quality_percent = 2 * (rssi_parsed + 100);
|
||||
let ap_detail = AccessPoint {
|
||||
detail: Some(ap),
|
||||
state: "Available".to_string(),
|
||||
signal: Some(quality_percent),
|
||||
};
|
||||
wlan_networks.insert(ssid, ap_detail);
|
||||
}
|
||||
|
||||
for network in wlan_list {
|
||||
// avoid repetition by checking that ssid is not already in list
|
||||
if !wlan_networks.contains_key(&network) {
|
||||
let ssid = network.clone();
|
||||
let net_detail = AccessPoint {
|
||||
detail: None,
|
||||
state: "Not in range".to_string(),
|
||||
signal: None,
|
||||
};
|
||||
wlan_networks.insert(ssid, net_detail);
|
||||
}
|
||||
}
|
||||
|
||||
NetworkDetailContext {
|
||||
saved_aps,
|
||||
wlan_ip,
|
||||
wlan_networks,
|
||||
wlan_rssi,
|
||||
wlan_ssid,
|
||||
wlan_state,
|
||||
wlan_status,
|
||||
wlan_traffic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NetworkListContext {
|
||||
pub ap_state: String,
|
||||
pub wlan_networks: HashMap<String, String>,
|
||||
pub wlan_ssid: String,
|
||||
}
|
||||
|
||||
impl NetworkListContext {
|
||||
pub fn build() -> NetworkListContext {
|
||||
// TODO: read these values from the config file
|
||||
let ap_iface = "ap0".to_string();
|
||||
let wlan_iface = "wlan0".to_string();
|
||||
//let wlan_iface = "wlp0s20f0u2".to_string();
|
||||
|
||||
// list of networks saved in wpa_supplicant.conf
|
||||
let wlan_list = match network::saved_networks() {
|
||||
Ok(Some(ssids)) => ssids,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
// list of networks currently in range (online & accessible)
|
||||
let wlan_scan = match network::available_networks(&wlan_iface) {
|
||||
Ok(Some(networks)) => networks,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
let wlan_ssid = match network::ssid(&wlan_iface) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not connected".to_string(),
|
||||
};
|
||||
|
||||
// create a hashmap to combine wlan_list & wlan_scan without repetition
|
||||
let mut wlan_networks = HashMap::new();
|
||||
for ap in wlan_scan {
|
||||
wlan_networks.insert(ap.ssid, "Available".to_string());
|
||||
}
|
||||
for network in wlan_list {
|
||||
// insert ssid (with state) only if it doesn't already exist
|
||||
wlan_networks
|
||||
.entry(network)
|
||||
.or_insert_with(|| "Not in range".to_string());
|
||||
}
|
||||
|
||||
let ap_state = match network::state(&ap_iface) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
|
||||
NetworkListContext {
|
||||
ap_state,
|
||||
wlan_networks,
|
||||
wlan_ssid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NetworkStatusContext {
|
||||
pub ap_ip: String,
|
||||
pub ap_ssid: String,
|
||||
pub ap_state: String,
|
||||
pub ap_traffic: Option<IfaceTraffic>,
|
||||
pub wlan_ip: String,
|
||||
pub wlan_rssi: Option<String>,
|
||||
pub wlan_ssid: String,
|
||||
pub wlan_state: String,
|
||||
pub wlan_status: Option<Status>,
|
||||
pub wlan_traffic: Option<IfaceTraffic>,
|
||||
}
|
||||
|
||||
impl NetworkStatusContext {
|
||||
pub fn build() -> Self {
|
||||
// TODO: read these values from config file
|
||||
let ap_iface = "ap0".to_string();
|
||||
let wlan_iface = "wlan0".to_string();
|
||||
|
||||
let ap_ip = match network::ip(&ap_iface) {
|
||||
Ok(Some(ip)) => ip,
|
||||
_ => "x.x.x.x".to_string(),
|
||||
};
|
||||
|
||||
let ap_ssid = match network::ssid(&ap_iface) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not currently activated".to_string(),
|
||||
};
|
||||
|
||||
let ap_state = match network::state(&ap_iface) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
|
||||
let ap_traffic = match network::traffic(&ap_iface) {
|
||||
// convert bytes to mb or gb and add appropriate units
|
||||
Ok(Some(traffic)) => convert_traffic(traffic),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let wlan_ip = match network::ip(&wlan_iface) {
|
||||
Ok(Some(ip)) => ip,
|
||||
_ => "x.x.x.x".to_string(),
|
||||
};
|
||||
|
||||
let wlan_rssi = match network::rssi_percent(&wlan_iface) {
|
||||
Ok(rssi) => rssi,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let wlan_ssid = match network::ssid(&wlan_iface) {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not connected".to_string(),
|
||||
};
|
||||
|
||||
let wlan_state = match network::state(&wlan_iface) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
|
||||
let wlan_status = match network::status(&wlan_iface) {
|
||||
Ok(status) => status,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let wlan_traffic = match network::traffic(&wlan_iface) {
|
||||
// convert bytes to mb or gb and add appropriate units
|
||||
Ok(Some(traffic)) => convert_traffic(traffic),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
NetworkStatusContext {
|
||||
ap_ip,
|
||||
ap_ssid,
|
||||
ap_state,
|
||||
ap_traffic,
|
||||
wlan_ip,
|
||||
wlan_rssi,
|
||||
wlan_ssid,
|
||||
wlan_state,
|
||||
wlan_status,
|
||||
wlan_traffic,
|
||||
}
|
||||
}
|
||||
}
|
76
peach-web-lite/src/context/status.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use peach_stats::{stats, stats::LoadAverage};
|
||||
|
||||
/// System statistics data.
|
||||
pub struct StatusContext {
|
||||
pub cpu_usage_percent: Option<f32>,
|
||||
pub disk_usage_percent: Option<u32>,
|
||||
pub disk_free: Option<u64>,
|
||||
pub load_average: Option<LoadAverage>,
|
||||
pub mem_usage_percent: Option<u64>,
|
||||
pub mem_used: Option<u64>,
|
||||
pub mem_free: Option<u64>,
|
||||
pub mem_total: Option<u64>,
|
||||
pub uptime: Option<u32>,
|
||||
}
|
||||
|
||||
impl StatusContext {
|
||||
pub fn build() -> StatusContext {
|
||||
// convert result to Option<CpuStatPercentages>, discard any error
|
||||
let cpu_usage_percent = stats::cpu_stats_percent()
|
||||
.ok()
|
||||
.map(|cpu| (cpu.nice + cpu.system + cpu.user).round());
|
||||
|
||||
let load_average = stats::load_average().ok();
|
||||
|
||||
let mem_stats = stats::mem_stats().ok();
|
||||
let (mem_usage_percent, mem_used, mem_free, mem_total) = match mem_stats {
|
||||
Some(mem) => (
|
||||
Some((mem.used / mem.total) * 100),
|
||||
Some(mem.used / 1024),
|
||||
Some(mem.free / 1024),
|
||||
Some(mem.total / 1024),
|
||||
),
|
||||
None => (None, None, None, None),
|
||||
};
|
||||
|
||||
let uptime = match stats::uptime() {
|
||||
Ok(secs) => {
|
||||
let uptime_mins = secs / 60;
|
||||
uptime_mins.to_string()
|
||||
}
|
||||
Err(_) => "Unavailable".to_string(),
|
||||
};
|
||||
|
||||
// parse the uptime string to a signed integer (for math)
|
||||
let uptime_parsed = uptime.parse::<u32>().ok();
|
||||
|
||||
let disk_usage_stats = match stats::disk_usage() {
|
||||
Ok(disks) => disks,
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
|
||||
// select only the partition we're interested in: "/"
|
||||
let disk_stats = disk_usage_stats.iter().find(|disk| disk.mountpoint == "/");
|
||||
|
||||
let (disk_usage_percent, disk_free) = match disk_stats {
|
||||
Some(disk) => (
|
||||
Some(disk.used_percentage),
|
||||
// calculate free disk space in megabytes
|
||||
Some(disk.one_k_blocks_free / 1024),
|
||||
),
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
StatusContext {
|
||||
cpu_usage_percent,
|
||||
disk_usage_percent,
|
||||
disk_free,
|
||||
load_average,
|
||||
mem_usage_percent,
|
||||
mem_used,
|
||||
mem_free,
|
||||
mem_total,
|
||||
uptime: uptime_parsed,
|
||||
}
|
||||
}
|
||||
}
|
12
peach-web-lite/src/context/test.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use golgi;
|
||||
|
||||
use golgi::sbot::Sbot;
|
||||
|
||||
pub async fn test_async() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut sbot_client = Sbot::init(Some("127.0.0.1:8009".to_string()), None).await?;
|
||||
|
||||
let id = sbot_client.whoami().await?;
|
||||
println!("whoami: {}", id);
|
||||
|
||||
Ok(())
|
||||
}
|
60
peach-web-lite/src/error.rs
Normal file
@ -0,0 +1,60 @@
|
||||
//! Custom error type representing all possible error variants for peach-web.
|
||||
|
||||
use peach_lib::error::PeachError;
|
||||
use peach_lib::{serde_json, serde_yaml};
|
||||
use serde_json::error::Error as JsonError;
|
||||
use serde_yaml::Error as YamlError;
|
||||
|
||||
/// Custom error type encapsulating all possible errors for the web application.
|
||||
#[derive(Debug)]
|
||||
pub enum PeachWebError {
|
||||
Json(JsonError),
|
||||
Yaml(YamlError),
|
||||
FailedToRegisterDynDomain(String),
|
||||
PeachLib { source: PeachError, msg: String },
|
||||
}
|
||||
|
||||
impl std::error::Error for PeachWebError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match *self {
|
||||
PeachWebError::Json(ref source) => Some(source),
|
||||
PeachWebError::Yaml(ref source) => Some(source),
|
||||
PeachWebError::FailedToRegisterDynDomain(_) => None,
|
||||
PeachWebError::PeachLib { ref source, .. } => Some(source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PeachWebError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match *self {
|
||||
PeachWebError::Json(ref source) => write!(f, "Serde JSON error: {}", source),
|
||||
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
|
||||
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
|
||||
write!(f, "DYN DNS error: {}", msg)
|
||||
}
|
||||
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonError> for PeachWebError {
|
||||
fn from(err: JsonError) -> PeachWebError {
|
||||
PeachWebError::Json(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<YamlError> for PeachWebError {
|
||||
fn from(err: YamlError) -> PeachWebError {
|
||||
PeachWebError::Yaml(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PeachError> for PeachWebError {
|
||||
fn from(err: PeachError) -> PeachWebError {
|
||||
PeachWebError::PeachLib {
|
||||
source: err,
|
||||
msg: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
42
peach-web-lite/src/main.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use std::env;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use log::info;
|
||||
|
||||
mod auth;
|
||||
mod context;
|
||||
mod error;
|
||||
mod router;
|
||||
mod routes;
|
||||
mod templates;
|
||||
|
||||
lazy_static! {
|
||||
// determine run-mode from env var; default to standalone mode (aka peachpub)
|
||||
static ref STANDALONE_MODE: bool = match env::var("PEACH_STANDALONE_MODE") {
|
||||
// parse the value to a boolean; default to true for any error
|
||||
Ok(val) => val.parse().unwrap_or(true),
|
||||
Err(_) => true
|
||||
};
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
rouille::start_server("localhost:8000", move |request| {
|
||||
info!("Now listening on localhost:8000");
|
||||
|
||||
// static file server: matches on assets in the `static` directory
|
||||
if let Some(request) = request.remove_prefix("/static") {
|
||||
return rouille::match_assets(&request, "static");
|
||||
}
|
||||
|
||||
// configure the router based on run-mode
|
||||
if *STANDALONE_MODE {
|
||||
info!("Running in standalone mode");
|
||||
router::minimal_router(request)
|
||||
} else {
|
||||
info!("Running in fully-featured mode");
|
||||
router::complete_router(request)
|
||||
}
|
||||
});
|
||||
}
|
194
peach-web-lite/src/router.rs
Normal file
@ -0,0 +1,194 @@
|
||||
use rouille::{router, Request, Response};
|
||||
|
||||
use crate::routes;
|
||||
|
||||
/// Define router for standalone mode (PeachPub).
|
||||
pub fn minimal_router(request: &Request) -> Response {
|
||||
router!(request,
|
||||
(GET) (/) => {
|
||||
routes::home::menu()
|
||||
},
|
||||
(GET) (/help) => {
|
||||
routes::help::menu()
|
||||
},
|
||||
(GET) (/login) => {
|
||||
routes::login::login()
|
||||
},
|
||||
(POST) (/login) => {
|
||||
routes::login::login_post(request)
|
||||
},
|
||||
(GET) (/scuttlebutt/blocks) => {
|
||||
routes::scuttlebutt::blocks()
|
||||
},
|
||||
(GET) (/scuttlebutt/follows) => {
|
||||
routes::scuttlebutt::follows()
|
||||
},
|
||||
(GET) (/scuttlebutt/followers) => {
|
||||
routes::scuttlebutt::followers()
|
||||
},
|
||||
(GET) (/scuttlebutt/friends) => {
|
||||
routes::scuttlebutt::friends()
|
||||
},
|
||||
(GET) (/scuttlebutt/peers) => {
|
||||
routes::scuttlebutt::peers()
|
||||
},
|
||||
(GET) (/scuttlebutt/private) => {
|
||||
routes::scuttlebutt::private()
|
||||
},
|
||||
(GET) (/scuttlebutt/profile) => {
|
||||
routes::scuttlebutt::profile()
|
||||
},
|
||||
(GET) (/settings) => {
|
||||
routes::settings::menu()
|
||||
},
|
||||
(GET) (/settings/admin) => {
|
||||
routes::settings::admin()
|
||||
},
|
||||
(GET) (/settings/admin/configure) => {
|
||||
routes::settings::admin_configure()
|
||||
},
|
||||
(GET) (/settings/admin/add) => {
|
||||
routes::settings::admin_add()
|
||||
},
|
||||
(POST) (/settings/admin/add) => {
|
||||
routes::settings::admin_add_post(request)
|
||||
},
|
||||
(GET) (/settings/admin/change_password) => {
|
||||
routes::settings::admin_change_password()
|
||||
},
|
||||
(POST) (/settings/admin/change_password) => {
|
||||
routes::settings::admin_change_password_post(request)
|
||||
},
|
||||
(POST) (/settings/admin/delete) => {
|
||||
routes::settings::admin_delete_post(request)
|
||||
},
|
||||
(GET) (/settings/scuttlebutt) => {
|
||||
routes::settings::scuttlebutt()
|
||||
},
|
||||
(GET) (/status) => {
|
||||
routes::status::status()
|
||||
},
|
||||
// return 404 if not match is found
|
||||
_ => routes::catchers::not_found()
|
||||
)
|
||||
}
|
||||
|
||||
/// Define router for fully-featured mode (PeachCloud).
|
||||
pub fn complete_router(request: &Request) -> Response {
|
||||
router!(request,
|
||||
(GET) (/async) => {
|
||||
routes::home::async_test()
|
||||
},
|
||||
(GET) (/) => {
|
||||
routes::home::menu()
|
||||
},
|
||||
(GET) (/help) => {
|
||||
routes::help::menu()
|
||||
},
|
||||
(GET) (/login) => {
|
||||
routes::login::login()
|
||||
},
|
||||
(POST) (/login) => {
|
||||
routes::login::login_post(request)
|
||||
},
|
||||
(GET) (/scuttlebutt/blocks) => {
|
||||
routes::scuttlebutt::blocks()
|
||||
},
|
||||
(GET) (/scuttlebutt/follows) => {
|
||||
routes::scuttlebutt::follows()
|
||||
},
|
||||
(GET) (/scuttlebutt/followers) => {
|
||||
routes::scuttlebutt::followers()
|
||||
},
|
||||
(GET) (/scuttlebutt/friends) => {
|
||||
routes::scuttlebutt::friends()
|
||||
},
|
||||
(GET) (/scuttlebutt/peers) => {
|
||||
routes::scuttlebutt::peers()
|
||||
},
|
||||
(GET) (/scuttlebutt/private) => {
|
||||
routes::scuttlebutt::private()
|
||||
},
|
||||
(GET) (/scuttlebutt/profile) => {
|
||||
routes::scuttlebutt::profile()
|
||||
},
|
||||
(GET) (/settings) => {
|
||||
routes::settings::menu()
|
||||
},
|
||||
(GET) (/settings/admin) => {
|
||||
routes::settings::admin()
|
||||
},
|
||||
(GET) (/settings/admin/configure) => {
|
||||
routes::settings::admin_configure()
|
||||
},
|
||||
(GET) (/settings/admin/add) => {
|
||||
routes::settings::admin_add()
|
||||
},
|
||||
(POST) (/settings/admin/add) => {
|
||||
routes::settings::admin_add_post(request)
|
||||
},
|
||||
(GET) (/settings/admin/change_password) => {
|
||||
routes::settings::admin_change_password()
|
||||
},
|
||||
(POST) (/settings/admin/change_password) => {
|
||||
routes::settings::admin_change_password_post(request)
|
||||
},
|
||||
(POST) (/settings/admin/delete) => {
|
||||
routes::settings::admin_delete_post(request)
|
||||
},
|
||||
(GET) (/settings/network) => {
|
||||
routes::settings::network()
|
||||
},
|
||||
(GET) (/settings/network/wifi) => {
|
||||
routes::settings::network_list_aps()
|
||||
},
|
||||
(POST) (/settings/network/wifi/connect) => {
|
||||
routes::settings::network_connect_wifi(request)
|
||||
},
|
||||
(POST) (/settings/network/wifi/disconnect) => {
|
||||
routes::settings::network_disconnect_wifi(request)
|
||||
},
|
||||
(POST) (/settings/network/wifi/forget) => {
|
||||
routes::settings::network_forget_wifi(request)
|
||||
},
|
||||
(GET) (/settings/network/wifi/ssid/{ssid: String}) => {
|
||||
routes::settings::network_detail(ssid)
|
||||
},
|
||||
(GET) (/settings/network/wifi/add) => {
|
||||
routes::settings::network_add_ap(None)
|
||||
},
|
||||
(GET) (/settings/network/wifi/add/{ssid: String}) => {
|
||||
routes::settings::network_add_ap(Some(ssid))
|
||||
},
|
||||
(POST) (/settings/network/wifi/add) => {
|
||||
routes::settings::network_add_ap_post(request)
|
||||
},
|
||||
(GET) (/settings/network/wifi/modify) => {
|
||||
routes::settings::network_modify_ap(None)
|
||||
},
|
||||
// TODO: see if we can use the ?= syntax for ssid param
|
||||
(GET) (/settings/network/wifi/modify/{ssid: String}) => {
|
||||
routes::settings::network_modify_ap(Some(ssid))
|
||||
},
|
||||
(POST) (/settings/network/wifi/modify) => {
|
||||
routes::settings::network_modify_ap_post(request)
|
||||
},
|
||||
(GET) (/settings/network/dns) => {
|
||||
routes::settings::network_configure_dns()
|
||||
},
|
||||
(POST) (/settings/network/dns) => {
|
||||
routes::settings::network_configure_dns_post(request)
|
||||
},
|
||||
(GET) (/settings/scuttlebutt) => {
|
||||
routes::settings::scuttlebutt()
|
||||
},
|
||||
(GET) (/status) => {
|
||||
routes::status::status()
|
||||
},
|
||||
(GET) (/status/network) => {
|
||||
routes::status::network()
|
||||
},
|
||||
// return 404 if not match is found
|
||||
_ => routes::catchers::not_found()
|
||||
)
|
||||
}
|
10
peach-web-lite/src/routes/catchers.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use log::debug;
|
||||
use rouille::Response;
|
||||
|
||||
use crate::templates;
|
||||
|
||||
pub fn not_found() -> Response {
|
||||
debug!("received GET request for a route which is not defined");
|
||||
|
||||
Response::html(templates::catchers::not_found()).with_status_code(404)
|
||||
}
|
10
peach-web-lite/src/routes/help.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use log::debug;
|
||||
use rouille::Response;
|
||||
|
||||
use crate::templates;
|
||||
|
||||
pub fn menu() -> Response {
|
||||
debug!("received GET request for: /help");
|
||||
|
||||
Response::html(templates::help::menu())
|
||||
}
|
14
peach-web-lite/src/routes/home.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use log::debug;
|
||||
use rouille::Response;
|
||||
|
||||
use crate::templates;
|
||||
|
||||
pub fn menu() -> Response {
|
||||
debug!("received GET request for: /");
|
||||
|
||||
Response::html(templates::home::menu())
|
||||
}
|
||||
|
||||
pub fn async_test() -> Response {
|
||||
Response::html(templates::home::async_test())
|
||||
}
|
26
peach-web-lite/src/routes/login.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use log::debug;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::templates;
|
||||
|
||||
pub fn login() -> Response {
|
||||
debug!("received GET request for: /login");
|
||||
|
||||
Response::html(templates::login::login())
|
||||
}
|
||||
|
||||
pub fn login_post(request: &Request) -> Response {
|
||||
debug!("received POST request for: /login");
|
||||
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
username: String,
|
||||
password: String,
|
||||
}));
|
||||
|
||||
// TODO: handle authentication...
|
||||
|
||||
debug!("{:?}", data);
|
||||
|
||||
// TODO: add flash message
|
||||
Response::redirect_302("/")
|
||||
}
|
7
peach-web-lite/src/routes/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub mod catchers;
|
||||
pub mod help;
|
||||
pub mod home;
|
||||
pub mod login;
|
||||
pub mod scuttlebutt;
|
||||
pub mod settings;
|
||||
pub mod status;
|
46
peach-web-lite/src/routes/scuttlebutt.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use log::debug;
|
||||
use rouille::Response;
|
||||
|
||||
use crate::templates;
|
||||
|
||||
pub fn blocks() -> Response {
|
||||
debug!("received GET request for: /scuttlebutt/blocks");
|
||||
|
||||
Response::html(templates::scuttlebutt::peers_list("Blocks".to_string()))
|
||||
}
|
||||
|
||||
pub fn follows() -> Response {
|
||||
debug!("received GET request for: /scuttlebutt/follows");
|
||||
|
||||
Response::html(templates::scuttlebutt::peers_list("Follows".to_string()))
|
||||
}
|
||||
|
||||
pub fn followers() -> Response {
|
||||
debug!("received GET request for: /scuttlebutt/followers");
|
||||
|
||||
Response::html(templates::scuttlebutt::peers_list("Followers".to_string()))
|
||||
}
|
||||
|
||||
pub fn friends() -> Response {
|
||||
debug!("received GET request for: /scuttlebutt/friends");
|
||||
|
||||
Response::html(templates::scuttlebutt::peers_list("Friends".to_string()))
|
||||
}
|
||||
|
||||
pub fn peers() -> Response {
|
||||
debug!("received GET request for: /scuttlebutt/peers");
|
||||
|
||||
Response::html(templates::scuttlebutt::peers())
|
||||
}
|
||||
|
||||
pub fn private() -> Response {
|
||||
debug!("received GET request for: /scuttlebutt/private");
|
||||
|
||||
Response::html(templates::scuttlebutt::private())
|
||||
}
|
||||
|
||||
pub fn profile() -> Response {
|
||||
debug!("received GET request for: /scuttlebutt/profile");
|
||||
|
||||
Response::html(templates::scuttlebutt::profile(None, None))
|
||||
}
|
340
peach-web-lite/src/routes/settings.rs
Normal file
@ -0,0 +1,340 @@
|
||||
use log::{debug, warn};
|
||||
use peach_lib::config_manager;
|
||||
use peach_network::network;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::auth;
|
||||
use crate::context;
|
||||
use crate::templates;
|
||||
|
||||
pub fn menu() -> Response {
|
||||
debug!("received GET request for: /settings");
|
||||
|
||||
Response::html(templates::settings::menu::menu())
|
||||
}
|
||||
|
||||
pub fn admin() -> Response {
|
||||
debug!("received GET request for: /settings/admin");
|
||||
|
||||
Response::html(templates::settings::admin::menu())
|
||||
}
|
||||
|
||||
pub fn admin_configure() -> Response {
|
||||
debug!("received GET request for: /settings/admin/configure");
|
||||
|
||||
Response::html(templates::settings::admin::configure(None, None))
|
||||
}
|
||||
|
||||
pub fn admin_add() -> Response {
|
||||
debug!("received GET request for: /settings/admin/add");
|
||||
|
||||
Response::html(templates::settings::admin::add(None, None))
|
||||
}
|
||||
|
||||
pub fn admin_add_post(request: &Request) -> Response {
|
||||
debug!("received POST request for: /settings/admin/add");
|
||||
|
||||
let data = try_or_400!(post_input!(request, { ssb_id: String }));
|
||||
|
||||
debug!("{:?}", data);
|
||||
|
||||
let (flash_name, flash_msg) = match config_manager::add_ssb_admin_id(&data.ssb_id) {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"Added new SSB administrator ID".to_string(),
|
||||
),
|
||||
Err(e) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to add new SSB administrator ID: {}", e),
|
||||
),
|
||||
};
|
||||
|
||||
Response::html(templates::settings::admin::add(
|
||||
Some(flash_msg),
|
||||
Some(flash_name),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn admin_change_password() -> Response {
|
||||
debug!("received GET request for: /settings/admin/change_password");
|
||||
|
||||
Response::html(templates::settings::admin::change_password(None, None))
|
||||
}
|
||||
|
||||
pub fn admin_change_password_post(request: &Request) -> Response {
|
||||
debug!("received POST request for: /settings/admin/change_password");
|
||||
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
current_password: String,
|
||||
new_password1: String,
|
||||
new_password2: String
|
||||
}));
|
||||
|
||||
// attempt to update the password
|
||||
let (flash_name, flash_msg) = match auth::save_password_form(
|
||||
data.current_password,
|
||||
data.new_password1,
|
||||
data.new_password2,
|
||||
) {
|
||||
Ok(_) => ("success".to_string(), "Saved new password".to_string()),
|
||||
Err(e) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to save new password: {}", e),
|
||||
),
|
||||
};
|
||||
|
||||
Response::html(templates::settings::admin::change_password(
|
||||
Some(flash_msg),
|
||||
Some(flash_name),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn admin_delete_post(request: &Request) -> Response {
|
||||
debug!("received POST request for: /settings/admin/delete");
|
||||
|
||||
let data = try_or_400!(post_input!(request, { ssb_id: String }));
|
||||
|
||||
let (flash_name, flash_msg) = match config_manager::delete_ssb_admin_id(&data.ssb_id) {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"Removed SSB administrator ID".to_string(),
|
||||
),
|
||||
Err(e) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to remove SSB administrator ID: {}", e),
|
||||
),
|
||||
};
|
||||
|
||||
Response::html(templates::settings::admin::configure(
|
||||
Some(flash_msg),
|
||||
Some(flash_name),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn network() -> Response {
|
||||
debug!("received GET request for: /settings/network");
|
||||
|
||||
Response::html(templates::settings::network::menu())
|
||||
}
|
||||
|
||||
pub fn network_add_ap(ssid: Option<String>) -> Response {
|
||||
debug!("received GET request for: /settings/network/wifi/add");
|
||||
|
||||
Response::html(templates::settings::network::add_ap(ssid, None, None))
|
||||
}
|
||||
|
||||
pub fn network_add_ap_post(request: &Request) -> Response {
|
||||
debug!("received POST request for: /settings/network/wifi/add");
|
||||
|
||||
// TODO: read this value from the config file instead
|
||||
let wlan_iface = "wlan0".to_string();
|
||||
|
||||
let data = try_or_400!(post_input!(request, { ssid: String, pass: String }));
|
||||
|
||||
/* ADD WIFI CREDENTIALS FOR AP */
|
||||
|
||||
// check if the credentials already exist for this access point
|
||||
let creds_exist = match network::saved_networks() {
|
||||
Ok(Some(ssids)) => ssids.contains(&data.ssid),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let (flash_name, flash_msg) = if creds_exist {
|
||||
(
|
||||
"error".to_string(),
|
||||
"Network credentials already exist for this access point".to_string(),
|
||||
)
|
||||
} else {
|
||||
// if credentials not found, generate and write wifi config to wpa_supplicant
|
||||
match network::add(&wlan_iface, &data.ssid, &data.pass) {
|
||||
Ok(_) => {
|
||||
debug!("Added WiFi credentials.");
|
||||
// force reread of wpa_supplicant.conf file with new credentials
|
||||
match network::reconfigure() {
|
||||
Ok(_) => debug!("Successfully reconfigured wpa_supplicant"),
|
||||
Err(_) => warn!("Failed to reconfigure wpa_supplicant"),
|
||||
}
|
||||
("success".to_string(), "Added WiFi credentials".to_string())
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to add WiFi credentials.");
|
||||
("error".to_string(), format!("{}", e))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Response::html(templates::settings::network::add_ap(
|
||||
None,
|
||||
Some(flash_msg),
|
||||
Some(flash_name),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn network_configure_dns() -> Response {
|
||||
debug!("received GET request for: /settings/network/dns");
|
||||
|
||||
Response::html(templates::settings::network::configure_dns(None, None))
|
||||
}
|
||||
|
||||
pub fn network_configure_dns_post(request: &Request) -> Response {
|
||||
debug!("received POST request for: /settings/network/dns");
|
||||
|
||||
let data = try_or_400!(
|
||||
post_input!(request, { external_domain: String, enable_dyndns: bool, dynamic_domain: String })
|
||||
);
|
||||
|
||||
let (flash_name, flash_msg) = match context::network::save_dns_configuration(
|
||||
data.external_domain,
|
||||
data.enable_dyndns,
|
||||
data.dynamic_domain,
|
||||
) {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"New dynamic DNS configuration is now enabled".to_string(),
|
||||
),
|
||||
Err(e) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to save DNS configuration: {}", e),
|
||||
),
|
||||
};
|
||||
|
||||
Response::html(templates::settings::network::configure_dns(
|
||||
Some(flash_msg),
|
||||
Some(flash_name),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn network_modify_ap(ssid: Option<String>) -> Response {
|
||||
debug!("received GET request for: /settings/network/wifi/modify");
|
||||
|
||||
Response::html(templates::settings::network::modify_ap(ssid, None, None))
|
||||
}
|
||||
|
||||
pub fn network_modify_ap_post(request: &Request) -> Response {
|
||||
debug!("received POST request for: /settings/network/wifi/modify");
|
||||
|
||||
// TODO: read this value from the config file instead
|
||||
let wlan_iface = "wlan0".to_string();
|
||||
|
||||
let data = try_or_400!(post_input!(request, { ssid: String, pass: String }));
|
||||
|
||||
/* MODIFY WIFI CREDENTIALS FOR AP */
|
||||
let (flash_name, flash_msg) = match network::update(&wlan_iface, &data.ssid, &data.pass) {
|
||||
Ok(_) => ("success".to_string(), "WiFi password updated".to_string()),
|
||||
Err(e) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to update WiFi password: {}", e),
|
||||
),
|
||||
};
|
||||
|
||||
Response::html(templates::settings::network::modify_ap(
|
||||
None,
|
||||
Some(flash_msg),
|
||||
Some(flash_name),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn network_connect_wifi(request: &Request) -> Response {
|
||||
debug!("received POST request for: /settings/network/wifi/connect");
|
||||
|
||||
// TODO: read this value from the config file instead
|
||||
let wlan_iface = "wlan0".to_string();
|
||||
|
||||
let data = try_or_400!(post_input!(request, { ssid: String }));
|
||||
|
||||
let (flash_name, flash_msg) = match network::id(&wlan_iface, &data.ssid) {
|
||||
Ok(Some(id)) => match network::connect(&id, &wlan_iface) {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"Connected to chosen network".to_string(),
|
||||
),
|
||||
Err(e) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to connect to chosen network: {}", e),
|
||||
),
|
||||
},
|
||||
|
||||
_ => (
|
||||
"error".to_string(),
|
||||
"Failed to retrieve the network ID".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
Response::html(templates::settings::network::network_detail(
|
||||
data.ssid,
|
||||
Some(flash_msg),
|
||||
Some(flash_name),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn network_disconnect_wifi(request: &Request) -> Response {
|
||||
debug!("received POST request for: /settings/network/wifi/disconnect");
|
||||
|
||||
// TODO: read this value from the config file instead
|
||||
let wlan_iface = "wlan0".to_string();
|
||||
|
||||
let data = try_or_400!(post_input!(request, { ssid: String }));
|
||||
|
||||
let (flash_name, flash_msg) = match network::disable(&wlan_iface, &data.ssid) {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"Disconnected from WiFi network".to_string(),
|
||||
),
|
||||
Err(e) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to disconnect from WiFi network: {}", e),
|
||||
),
|
||||
};
|
||||
|
||||
Response::html(templates::settings::network::network_detail(
|
||||
data.ssid,
|
||||
Some(flash_msg),
|
||||
Some(flash_name),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn network_forget_wifi(request: &Request) -> Response {
|
||||
debug!("received POST request for: /settings/network/wifi/forget");
|
||||
|
||||
// TODO: read this value from the config file instead
|
||||
let wlan_iface = "wlan0".to_string();
|
||||
|
||||
let data = try_or_400!(post_input!(request, { ssid: String }));
|
||||
|
||||
let (flash_name, flash_msg) = match network::forget(&wlan_iface, &data.ssid) {
|
||||
Ok(_) => (
|
||||
"success".to_string(),
|
||||
"WiFi credentials removed".to_string(),
|
||||
),
|
||||
Err(e) => (
|
||||
"error".to_string(),
|
||||
format!("Failed to remove WiFi credentials: {}", e),
|
||||
),
|
||||
};
|
||||
|
||||
Response::html(templates::settings::network::network_detail(
|
||||
data.ssid,
|
||||
Some(flash_msg),
|
||||
Some(flash_name),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn network_detail(ssid: String) -> Response {
|
||||
debug!("received GET request for: /settings/network/wifi/<selected>");
|
||||
|
||||
Response::html(templates::settings::network::network_detail(
|
||||
ssid, None, None,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn network_list_aps() -> Response {
|
||||
debug!("received GET request for: /settings/network/wifi");
|
||||
|
||||
Response::html(templates::settings::network::list_aps())
|
||||
}
|
||||
|
||||
pub fn scuttlebutt() -> Response {
|
||||
debug!("received GET request for: /settings/scuttlebutt");
|
||||
|
||||
Response::html(templates::settings::scuttlebutt::scuttlebutt())
|
||||
}
|
16
peach-web-lite/src/routes/status.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use log::debug;
|
||||
use rouille::Response;
|
||||
|
||||
use crate::templates;
|
||||
|
||||
pub fn network() -> Response {
|
||||
debug!("received GET request for: /status/network");
|
||||
|
||||
Response::html(templates::status::network())
|
||||
}
|
||||
|
||||
pub fn status() -> Response {
|
||||
debug!("received GET request for: /status");
|
||||
|
||||
Response::html(templates::status::status())
|
||||
}
|
51
peach-web-lite/src/templates/base.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use maud::{html, PreEscaped, DOCTYPE};
|
||||
|
||||
pub fn base(back: String, title: String, content: PreEscaped<String>) -> PreEscaped<String> {
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
html lang="en" {
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
title { "PeachCloud" }
|
||||
meta name="description" content="PeachCloud Network";
|
||||
meta name="author" content="glyph";
|
||||
meta name="viewport" content="width=device-width, initial-scale=1.0";
|
||||
link rel="icon" type="image/x-icon" href="/static/icons/peach-icon.png";
|
||||
link rel="stylesheet" href="/static/css/peachcloud.css";
|
||||
}
|
||||
body {
|
||||
// render the navigation template
|
||||
(nav(back, title, content))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nav(back: String, title: String, content: PreEscaped<String>) -> PreEscaped<String> {
|
||||
html! {
|
||||
(PreEscaped("<!-- TOP NAV BAR -->"))
|
||||
nav class="nav-bar" {
|
||||
a class="nav-item" href=(back) title="Back" {
|
||||
img class="icon-medium nav-icon-left icon-active" src="/static/icons/back.svg" alt="Back";
|
||||
}
|
||||
h1 class="nav-title" { (title) }
|
||||
a class="nav-item" id="logoutButton" href="/logout" title="Logout" {
|
||||
img class="icon-medium nav-icon-right icon-active" src="/static/icons/enter.svg" alt="Enter";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- MAIN CONTENT CONTAINER -->"))
|
||||
main { (content) }
|
||||
(PreEscaped("<!-- BOTTOM NAV BAR -->"))
|
||||
nav class="nav-bar" {
|
||||
a class="nav-item" href="https://scuttlebutt.nz/" {
|
||||
img class="icon-medium nav-icon-left" title="Scuttlebutt Website" src="/static/icons/hermies.png" alt="Secure Scuttlebutt";
|
||||
}
|
||||
a class="nav-item" href="/" {
|
||||
img class="icon nav-icon-left" src="/static/icons/peach-icon.png" alt="PeachCloud" title="Home";
|
||||
}
|
||||
a class="nav-item" href="/power" {
|
||||
img class="icon-medium nav-icon-right icon-active" title="Shutdown" src="/static/icons/power.svg" alt="Power switch";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
peach-web-lite/src/templates/catchers.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::templates;
|
||||
|
||||
pub fn not_found() -> PreEscaped<String> {
|
||||
let back = "/".to_string();
|
||||
let title = "404 Not Found".to_string();
|
||||
|
||||
let content = html! {
|
||||
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 { "Click the back arrow in the top-left or the PeachCloud logo at the bottom of your screen to return Home." }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
19
peach-web-lite/src/templates/help.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::templates;
|
||||
|
||||
// /help
|
||||
pub fn menu() -> PreEscaped<String> {
|
||||
let back = "/".to_string();
|
||||
let title = "Help".to_string();
|
||||
|
||||
let content = html! {
|
||||
div class="card center" {
|
||||
div class="card-container" {
|
||||
p { "help content goes here" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
77
peach-web-lite/src/templates/home.rs
Normal file
@ -0,0 +1,77 @@
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::context;
|
||||
use crate::templates;
|
||||
|
||||
pub async fn async_test() -> PreEscaped<String> {
|
||||
let back = "".to_string();
|
||||
let title = "".to_string();
|
||||
|
||||
let whoami = context::test::test_async().await.unwrap();
|
||||
|
||||
let content = html! {
|
||||
p { (whoami) }
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
||||
|
||||
pub fn menu() -> PreEscaped<String> {
|
||||
let back = "".to_string();
|
||||
let title = "".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- RADIAL MENU -->"))
|
||||
div class="grid" {
|
||||
(PreEscaped("<!-- top-left -->"))
|
||||
(PreEscaped("<!-- PEERS LINK AND ICON -->"))
|
||||
a class="top-left" href="/scuttlebutt/peers" title="Scuttlebutt Peers" {
|
||||
div class="circle circle-small" {
|
||||
img class="icon-medium" src="/static/icons/users.svg";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- top-middle -->"))
|
||||
(PreEscaped("<!-- CURRENT USER LINK AND ICON -->"))
|
||||
a class="top-middle" href="/scuttlebutt/profile" title="Profile" {
|
||||
div class="circle circle-small" {
|
||||
img class="icon-medium" src="/static/icons/user.svg";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- top-right -->"))
|
||||
(PreEscaped("<!-- MESSAGES LINK AND ICON -->"))
|
||||
a class="top-right" href="/scuttlebutt/private" title="Private Messages" {
|
||||
div class="circle circle-small" {
|
||||
img class="icon-medium" src="/static/icons/envelope.svg";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- middle -->"))
|
||||
a class="middle" href="/hello" {
|
||||
div class="circle circle-large" { }
|
||||
}
|
||||
(PreEscaped("<!-- bottom-left -->"))
|
||||
(PreEscaped("<!-- SYSTEM STATUS LINK AND ICON -->"))
|
||||
a class="bottom-left" href="/status" title="Status" {
|
||||
div class="circle circle-small" {
|
||||
img class="icon-medium" src="/static/icons/heart-pulse.svg";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- bottom-middle -->"))
|
||||
(PreEscaped("<!-- PEACHCLOUD GUIDEBOOK LINK AND ICON -->"))
|
||||
a class="bottom-middle" href="/help" title="Help Menu" {
|
||||
div class="circle circle-small" {
|
||||
img class="icon-medium" src="/static/icons/book.svg";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- bottom-right -->"))
|
||||
(PreEscaped("<!-- SYSTEM SETTINGS LINK AND ICON -->"))
|
||||
a class="bottom-right" href="/settings" title="Settings Menu" {
|
||||
div class="circle circle-small" {
|
||||
img class="icon-medium" src="/static/icons/cog.svg";
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// we pass the content of this template into the base template
|
||||
templates::base::base(back, title, content)
|
||||
}
|
30
peach-web-lite/src/templates/login.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::templates;
|
||||
|
||||
// https://github.com/tomaka/rouille/blob/master/examples/login-session.rs
|
||||
|
||||
// /login
|
||||
pub fn login() -> PreEscaped<String> {
|
||||
let back = "/".to_string();
|
||||
let title = "Login".to_string();
|
||||
|
||||
let content = html! {
|
||||
div class="card center" {
|
||||
div class="card-container" {
|
||||
form id="login_form" action="/login" method="post" {
|
||||
// input field for username
|
||||
input id="username" name="username" class="center input" type="text" placeholder="Username" title="Username for authentication" autofocus { }
|
||||
// input field for password
|
||||
input id="password" name="password" class="center input" type="password" placeholder="Password" title="Password for given username" { }
|
||||
div id="buttonDiv" {
|
||||
// login button
|
||||
input id="loginUser" class="button button-primary center" title="Login" type="submit" value="Login" { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
9
peach-web-lite/src/templates/mod.rs
Normal file
@ -0,0 +1,9 @@
|
||||
pub mod base;
|
||||
pub mod catchers;
|
||||
pub mod help;
|
||||
pub mod home;
|
||||
pub mod login;
|
||||
pub mod scuttlebutt;
|
||||
pub mod settings;
|
||||
pub mod snippets;
|
||||
pub mod status;
|
109
peach-web-lite/src/templates/scuttlebutt.rs
Normal file
@ -0,0 +1,109 @@
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::templates;
|
||||
|
||||
// /scuttlebutt/peers
|
||||
pub fn peers() -> PreEscaped<String> {
|
||||
let back = "/".to_string();
|
||||
let title = "Scuttlebutt Peers".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- SCUTTLEBUTT PEERS -->"))
|
||||
div class="card center" {
|
||||
div class="card-container" {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="buttons" {
|
||||
a id="friends" class="button button-primary center" href="/scuttlebutt/friends" title="List Friends" { "Friends" }
|
||||
a id="follows" class="button button-primary center" href="/scuttlebutt/follows" title="List Follows" { "Follows" }
|
||||
a id="followers" class="button button-primary center" href="/scuttlebutt/followers" title="List Followers" { "Followers" }
|
||||
a id="blocks" class="button button-primary center" href="/scuttlebutt/blocks" title="List Blocks" { "Blocks" }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
||||
|
||||
// /scuttlebutt/friends
|
||||
// /scuttlebutt/follows
|
||||
// /scuttlebutt/followers
|
||||
// /scuttlebutt/blocks
|
||||
pub fn peers_list(title: String) -> PreEscaped<String> {
|
||||
let back = "/scuttlebutt/peers".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- SCUTTLEBUTT PEERS LIST -->"))
|
||||
div class="card center" {
|
||||
ul class="list" {
|
||||
// for peer in peers
|
||||
li {
|
||||
a class="list-item link light-bg" href="/scuttlebutt/profile/(pub_key)" {
|
||||
img id="peerImage" class="icon list-icon" src="{ image_path }" alt="{ peer_name }'s profile image";
|
||||
p id="peerName" class="list-text" { "(name)" }
|
||||
label class="label-small label-ellipsis list-label font-gray" for="peerName" title="{ peer_name }'s Public Key" { "(public_key)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
||||
|
||||
// /scuttlebutt/private
|
||||
pub fn private() -> PreEscaped<String> {
|
||||
let back = "/".to_string();
|
||||
let title = "Private Messages".to_string();
|
||||
|
||||
let content = html! {
|
||||
div class="card center" {
|
||||
div class="card-container" {
|
||||
p { "private message content goes here" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
||||
|
||||
// /scuttlebutt/profile
|
||||
pub fn profile(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
|
||||
let back = "/".to_string();
|
||||
let title = "Scuttlebutt Profile".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- USER PROFILE -->"))
|
||||
div class="card center" {
|
||||
(PreEscaped("<!-- PROFILE INFO BOX -->"))
|
||||
div class="capsule capsule-profile" title="Scuttlebutt account profile information" {
|
||||
(PreEscaped("<!-- edit profile button -->"))
|
||||
img id="editProfile" class="icon-small nav-icon-right" src="/icons/pencil.svg" alt="Profile picture";
|
||||
(PreEscaped("<!-- PROFILE BIO -->"))
|
||||
(PreEscaped("<!-- profile picture -->"))
|
||||
img id="profilePicture" class="icon-large" src="{ image_path }" alt="Profile picture";
|
||||
(PreEscaped("<!-- name, public key & description -->"))
|
||||
p id="profileName" class="card-text" title="Name" { "(name)" }
|
||||
label class="label-small label-ellipsis font-gray" style="user-select: all;" for="profileName" title="Public Key" { "(public_key)" }
|
||||
p id="profileDescription" style="margin-top: 1rem" class="card-text" title="Description" { "(description)" }
|
||||
}
|
||||
(PreEscaped("<!-- PUBLIC POST FORM -->"))
|
||||
form id="postForm" action="/scuttlebutt/post" method="post" {
|
||||
(PreEscaped("<!-- input for message contents -->"))
|
||||
textarea id="publicPost" class="center input message-input" title="Compose Public Post" { }
|
||||
input id="publishPost" class="button button-primary center" title="Publish" type="submit" value="Publish";
|
||||
}
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
(PreEscaped("<!-- TODO: each of these buttons needs to be a form with a public key -->"))
|
||||
div id="buttons" {
|
||||
a id="followPeer" class="button button-primary center" href="/scuttlebutt/follow" title="Follow Peer" { "Follow" }
|
||||
a id="blockPeer" class="button button-warning center" href="/scuttlebutt/block" title="Block Peer" { "Block" }
|
||||
a id="privateMessage" class="button button-primary center" href="/scuttlebutt/private_message" title="Private Message" { "Private Message" }
|
||||
}
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::snippets::flash_message(flash_msg, flash_name))
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
122
peach-web-lite/src/templates/settings/admin.rs
Normal file
@ -0,0 +1,122 @@
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::context::admin;
|
||||
use crate::templates;
|
||||
|
||||
// /settings/admin
|
||||
pub fn menu() -> PreEscaped<String> {
|
||||
let back = "/settings".to_string();
|
||||
let title = "Administrator Settings".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- ADMIN SETTINGS MENU -->"))
|
||||
div class="card center" {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="settingsButtons" {
|
||||
a id="change" class="button button-primary center" href="/settings/admin/change_password" title="Change Password" { "Change Password" }
|
||||
a id="configure" class="button button-primary center" href="/settings/admin/configure" title="Configure Admin" { "Configure Admin" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
||||
|
||||
// /settings/admin/add
|
||||
pub fn add(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
|
||||
let back = "/settings/admin/configure".to_string();
|
||||
let title = "Add Administrator".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- ADD ADMIN FORM -->"))
|
||||
div class="card center" {
|
||||
div class="card-container" {
|
||||
form id="addAdminForm" action="/settings/admin/add" method="post" {
|
||||
input id="ssb_id" name="ssb_id" class="center input" type="text" placeholder="SSB ID" title="SSB ID of Admin" value="";
|
||||
div id="buttonDiv" {
|
||||
input id="addAdmin" class="button button-primary center" title="Add" type="submit" value="Add";
|
||||
a class="button button-secondary center" href="/settings/admin/configure" title="Cancel" { "Cancel" }
|
||||
}
|
||||
}
|
||||
// render flash message, if any
|
||||
(templates::snippets::flash_message(flash_msg, flash_name))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
||||
|
||||
// /settings/admin/configure
|
||||
pub fn configure(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
|
||||
let ssb_admin_ids = admin::ssb_admin_ids();
|
||||
|
||||
let back = "/settings/admin".to_string();
|
||||
let title = "Administrator Configuration".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- CONFIGURE ADMIN PAGE -->"))
|
||||
div class="card center" {
|
||||
div class="text-container" {
|
||||
h4 { "Current Admins" }
|
||||
@match ssb_admin_ids {
|
||||
Ok(admins) => {
|
||||
@if admins.is_empty() {
|
||||
div { "No administators are currently configured" }
|
||||
} else {
|
||||
@for admin in admins {
|
||||
div {
|
||||
form action="/settings/admin/delete" method="post" {
|
||||
input type="hidden" name="ssb_id" value="{{admin}}";
|
||||
input type="submit" value="X" title="Delete" {
|
||||
span { (admin) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => div { "Encountered an error while trying to retrieve list of administrators: " (e) }
|
||||
}
|
||||
a class="button button-primary center full-width" style="margin-top: 25px;" href="/settings/admin/add" title="Add Admin" { "Add Admin" }
|
||||
}
|
||||
// render flash message, if any
|
||||
(templates::snippets::flash_message(flash_msg, flash_name))
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
||||
|
||||
// /settings/admin/change_password
|
||||
pub fn change_password(
|
||||
flash_msg: Option<String>,
|
||||
flash_name: Option<String>,
|
||||
) -> PreEscaped<String> {
|
||||
let back = "/settings/admin".to_string();
|
||||
let title = "Change Password".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- CHANGE PASSWORD FORM -->"))
|
||||
div class="card center" {
|
||||
div class="form-container" {
|
||||
form id="changePassword" action="/settings/admin/change_password" method="post" {
|
||||
(PreEscaped("<!-- input for current password -->"))
|
||||
input id="currentPassword" class="center input" name="current_password" type="password" placeholder="Current password" title="Current password" autofocus;
|
||||
(PreEscaped("<!-- input for new password -->"))
|
||||
input id="newPassword" class="center input" name="new_password1" type="password" placeholder="New password" title="New password";
|
||||
(PreEscaped("<!-- 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="savePassword" class="button button-primary center" title="Add" type="submit" value="Save";
|
||||
a class="button button-secondary center" href="/settings/admin" title="Cancel" { "Cancel" }
|
||||
}
|
||||
}
|
||||
// render flash message, if any
|
||||
(templates::snippets::flash_message(flash_msg, flash_name))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
37
peach-web-lite/src/templates/settings/menu.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::{templates, STANDALONE_MODE};
|
||||
|
||||
// /settings
|
||||
pub fn menu() -> PreEscaped<String> {
|
||||
let back = "/".to_string();
|
||||
let title = "Settings".to_string();
|
||||
|
||||
// render a minimal menu (no network settings) if running in standalone mode
|
||||
let content = if *STANDALONE_MODE {
|
||||
html! {
|
||||
(PreEscaped("<!-- SETTINGS MENU -->"))
|
||||
div class="card center" {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="settingsButtons" {
|
||||
a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" }
|
||||
a id="admin" class="button button-primary center" href="/settings/admin" title="Administrator Settings" { "Administration" }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
(PreEscaped("<!-- SETTINGS MENU -->"))
|
||||
div class="card center" {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="settingsButtons" {
|
||||
a id="network" class="button button-primary center" href="/settings/network" title="Network Settings" { "Network" }
|
||||
a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" }
|
||||
a id="admin" class="button button-primary center" href="/settings/admin" title="Administrator Settings" { "Administration" }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
4
peach-web-lite/src/templates/settings/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod admin;
|
||||
pub mod menu;
|
||||
pub mod network;
|
||||
pub mod scuttlebutt;
|
319
peach-web-lite/src/templates/settings/network.rs
Normal file
@ -0,0 +1,319 @@
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::context::{
|
||||
network,
|
||||
network::{ConfigureDNSContext, NetworkDetailContext, NetworkListContext},
|
||||
};
|
||||
use crate::templates;
|
||||
|
||||
// /settings/network/wifi/add
|
||||
pub fn add_ap(
|
||||
ssid: Option<String>,
|
||||
flash_msg: Option<String>,
|
||||
flash_name: Option<String>,
|
||||
) -> PreEscaped<String> {
|
||||
let back = "/settings/network".to_string();
|
||||
let title = "Add WiFi Network".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- NETWORK ADD CREDENTIALS FORM -->"))
|
||||
div class="card center" {
|
||||
div class="card-container" {
|
||||
form id="wifiCreds" action="/settings/network/wifi/add" method="post" {
|
||||
(PreEscaped("<!-- input for network ssid -->"))
|
||||
input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value=[ssid] autofocus;
|
||||
(PreEscaped("<!-- input for network password -->"))
|
||||
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
|
||||
div id="buttonDiv" {
|
||||
input id="addWifi" class="button button-primary center" title="Add" type="submit" value="Add";
|
||||
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::snippets::flash_message(flash_msg, flash_name))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
||||
|
||||
/* TODO: I JUST OVERWROTE THE network_detail FUNCTION :'( :'( :'( */
|
||||
// /settings/network/wifi
|
||||
pub fn network_detail(
|
||||
// the ssid of the network we wish to examine in detail
|
||||
selected: String,
|
||||
flash_msg: Option<String>,
|
||||
flash_name: Option<String>,
|
||||
) -> PreEscaped<String> {
|
||||
// retrieve network detail data
|
||||
let context = NetworkDetailContext::build();
|
||||
// have credentials for the access point we're viewing previously been saved?
|
||||
// ie. is this a known access point?
|
||||
let selected_is_saved = context.saved_aps.contains(&selected);
|
||||
|
||||
let back = "/settings/network".to_string();
|
||||
let title = "WiFi Network Detail".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- NETWORK DETAIL -->"))
|
||||
// select only the access point we are interested in
|
||||
@match context.wlan_networks.get_key_value(&selected) {
|
||||
Some((ssid, ap)) => {
|
||||
@let capsule_class = if ssid == &context.wlan_ssid {
|
||||
"two-grid capsule success-border"
|
||||
} else {
|
||||
"two-grid capsule"
|
||||
};
|
||||
@let ap_status = if ssid == &context.wlan_ssid {
|
||||
"CONNECTED"
|
||||
} else if ap.state == "Available" {
|
||||
"AVAILABLE"
|
||||
} else {
|
||||
"NOT IN RANGE"
|
||||
};
|
||||
@let ap_protocol = match &ap.detail {
|
||||
Some(scan) => scan.protocol.clone(),
|
||||
None => "Unknown".to_string()
|
||||
};
|
||||
@let ap_signal = match ap.signal {
|
||||
Some(signal) => signal.to_string(),
|
||||
None => "Unknown".to_string()
|
||||
};
|
||||
(PreEscaped("<!-- NETWORK CARD -->"))
|
||||
div class="card center" {
|
||||
(PreEscaped("<!-- NETWORK INFO BOX -->"))
|
||||
div class=(capsule_class) title="PeachCloud network mode and status" {
|
||||
(PreEscaped("<!-- left column -->"))
|
||||
(PreEscaped("<!-- NETWORK STATUS ICON -->"))
|
||||
div class="grid-column-1" {
|
||||
img id="wifiIcon" class="center icon" src="/icons/wifi.svg" alt="WiFi icon";
|
||||
label class="center label-small font-gray" for="wifiIcon" title="Access Point Status" { (ap_status) }
|
||||
}
|
||||
(PreEscaped("<!-- right column -->"))
|
||||
(PreEscaped("<!-- NETWORK DETAILED INFO -->"))
|
||||
div class="grid-column-2" {
|
||||
label class="label-small font-gray" for="netSsid" title="WiFi network SSID" { "SSID" }
|
||||
p id="netSsid" class="card-text" title="SSID" { (ssid) }
|
||||
label class="label-small font-gray" for="netSec" title="Security protocol" { "SECURITY" }
|
||||
p id="netSec" class="card-text" title="Security protocol in use by "{(ssid)}"" { (ap_protocol) }
|
||||
label class="label-small font-gray" for="netSig" title="Signal Strength" { "SIGNAL" }
|
||||
p id="netSig" class="card-text" title="Signal strength of WiFi access point" { (ap_signal) }
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div class="card-container" style="padding-top: 0;" {
|
||||
div id="buttonDiv" {
|
||||
@if context.wlan_ssid == selected {
|
||||
form id="wifiDisconnect" action="/settings/network/wifi/disconnect" method="post" {
|
||||
// hidden element: allows ssid to be sent in request
|
||||
input id="disconnectSsid" name="ssid" type="text" value=(ssid) style="display: none;";
|
||||
input id="disconnectWifi" class="button button-warning center" title="Disconnect from Network" type="submit" value="Disconnect";
|
||||
}
|
||||
}
|
||||
// If the selected access point appears in the list,
|
||||
// display the Modify and Forget buttons.
|
||||
@if context.saved_aps.contains(&selected) {
|
||||
@if context.wlan_ssid != selected && ap.state == "Available" {
|
||||
form id="wifiConnect" action="/settings/network/wifi/connect" method="post" {
|
||||
// hidden element: allows ssid to be sent in request
|
||||
input id="connectSsid" name="ssid" type="text" value=(ssid) style="display: none;";
|
||||
input id="connectWifi" class="button button-primary center" title="Connect to Network" type="submit" value="Connect";
|
||||
}
|
||||
}
|
||||
a class="button button-primary center" href="/settings/network/wifi/modify/"{(ssid)}"" { "Modify" }
|
||||
form id="wifiForget" action="/settings/network/wifi/forget" method="post" {
|
||||
// hidden element: allows ssid to be sent in request
|
||||
input id="forgetSsid" name="ssid" type="text" value=(ssid) style="display: none;";
|
||||
input id="forgetWifi" class="button button-warning center" title="Forget Network" type="submit" value="Forget";
|
||||
}
|
||||
}
|
||||
@if !selected_is_saved {
|
||||
// Display the Add button if AP creds not already in saved networks list
|
||||
a class="button button-primary center" href="/settings/network/wifi/add/"{(ssid)}"" { "Add" };
|
||||
}
|
||||
a class="button button-secondary center" href="/settings/network/wifi" title="Cancel" { "Cancel" }
|
||||
}
|
||||
(templates::snippets::flash_message(flash_msg, flash_name))
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: improve the styling of this
|
||||
None => {
|
||||
div class="card center" {
|
||||
p { "Selected access point was not found in-range or in the list of saved access points" }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
||||
|
||||
// /settings/network/wifi
|
||||
pub fn list_aps() -> PreEscaped<String> {
|
||||
// retrieve network list data
|
||||
let context = NetworkListContext::build();
|
||||
|
||||
let back = "/settings/network".to_string();
|
||||
let title = "WiFi Networks".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- NETWORK ACCESS POINT LIST -->"))
|
||||
div class="card center" {
|
||||
div class="center list-container" {
|
||||
ul class="list" {
|
||||
@if context.ap_state == *"up" {
|
||||
li class="list-item light-bg warning-border" { "Enable WiFi client mode to view saved and available networks." }
|
||||
} @else if !context.wlan_networks.is_empty() {
|
||||
@for (ssid, state) in context.wlan_networks {
|
||||
li {
|
||||
@if ssid == context.wlan_ssid {
|
||||
a class="list-item link primary-bg" href="/settings/network/wifi/ssid/"{(ssid)}"" {
|
||||
img id="netStatus" class="icon icon-active icon-medium list-icon" src="/static/icons/wifi.svg" alt="WiFi online";
|
||||
p class="list-text" { (context.wlan_ssid) }
|
||||
label class="label-small list-label font-gray" for="netStatus" title="Status" { "Connected" }
|
||||
}
|
||||
} @else if state == "Available" {
|
||||
a class="list-item link light-bg" href="/settings/network/wifi/ssid/"{(ssid)}"" {
|
||||
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/static/icons/wifi.svg" alt="WiFi offline";
|
||||
p class="list-text" { (ssid) }
|
||||
label class="label-small list-label font-gray" for="netStatus" title="Status" { (state) }
|
||||
}
|
||||
} @else {
|
||||
a class="list-item link" href="/settings/network/wifi/ssid/"{(ssid)}"" {
|
||||
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/static/icons/wifi.svg" alt="WiFi offline";
|
||||
p class="list-text" { (ssid) }
|
||||
label class="label-small list-label font-gray" for="netStatus" title="Status" { (state) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
li class="list-item light-bg" { "No saved or available networks found." }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
||||
|
||||
// /settings/network/wifi
|
||||
pub fn modify_ap(
|
||||
ssid: Option<String>,
|
||||
flash_msg: Option<String>,
|
||||
flash_name: Option<String>,
|
||||
) -> PreEscaped<String> {
|
||||
let back = "/settings/network".to_string();
|
||||
let title = "Modify WiFi Network".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- NETWORK MODIFY AP PASSWORD FORM -->"))
|
||||
div class="card center" {
|
||||
div class="card-container" {
|
||||
form id="wifiModify" action="/settings/network/wifi/modify" method="post" {
|
||||
(PreEscaped("<!-- input for network ssid -->"))
|
||||
input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value=[ssid] autofocus;
|
||||
(PreEscaped("<!-- input for network password -->"))
|
||||
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
|
||||
div id="buttonDiv" {
|
||||
input id="savePassword" class="button button-primary center" title="Save" type="submit" value="Save";
|
||||
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
|
||||
}
|
||||
}
|
||||
(templates::snippets::flash_message(flash_msg, flash_name))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
||||
|
||||
// /settings/network/dns
|
||||
pub fn configure_dns(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
|
||||
// retrieve dyndns-related data
|
||||
let context = ConfigureDNSContext::build();
|
||||
|
||||
let back = "/settings/network".to_string();
|
||||
let title = "Configure DNS".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- CONFIGURE DNS FORM -->"))
|
||||
div class="card center" {
|
||||
div class="form-container" {
|
||||
@if context.enable_dyndns {
|
||||
(PreEscaped("<!-- DYNDNS STATUS INDICATOR -->"))
|
||||
div id="dyndns-status-indicator" class="stack capsule{% if is_dyndns_online %} success-border{% else %} warning-border{% endif %}" {
|
||||
div class="stack" {
|
||||
@if context.is_dyndns_online {
|
||||
label class="label-small font-near-black" { "Dynamic DNS is currently online." }
|
||||
} else {
|
||||
label class="label-small font-near-black" { "Dynamic DNS is enabled but may be offline." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
form id="configureDNS" action="/settings/network/dns" method="post" {
|
||||
div class="input-wrapper" {
|
||||
(PreEscaped("<!-- input for externaldomain -->"))
|
||||
label id="external_domain" class="label-small input-label font-near-black" {
|
||||
label class="label-small input-label font-gray" for="external_domain" style="padding-top: 0.25rem;" { "External Domain (optional)" }
|
||||
input id="external_domain" class="form-input" style="margin-bottom: 0;" name="external_domain" type="text" title="external domain" value=(context.external_domain);
|
||||
}
|
||||
}
|
||||
div class="input-wrapper" {
|
||||
div {
|
||||
(PreEscaped("<!-- checkbox for dynds flag -->"))
|
||||
label class="label-small input-label font-gray" { "Enable Dynamic DNS" }
|
||||
input style="margin-left: 0px;" id="enable_dyndns" name="enable_dyndns" title="Activate dyndns" type="checkbox" { @if context.enable_dyndns { "checked" } };
|
||||
}
|
||||
}
|
||||
div class="input-wrapper" {
|
||||
(PreEscaped("<!-- input for dyndns -->"))
|
||||
label id="cut" class="label-small input-label font-near-black" {
|
||||
label class="label-small input-label font-gray" for="cut" style="padding-top: 0.25rem;" { "Dynamic DNS Domain" }
|
||||
input id="dyndns_domain" class="alert-input" name="dynamic_domain" placeholder="" type="text" title="dyndns_domain" value=(context.dyndns_subdomain) { ".dyn.peachcloud.org" }
|
||||
}
|
||||
}
|
||||
div id="buttonDiv" {
|
||||
input id="configureDNSButton" class="button button-primary center" title="Add" type="submit" value="Save";
|
||||
}
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::snippets::flash_message(flash_msg, flash_name))
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
||||
|
||||
// /settings/network
|
||||
pub fn menu() -> PreEscaped<String> {
|
||||
let ap_state = network::ap_state();
|
||||
|
||||
let back = "/settings".to_string();
|
||||
let title = "Network Settings".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- NETWORK SETTINGS CARD -->"))
|
||||
div class="card center" {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="buttons" {
|
||||
a class="button button-primary center" href="/settings/network/wifi/add" title="Add WiFi Network" { "Add WiFi Network" }
|
||||
a id="configureDNS" class="button button-primary center" href="/settings/network/dns" title="Configure DNS" { "Configure DNS" }
|
||||
(PreEscaped("<!-- if ap is up, show \"Enable WiFi\" button, else show \"Deploy Access Point\" -->"))
|
||||
@if ap_state == *"up" {
|
||||
a id="connectWifi" class="button button-primary center" href="/settings/network/wifi/activate" title="Enable WiFi" { "Enable WiFi" }
|
||||
} @else {
|
||||
a id="deployAccessPoint" class="button button-primary center" href="/settings/network/ap/activate" title="Deploy Access Point" { "Deploy Access Point" }
|
||||
}
|
||||
a id="listWifi" class="button button-primary center" href="/settings/network/wifi" title="List WiFi Networks" { "List WiFi Networks" }
|
||||
a id="viewStatus" class="button button-primary center" href="/status/network" title="View Network Status" { "View Network Status" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
29
peach-web-lite/src/templates/settings/scuttlebutt.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::templates;
|
||||
|
||||
// /settings/scuttlebutt
|
||||
pub fn scuttlebutt() -> PreEscaped<String> {
|
||||
let back = "/settings".to_string();
|
||||
let title = "Scuttlebutt Settings".to_string();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- SCUTTLEBUTT SETTINGS MENU -->"))
|
||||
div class="card center" {
|
||||
(PreEscaped("<!-- 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 id="replicationHops" class="button button-primary center" href="/settings/scuttlebutt/hops" title="Set Replication Hops" { "Set Replication Hops" }
|
||||
a id="removeFeeds" class="button button-primary center" href="/settings/scuttlebutt/remove_feeds" title="Remove Blocked Feeds" { "Remove Blocked Feeds" }
|
||||
a id="setDirectory" class="button button-primary center" href="/settings/scuttlebutt/set_directory" title="Set Database Directory" { "Set Database Directory" }
|
||||
a id="checkFilesystem" class="button button-primary center" href="/settings/scuttlebutt/check_fs" title="Check Filesystem" { "Check Filesystem" }
|
||||
a id="repairFilesystem" class="button button-primary center" href="/settings/scuttlebutt/repair" title="Repair Filesystem" { "Repair Filesystem" }
|
||||
a id="disable" class="button button-primary center" href="/settings/scuttlebutt/disable" title="Disable Sbot" { "Disable Sbot" }
|
||||
a id="enable" class="button button-primary center" href="/settings/scuttlebutt/enable" title="Enable Sbot" { "Enable Sbot" }
|
||||
a id="restart" class="button button-primary center" href="/settings/scuttlebutt/restart" title="Restart Sbot" { "Restart Sbot" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
22
peach-web-lite/src/templates/snippets.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
pub fn flash_message(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
|
||||
html! {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
@if flash_msg.is_some() {
|
||||
@if flash_name == Some("success".to_string()) {
|
||||
div class="capsule center-text flash-message font-success" {
|
||||
(flash_msg.unwrap())
|
||||
}
|
||||
} @else if flash_name == Some("info".to_string()) {
|
||||
div class="capsule center-text flash-message font-info" {
|
||||
(flash_msg.unwrap())
|
||||
}
|
||||
} @else if flash_name == Some("error".to_string()) {
|
||||
div class="capsule center-text flash-message font-failure" {
|
||||
(flash_msg.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
496
peach-web-lite/src/templates/status.rs
Normal file
@ -0,0 +1,496 @@
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::context::{network::NetworkStatusContext, status::StatusContext};
|
||||
use crate::{templates, STANDALONE_MODE};
|
||||
|
||||
fn ap_network_card(status: NetworkStatusContext) -> PreEscaped<String> {
|
||||
html! {
|
||||
(PreEscaped("<!-- NETWORK CARD -->"))
|
||||
div class="card center" {
|
||||
(PreEscaped("<!-- NETWORK INFO BOX -->"))
|
||||
div class="capsule capsule-container success-border" {
|
||||
(PreEscaped("<!-- NETWORK STATUS GRID -->"))
|
||||
div class="two-grid" title="PeachCloud network mode and status" {
|
||||
(PreEscaped("<!-- top-right config icon -->"))
|
||||
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
|
||||
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
|
||||
}
|
||||
(PreEscaped("<!-- left column -->"))
|
||||
(PreEscaped("<!-- network mode icon with label -->"))
|
||||
div class="grid-column-1" {
|
||||
img id="netModeIcon" class="center icon icon-active" src="/static/icons/router.svg" alt="WiFi router";
|
||||
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="Access Point Online" { "ONLINE" }
|
||||
}
|
||||
(PreEscaped("<!-- right column -->"))
|
||||
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
|
||||
div class="grid-column-2" {
|
||||
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
|
||||
p id="netMode" class="card-text" title="Network Mode" { "Access Point" }
|
||||
label class="label-small font-gray" for="netSsid" title="Access Point SSID" { "SSID" }
|
||||
p id="netSsid" class="card-text" title="SSID" { (status.ap_ssid) }
|
||||
label class="label-small font-gray" for="netIp" title="Access Point IP Address" { "IP" }
|
||||
p id="netIp" class="card-text" title="IP" { (status.ap_ip) }
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- horizontal dividing line -->"))
|
||||
hr;
|
||||
(PreEscaped("<!-- DEVICES AND TRAFFIC GRID -->"))
|
||||
div class="three-grid card-container" {
|
||||
// devices stack
|
||||
div class="stack" {
|
||||
img id="devices" class="icon icon-medium" title="Connected devices" src="/static/icons/devices.svg" alt="Digital devices";
|
||||
div class="flex-grid" style="padding-top: 0.5rem;" {
|
||||
label class="label-medium" for="devices" style="padding-right: 3px;" title="Number of connected devices";
|
||||
}
|
||||
label class="label-small font-gray" { "DEVICES" }
|
||||
}
|
||||
// download stack
|
||||
div class="stack" {
|
||||
img id="dataDownload" class="icon icon-medium" title="Download" src="/static/icons/down-arrow.svg" alt="Download";
|
||||
div class="flex-grid" style="padding-top: 0.5rem;" {
|
||||
@if let Some(traffic) = &status.ap_traffic {
|
||||
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in "{ (traffic.rx_unit) }"" { (traffic.rx) }
|
||||
label class="label-small font-near-black" { (traffic.rx_unit) }
|
||||
} @else {
|
||||
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total";
|
||||
label class="label-small font-near-black" { "0" }
|
||||
}
|
||||
}
|
||||
label class="label-small font-gray" { "DOWNLOAD" }
|
||||
}
|
||||
// upload stack
|
||||
div class="stack" {
|
||||
img id="dataUpload" class="icon icon-medium" title="Upload" src="/static/icons/up-arrow.svg" alt="Upload";
|
||||
div class="flex-grid" style="padding-top: 0.5rem;" {
|
||||
@if let Some(traffic) = status.ap_traffic {
|
||||
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in "{ (traffic.tx_unit) }"" { (traffic.tx) }
|
||||
label class="label-small font-near-black" { (traffic.tx_unit) }
|
||||
} @else {
|
||||
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total";
|
||||
label class="label-small font-near-black" { "0" }
|
||||
}
|
||||
}
|
||||
label class="label-small font-gray" { "UPLOAD" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wlan_network_card(status: NetworkStatusContext) -> PreEscaped<String> {
|
||||
let capsule = if status.wlan_state == *"up" {
|
||||
"capsule capsule-container success-border"
|
||||
} else {
|
||||
"capsule capsule-container warning-border"
|
||||
};
|
||||
html! {
|
||||
(PreEscaped("<!-- NETWORK CARD -->"))
|
||||
div class="card center" {
|
||||
(PreEscaped("<!-- NETWORK INFO BOX -->"))
|
||||
div class=(capsule) {
|
||||
@if status.wlan_state == *"up" {
|
||||
(PreEscaped("<!-- NETWORK STATUS GRID -->"))
|
||||
div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" {
|
||||
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
|
||||
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
|
||||
}
|
||||
(PreEscaped("<!-- NETWORK STATUS -->"))
|
||||
(PreEscaped("<!-- left column -->"))
|
||||
(PreEscaped("<!-- network mode icon with label -->"))
|
||||
div class="grid-column-1" {
|
||||
img id="netModeIcon" class="center icon icon-active" src="/static/icons/wifi.svg" alt="WiFi online";
|
||||
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "ONLINE" }
|
||||
}
|
||||
div class="grid-column-2" {
|
||||
(PreEscaped("<!-- right column -->"))
|
||||
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
|
||||
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
|
||||
p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" }
|
||||
label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" }
|
||||
p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) }
|
||||
label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" }
|
||||
p id="netIp" class="card-text" title="IP" { (status.wlan_ip) }
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" {
|
||||
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
|
||||
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
|
||||
}
|
||||
div class="grid-column-1" {
|
||||
img id="netModeIcon" class="center icon icon-inactive" src="/static/icons/wifi.svg" alt="WiFi offline";
|
||||
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "OFFLINE" }
|
||||
}
|
||||
div class="grid-column-2" {
|
||||
(PreEscaped("<!-- right column -->"))
|
||||
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
|
||||
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
|
||||
p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" }
|
||||
label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" }
|
||||
p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) }
|
||||
label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" }
|
||||
p id="netIp" class="card-text" title="IP" { (status.wlan_ip) }
|
||||
}
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- horizontal dividing line -->"))
|
||||
hr;
|
||||
(PreEscaped("<!-- SIGNAL AND TRAFFIC GRID -->"))
|
||||
(PreEscaped("<!-- row of icons representing network statistics -->"))
|
||||
div class="three-grid card-container" {
|
||||
div class="stack" {
|
||||
img id="netSignal" class="icon icon-medium" alt="Signal" title="WiFi Signal (%)" src="/static/icons/low-signal.svg";
|
||||
div class="flex-grid" style="padding-top: 0.5rem;" {
|
||||
label class="label-medium" for="netSignal" style="padding-right: 3px;" title="Signal strength of WiFi connection (%)" {
|
||||
@if let Some(wlan_rssi) = status.wlan_rssi { (wlan_rssi) } @else { "0" }
|
||||
}
|
||||
}
|
||||
label class="label-small font-gray" { "SIGNAL" }
|
||||
}
|
||||
div class="stack" {
|
||||
img id="dataDownload" class="icon icon-medium" alt="Download" title="WiFi download total" src="/static/icons/down-arrow.svg";
|
||||
div class="flex-grid" style="padding-top: 0.5rem;" {
|
||||
@if let Some(traffic) = &status.wlan_traffic {
|
||||
(PreEscaped("<!-- display wlan traffic data -->"))
|
||||
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in "{ (traffic.rx_unit) }"" { (traffic.rx) }
|
||||
label class="label-small font-near-black" { (traffic.rx_unit) }
|
||||
} @else {
|
||||
(PreEscaped("<!-- no wlan traffic data to display -->"))
|
||||
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total" { "0" }
|
||||
label class="label-small font-near-black" { "MB" }
|
||||
}
|
||||
}
|
||||
label class="label-small font-gray" { "DOWNLOAD" }
|
||||
}
|
||||
div class="stack" {
|
||||
img id="dataUpload" class="icon icon-medium" alt="Upload" title="WiFi upload total" src="/static/icons/up-arrow.svg";
|
||||
div class="flex-grid" style="padding-top: 0.5rem;" {
|
||||
@if let Some(traffic) = status.wlan_traffic {
|
||||
(PreEscaped("<!-- display wlan traffic data -->"))
|
||||
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in "{ (traffic.tx_unit) }"" { (traffic.tx) }
|
||||
label class="label-small font-near-black" { (traffic.tx_unit) }
|
||||
} @else {
|
||||
(PreEscaped("<!-- no wlan traffic data to display -->"))
|
||||
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total" { "0" }
|
||||
label class="label-small font-near-black" { "MB" }
|
||||
}
|
||||
}
|
||||
label class="label-small font-gray" { "UPLOAD" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* WORKS ... kinda
|
||||
fn wlan_network_card(status: NetworkStatusContext) -> PreEscaped<String> {
|
||||
html! {
|
||||
(PreEscaped("<!-- NETWORK CARD -->"))
|
||||
div class="card center" {
|
||||
(PreEscaped("<!-- NETWORK INFO BOX -->"))
|
||||
@if status.wlan_state == *"up" {
|
||||
div class="capsule capsule-container success-border" {
|
||||
(PreEscaped("<!-- NETWORK STATUS GRID -->"))
|
||||
div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" {
|
||||
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
|
||||
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
|
||||
}
|
||||
(PreEscaped("<!-- NETWORK STATUS -->"))
|
||||
(PreEscaped("<!-- left column -->"))
|
||||
(PreEscaped("<!-- network mode icon with label -->"))
|
||||
div class="grid-column-1" {
|
||||
img id="netModeIcon" class="center icon icon-active" src="/static/icons/wifi.svg" alt="WiFi online";
|
||||
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "ONLINE" }
|
||||
}
|
||||
div class="grid-column-2" {
|
||||
(PreEscaped("<!-- right column -->"))
|
||||
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
|
||||
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
|
||||
p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" }
|
||||
label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" }
|
||||
p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) }
|
||||
label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" }
|
||||
p id="netIp" class="card-text" title="IP" { (status.wlan_ip) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
div class="capsule capsule-container warning-border" {
|
||||
div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" {
|
||||
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
|
||||
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
|
||||
}
|
||||
div class="grid-column-1" {
|
||||
img id="netModeIcon" class="center icon icon-inactive" src="/static/icons/wifi.svg" alt="WiFi offline";
|
||||
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "OFFLINE" }
|
||||
}
|
||||
div class="grid-column-2" {
|
||||
(PreEscaped("<!-- right column -->"))
|
||||
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
|
||||
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
|
||||
p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" }
|
||||
label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" }
|
||||
p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) }
|
||||
label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" }
|
||||
p id="netIp" class="card-text" title="IP" { (status.wlan_ip) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- horizontal dividing line -->"))
|
||||
hr;
|
||||
(PreEscaped("<!-- SIGNAL AND TRAFFIC GRID -->"))
|
||||
(PreEscaped("<!-- row of icons representing network statistics -->"))
|
||||
div class="three-grid card-container" {
|
||||
div class="stack" {
|
||||
img id="netSignal" class="icon icon-medium" alt="Signal" title="WiFi Signal (%)" src="/static/icons/low-signal.svg";
|
||||
div class="flex-grid" style="padding-top: 0.5rem;" {
|
||||
label class="label-medium" for="netSignal" style="padding-right: 3px;" title="Signal strength of WiFi connection (%)" {
|
||||
@if let Some(wlan_rssi) = status.wlan_rssi { (wlan_rssi) } @else { "0" }
|
||||
}
|
||||
}
|
||||
label class="label-small font-gray" { "SIGNAL" }
|
||||
}
|
||||
div class="stack" {
|
||||
img id="dataDownload" class="icon icon-medium" alt="Download" title="WiFi download total" src="/static/icons/down-arrow.svg";
|
||||
div class="flex-grid" style="padding-top: 0.5rem;" {
|
||||
@if let Some(traffic) = &status.wlan_traffic {
|
||||
(PreEscaped("<!-- display wlan traffic data -->"))
|
||||
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in "{ (traffic.rx_unit) }"" { (traffic.rx) }
|
||||
label class="label-small font-near-black" { (traffic.rx_unit) }
|
||||
} @else {
|
||||
(PreEscaped("<!-- no wlan traffic data to display -->"))
|
||||
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total" { "0" }
|
||||
label class="label-small font-near-black" { "MB" }
|
||||
}
|
||||
}
|
||||
label class="label-small font-gray" { "DOWNLOAD" }
|
||||
}
|
||||
div class="stack" {
|
||||
img id="dataUpload" class="icon icon-medium" alt="Upload" title="WiFi upload total" src="/static/icons/up-arrow.svg";
|
||||
div class="flex-grid" style="padding-top: 0.5rem;" {
|
||||
@if let Some(traffic) = status.wlan_traffic {
|
||||
(PreEscaped("<!-- display wlan traffic data -->"))
|
||||
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in "{ (traffic.tx_unit) }"" { (traffic.tx) }
|
||||
label class="label-small font-near-black" { (traffic.tx_unit) }
|
||||
} @else {
|
||||
(PreEscaped("<!-- no wlan traffic data to display -->"))
|
||||
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total" { "0" }
|
||||
label class="label-small font-near-black" { "MB" }
|
||||
}
|
||||
}
|
||||
label class="label-small font-gray" { "UPLOAD" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
pub fn network() -> PreEscaped<String> {
|
||||
let back = "/status".to_string();
|
||||
let title = "Network Status".to_string();
|
||||
|
||||
// retrieve network status data
|
||||
let status = NetworkStatusContext::build();
|
||||
|
||||
let content = html! {
|
||||
(PreEscaped("<!-- NETWORK STATUS -->"))
|
||||
// if ap is up, show ap card, else show wlan card
|
||||
@if status.ap_state == *"up" {
|
||||
(ap_network_card(status))
|
||||
} @else {
|
||||
(wlan_network_card(status))
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
||||
|
||||
fn scuttlebutt_status() -> PreEscaped<String> {
|
||||
html! {
|
||||
(PreEscaped("<!-- SCUTTLEBUTT STATUS -->"))
|
||||
div class="card center" {
|
||||
div class="card-container" {
|
||||
p { "Network key: " }
|
||||
p { "Replication hops: " }
|
||||
p { "Sbot version: " }
|
||||
p { "Process status: " }
|
||||
p { "Process uptime: " }
|
||||
p { "Blobstore size: " }
|
||||
p { "Latest sequence number: " }
|
||||
p { "Last time you visited this page, latest sequence was x ... now it's y" }
|
||||
p { "Number of follows / followers / friends / blocks" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status() -> PreEscaped<String> {
|
||||
let back = "/".to_string();
|
||||
let title = if *STANDALONE_MODE {
|
||||
"Scuttlebutt Status".to_string()
|
||||
} else {
|
||||
"System Status".to_string()
|
||||
};
|
||||
|
||||
// retrieve system status data
|
||||
let status = StatusContext::build();
|
||||
|
||||
// render the scuttlebutt status template
|
||||
let content = if *STANDALONE_MODE {
|
||||
scuttlebutt_status()
|
||||
// or render the complete system status template
|
||||
} else {
|
||||
html! {
|
||||
(PreEscaped("<!-- SYSTEM STATUS -->"))
|
||||
div class="card center" {
|
||||
div class="card-container" {
|
||||
div class="three-grid" {
|
||||
(PreEscaped("<!-- PEACH-NETWORK STATUS STACK -->"))
|
||||
(PreEscaped("<!-- Display microservice status for network, oled & stats -->"))
|
||||
a class="link" href="/status/network" {
|
||||
div class="stack capsule success-border" {
|
||||
img id="networkIcon" class="icon icon-medium" alt="Network" title="Network microservice status" src="/static/icons/wifi.svg";
|
||||
div class="stack" style="padding-top: 0.5rem;" {
|
||||
label class="label-small font-near-black" { "Networking" };
|
||||
label class="label-small font-near-black" { "(network_ping)" };
|
||||
}
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- PEACH-OLED STATUS STACK -->"))
|
||||
div class="stack capsule success-border" {
|
||||
img id="oledIcon" class="icon icon-medium" alt="Display" title="OLED display microservice status" src="/static/icons/lcd.svg";
|
||||
div class="stack" style="padding-top: 0.5rem;" {
|
||||
label class="label-small font-near-black" { "Display" };
|
||||
label class="label-small font-near-black" { "(oled_ping)" };
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- PEACH-STATS STATUS STACK -->"))
|
||||
div class="stack capsule success-border" {
|
||||
img id="statsIcon" class="icon icon-medium" alt="Stats" title="System statistics microservice status" src="/static/icons/chart.svg";
|
||||
div class="stack" style="padding-top: 0.5rem;" {
|
||||
label class="label-small font-near-black" { "Statistics" };
|
||||
label class="label-small font-near-black" { "AVAILABLE" };
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- DYNDNS STATUS STACK -->"))
|
||||
(PreEscaped("<!-- Display status for dynsdns, config & sbot -->"))
|
||||
div class="stack capsule success-border" {
|
||||
img id="networkIcon" class="icon icon-medium" alt="Dyndns" title="Dyndns status" src="/static/icons/dns.png";
|
||||
div class="stack" style="padding-top: 0.5rem;" {
|
||||
label class="label-small font-near-black" { "Dyn DNS" };
|
||||
label class="label-small font-near-black" { "(dns_ping)" };
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- CONFIG STATUS STACK -->"))
|
||||
// TODO: render capsule border according to status
|
||||
div class="stack capsule success-border" {
|
||||
img id="networkIcon" class="icon icon-medium" alt="Config" title="Config status" src="/static/icons/clipboard.png";
|
||||
div class="stack" style="padding-top: 0.5rem;" {
|
||||
label class="label-small font-near-black" { "Config" };
|
||||
label class="label-small font-near-black" { "(status)" };
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- SBOT STATUS STACK -->"))
|
||||
div class="stack capsule success-border" {
|
||||
img id="networkIcon" class="icon icon-medium" alt="Sbot" title="Sbot status" src="/static/icons/hermies.svg";
|
||||
div class="stack" style="padding-top: 0.5rem;" {
|
||||
label class="label-small font-near-black" { "Sbot" };
|
||||
label class="label-small font-near-black" { "(sbot_status)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="card-container" {
|
||||
(PreEscaped("<!-- Display CPU usage meter -->"))
|
||||
@match status.cpu_usage_percent {
|
||||
Some(x) => {
|
||||
div class="flex-grid" {
|
||||
span class="card-text" { "CPU" }
|
||||
span class="label-small push-right" { (x) "%" }
|
||||
}
|
||||
meter value=(x) min="0" max="100" title="CPU usage" {
|
||||
div class="meter-gauge" {
|
||||
span style="width: "{(x)}"%;" { "CPU Usage" }
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => p class="card-text" { "CPU usage data unavailable" }
|
||||
}
|
||||
(PreEscaped("<!-- Display memory usage meter -->"))
|
||||
@match status.mem_usage_percent {
|
||||
Some(x) => {
|
||||
@let (mem_free, mem_unit) = match status.mem_free {
|
||||
Some(x) => {
|
||||
if x > 1024 {
|
||||
((x / 1024), "GB".to_string())
|
||||
} else {
|
||||
(x, "MB".to_string())
|
||||
}
|
||||
},
|
||||
_ => (0, "MB".to_string())
|
||||
};
|
||||
@let mem_total = status.mem_total.unwrap_or(0);
|
||||
@let mem_used = status.mem_used.unwrap_or(0);
|
||||
div class="flex-grid" {
|
||||
span class="card-text" { "Memory" }
|
||||
span class="label-small push-right" { (x) " % ("(mem_free)" "(mem_unit)" free)" }
|
||||
}
|
||||
meter value=(mem_used) min="0" max=(mem_total) title="Memory usage" {
|
||||
div class="meter-gauge" {
|
||||
span style="width: "{(x)}"%;" { "Memory Usage" }
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => p class="card-text" { "Memory usage data unavailable" }
|
||||
}
|
||||
(PreEscaped("<!-- Display disk usage meter -->"))
|
||||
@match status.disk_usage_percent {
|
||||
Some(x) => {
|
||||
// define free disk space with appropriate unit (GB if > 1024)
|
||||
@let (disk_free, disk_unit) = match status.disk_free {
|
||||
Some(x) => {
|
||||
if x > 1024 {
|
||||
((x / 1024), "GB".to_string())
|
||||
} else {
|
||||
(x, "MB".to_string())
|
||||
}
|
||||
},
|
||||
_ => (0, "MB".to_string())
|
||||
};
|
||||
div class="flex-grid" {
|
||||
span class="card-text" { "Disk" };
|
||||
span class="label-small push-right" { (x) " % ("(disk_free)" "(disk_unit)")" };
|
||||
}
|
||||
meter value=(x) min="0" max="100" title="Disk usage" {
|
||||
div class="meter-gauge" {
|
||||
span style="width: "{(x)}"%;" { "Disk Usage" };
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => p class="card-text" { "Disk usage data unavailable" }
|
||||
}
|
||||
(PreEscaped("<!-- Display system uptime in minutes -->"))
|
||||
@match status.uptime {
|
||||
Some(x) => {
|
||||
@if x < 60 {
|
||||
p class="capsule center-text" { "Uptime: "(x)" minutes" }
|
||||
} @else {
|
||||
@let hours = x / 60;
|
||||
@let mins = x % 60;
|
||||
p class="capsule center-text" { "Uptime: "(hours)" hours, "(mins)" minutes" }
|
||||
}
|
||||
},
|
||||
_ => p class="card-text" { "Uptime data unavailable" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
templates::base::base(back, title, content)
|
||||
}
|
177
peach-web-lite/static/css/_variables.css
Normal file
@ -0,0 +1,177 @@
|
||||
/*
|
||||
|
||||
VARIABLES
|
||||
|
||||
*/
|
||||
|
||||
@custom-media --breakpoint-not-small screen and (min-width: 30em);
|
||||
@custom-media --breakpoint-medium screen and (min-width: 30em) and (max-width: 60em);
|
||||
@custom-media --breakpoint-large screen and (min-width: 60em);
|
||||
|
||||
:root {
|
||||
|
||||
--sans-serif: -apple-system, BlinkMacSystemFont, 'avenir next', avenir, helvetica, 'helvetica neue', ubuntu, roboto, noto, 'segoe ui', arial, sans-serif;
|
||||
--serif: georgia, serif;
|
||||
--code: consolas, monaco, monospace;
|
||||
|
||||
--font-size-headline: 6rem;
|
||||
--font-size-subheadline: 5rem;
|
||||
--font-size-1: 3rem;
|
||||
--font-size-2: 2.25rem;
|
||||
--font-size-3: 1.5rem;
|
||||
--font-size-4: 1.25rem;
|
||||
--font-size-5: 1rem;
|
||||
--font-size-6: .875rem;
|
||||
--font-size-7: .75rem;
|
||||
|
||||
--letter-spacing-tight:-.05em;
|
||||
--letter-spacing-1:.1em;
|
||||
--letter-spacing-2:.25em;
|
||||
|
||||
--line-height-solid: 1;
|
||||
--line-height-title: 1.25;
|
||||
--line-height-copy: 1.5;
|
||||
|
||||
--measure: 30em;
|
||||
--measure-narrow: 20em;
|
||||
--measure-wide: 34em;
|
||||
|
||||
--spacing-none: 0;
|
||||
--spacing-extra-small: .25rem;
|
||||
--spacing-small: .5rem;
|
||||
--spacing-medium: 1rem;
|
||||
--spacing-large: 2rem;
|
||||
--spacing-extra-large: 4rem;
|
||||
--spacing-extra-extra-large: 8rem;
|
||||
--spacing-extra-extra-extra-large: 16rem;
|
||||
--spacing-copy-separator: 1.5em;
|
||||
|
||||
--height-1: 1rem;
|
||||
--height-2: 2rem;
|
||||
--height-3: 4rem;
|
||||
--height-4: 8rem;
|
||||
--height-5: 16rem;
|
||||
|
||||
--width-1: 1rem;
|
||||
--width-2: 2rem;
|
||||
--width-3: 4rem;
|
||||
--width-4: 8rem;
|
||||
--width-5: 16rem;
|
||||
|
||||
--max-width-1: 1rem;
|
||||
--max-width-2: 2rem;
|
||||
--max-width-3: 4rem;
|
||||
--max-width-4: 8rem;
|
||||
--max-width-5: 16rem;
|
||||
--max-width-6: 32rem;
|
||||
--max-width-7: 48rem;
|
||||
--max-width-8: 64rem;
|
||||
--max-width-9: 96rem;
|
||||
|
||||
--border-radius-none: 0;
|
||||
--border-radius-1: .125rem;
|
||||
--border-radius-2: .25rem;
|
||||
--border-radius-3: .5rem;
|
||||
--border-radius-4: 1rem;
|
||||
--border-radius-circle: 100%;
|
||||
--border-radius-pill: 9999px;
|
||||
|
||||
--border-width-none: 0;
|
||||
--border-width-1: .125rem;
|
||||
--border-width-2: .25rem;
|
||||
--border-width-3: .5rem;
|
||||
--border-width-4: 1rem;
|
||||
--border-width-5: 2rem;
|
||||
|
||||
--box-shadow-1: 0px 0px 4px 2px rgba( 0, 0, 0, 0.2 );
|
||||
--box-shadow-2: 0px 0px 8px 2px rgba( 0, 0, 0, 0.2 );
|
||||
--box-shadow-3: 2px 2px 4px 2px rgba( 0, 0, 0, 0.2 );
|
||||
--box-shadow-4: 2px 2px 8px 0px rgba( 0, 0, 0, 0.2 );
|
||||
--box-shadow-5: 4px 4px 8px 0px rgba( 0, 0, 0, 0.2 );
|
||||
|
||||
--black: #000;
|
||||
--near-black: #111;
|
||||
--dark-gray:#333;
|
||||
--mid-gray:#555;
|
||||
--gray: #777;
|
||||
--silver: #999;
|
||||
--light-silver: #aaa;
|
||||
--moon-gray: #ccc;
|
||||
--light-gray: #eee;
|
||||
--near-white: #f4f4f4;
|
||||
--white: #fff;
|
||||
|
||||
--transparent: transparent;
|
||||
|
||||
--black-90: rgba(0,0,0,.9);
|
||||
--black-80: rgba(0,0,0,.8);
|
||||
--black-70: rgba(0,0,0,.7);
|
||||
--black-60: rgba(0,0,0,.6);
|
||||
--black-50: rgba(0,0,0,.5);
|
||||
--black-40: rgba(0,0,0,.4);
|
||||
--black-30: rgba(0,0,0,.3);
|
||||
--black-20: rgba(0,0,0,.2);
|
||||
--black-10: rgba(0,0,0,.1);
|
||||
--black-05: rgba(0,0,0,.05);
|
||||
--black-025: rgba(0,0,0,.025);
|
||||
--black-0125: rgba(0,0,0,.0125);
|
||||
|
||||
--white-90: rgba(255,255,255,.9);
|
||||
--white-80: rgba(255,255,255,.8);
|
||||
--white-70: rgba(255,255,255,.7);
|
||||
--white-60: rgba(255,255,255,.6);
|
||||
--white-50: rgba(255,255,255,.5);
|
||||
--white-40: rgba(255,255,255,.4);
|
||||
--white-30: rgba(255,255,255,.3);
|
||||
--white-20: rgba(255,255,255,.2);
|
||||
--white-10: rgba(255,255,255,.1);
|
||||
--white-05: rgba(255,255,255,.05);
|
||||
--white-025: rgba(255,255,255,.025);
|
||||
--white-0125: rgba(255,255,255,.0125);
|
||||
|
||||
--dark-red: #e7040f;
|
||||
--red: #ff4136;
|
||||
--light-red: #ff725c;
|
||||
--orange: #ff6300;
|
||||
--gold: #ffb700;
|
||||
--yellow: #ffd700;
|
||||
--light-yellow: #fbf1a9;
|
||||
--purple: #5e2ca5;
|
||||
--light-purple: #a463f2;
|
||||
--dark-pink: #d5008f;
|
||||
--hot-pink: #ff41b4;
|
||||
--pink: #ff80cc;
|
||||
--light-pink: #ffa3d7;
|
||||
--dark-green: #137752;
|
||||
--green: #19a974;
|
||||
--light-green: #9eebcf;
|
||||
--navy: #001b44;
|
||||
--dark-blue: #00449e;
|
||||
--blue: #357edd;
|
||||
--light-blue: #96ccff;
|
||||
--lightest-blue: #cdecff;
|
||||
--washed-blue: #f6fffe;
|
||||
--washed-green: #e8fdf5;
|
||||
--washed-yellow: #fffceb;
|
||||
--washed-red: #ffdfdf;
|
||||
|
||||
/* PEACHCLOUD-SPECIFIC VARIABLES */
|
||||
|
||||
--primary: var(--light-green);
|
||||
--secondary: var(--near-white);
|
||||
--success: var(--green);
|
||||
--info: var(--blue);
|
||||
--warning: var(--orange);
|
||||
--danger: var(--red);
|
||||
--light: var(--light-gray);
|
||||
--dark: var(--near-black);
|
||||
|
||||
/* we need to add shades for each accent colour
|
||||
*
|
||||
* --info-100
|
||||
* --info-200
|
||||
* --info-300 -> var(--blue)
|
||||
* --info-400
|
||||
* --info-500
|
||||
*/
|
||||
}
|
971
peach-web-lite/static/css/peachcloud.css
Normal file
@ -0,0 +1,971 @@
|
||||
@import url('/static/css/_variables.css');
|
||||
|
||||
/* ------------------------------ *\
|
||||
* peachcloud.css
|
||||
*
|
||||
* Index
|
||||
* - ALIGNMENT
|
||||
* - BODY
|
||||
* - BUTTONS
|
||||
* - CARDS
|
||||
* - CAPSULES
|
||||
* - CIRCLES
|
||||
* - COLORS
|
||||
* - GRIDS
|
||||
* - HTML
|
||||
* - FLASH MESSAGE
|
||||
* - FONTS
|
||||
* - ICONS
|
||||
* - INPUTS
|
||||
* - LABELS
|
||||
* - LINKS
|
||||
* - LISTS
|
||||
* - MAIN
|
||||
* - METERS
|
||||
* - NAVIGATION
|
||||
* - PARAGRAPHS
|
||||
* - SWITCHES / SLIDERS
|
||||
*
|
||||
\* ------------------------------ */
|
||||
|
||||
/*
|
||||
* ALIGNMENT
|
||||
*/
|
||||
|
||||
.center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.center-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.center-vert {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.push-right {
|
||||
margin-left: auto;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
/* place-self combines align-self and justify-self */
|
||||
place-self: end center;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
.top-left {
|
||||
place-self: end;
|
||||
}
|
||||
}
|
||||
|
||||
.top-right {
|
||||
place-self: end center;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
.top-right {
|
||||
place-self: end start;
|
||||
}
|
||||
}
|
||||
|
||||
.top-middle {
|
||||
place-self: center;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
.top-middle {
|
||||
padding-bottom: 2rem;
|
||||
place-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.middle {
|
||||
place-self: center;
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 4;
|
||||
}
|
||||
|
||||
.bottom-middle {
|
||||
place-self: center;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
.bottom-middle {
|
||||
padding-top: 2rem;
|
||||
place-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-left {
|
||||
place-self: start center;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
.bottom-left {
|
||||
place-self: start end;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-right {
|
||||
place-self: start center;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
.bottom-right {
|
||||
place-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* BODY
|
||||
*/
|
||||
|
||||
body {
|
||||
background-color: var(--moon-gray);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* BUTTONS
|
||||
*/
|
||||
|
||||
.button {
|
||||
border: 1px solid var(--near-black);
|
||||
border-radius: var(--border-radius-2);
|
||||
/* Needed to render inputs & buttons of equal width */
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
color: var(--near-black);
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-5);
|
||||
font-family: var(--sans-serif);
|
||||
width: 80%;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.button.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-div {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 4;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
background-color: var(--light-gray);
|
||||
}
|
||||
|
||||
.button-primary:hover {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
.button-primary:focus {
|
||||
background-color: var(--primary);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
background-color: var(--light-gray);
|
||||
}
|
||||
|
||||
.button-secondary:hover {
|
||||
background-color: var(--light-silver);
|
||||
}
|
||||
|
||||
.button-secondary:focus {
|
||||
background-color: var(--light-silver);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button-warning {
|
||||
background-color: var(--light-gray);
|
||||
}
|
||||
|
||||
.button-warning:hover {
|
||||
background-color: var(--light-red);
|
||||
}
|
||||
|
||||
.button-warning:focus {
|
||||
background-color: var(--light-red);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* CAPSULES
|
||||
*/
|
||||
|
||||
.capsule {
|
||||
padding: 1rem;
|
||||
border: var(--border-width-1) solid;
|
||||
border-radius: var(--border-radius-3);
|
||||
background-color: var(--light-gray);
|
||||
/* margin-top: 1rem; */
|
||||
/* margin-bottom: 1rem; */
|
||||
}
|
||||
|
||||
.capsule-container {
|
||||
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
|
||||
*/
|
||||
|
||||
.card {
|
||||
min-height: 50vh;
|
||||
max-height: 90vh;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 600px) {
|
||||
.card {
|
||||
min-height: 50vh;
|
||||
max-height: 90vh;
|
||||
width: 20rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card-container {
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
justify-content: center;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.text-container {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-5);
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 5fr 2fr;
|
||||
grid-template-rows: auto;
|
||||
grid-row-gap: 1rem;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* CIRCLES
|
||||
*/
|
||||
|
||||
.circle {
|
||||
align-items: center;
|
||||
background: var(--light-gray);
|
||||
border-radius: 50%;
|
||||
box-shadow: var(--box-shadow-3);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.circle-small {
|
||||
height: 5rem;
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
.circle-medium {
|
||||
height: 8rem;
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.circle-large {
|
||||
height: 13rem;
|
||||
width: 13rem;
|
||||
}
|
||||
|
||||
.circle-success {
|
||||
background-color: var(--success);
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-4);
|
||||
}
|
||||
|
||||
.circle-warning {
|
||||
background-color: var(--warning);
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-4);
|
||||
}
|
||||
|
||||
.circle-error {
|
||||
background-color: var(--danger);
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-4);
|
||||
}
|
||||
|
||||
/* quartered-circle: circle for the center of radial-menu */
|
||||
|
||||
.quartered-circle {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.quarter {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
.quarter-link {
|
||||
left: 50%;
|
||||
margin: -2em;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.quarter-icon {
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
left: 1.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
* COLORS
|
||||
*/
|
||||
|
||||
.primary-bg {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
.secondary-bg {
|
||||
background-color: var(--secondary);
|
||||
}
|
||||
|
||||
.success-bg {
|
||||
background-color: var(--success);
|
||||
}
|
||||
|
||||
.info-bg {
|
||||
background-color: var(--info);
|
||||
}
|
||||
|
||||
.warning-bg {
|
||||
background-color: var(--warning);
|
||||
}
|
||||
|
||||
.danger-bg {
|
||||
background-color: var(--danger);
|
||||
}
|
||||
|
||||
.light-bg {
|
||||
background-color: var(--light);
|
||||
}
|
||||
|
||||
.primary-border {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.success-border {
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.info-border {
|
||||
border-color: var(--info);
|
||||
}
|
||||
|
||||
.warning-border {
|
||||
border-color: var(--warning);
|
||||
}
|
||||
|
||||
.danger-border {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.dark-gray-border {
|
||||
border-color: var(--dark-gray);
|
||||
}
|
||||
|
||||
/*
|
||||
* GRIDS
|
||||
*/
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 2fr;
|
||||
grid-template-rows: 2fr 1fr 2fr;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
.flex-grid {
|
||||
display: flex;
|
||||
align-content: space-between;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.two-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
padding-bottom: 1rem;
|
||||
/* margin-right: 2rem; */
|
||||
/* margin-left: 2rem; */
|
||||
/* padding-top: 1.5rem; */
|
||||
}
|
||||
|
||||
.two-grid-top-right {
|
||||
grid-column: 2;
|
||||
justify-self: right;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.three-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: auto;
|
||||
grid-gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-template-rows: auto;
|
||||
grid-gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
margin-right: 2rem;
|
||||
margin-left: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: grid;
|
||||
align-items: flex-end;
|
||||
justify-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.three-grid-icon-1 {
|
||||
align-self: center;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
justify-self: center;
|
||||
margin-bottom: 10px;
|
||||
max-width: 55%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.three-grid-icon-2 {
|
||||
align-self: center;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
justify-self: center;
|
||||
margin-bottom: 10px;
|
||||
max-width: 55%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.three-grid-icon-3 {
|
||||
align-self: center;
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
justify-self: center;
|
||||
margin-bottom: 10px;
|
||||
max-width: 55%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.three-grid-label-1 {
|
||||
align-self: center;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
justify-self: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.three-grid-label-2 {
|
||||
align-self: center;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
justify-self: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.three-grid-label-3 {
|
||||
align-self: center;
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
justify-self: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.grid-column-1 {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.grid-column-2 {
|
||||
grid-column: 2;
|
||||
justify-self: left;
|
||||
}
|
||||
|
||||
.grid-column-3 {
|
||||
grid-column: 3;
|
||||
}
|
||||
|
||||
/*
|
||||
* HTML
|
||||
*/
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/*
|
||||
* FLASH MESSAGE
|
||||
*/
|
||||
|
||||
.flash-message {
|
||||
font-family: var(--sans-serif);
|
||||
font-size: var(--font-size-6);
|
||||
margin-left: 2rem;
|
||||
margin-right: 2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* FONTS
|
||||
*/
|
||||
|
||||
.font-near-black {
|
||||
color: var(--near-black);
|
||||
}
|
||||
|
||||
.font-gray {
|
||||
color: var(--mid-gray);
|
||||
}
|
||||
|
||||
.font-light-gray {
|
||||
color: var(--silver);
|
||||
}
|
||||
|
||||
.font-success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.font-warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.font-failure {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/*
|
||||
* ICONS
|
||||
*/
|
||||
|
||||
.icon {
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.icon-small {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.icon-medium {
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.icon-large {
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
.icon-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* icon-active: sets color of icon svg to near-black */
|
||||
.icon-active {
|
||||
filter: invert(0%) sepia(1%) saturate(4171%) hue-rotate(79deg) brightness(86%) contrast(87%);
|
||||
}
|
||||
|
||||
/* icon-inactive: sets color of icon svg to gray */
|
||||
.icon-inactive {
|
||||
filter: invert(72%) sepia(8%) saturate(14%) hue-rotate(316deg) brightness(93%) contrast(92%);
|
||||
}
|
||||
|
||||
/*
|
||||
* INPUTS
|
||||
*/
|
||||
|
||||
.input {
|
||||
/* Needed to render inputs & buttons of equal width */
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 5px;
|
||||
line-height: 1.5rem;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
margin-bottom: 0;
|
||||
margin-left: 0px;
|
||||
border: 0px;
|
||||
padding-left: 5px;
|
||||
line-height: 1.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
height: 7rem;
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.alert-input {
|
||||
/* Needed to render inputs & buttons of equal width */
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
margin-right: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
text-align: right;
|
||||
width: 7rem;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/*
|
||||
* LABELS
|
||||
*/
|
||||
|
||||
.label-small {
|
||||
font-family: var(--sans-serif);
|
||||
font-size: var(--font-size-7);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.label-medium {
|
||||
font-size: var(--font-size-3);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.label-large {
|
||||
font-size: var(--font-size-2);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.label-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* LINKS
|
||||
*/
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
color: var(--font-near-black);
|
||||
}
|
||||
|
||||
/*
|
||||
* LISTS
|
||||
*/
|
||||
|
||||
.list {
|
||||
padding-left: 0;
|
||||
margin-left: 0;
|
||||
max-width: var(--max-width-6);
|
||||
border: 1px solid var(--light-silver);
|
||||
border-radius: var(--border-radius-2);
|
||||
list-style-type: none;
|
||||
font-family: var(--sans-serif);
|
||||
}
|
||||
|
||||
.list-container {
|
||||
width: var(--max-width-5);
|
||||
}
|
||||
|
||||
.list-icon {
|
||||
align-self: center;
|
||||
justify-self: right;
|
||||
grid-column: 2;
|
||||
grid-row: 1/3;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: grid;
|
||||
padding: 1rem;
|
||||
border-bottom-color: var(--light-silver);
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.list-text {
|
||||
justify-self: left;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
margin: 0;
|
||||
font-size: var(--font-size-5);
|
||||
}
|
||||
|
||||
.list-label {
|
||||
justify-self: left;
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
/*
|
||||
* MAIN
|
||||
*/
|
||||
|
||||
main {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* METERS
|
||||
*/
|
||||
|
||||
meter {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
/* height: 1rem; */
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
/* remove default styling */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
/* Firefox */
|
||||
background: none; /* remove default background */
|
||||
background-color: var(--near-white);
|
||||
box-shadow: 0 5px 5px -5px #333 inset;
|
||||
}
|
||||
|
||||
meter::-webkit-meter-bar {
|
||||
background: none; /* remove default background */
|
||||
background-color: var(--near-white);
|
||||
box-shadow: 0 5px 5px -5px #333 inset;
|
||||
}
|
||||
|
||||
meter::-webkit-meter-optimum-value {
|
||||
background-size: 100% 100%;
|
||||
box-shadow: 0 5px 5px -5px #999 inset;
|
||||
transition: width .5s;
|
||||
}
|
||||
|
||||
/* Firefox styling */
|
||||
meter::-moz-meter-bar {
|
||||
background: var(--mid-gray);
|
||||
background-size: 100% 100%;
|
||||
box-shadow: 0 5px 5px -5px #999 inset;
|
||||
}
|
||||
|
||||
.meter-gauge {
|
||||
background-color: var(--near-white);
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 5px 5px -5px #333 inset;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Chrome styling */
|
||||
.meter-gauge > span {
|
||||
background: var(--mid-gray);
|
||||
background-size: 100% 100%;
|
||||
box-shadow: 0 5px 5px -5px #999 inset;
|
||||
display: block;
|
||||
height: inherit;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
|
||||
/*
|
||||
* NAVIGATION
|
||||
*/
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-family: var(--sans-serif);
|
||||
font-size: var(--font-size-4);
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: auto;
|
||||
height: 90%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-icon-left {
|
||||
float: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.nav-icon-right {
|
||||
float: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: inline-block;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* PARAGRAPHS
|
||||
*/
|
||||
|
||||
p {
|
||||
font-family: var(--sans-serif);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/*
|
||||
* SWITCHES / SLIDERS
|
||||
*/
|
||||
|
||||
/* switch: the box around the slider */
|
||||
.switch {
|
||||
display: inline-block;
|
||||
height: 34px;
|
||||
position: relative;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
/* hide default HTML checkbox */
|
||||
.switch input {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.switch-icon-left {
|
||||
align-self: center;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.switch-icon-right {
|
||||
align-self: center;
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.slider {
|
||||
background-color: var(--moon-gray);
|
||||
bottom: 0;
|
||||
cursor: pointer;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: .4s;
|
||||
-webkit-transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
background-color: var(--white);
|
||||
bottom: 4px;
|
||||
content: "";
|
||||
height: 26px;
|
||||
left: 4px;
|
||||
position: absolute;
|
||||
transition: .4s;
|
||||
-webkit-transition: .4s;
|
||||
width: 26px;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--near-black);
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px var(--near-black);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-ms-transform: translateX(26px);
|
||||
transform: translateX(26px);
|
||||
-webkit-transform: translateX(26px);
|
||||
}
|
||||
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/*
|
||||
* TITLES
|
||||
*/
|
||||
|
||||
.title-medium {
|
||||
font-size: var(--font-size-4);
|
||||
font-family: var(--sans-serif);
|
||||
max-width: var(--max-width-6);
|
||||
}
|
1
peach-web-lite/static/hi.txt
Normal file
@ -0,0 +1 @@
|
||||
yo :)
|
56
peach-web-lite/static/icons/alert.svg
Normal file
@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512.001 512.001" style="enable-background:new 0 0 512.001 512.001;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M503.839,395.379l-195.7-338.962C297.257,37.569,277.766,26.315,256,26.315c-21.765,0-41.257,11.254-52.139,30.102
|
||||
L8.162,395.378c-10.883,18.85-10.883,41.356,0,60.205c10.883,18.849,30.373,30.102,52.139,30.102h391.398
|
||||
c21.765,0,41.256-11.254,52.14-30.101C514.722,436.734,514.722,414.228,503.839,395.379z M477.861,440.586
|
||||
c-5.461,9.458-15.241,15.104-26.162,15.104H60.301c-10.922,0-20.702-5.646-26.162-15.104c-5.46-9.458-5.46-20.75,0-30.208
|
||||
L229.84,71.416c5.46-9.458,15.24-15.104,26.161-15.104c10.92,0,20.701,5.646,26.161,15.104l195.7,338.962
|
||||
C483.321,419.836,483.321,431.128,477.861,440.586z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="241.001" y="176.01" width="29.996" height="149.982"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M256,355.99c-11.027,0-19.998,8.971-19.998,19.998s8.971,19.998,19.998,19.998c11.026,0,19.998-8.971,19.998-19.998
|
||||
S267.027,355.99,256,355.99z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
39
peach-web-lite/static/icons/back.svg
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 477.175 477.175" style="enable-background:new 0 0 477.175 477.175;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M145.188,238.575l215.5-215.5c5.3-5.3,5.3-13.8,0-19.1s-13.8-5.3-19.1,0l-225.1,225.1c-5.3,5.3-5.3,13.8,0,19.1l225.1,225
|
||||
c2.6,2.6,6.1,4,9.5,4s6.9-1.3,9.5-4c5.3-5.3,5.3-13.8,0-19.1L145.188,238.575z"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 768 B |
7
peach-web-lite/static/icons/book.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
|
||||
<path fill="#000000" d="M14.5 18h-10c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h10c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z"></path>
|
||||
<path fill="#000000" d="M16.5 3c-0.276 0-0.5 0.224-0.5 0.5v15c0 0.276-0.224 0.5-0.5 0.5h-11c-0.827 0-1.5-0.673-1.5-1.5s0.673-1.5 1.5-1.5h9c0.827 0 1.5-0.673 1.5-1.5v-12c0-0.827-0.673-1.5-1.5-1.5h-10c-0.827 0-1.5 0.673-1.5 1.5v15c0 1.378 1.122 2.5 2.5 2.5h11c0.827 0 1.5-0.673 1.5-1.5v-15c0-0.276-0.224-0.5-0.5-0.5zM3.5 2h10c0.276 0 0.5 0.224 0.5 0.5v12c0 0.276-0.224 0.5-0.5 0.5h-9c-0.562 0-1.082 0.187-1.5 0.501v-13.001c0-0.276 0.224-0.5 0.5-0.5z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 916 B |
9
peach-web-lite/static/icons/chart.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
|
||||
<path fill="#000000" d="M17.5 20h-16c-0.827 0-1.5-0.673-1.5-1.5v-16c0-0.827 0.673-1.5 1.5-1.5h16c0.827 0 1.5 0.673 1.5 1.5v16c0 0.827-0.673 1.5-1.5 1.5zM1.5 2c-0.276 0-0.5 0.224-0.5 0.5v16c0 0.276 0.224 0.5 0.5 0.5h16c0.276 0 0.5-0.224 0.5-0.5v-16c0-0.276-0.224-0.5-0.5-0.5h-16z"></path>
|
||||
<path fill="#000000" d="M6.5 17h-2c-0.276 0-0.5-0.224-0.5-0.5v-9c0-0.276 0.224-0.5 0.5-0.5h2c0.276 0 0.5 0.224 0.5 0.5v9c0 0.276-0.224 0.5-0.5 0.5zM5 16h1v-8h-1v8z"></path>
|
||||
<path fill="#000000" d="M10.5 17h-2c-0.276 0-0.5-0.224-0.5-0.5v-12c0-0.276 0.224-0.5 0.5-0.5h2c0.276 0 0.5 0.224 0.5 0.5v12c0 0.276-0.224 0.5-0.5 0.5zM9 16h1v-11h-1v11z"></path>
|
||||
<path fill="#000000" d="M14.5 17h-2c-0.276 0-0.5-0.224-0.5-0.5v-5c0-0.276 0.224-0.5 0.5-0.5h2c0.276 0 0.5 0.224 0.5 0.5v5c0 0.276-0.224 0.5-0.5 0.5zM13 16h1v-4h-1v4z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
BIN
peach-web-lite/static/icons/clipboard.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
45
peach-web-lite/static/icons/cloud-disconnected.svg
Normal file
@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="612px" height="612px" viewBox="0 0 612 612" style="enable-background:new 0 0 612 612;" xml:space="preserve">
|
||||
<g>
|
||||
<g id="cloud-off">
|
||||
<path d="M494.7,229.5c-17.851-86.7-94.351-153-188.7-153c-38.25,0-73.95,10.2-102,30.6l38.25,38.25
|
||||
c17.85-12.75,40.8-17.85,63.75-17.85c76.5,0,140.25,63.75,140.25,140.25v12.75h38.25c43.35,0,76.5,33.15,76.5,76.5
|
||||
c0,28.05-15.3,53.55-40.8,66.3l38.25,38.25C591.6,438.6,612,400.35,612,357C612,290.7,558.45,234.6,494.7,229.5z M76.5,109.65
|
||||
l71.4,68.85C66.3,183.6,0,249.9,0,331.5c0,84.15,68.85,153,153,153h298.35l51,51l33.15-33.15L109.65,76.5L76.5,109.65z
|
||||
M196.35,229.5l204,204H153c-56.1,0-102-45.9-102-102c0-56.1,45.9-102,102-102H196.35z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
peach-web-lite/static/icons/cloud.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="638pt" viewBox="-20 -129 638.67144 638" width="638pt" xmlns="http://www.w3.org/2000/svg"><path d="m478.90625 132.8125c-4.785156.003906-9.5625.292969-14.3125.863281-12.894531-41.988281-51.628906-70.683593-95.550781-70.773437-10.933594-.011719-21.789063 1.804687-32.121094 5.363281-25.578125-55.308594-86.195313-85.367187-145.699219-72.25-59.511718 13.121094-101.867187 65.875-101.824218 126.808594.003906 10.53125 1.316406 21.019531 3.890624 31.222656-56.695312 8.65625-97.203124 59.46875-92.988281 116.667969 4.207031 57.203125 51.71875 101.542968 109.070313 101.796875h369.535156c66.191406 0 119.847656-53.660157 119.847656-119.851563s-53.65625-119.847656-119.847656-119.847656zm0 219.722656h-369.535156c-49.238282.214844-89.472656-39.253906-90.207032-88.488281-.730468-49.234375 38.304688-89.878906 87.53125-91.132813 3.195313-.089843 6.152344-1.703124 7.957032-4.339843 1.8125-2.640625 2.246094-5.980469 1.171875-8.992188-19.824219-56.730469 9.664062-118.855469 66.152343-139.367187 56.484376-20.511719 118.964844 8.226562 140.15625 64.460937.96875 2.609375 2.976563 4.691407 5.546876 5.753907 2.574218 1.0625 5.46875 1.003906 7.992187-.160157 10.457031-4.863281 21.84375-7.382812 33.371094-7.394531 38 .070312 70.722656 26.835938 78.3125 64.070312 1.085937 5.414063 6.359375 8.914063 11.765625 7.820313 6.511718-1.304687 13.136718-1.96875 19.785156-1.976563 55.160156 0 99.875 44.71875 99.875 99.871094 0 55.160156-44.714844 99.875-99.875 99.875zm0 0"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
7
peach-web-lite/static/icons/cog.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
|
||||
<path fill="#000000" d="M7.631 19.702c-0.041 0-0.083-0.005-0.125-0.016-0.898-0.231-1.761-0.587-2.564-1.059-0.233-0.137-0.315-0.434-0.186-0.671 0.159-0.292 0.243-0.622 0.243-0.957 0-1.103-0.897-2-2-2-0.334 0-0.665 0.084-0.957 0.243-0.237 0.129-0.534 0.047-0.671-0.186-0.472-0.804-0.828-1.666-1.059-2.564-0.065-0.254 0.077-0.515 0.325-0.598 0.814-0.274 1.362-1.036 1.362-1.895s-0.547-1.621-1.362-1.895c-0.248-0.084-0.39-0.344-0.325-0.598 0.231-0.898 0.587-1.761 1.059-2.564 0.137-0.233 0.434-0.315 0.671-0.186 0.291 0.159 0.622 0.243 0.957 0.243 1.103 0 2-0.897 2-2 0-0.334-0.084-0.665-0.243-0.957-0.129-0.237-0.047-0.534 0.186-0.671 0.804-0.472 1.666-0.828 2.564-1.059 0.254-0.065 0.515 0.077 0.598 0.325 0.274 0.814 1.036 1.362 1.895 1.362s1.621-0.547 1.895-1.362c0.084-0.248 0.345-0.39 0.598-0.325 0.898 0.231 1.761 0.587 2.564 1.059 0.233 0.137 0.315 0.434 0.186 0.671-0.159 0.292-0.243 0.622-0.243 0.957 0 1.103 0.897 2 2 2 0.334 0 0.665-0.084 0.957-0.243 0.237-0.129 0.534-0.047 0.671 0.186 0.472 0.804 0.828 1.666 1.059 2.564 0.065 0.254-0.077 0.515-0.325 0.598-0.814 0.274-1.362 1.036-1.362 1.895s0.547 1.621 1.362 1.895c0.248 0.084 0.39 0.344 0.325 0.598-0.231 0.898-0.587 1.761-1.059 2.564-0.137 0.233-0.434 0.315-0.671 0.186-0.292-0.159-0.622-0.243-0.957-0.243-1.103 0-2 0.897-2 2 0 0.334 0.084 0.665 0.243 0.957 0.129 0.237 0.047 0.534-0.186 0.671-0.804 0.472-1.666 0.828-2.564 1.059-0.254 0.065-0.515-0.077-0.598-0.325-0.274-0.814-1.036-1.362-1.895-1.362s-1.621 0.547-1.895 1.362c-0.070 0.207-0.264 0.341-0.474 0.341zM10 17c1.127 0 2.142 0.628 2.655 1.602 0.52-0.161 1.026-0.369 1.51-0.622-0.108-0.314-0.164-0.646-0.164-0.98 0-1.654 1.346-3 3-3 0.334 0 0.666 0.056 0.98 0.164 0.253-0.484 0.462-0.989 0.622-1.51-0.974-0.512-1.602-1.527-1.602-2.655s0.628-2.142 1.602-2.655c-0.161-0.52-0.369-1.026-0.622-1.51-0.314 0.108-0.646 0.164-0.98 0.164-1.654 0-3-1.346-3-3 0-0.334 0.056-0.666 0.164-0.98-0.484-0.253-0.989-0.462-1.51-0.622-0.512 0.974-1.527 1.602-2.655 1.602s-2.142-0.628-2.655-1.602c-0.52 0.16-1.026 0.369-1.51 0.622 0.108 0.314 0.164 0.646 0.164 0.98 0 1.654-1.346 3-3 3-0.334 0-0.666-0.056-0.98-0.164-0.253 0.484-0.462 0.989-0.622 1.51 0.974 0.512 1.602 1.527 1.602 2.655s-0.628 2.142-1.602 2.655c0.16 0.52 0.369 1.026 0.622 1.51 0.314-0.108 0.646-0.164 0.98-0.164 1.654 0 3 1.346 3 3 0 0.334-0.056 0.666-0.164 0.98 0.484 0.253 0.989 0.462 1.51 0.622 0.512-0.974 1.527-1.602 2.655-1.602z"></path>
|
||||
<path fill="#000000" d="M10 13c-1.654 0-3-1.346-3-3s1.346-3 3-3 3 1.346 3 3-1.346 3-3 3zM10 8c-1.103 0-2 0.897-2 2s0.897 2 2 2c1.103 0 2-0.897 2-2s-0.897-2-2-2z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
5
peach-web-lite/static/icons/devices.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version='1.0' encoding='iso-8859-1'?>
|
||||
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 457.68 457.68" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 457.68 457.68">
|
||||
<path d="m439.48,167.086v-111.249c0-17.81-14.49-32.3-32.3-32.3h-374.88c-17.811,0-32.3,14.49-32.3,32.3v226.63c0,17.81 14.49,32.3 32.3,32.3h106.243l-12.162,13.09h-18.221c-4.142,0-7.5,3.358-7.5,7.5s3.358,7.5 7.5,7.5h104.361v72.334c0,10.449 8.501,18.951 18.951,18.951h80.627c10.449,0 18.951-8.501 18.951-18.951v-15.234h100.94c14.166,0 25.69-11.529 25.69-25.7v-182.59c0-11.563-7.674-21.364-18.2-24.581zm3.2,24.581v2.049h-172.49v-2.049c0-5.9 4.8-10.7 10.7-10.7h151.1c5.895,0.001 10.69,4.801 10.69,10.7zm-130.581,63.364h-41.909v-46.315h172.49v148.491h-111.63v-83.226c0-10.449-8.502-18.95-18.951-18.95zm3.951,28.809h-88.528v-9.858c0-2.178 1.772-3.951 3.951-3.951h80.627c2.178,0 3.951,1.772 3.951,3.951v9.858zm108.429-220.503v102.63h-143.59c-14.171,0-25.7,11.529-25.7,25.7v63.364h-23.718c-10.441,0-18.936,8.488-18.949,18.926h-197.523v-210.62h409.48zm-196.959,235.503h88.528v91.495h-88.528v-91.495zm-195.221-260.303h374.88c6.85,2.13163e-14 12.765,4.012 15.565,9.8h-406.011c2.801-5.788 8.716-9.8 15.566-9.8zm-16.025,250.421h196.247v10.81h-180.222c-7.243-0.001-13.452-4.48-16.025-10.81zm130.582,38.899l12.162-13.09h53.503v13.09h-65.665zm165.242,91.286h-80.627c-2.178,0-3.951-1.772-3.951-3.951v-9.857h88.528v9.857c0.001,2.178-1.772,3.951-3.95,3.951zm119.891-34.185h-100.94v-12.75h111.63v2.05c0,5.899-4.795,10.7-10.69,10.7z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
BIN
peach-web-lite/static/icons/dns.png
Normal file
After Width: | Height: | Size: 24 KiB |
46
peach-web-lite/static/icons/down-arrow.svg
Normal file
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M441.156,322.876l-48.666-47.386c-3.319-3.243-8.619-3.234-11.93,0.017l-81.894,80.299V8.533
|
||||
c0-4.71-3.823-8.533-8.533-8.533h-68.267c-4.71,0-8.533,3.823-8.533,8.533v347.273l-81.894-80.299
|
||||
c-3.311-3.243-8.602-3.251-11.921-0.017l-48.666,47.386c-1.655,1.604-2.586,3.806-2.586,6.11c0,2.304,0.939,4.506,2.586,6.11
|
||||
l179.2,174.481c1.655,1.613,3.806,2.423,5.948,2.423c2.15,0,4.292-0.811,5.956-2.423l179.2-174.481
|
||||
c1.647-1.604,2.577-3.806,2.577-6.11C443.733,326.682,442.803,324.48,441.156,322.876z M255.991,491.563L89.028,328.986
|
||||
l36.412-35.456l90.445,88.695c2.449,2.406,6.11,3.115,9.276,1.775c3.174-1.331,5.231-4.429,5.231-7.868V17.067h51.2v359.066
|
||||
c0,3.439,2.065,6.537,5.231,7.868c3.166,1.34,6.818,0.631,9.276-1.775l90.445-88.695l36.42,35.456L255.991,491.563z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
peach-web-lite/static/icons/enter.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg height="512pt" viewBox="0 0 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m218.667969 240h-202.667969c-8.832031 0-16-7.167969-16-16s7.167969-16 16-16h202.667969c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16zm0 0"/><path d="m138.667969 320c-4.097657 0-8.191407-1.558594-11.308594-4.691406-6.25-6.253906-6.25-16.386719 0-22.636719l68.695313-68.691406-68.695313-68.671875c-6.25-6.253906-6.25-16.386719 0-22.636719s16.382813-6.25 22.636719 0l80 80c6.25 6.25 6.25 16.382813 0 22.636719l-80 80c-3.136719 3.132812-7.234375 4.691406-11.328125 4.691406zm0 0"/><path d="m341.332031 512c-23.53125 0-42.664062-19.136719-42.664062-42.667969v-384c0-18.238281 11.605469-34.515625 28.882812-40.511719l128.171875-42.730468c28.671875-8.789063 56.277344 12.480468 56.277344 40.578125v384c0 18.21875-11.605469 34.472656-28.863281 40.488281l-128.214844 42.753906c-4.671875 1.449219-9 2.089844-13.589844 2.089844zm128-480c-1.386719 0-2.558593.171875-3.816406.554688l-127.636719 42.558593c-4.183594 1.453125-7.210937 5.675781-7.210937 10.21875v384c0 7.277344 7.890625 12.183594 14.484375 10.113281l127.636718-42.558593c4.160157-1.453125 7.210938-5.675781 7.210938-10.21875v-384c0-5.867188-4.777344-10.667969-10.667969-10.667969zm0 0"/><path d="m186.667969 106.667969c-8.832031 0-16-7.167969-16-16v-32c0-32.363281 26.300781-58.667969 58.664062-58.667969h240c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16h-240c-14.699219 0-26.664062 11.96875-26.664062 26.667969v32c0 8.832031-7.167969 16-16 16zm0 0"/><path d="m314.667969 448h-85.335938c-32.363281 0-58.664062-26.304688-58.664062-58.667969v-32c0-8.832031 7.167969-16 16-16s16 7.167969 16 16v32c0 14.699219 11.964843 26.667969 26.664062 26.667969h85.335938c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16zm0 0"/></svg>
|
After Width: | Height: | Size: 1.7 KiB |
6
peach-web-lite/static/icons/envelope.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
|
||||
<path fill="#000000" d="M17.5 6h-16c-0.827 0-1.5 0.673-1.5 1.5v9c0 0.827 0.673 1.5 1.5 1.5h16c0.827 0 1.5-0.673 1.5-1.5v-9c0-0.827-0.673-1.5-1.5-1.5zM17.5 7c0.030 0 0.058 0.003 0.087 0.008l-7.532 5.021c-0.29 0.193-0.819 0.193-1.109 0l-7.532-5.021c0.028-0.005 0.057-0.008 0.087-0.008h16zM17.5 17h-16c-0.276 0-0.5-0.224-0.5-0.5v-8.566l7.391 4.927c0.311 0.207 0.71 0.311 1.109 0.311s0.798-0.104 1.109-0.311l7.391-4.927v8.566c0 0.276-0.224 0.5-0.5 0.5z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 777 B |
7
peach-web-lite/static/icons/exit.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
|
||||
<path fill="#000000" d="M11.5 8c0.276 0 0.5-0.224 0.5-0.5v-4c0-0.827-0.673-1.5-1.5-1.5h-9c-0.827 0-1.5 0.673-1.5 1.5v12c0 0.746 0.537 1.56 1.222 1.853l5.162 2.212c0.178 0.076 0.359 0.114 0.532 0.114 0.213-0 0.416-0.058 0.589-0.172 0.314-0.207 0.495-0.575 0.495-1.008v-1.5h2.5c0.827 0 1.5-0.673 1.5-1.5v-4c0-0.276-0.224-0.5-0.5-0.5s-0.5 0.224-0.5 0.5v4c0 0.276-0.224 0.5-0.5 0.5h-2.5v-9.5c0-0.746-0.537-1.56-1.222-1.853l-3.842-1.647h7.564c0.276 0 0.5 0.224 0.5 0.5v4c0 0.276 0.224 0.5 0.5 0.5zM6.384 5.566c0.322 0.138 0.616 0.584 0.616 0.934v12c0 0.104-0.028 0.162-0.045 0.173s-0.081 0.014-0.177-0.027l-5.162-2.212c-0.322-0.138-0.616-0.583-0.616-0.934v-12c0-0.079 0.018-0.153 0.051-0.22l5.333 2.286z"></path>
|
||||
<path fill="#000000" d="M18.354 9.146l-3-3c-0.195-0.195-0.512-0.195-0.707 0s-0.195 0.512 0 0.707l2.146 2.146h-6.293c-0.276 0-0.5 0.224-0.5 0.5s0.224 0.5 0.5 0.5h6.293l-2.146 2.146c-0.195 0.195-0.195 0.512 0 0.707 0.098 0.098 0.226 0.146 0.354 0.146s0.256-0.049 0.354-0.146l3-3c0.195-0.195 0.195-0.512 0-0.707z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
8
peach-web-lite/static/icons/heart-pulse.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
|
||||
<path fill="#000000" d="M9.5 19c-0.084 0-0.167-0.021-0.243-0.063-0.116-0.065-2.877-1.611-5.369-4.082-0.196-0.194-0.197-0.511-0.003-0.707s0.511-0.197 0.707-0.003c1.979 1.962 4.186 3.346 4.908 3.776 0.723-0.431 2.932-1.817 4.908-3.776 0.196-0.194 0.513-0.193 0.707 0.003s0.193 0.513-0.003 0.707c-2.493 2.471-5.253 4.017-5.369 4.082-0.076 0.042-0.159 0.063-0.243 0.063z"></path>
|
||||
<path fill="#000000" d="M1.279 11c-0.188 0-0.368-0.106-0.453-0.287-0.548-1.165-0.826-2.33-0.826-3.463 0-2.895 2.355-5.25 5.25-5.25 0.98 0 2.021 0.367 2.931 1.034 0.532 0.39 0.985 0.86 1.319 1.359 0.334-0.499 0.787-0.969 1.319-1.359 0.91-0.667 1.951-1.034 2.931-1.034 2.895 0 5.25 2.355 5.25 5.25 0 1.133-0.278 2.298-0.826 3.463-0.118 0.25-0.415 0.357-0.665 0.24s-0.357-0.415-0.24-0.665c0.485-1.031 0.731-2.053 0.731-3.037 0-2.343-1.907-4.25-4.25-4.25-1.703 0-3.357 1.401-3.776 2.658-0.068 0.204-0.259 0.342-0.474 0.342s-0.406-0.138-0.474-0.342c-0.419-1.257-2.073-2.658-3.776-2.658-2.343 0-4.25 1.907-4.25 4.25 0 0.984 0.246 2.006 0.731 3.037 0.118 0.25 0.010 0.548-0.24 0.665-0.069 0.032-0.141 0.048-0.212 0.048z"></path>
|
||||
<path fill="#000000" d="M10.515 15c-0.005 0-0.009-0-0.013-0-0.202-0.004-0.569-0.109-0.753-0.766l-1.217-4.334-0.807 3.279c-0.158 0.643-0.525 0.778-0.73 0.8s-0.592-0.027-0.889-0.62l-0.606-1.211c-0.029-0.058-0.056-0.094-0.076-0.117-0.003 0.004-0.007 0.009-0.011 0.015-0.37 0.543-1.192 0.953-1.913 0.953h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.421 0 0.921-0.272 1.087-0.516 0.223-0.327 0.547-0.501 0.891-0.478 0.374 0.025 0.708 0.279 0.917 0.696l0.445 0.89 0.936-3.803c0.158-0.64 0.482-0.779 0.726-0.783s0.572 0.125 0.751 0.76l1.284 4.576 1.178-3.608c0.205-0.628 0.582-0.736 0.788-0.745s0.59 0.068 0.847 0.677l0.724 1.719c0.136 0.322 0.578 0.616 0.927 0.616h1.5c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5h-1.5c-0.747 0-1.559-0.539-1.849-1.228l-0.592-1.406-1.274 3.9c-0.207 0.634-0.566 0.733-0.771 0.733z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
BIN
peach-web-lite/static/icons/hermies.png
Normal file
After Width: | Height: | Size: 3.4 KiB |