Compare commits
338 Commits
lib_error_
...
fix_clippy
Author | SHA1 | Date | |
---|---|---|---|
00d33c2c69 | |||
126609a605 | |||
e4078bd1ba | |||
4662b15ba3 | |||
abde4ce1b4 | |||
c792aea2f6 | |||
b158fba147 | |||
da8d8f0ec3 | |||
271aa14322 | |||
d31825f688 | |||
defb8f5f09 | |||
27e9a8295c | |||
9ad580b86f | |||
a76ec08da6 | |||
cf64cd9c76 | |||
169149d607 | |||
f1eaa07f7b | |||
52c3e88b44 | |||
e659102495 | |||
57b1a786a4 | |||
fded48908d | |||
46ded85feb | |||
f29659669c | |||
d6695b291d | |||
aefa1525fb | |||
367f0307b6 | |||
b1c5c701e5 | |||
2ae9cb5c48 | |||
30aff5d7ac | |||
25e3a145fc | |||
0bfad25d3d | |||
952951515b | |||
c65f568e40 | |||
979ec4eb64 | |||
d9019d6a4b | |||
07147f8a4f | |||
5fc0094146 | |||
50afb61955 | |||
928afb35d3 | |||
1bdacf3632 | |||
deaedc4428 | |||
4a94f14dc5 | |||
b78fafe84d | |||
5d37c12913 | |||
3a05396afb | |||
41bd39d422 | |||
77c1ccb1c7 | |||
7d9bc2d7cd | |||
b20822a644 | |||
65f0ac7630 | |||
703f35d8b1 | |||
084af1b486 | |||
3e918f66cf | |||
98121f4922 | |||
e19fa0f99d | |||
3a7b499742 | |||
85231a20c7 | |||
602c6a90f1 | |||
34b4cbff32 | |||
112cfca67b | |||
a379de179d | |||
0353586705 | |||
4e8d93c388 | |||
6db5e7c169 | |||
60539adf41 | |||
e8b9cb2cc1 | |||
cad3fc94c8 | |||
976fac973d | |||
cd7c2bc230 | |||
40c4f8aaf2 | |||
70f7ad0dc6 | |||
31628a7155 | |||
3c49c067dd | |||
729580729c | |||
59739cf6e5 | |||
7fe919d9a1 | |||
7cdf8c553d | |||
fe04195030 | |||
8455e8089c | |||
97206e0573 | |||
7acf6ef395 | |||
3828998769 | |||
440d6f9bd5 | |||
59a6c7fdca | |||
3a4b0ffffd | |||
447f81a41c | |||
fadad1c30b | |||
6395fb05e3 | |||
d652f1a020 | |||
7c98cfcd5d | |||
4a1d3e81c1 | |||
5a07eda910 | |||
580771ebf2 | |||
c794d398b8 | |||
4d06eb167f | |||
eba15605c2 | |||
07c18ea64d | |||
ec288658f3 | |||
6b145d66f8 | |||
23d6870f77 | |||
b7cf3c1aab | |||
5b70353d6f | |||
67c727716c | |||
5ab47cf742 | |||
b092f1e1c4 | |||
983aa0689c | |||
1a8ac3f57f | |||
af34829cb0 | |||
824cbdbc0c | |||
84656ff251 | |||
6cdd6dc41b | |||
3572fd4e7b | |||
7fdf88eaa8 | |||
10049f0bc6 | |||
486518002d | |||
3991af11c7 | |||
59ef5960a4 | |||
69a8cc262e | |||
1479a65d59 | |||
ffe190148d | |||
b1724f6eb4 | |||
9a07ab3ac0 | |||
06a55ade06 | |||
6cba477f15 | |||
020d18731b | |||
a38394054d | |||
814162ce7d | |||
03028a2278 | |||
e10468c337 | |||
436a516c3e | |||
786e3f41d9 | |||
a491892bd9 | |||
02a1078ece | |||
6d2502257d | |||
ebbcc35fbb | |||
e3eb3be2e3 | |||
799d9de001 | |||
e05de8284d | |||
9013ccb3d6 | |||
a37288225a | |||
4665a9e6fa | |||
1a3ddccbd6 | |||
fe1da62058 | |||
68c926609e | |||
17ea3e7f44 | |||
a174027ff5 | |||
f459fe47d1 | |||
4709ec77f9 | |||
4e6bb15a23 | |||
62191e5509 | |||
da976ff4fe | |||
0737c435a8 | |||
435e819648 | |||
8f49fa55ad | |||
f6292407d0 | |||
dfc173d941 | |||
a46b58b206 | |||
33604ac0dc | |||
f0d972f46b | |||
89b502be25 | |||
90a90096f4 | |||
46926bf468 | |||
d801c957bd | |||
3397e5eb75 | |||
e474ea519f | |||
f715644e25 | |||
c7cc310a32 | |||
4470f949bd | |||
8e5c29ca6d | |||
00554706cb | |||
6d9ced5ebc | |||
abda4373ae | |||
e718889485 | |||
b7ec1a42be | |||
445c05e3ee | |||
476eaa540e | |||
6f03063f8d | |||
de9b8f5d73 | |||
51eff6a298 | |||
1c90e45f11 | |||
178af281ed | |||
e1aa7b1bb6 | |||
816d6c8a73 | |||
b098f73a5f | |||
2bfba66dab | |||
43344566de | |||
bfb53747db | |||
d0321d17d0 | |||
f3ddbcf07c | |||
680044cba8 | |||
66555f19bf | |||
792779f60f | |||
44b68a8b71 | |||
205dd145b4 | |||
7346c37c86 | |||
72fbbe83f0 | |||
b4a930e774 | |||
8c4cf6261e | |||
8f5b257ed1 | |||
3bb00c4eb7 | |||
5d75aebf0d | |||
4d2a3771b8 | |||
ed6da528a2 | |||
aca687974a | |||
6e4b8faf40 | |||
552c4b419e | |||
6fb4a2406b | |||
65dbc6bdd4 | |||
dbab6f1762 | |||
d3ae25934c | |||
c6f68de516 | |||
2ccd7e65d3 | |||
bf3325a41e | |||
e4b3479417 | |||
0561b6a9be | |||
166f4d25ae | |||
a5f0d991fa | |||
60a0d7f293 | |||
d8c40e0724 | |||
f4ad230d58 | |||
b0b21ad8a0 | |||
08ee9cd776 | |||
cfd50ca359 | |||
fd94ba27ac | |||
bb5cd0f0d3 | |||
72b7281587 | |||
cbb4027099 | |||
5e1520aa3f | |||
a8f3730b7c | |||
c1432bd29e | |||
eb77290a93 | |||
5dcba8e2ad | |||
69ba400b69 | |||
2a7c893d94 | |||
2135ab1a5b | |||
6f5cefa367 | |||
c6f8591600 | |||
cd1fb697f7 | |||
a5415aad99 | |||
037e5c34b6 | |||
699f2b13c9 | |||
c3fbc5cd73 | |||
4a27892ab6 | |||
4adf5547c9 | |||
bdfbd7057f | |||
171d051710 | |||
1ea0ea2ed1 | |||
42774674e5 | |||
57ed0ab66a | |||
49ad74595c | |||
17d52c771f | |||
6792e4702d | |||
446927f587 | |||
567b0bbc2a | |||
3ab3e65eb7 | |||
a0e80fcda7 | |||
731bc1958b | |||
58f2ddde05 | |||
4b0b2626a4 | |||
a05e67c22f | |||
c75608fb1a | |||
068d3430d7 | |||
62793f401e | |||
b8f394b901 | |||
9324b3ec0b | |||
f43fbf19f5 | |||
29cc40be48 | |||
570f6a679b | |||
399af51ccc | |||
94bac00664 | |||
c41dae8d04 | |||
e34df3b656 | |||
3399a3c80f | |||
1c26cb70fa | |||
c79bd4b19f | |||
7743511923 | |||
10833078fa | |||
244a2132fa | |||
f737236abc | |||
b5ce677a5b | |||
4d6dbd511e | |||
7fe4715014 | |||
dd33fdd47d | |||
1986d31461 | |||
a824be53b9 | |||
287082381e | |||
9f40378fce | |||
4f5eb3aa04 | |||
f4113f0632 | |||
8032b83c41 | |||
dfa1306b2d | |||
2f7c7aac8f | |||
46b7c0fc2b | |||
f62c8f0b51 | |||
06e48deb3a | |||
fb6d0317b6 | |||
cfbf052d27 | |||
d240741958 | |||
33486b4e1d | |||
8c3fecb875 | |||
0907fbc474 | |||
b747ff6db2 | |||
220c7fd540 | |||
ed7e172efb | |||
bc0f2d595b | |||
61b33d1613 | |||
c3fa188400 | |||
a1444cf478 | |||
79c94e6af0 | |||
cd8e5737c4 | |||
2429ea8fdd | |||
c8d0a2ddf6 | |||
adc1a5bd77 | |||
d760f9f92c | |||
5c4ef4a529 | |||
35ff408365 | |||
c2b785f54b | |||
1dc740eeae | |||
b3c6138e03 | |||
b59e62f920 | |||
3325706dcb | |||
bc28a84ad4 | |||
bb34bdd653 | |||
ac98bde760 | |||
361b159299 | |||
7344d6f4e0 | |||
8d18e712a1 | |||
116afe78fd | |||
7e3c500b1e | |||
b59eb22082 | |||
ee1da0599c | |||
e5f9a9be83 | |||
e54ff8829a | |||
554997a5c0 | |||
da51070ccd | |||
925051a379 | |||
380ee2683a | |||
bae3b7c2ce |
34
.drone.yml
Normal file
34
.drone.yml
Normal file
@ -0,0 +1,34 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: test-on-amd64
|
||||
|
||||
platform:
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: rustfmt
|
||||
image: rust:buster
|
||||
commands:
|
||||
- rustup component add rustfmt
|
||||
- cargo fmt --check
|
||||
|
||||
- name: clippy
|
||||
image: rust:buster
|
||||
commands:
|
||||
- rustup component add clippy
|
||||
- cargo clippy -- -D warnings
|
||||
|
||||
- name: test
|
||||
image: rust:buster
|
||||
commands:
|
||||
- cargo test
|
||||
|
||||
- name: build
|
||||
image: rust:buster
|
||||
commands:
|
||||
- cargo build
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
.idea
|
||||
target
|
||||
*peachdeploy.sh
|
||||
*vpsdeploy.sh
|
||||
*bindeploy.sh
|
||||
|
2804
Cargo.lock
generated
2804
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,6 @@ members = [
|
||||
"peach-menu",
|
||||
"peach-monitor",
|
||||
"peach-stats",
|
||||
"peach-probe",
|
||||
"peach-jsonrpc-server",
|
||||
"peach-dyndns-updater"
|
||||
]
|
||||
|
@ -4,6 +4,8 @@ _Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware
|
||||
|
||||
[**_Support us on OpenCollective!_**](https://opencollective.com/peachcloud)
|
||||
|
||||
[](https://build.coopcloud.tech/PeachCloud/peach-workspace)
|
||||
|
||||
## Background
|
||||
|
||||
- April 2018 project proposal: [`%HqwAsltORROCh4uyOq6iV+SsqU3OuNUevnq+5dwCqVI=.sha256`](https://viewer.scuttlebot.io/%25HqwAsltORROCh4uyOq6iV%2BSsqU3OuNUevnq%2B5dwCqVI%3D.sha256)
|
||||
@ -56,4 +58,4 @@ _Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware
|
||||
- [GitHub](https://github.com/peachcloud)
|
||||
- [Twitter](https://twitter.com/peachcloudorg)
|
||||
- [Email](mailto:peachcloudorg@gmail.com)
|
||||
- [OpenCollective](https://opencollective.com/peachcloud)
|
||||
- [OpenCollective](https://opencollective.com/peachcloud)
|
||||
|
34
issue_template/BUG_TEMPLATE.md
Normal file
34
issue_template/BUG_TEMPLATE.md
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
|
||||
name: "Bug Report Template"
|
||||
about: "This template is for submitting bugs."
|
||||
title: "[BUG] "
|
||||
ref: "main"
|
||||
labels:
|
||||
|
||||
- bug
|
||||
- "help needed"
|
||||
|
||||
---
|
||||
|
||||
> Please fill out the sections below.
|
||||
> Be kind and objective when writing in text.
|
||||
> Thanks for the report! :)
|
||||
|
||||
**Brief description of the bug:**
|
||||
|
||||
|
||||
**Steps to reproduce the bug:**
|
||||
|
||||
|
||||
**Expected behaviour:**
|
||||
|
||||
|
||||
**Technical details:**
|
||||
|
||||
_Is peach-web running on an x86-64 or arm64 machine?_
|
||||
|
||||
|
||||
_What operating system distribution is it running on?_
|
||||
|
||||
|
15
issue_template/FEATURE_SUGGESTION.md
Normal file
15
issue_template/FEATURE_SUGGESTION.md
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
|
||||
name: "Feature Suggestion Template"
|
||||
about: "This template is for submitting feature suggestions."
|
||||
title: "[FEATURE] "
|
||||
ref: "main"
|
||||
labels:
|
||||
|
||||
- enhancement
|
||||
|
||||
---
|
||||
|
||||
**Brief description of the feature you'd like to suggest:**
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "peach-config"
|
||||
version = "0.1.10"
|
||||
version = "0.1.17"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
|
||||
edition = "2018"
|
||||
description = "Command line tool for installing, updating and configuring PeachCloud"
|
||||
@ -35,3 +35,5 @@ structopt = "0.3.13"
|
||||
clap = "2.33.3"
|
||||
log = "0.4"
|
||||
lazy_static = "1.4.0"
|
||||
peach-lib = { path = "../peach-lib" }
|
||||
rpassword = "5.0"
|
||||
|
@ -8,7 +8,7 @@ dtparam=i2c_arm=on
|
||||
# Apply device tree overlay to enable pull-up resistors for buttons
|
||||
device_tree_overlay=overlays/mygpio.dtbo
|
||||
|
||||
kernel=vmlinuz-4.19.0-17-arm64
|
||||
kernel=vmlinuz-4.19.0-18-arm64
|
||||
# For details on the initramfs directive, see
|
||||
# https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=10532
|
||||
initramfs initrd.img-4.19.0-17-arm64
|
||||
initramfs initrd.img-4.19.0-18-arm64
|
||||
|
35
peach-config/src/change_password.rs
Normal file
35
peach-config/src/change_password.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use crate::error::PeachConfigError;
|
||||
use crate::ChangePasswordOpts;
|
||||
use peach_lib::password_utils::set_new_password;
|
||||
|
||||
/// Utility function to set the admin password for peach-web from the command-line.
|
||||
pub fn set_peach_web_password(opts: ChangePasswordOpts) -> Result<(), PeachConfigError> {
|
||||
match opts.password {
|
||||
// read password from CLI arg
|
||||
Some(password) => {
|
||||
set_new_password(&password)
|
||||
.map_err(|err| PeachConfigError::ChangePasswordError { source: err })?;
|
||||
println!(
|
||||
"Your new password has been set for peach-web. You can login through the \
|
||||
web interface with username admin."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
// read password from tty
|
||||
None => {
|
||||
let pass1 = rpassword::read_password_from_tty(Some("New password: "))?;
|
||||
let pass2 = rpassword::read_password_from_tty(Some("Confirm password: "))?;
|
||||
if pass1 != pass2 {
|
||||
Err(PeachConfigError::InvalidPassword)
|
||||
} else {
|
||||
set_new_password(&pass1)
|
||||
.map_err(|err| PeachConfigError::ChangePasswordError { source: err })?;
|
||||
println!(
|
||||
"Your new password has been set for peach-web. You can login through the \
|
||||
web interface with username admin."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,15 +3,12 @@
|
||||
pub const CONF: &str = "/var/lib/peachcloud/conf";
|
||||
|
||||
// List of package names which are installed via apt-get
|
||||
pub const SERVICES: [&str; 11] = [
|
||||
"peach-oled",
|
||||
"peach-network",
|
||||
"peach-stats",
|
||||
pub const SERVICES: [&str; 8] = [
|
||||
"peach-web",
|
||||
"peach-probe",
|
||||
"peach-menu",
|
||||
"peach-buttons",
|
||||
"peach-monitor",
|
||||
"peach-probe",
|
||||
"peach-oled",
|
||||
"peach-dyndns-updater",
|
||||
"peach-go-sbot",
|
||||
"peach-config",
|
||||
|
@ -1,4 +1,5 @@
|
||||
#![allow(clippy::nonstandard_macro_braces)]
|
||||
use peach_lib::error::PeachError;
|
||||
pub use snafu::ResultExt;
|
||||
use snafu::Snafu;
|
||||
|
||||
@ -30,6 +31,10 @@ pub enum PeachConfigError {
|
||||
},
|
||||
#[snafu(display("Error serializing json: {}", source))]
|
||||
SerdeError { source: serde_json::Error },
|
||||
#[snafu(display("Error changing password: {}", source))]
|
||||
ChangePasswordError { source: PeachError },
|
||||
#[snafu(display("Entered passwords did not match. Please try again."))]
|
||||
InvalidPassword,
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for PeachConfigError {
|
||||
|
@ -1,6 +1,8 @@
|
||||
mod change_password;
|
||||
mod constants;
|
||||
mod error;
|
||||
mod generate_manifest;
|
||||
mod set_permissions;
|
||||
mod setup_networking;
|
||||
mod setup_peach;
|
||||
mod setup_peach_deb;
|
||||
@ -12,10 +14,6 @@ use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use structopt::StructOpt;
|
||||
|
||||
use crate::generate_manifest::generate_manifest;
|
||||
use crate::setup_peach::setup_peach;
|
||||
use crate::update::update;
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(
|
||||
name = "peach-config",
|
||||
@ -44,6 +42,14 @@ enum PeachConfig {
|
||||
/// Updates all PeachCloud microservices
|
||||
#[structopt(name = "update")]
|
||||
Update(UpdateOpts),
|
||||
|
||||
/// Changes the password for the peach-web interface
|
||||
#[structopt(name = "changepassword")]
|
||||
ChangePassword(ChangePasswordOpts),
|
||||
|
||||
/// Updates file permissions on PeachCloud device
|
||||
#[structopt(name = "permissions")]
|
||||
SetPermissions,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
@ -76,6 +82,14 @@ pub struct UpdateOpts {
|
||||
list: bool,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub struct ChangePasswordOpts {
|
||||
/// Optional argument to specify password as CLI argument
|
||||
/// if not specified, this command asks for user input for the passwords
|
||||
#[structopt(short, long)]
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
arg_enum! {
|
||||
/// enum options for real-time clock choices
|
||||
#[derive(Debug)]
|
||||
@ -99,28 +113,48 @@ fn main() {
|
||||
if let Some(subcommand) = opt.commands {
|
||||
match subcommand {
|
||||
PeachConfig::Setup(cfg) => {
|
||||
match setup_peach(cfg.no_input, cfg.default_locale, cfg.i2c, cfg.rtc) {
|
||||
match setup_peach::setup_peach(cfg.no_input, cfg.default_locale, cfg.i2c, cfg.rtc) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!("peach-config encountered an error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
PeachConfig::Manifest => match generate_manifest() {
|
||||
PeachConfig::Manifest => match generate_manifest::generate_manifest() {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"peach-config countered an error generating manifest: {}",
|
||||
"peach-config encountered an error generating manifest: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
},
|
||||
PeachConfig::Update(opts) => match update(opts) {
|
||||
PeachConfig::Update(opts) => match update::update(opts) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!("peach-config encountered an error during update: {}", err)
|
||||
}
|
||||
},
|
||||
PeachConfig::ChangePassword(opts) => {
|
||||
match change_password::set_peach_web_password(opts) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"peach-config encountered an error during password update: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
PeachConfig::SetPermissions => match set_permissions::set_permissions() {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"peach-config ecountered an error updating file permissions: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
peach-config/src/set_permissions.rs
Normal file
21
peach-config/src/set_permissions.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use crate::error::PeachConfigError;
|
||||
use crate::utils::cmd;
|
||||
|
||||
/// All configs are stored in this folder, and should be read/writeable by peach group
|
||||
/// so they can be read and written by all PeachCloud services.
|
||||
pub const CONFIGS_DIR: &str = "/var/lib/peachcloud";
|
||||
pub const PEACH_WEB_DIR: &str = "/usr/share/peach-web";
|
||||
|
||||
/// Utility function to set correct file permissions on the PeachCloud device.
|
||||
/// Accidentally changing file permissions is a fairly common thing to happen,
|
||||
/// so this is a useful CLI function for quickly correcting anything that may be out of order.
|
||||
pub fn set_permissions() -> Result<(), PeachConfigError> {
|
||||
println!("[ UPDATING FILE PERMISSIONS ON PEACHCLOUD DEVICE ]");
|
||||
cmd(&["chmod", "-R", "u+rwX,g+rwX", CONFIGS_DIR])?;
|
||||
cmd(&["chown", "-R", "peach", CONFIGS_DIR])?;
|
||||
cmd(&["chgrp", "-R", "peach", CONFIGS_DIR])?;
|
||||
cmd(&["chmod", "-R", "u+rwX,g+rwX", PEACH_WEB_DIR])?;
|
||||
cmd(&["chown", "-R", "peach-web:peach", PEACH_WEB_DIR])?;
|
||||
println!("[ PERMISSIONS SUCCESSFULLY UPDATED ]");
|
||||
Ok(())
|
||||
}
|
@ -68,6 +68,7 @@ pub fn setup_peach(
|
||||
"libssl-dev",
|
||||
"nginx",
|
||||
"wget",
|
||||
"dnsutils",
|
||||
"-y",
|
||||
])?;
|
||||
|
||||
|
@ -47,8 +47,8 @@ pub fn update_microservices() -> Result<(), PeachConfigError> {
|
||||
cmd(&["apt-get", "update"])?;
|
||||
// filter out peach-config from list of services
|
||||
let services_to_update: Vec<&str> = SERVICES
|
||||
.to_vec()
|
||||
.into_iter()
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|&x| x != "peach-config")
|
||||
.collect();
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "peach-dyndns-updater"
|
||||
version = "0.1.6"
|
||||
version = "0.1.8"
|
||||
authors = ["Max Fowler <mfowler@commoninternet.net>"]
|
||||
edition = "2018"
|
||||
description = "Sytemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate."
|
||||
|
29
peach-dyndns-updater/bindeploy.sh
Executable file
29
peach-dyndns-updater/bindeploy.sh
Executable file
@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# exit when any command fails
|
||||
set -e
|
||||
|
||||
KEYFILE=/Users/notplants/.ssh/id_rsa
|
||||
SERVICE=peach-dyndns-updater
|
||||
|
||||
# deploy
|
||||
rsync -avzh --exclude target --exclude .idea --exclude .git -e "ssh -i $KEYFILE" . rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/$SERVICE/
|
||||
rsync -avzh --exclude target --exclude .idea --exclude .git -e "ssh -i $KEYFILE" ~/computer/projects/peachcloud/peach-workspace/peach-lib/ rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/peach-lib/
|
||||
|
||||
echo "++ cross compiling on vps"
|
||||
BIN_PATH=$(ssh -i $KEYFILE rust@167.99.136.83 'cd /srv/peachcloud/automation/peach-workspace/peach-dyndns-updater; /home/rust/.cargo/bin/cargo clean -p peach-lib; /home/rust/.cargo/bin/cargo build --release --target=aarch64-unknown-linux-gnu')
|
||||
|
||||
echo "++ copying ${BIN_PATH} to local"
|
||||
rm -f target/$SERVICE
|
||||
scp -i $KEYFILE rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/target/aarch64-unknown-linux-gnu/release/peach-dyndns-updater ../target/vps-bin-$SERVICE
|
||||
|
||||
#echo "++ cross compiling"
|
||||
BINFILE="../target/vps-bin-$SERVICE"
|
||||
echo $BINFILE
|
||||
|
||||
|
||||
echo "++ build successful"
|
||||
|
||||
echo "++ copying to pi"
|
||||
ssh -t -i $KEYFILE peach@peach.link 'mkdir -p /srv/dev/bins'
|
||||
scp -i $KEYFILE $BINFILE peach@peach.link:/srv/dev/bins/$SERVICE
|
||||
|
@ -1,6 +1,5 @@
|
||||
use log::info;
|
||||
use peach_lib::dyndns_client::dyndns_update_ip;
|
||||
use log::{info};
|
||||
|
||||
|
||||
fn main() {
|
||||
// initalize the logger
|
||||
@ -9,4 +8,4 @@ fn main() {
|
||||
info!("Running peach-dyndns-updater");
|
||||
let result = dyndns_update_ip();
|
||||
info!("result: {:?}", result);
|
||||
}
|
||||
}
|
||||
|
25
peach-jsonrpc-server/Cargo.toml
Normal file
25
peach-jsonrpc-server/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "peach-jsonrpc-server"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "JSON-RPC over HTTP for the PeachCloud system. Provides a JSON-RPC wrapper around the stats, network and oled libraries."
|
||||
homepage = "https://opencollective.com/peachcloud"
|
||||
repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace"
|
||||
readme = "README.md"
|
||||
license = "AGPL-3.0-only"
|
||||
publish = false
|
||||
|
||||
[badges]
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.9"
|
||||
jsonrpc-core = "18"
|
||||
jsonrpc-http-server = "18"
|
||||
log = "0.4"
|
||||
peach-stats = { path = "../peach-stats", features = ["serde_support"] }
|
||||
serde_json = "1.0.74"
|
||||
|
||||
[dev-dependencies]
|
||||
jsonrpc-test = "18"
|
72
peach-jsonrpc-server/README.md
Normal file
72
peach-jsonrpc-server/README.md
Normal file
@ -0,0 +1,72 @@
|
||||
# peach-jsonrpc-server
|
||||
|
||||
A JSON-RPC server for the PeachCloud system which exposes an API over HTTP.
|
||||
|
||||
Currently includes peach-stats capability (system statistics).
|
||||
|
||||
## JSON-RPC API
|
||||
|
||||
| Method | Description | Returns |
|
||||
| --- | --- | --- |
|
||||
| `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` |
|
||||
| `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` |
|
||||
| `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` |
|
||||
| `load_average` | Load average statistics | `one`, `five`, `fifteen` |
|
||||
| `mem_stats` | Memory statistics | `total`, `free`, `used` |
|
||||
| `ping` | Microservice status | `success` if running |
|
||||
| `uptime` | System uptime | `secs` |
|
||||
|
||||
## Environment
|
||||
|
||||
The JSON-RPC HTTP server is currently hardcoded to run on "127.0.0.1:5110". Address and port configuration settings will later be exposed via CLI arguments and possibly an environment variable.
|
||||
|
||||
Logging is made available with `env_logger`:
|
||||
|
||||
`export RUST_LOG=info`
|
||||
|
||||
Other logging levels include `debug`, `warn` and `error`.
|
||||
|
||||
## Setup
|
||||
|
||||
Clone the peach-workspace repo:
|
||||
|
||||
`git clone https://git.coopcloud.tech/PeachCloud/peach-workspace`
|
||||
|
||||
Move into the repo peaach-jsonrpc-server directory and compile a release build:
|
||||
|
||||
`cd peach-jsonrpc-server`
|
||||
`cargo build --release`
|
||||
|
||||
Run the binary:
|
||||
|
||||
`./peach-workspace/target/release/peach-jsonrpc-server`
|
||||
|
||||
## Debian Packaging
|
||||
|
||||
TODO.
|
||||
|
||||
## Example Usage
|
||||
|
||||
**Get CPU Statistics**
|
||||
|
||||
With microservice running, open a second terminal window and use `curl` to call server methods:
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "cpu_stats", "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server responds with:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"{\"user\":4661083,\"system\":1240371,\"idle\":326838290,\"nice\":0}","id":1}`
|
||||
|
||||
**Get System Uptime**
|
||||
|
||||
With microservice running, open a second terminal window and use `curl` to call server methods:
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "uptime", "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server responds with:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"{\"secs\":840968}","id":1}`
|
||||
|
||||
### Licensing
|
||||
|
||||
AGPL-3.0
|
58
peach-jsonrpc-server/src/error.rs
Normal file
58
peach-jsonrpc-server/src/error.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use std::fmt;
|
||||
|
||||
use jsonrpc_core::{Error as JsonRpcError, ErrorCode};
|
||||
use serde_json::error::Error as SerdeJsonError;
|
||||
|
||||
use peach_stats::StatsError;
|
||||
|
||||
/// Custom error type encapsulating all possible errors for a JSON-RPC server
|
||||
/// and associated methods.
|
||||
#[derive(Debug)]
|
||||
pub enum JsonRpcServerError {
|
||||
/// Failed to serialize a string from a data structure.
|
||||
Serde(SerdeJsonError),
|
||||
/// An error returned from the `peach-stats` library.
|
||||
Stats(StatsError),
|
||||
/// An expected JSON-RPC method parameter was not provided.
|
||||
MissingParameter(JsonRpcError),
|
||||
/// Failed to parse a provided JSON-RPC method parameter.
|
||||
ParseParameter(JsonRpcError),
|
||||
}
|
||||
|
||||
impl fmt::Display for JsonRpcServerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
JsonRpcServerError::ParseParameter(ref source) => {
|
||||
write!(f, "Failed to parse parameter: {}", source)
|
||||
}
|
||||
JsonRpcServerError::MissingParameter(ref source) => {
|
||||
write!(f, "Missing expected parameter: {}", source)
|
||||
}
|
||||
JsonRpcServerError::Serde(ref source) => {
|
||||
write!(f, "{}", source)
|
||||
}
|
||||
JsonRpcServerError::Stats(ref source) => {
|
||||
write!(f, "{}", source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonRpcServerError> for JsonRpcError {
|
||||
fn from(err: JsonRpcServerError) -> Self {
|
||||
match &err {
|
||||
JsonRpcServerError::Serde(source) => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32002),
|
||||
message: format!("{}", source),
|
||||
data: None,
|
||||
},
|
||||
JsonRpcServerError::Stats(source) => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32001),
|
||||
message: format!("{}", source),
|
||||
data: None,
|
||||
},
|
||||
JsonRpcServerError::MissingParameter(source) => source.clone(),
|
||||
JsonRpcServerError::ParseParameter(source) => source.clone(),
|
||||
}
|
||||
}
|
||||
}
|
140
peach-jsonrpc-server/src/lib.rs
Normal file
140
peach-jsonrpc-server/src/lib.rs
Normal file
@ -0,0 +1,140 @@
|
||||
//! # peach-jsonrpc-server
|
||||
//!
|
||||
//! A JSON-RPC server which exposes an API over HTTP.
|
||||
|
||||
use std::env;
|
||||
use std::result::Result;
|
||||
|
||||
use jsonrpc_core::{IoHandler, Value};
|
||||
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
|
||||
use log::info;
|
||||
use peach_stats::stats;
|
||||
|
||||
mod error;
|
||||
use crate::error::JsonRpcServerError;
|
||||
|
||||
/// Create JSON-RPC I/O handler, add RPC methods and launch HTTP server.
|
||||
pub fn run() -> Result<(), JsonRpcServerError> {
|
||||
info!("Starting up.");
|
||||
|
||||
info!("Creating JSON-RPC I/O handler.");
|
||||
let mut io = IoHandler::default();
|
||||
|
||||
io.add_sync_method("ping", |_| Ok(Value::String("success".to_string())));
|
||||
|
||||
// TODO: add blocks of methods according to provided flags
|
||||
|
||||
/* PEACH-STATS RPC METHODS */
|
||||
|
||||
io.add_sync_method("cpu_stats", move |_| {
|
||||
info!("Fetching CPU statistics.");
|
||||
let cpu = stats::cpu_stats().map_err(JsonRpcServerError::Stats)?;
|
||||
let json_cpu = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?;
|
||||
|
||||
Ok(Value::String(json_cpu))
|
||||
});
|
||||
|
||||
io.add_sync_method("cpu_stats_percent", move |_| {
|
||||
info!("Fetching CPU statistics as percentages.");
|
||||
let cpu = stats::cpu_stats_percent().map_err(JsonRpcServerError::Stats)?;
|
||||
let json_cpu = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?;
|
||||
|
||||
Ok(Value::String(json_cpu))
|
||||
});
|
||||
|
||||
io.add_sync_method("disk_usage", move |_| {
|
||||
info!("Fetching disk usage statistics.");
|
||||
let disks = stats::disk_usage().map_err(JsonRpcServerError::Stats)?;
|
||||
let json_disks = serde_json::to_string(&disks).map_err(JsonRpcServerError::Serde)?;
|
||||
|
||||
Ok(Value::String(json_disks))
|
||||
});
|
||||
|
||||
io.add_sync_method("load_average", move |_| {
|
||||
info!("Fetching system load average statistics.");
|
||||
let avg = stats::load_average().map_err(JsonRpcServerError::Stats)?;
|
||||
let json_avg = serde_json::to_string(&avg).map_err(JsonRpcServerError::Serde)?;
|
||||
|
||||
Ok(Value::String(json_avg))
|
||||
});
|
||||
|
||||
io.add_sync_method("mem_stats", move |_| {
|
||||
info!("Fetching current memory statistics.");
|
||||
let mem = stats::mem_stats().map_err(JsonRpcServerError::Stats)?;
|
||||
let json_mem = serde_json::to_string(&mem).map_err(JsonRpcServerError::Serde)?;
|
||||
|
||||
Ok(Value::String(json_mem))
|
||||
});
|
||||
|
||||
io.add_sync_method("uptime", move |_| {
|
||||
info!("Fetching system uptime.");
|
||||
let uptime = stats::uptime().map_err(JsonRpcServerError::Stats)?;
|
||||
let json_uptime = serde_json::to_string(&uptime).map_err(JsonRpcServerError::Serde)?;
|
||||
|
||||
Ok(Value::String(json_uptime))
|
||||
});
|
||||
|
||||
let http_server =
|
||||
env::var("PEACH_JSONRPC_SERVER").unwrap_or_else(|_| "127.0.0.1:5110".to_string());
|
||||
|
||||
info!("Starting JSON-RPC server on {}.", http_server);
|
||||
let server = ServerBuilder::new(io)
|
||||
.cors(DomainsValidation::AllowOnly(vec![
|
||||
AccessControlAllowOrigin::Null,
|
||||
]))
|
||||
.start_http(
|
||||
&http_server
|
||||
.parse()
|
||||
.expect("Invalid HTTP address and port combination"),
|
||||
)
|
||||
.expect("Unable to start RPC server");
|
||||
|
||||
server.wait();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use jsonrpc_core::{Error as JsonRpcError, ErrorCode};
|
||||
use jsonrpc_test as test_rpc;
|
||||
|
||||
#[test]
|
||||
fn rpc_success() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_sync_method("rpc_success_response", |_| {
|
||||
Ok(Value::String("success".into()))
|
||||
});
|
||||
test_rpc::Rpc::from(io)
|
||||
};
|
||||
|
||||
assert_eq!(rpc.request("rpc_success_response", &()), r#""success""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_parse_error() {
|
||||
let rpc = {
|
||||
let mut io = IoHandler::new();
|
||||
io.add_sync_method("rpc_parse_error", |_| {
|
||||
let e = JsonRpcError {
|
||||
code: ErrorCode::ParseError,
|
||||
message: String::from("Parse error"),
|
||||
data: None,
|
||||
};
|
||||
Err(JsonRpcError::from(JsonRpcServerError::MissingParameter(e)))
|
||||
});
|
||||
test_rpc::Rpc::from(io)
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
rpc.request("rpc_parse_error", &()),
|
||||
r#"{
|
||||
"code": -32700,
|
||||
"message": "Parse error"
|
||||
}"#
|
||||
);
|
||||
}
|
||||
}
|
34
peach-jsonrpc-server/src/main.rs
Normal file
34
peach-jsonrpc-server/src/main.rs
Normal file
@ -0,0 +1,34 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
//! # peach-jsonrpc-server
|
||||
//!
|
||||
//! A JSON-RPC server which exposes an over HTTP.
|
||||
//!
|
||||
//! Currently includes peach-stats capability (system statistics).
|
||||
//!
|
||||
//! ## API
|
||||
//!
|
||||
//! | Method | Description | Returns |
|
||||
//! | --- | --- | --- |
|
||||
//! | `cpu_stats` | CPU statistics | `user`, `system`, `nice`, `idle` |
|
||||
//! | `cpu_stats_percent` | CPU statistics as percentages | `user`, `system`, `nice`, `idle` |
|
||||
//! | `disk_usage` | Disk usage statistics (array of disks) | `filesystem`, `one_k_blocks`, `one_k_blocks_used`, `one_k_blocks_free`, `used_percentage`, `mountpoint` |
|
||||
//! | `load_average` | Load average statistics | `one`, `five`, `fifteen` |
|
||||
//! | `mem_stats` | Memory statistics | `total`, `free`, `used` |
|
||||
//! | `ping` | Microservice status | `success` if running |
|
||||
//! | `uptime` | System uptime | `secs` |
|
||||
|
||||
use std::process;
|
||||
|
||||
use log::error;
|
||||
|
||||
fn main() {
|
||||
// initalize the logger
|
||||
env_logger::init();
|
||||
|
||||
// handle errors returned from `run`
|
||||
if let Err(e) = peach_jsonrpc_server::run() {
|
||||
error!("Application error: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
@ -1,19 +1,23 @@
|
||||
[package]
|
||||
name = "peach-lib"
|
||||
version = "1.3.0"
|
||||
version = "1.3.2"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
async-std = "1.10"
|
||||
chrono = "0.4"
|
||||
dirs = "4.0"
|
||||
fslock="0.1"
|
||||
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi" }
|
||||
jsonrpc-client-core = "0.5"
|
||||
jsonrpc-client-http = "0.5"
|
||||
jsonrpc-core = "8.0.1"
|
||||
jsonrpc-core = "8.0"
|
||||
log = "0.4"
|
||||
nanorand = "0.6"
|
||||
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"
|
||||
toml = "0.5"
|
||||
sha3 = "0.10"
|
||||
|
@ -1,12 +1,14 @@
|
||||
//! Interfaces for writing and reading PeachCloud configurations, stored in yaml.
|
||||
//!
|
||||
//! Different PeachCloud microservices import peach-lib, so that they can share this interface.
|
||||
//! Different PeachCloud microservices import peach-lib, so that they can share
|
||||
//! this interface.
|
||||
//!
|
||||
//! The configuration file is located at: "/var/lib/peachcloud/config.yml"
|
||||
|
||||
use std::fs;
|
||||
|
||||
use fslock::LockFile;
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::PeachError;
|
||||
@ -17,6 +19,10 @@ pub const YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
|
||||
// lock file (used to avoid race conditions during config reading & writing)
|
||||
pub const LOCK_FILE_PATH: &str = "/var/lib/peachcloud/config.lock";
|
||||
|
||||
// default values
|
||||
pub const DEFAULT_DYN_SERVER_ADDRESS: &str = "http://dynserver.dyn.peachcloud.org";
|
||||
pub const DEFAULT_DYN_NAMESERVER: &str = "ns.peachcloud.org";
|
||||
|
||||
// we make use of Serde default values in order to make PeachCloud
|
||||
// robust and keep running even with a not fully complete config.yml
|
||||
// main type which represents all peachcloud configurations
|
||||
@ -29,6 +35,10 @@ pub struct PeachConfig {
|
||||
#[serde(default)]
|
||||
pub dyn_dns_server_address: String,
|
||||
#[serde(default)]
|
||||
pub dyn_use_custom_server: bool,
|
||||
#[serde(default)]
|
||||
pub dyn_nameserver: String,
|
||||
#[serde(default)]
|
||||
pub dyn_tsig_key_path: String,
|
||||
#[serde(default)] // default is false
|
||||
pub dyn_enabled: bool,
|
||||
@ -41,7 +51,7 @@ pub struct PeachConfig {
|
||||
}
|
||||
|
||||
// helper functions for serializing and deserializing PeachConfig from disc
|
||||
fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachError> {
|
||||
pub fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachError> {
|
||||
// use a file lock to avoid race conditions while saving config
|
||||
let mut lock = LockFile::open(LOCK_FILE_PATH)?;
|
||||
lock.lock()?;
|
||||
@ -63,29 +73,31 @@ fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachErro
|
||||
pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
|
||||
let peach_config_exists = std::path::Path::new(YAML_PATH).exists();
|
||||
|
||||
let peach_config: PeachConfig;
|
||||
|
||||
// if this is the first time loading peach_config, we can create a default here
|
||||
if !peach_config_exists {
|
||||
peach_config = PeachConfig {
|
||||
let peach_config: PeachConfig = if !peach_config_exists {
|
||||
debug!("Loading peach config: {} does not exist", YAML_PATH);
|
||||
PeachConfig {
|
||||
external_domain: "".to_string(),
|
||||
dyn_domain: "".to_string(),
|
||||
dyn_dns_server_address: "".to_string(),
|
||||
dyn_dns_server_address: DEFAULT_DYN_SERVER_ADDRESS.to_string(),
|
||||
dyn_use_custom_server: false,
|
||||
dyn_nameserver: DEFAULT_DYN_NAMESERVER.to_string(),
|
||||
dyn_tsig_key_path: "".to_string(),
|
||||
dyn_enabled: false,
|
||||
ssb_admin_ids: Vec::new(),
|
||||
admin_password_hash: "".to_string(),
|
||||
// default password is `peach`
|
||||
admin_password_hash: "146".to_string(),
|
||||
temporary_password_hash: "".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
// otherwise we load peach config from disk
|
||||
else {
|
||||
debug!("Loading peach config: {} exists", YAML_PATH);
|
||||
let contents = fs::read_to_string(YAML_PATH).map_err(|source| PeachError::Read {
|
||||
source,
|
||||
path: YAML_PATH.to_string(),
|
||||
})?;
|
||||
peach_config = serde_yaml::from_str(&contents)?;
|
||||
}
|
||||
serde_yaml::from_str(&contents)?
|
||||
};
|
||||
|
||||
Ok(peach_config)
|
||||
}
|
||||
@ -122,6 +134,18 @@ pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_dyndns_server_address() -> Result<String, PeachError> {
|
||||
let peach_config = load_peach_config()?;
|
||||
// if the user is using a custom dyn server then load the address from the config
|
||||
if peach_config.dyn_use_custom_server {
|
||||
Ok(peach_config.dyn_dns_server_address)
|
||||
}
|
||||
// otherwise hardcode the address
|
||||
else {
|
||||
Ok(DEFAULT_DYN_SERVER_ADDRESS.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result<PeachConfig, PeachError> {
|
||||
let mut peach_config = load_peach_config()?;
|
||||
peach_config.dyn_enabled = enabled_value;
|
||||
|
@ -9,13 +9,8 @@
|
||||
//!
|
||||
//! The domain for dyndns updates is stored in /var/lib/peachcloud/config.yml
|
||||
//! The tsig key for authenticating the updates is stored in /var/lib/peachcloud/peach-dyndns/tsig.key
|
||||
use std::{
|
||||
fs,
|
||||
fs::OpenOptions,
|
||||
io::Write,
|
||||
process::{Command, Stdio},
|
||||
str::FromStr,
|
||||
};
|
||||
use std::ffi::OsStr;
|
||||
use std::{fs, fs::OpenOptions, io::Write, process::Command, str::FromStr};
|
||||
|
||||
use chrono::prelude::*;
|
||||
use jsonrpc_client_core::{expand_params, jsonrpc_client};
|
||||
@ -23,13 +18,10 @@ use jsonrpc_client_http::HttpTransport;
|
||||
use log::{debug, info};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::{
|
||||
config_manager::{load_peach_config, set_peach_dyndns_config},
|
||||
error::PeachError,
|
||||
};
|
||||
use crate::config_manager::get_dyndns_server_address;
|
||||
use crate::{config_manager, error::PeachError};
|
||||
|
||||
/// constants for dyndns configuration
|
||||
pub const PEACH_DYNDNS_URL: &str = "http://dynserver.dyn.peachcloud.org";
|
||||
pub const TSIG_KEY_PATH: &str = "/var/lib/peachcloud/peach-dyndns/tsig.key";
|
||||
pub const PEACH_DYNDNS_CONFIG_PATH: &str = "/var/lib/peachcloud/peach-dyndns";
|
||||
pub const DYNDNS_LOG_PATH: &str = "/var/lib/peachcloud/peach-dyndns/latest_result.log";
|
||||
@ -62,9 +54,10 @@ pub fn save_dyndns_key(key: &str) -> Result<(), PeachError> {
|
||||
pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError> {
|
||||
debug!("Creating HTTP transport for dyndns client.");
|
||||
let transport = HttpTransport::new().standalone()?;
|
||||
let http_server = PEACH_DYNDNS_URL;
|
||||
debug!("Creating HTTP transport handle on {}.", http_server);
|
||||
let transport_handle = transport.handle(http_server)?;
|
||||
let http_server = get_dyndns_server_address()?;
|
||||
info!("Using dyndns http server address: {:?}", http_server);
|
||||
debug!("Creating HTTP transport handle on {}.", &http_server);
|
||||
let transport_handle = transport.handle(&http_server)?;
|
||||
info!("Creating client for peach-dyndns service.");
|
||||
let mut client = PeachDynDnsClient::new(transport_handle);
|
||||
|
||||
@ -73,7 +66,8 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError>
|
||||
// save new TSIG key
|
||||
save_dyndns_key(&key)?;
|
||||
// save new configuration values
|
||||
let set_config_result = set_peach_dyndns_config(domain, PEACH_DYNDNS_URL, TSIG_KEY_PATH, true);
|
||||
let set_config_result =
|
||||
config_manager::set_peach_dyndns_config(domain, &http_server, TSIG_KEY_PATH, true);
|
||||
match set_config_result {
|
||||
Ok(_) => {
|
||||
let response = "success".to_string();
|
||||
@ -87,9 +81,9 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError>
|
||||
pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError> {
|
||||
debug!("Creating HTTP transport for dyndns client.");
|
||||
let transport = HttpTransport::new().standalone()?;
|
||||
let http_server = PEACH_DYNDNS_URL;
|
||||
debug!("Creating HTTP transport handle on {}.", http_server);
|
||||
let transport_handle = transport.handle(http_server)?;
|
||||
let http_server = get_dyndns_server_address()?;
|
||||
debug!("Creating HTTP transport handle on {}.", &http_server);
|
||||
let transport_handle = transport.handle(&http_server)?;
|
||||
info!("Creating client for peach_network service.");
|
||||
let mut client = PeachDynDnsClient::new(transport_handle);
|
||||
|
||||
@ -105,7 +99,7 @@ pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError
|
||||
/// Helper function to get public ip address of PeachCloud device.
|
||||
fn get_public_ip_address() -> Result<String, PeachError> {
|
||||
// TODO: consider other ways to get public IP address
|
||||
let output = Command::new("/usr/bin/curl").arg("ifconfig.me").output()?;
|
||||
let output = Command::new("curl").arg("ifconfig.me").output()?;
|
||||
let command_output = String::from_utf8(output.stdout)?;
|
||||
Ok(command_output)
|
||||
}
|
||||
@ -113,31 +107,31 @@ fn get_public_ip_address() -> Result<String, PeachError> {
|
||||
/// Reads dyndns configurations from config.yml
|
||||
/// and then uses nsupdate to update the IP address for the configured domain
|
||||
pub fn dyndns_update_ip() -> Result<bool, PeachError> {
|
||||
info!("Running dyndns_update_ip");
|
||||
let peach_config = load_peach_config()?;
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
info!(
|
||||
"Using config:
|
||||
dyn_tsig_key_path: {:?}
|
||||
dyn_domain: {:?}
|
||||
dyn_dns_server_address: {:?}
|
||||
dyn_enabled: {:?}
|
||||
dyn_nameserver: {:?}
|
||||
",
|
||||
peach_config.dyn_tsig_key_path,
|
||||
peach_config.dyn_domain,
|
||||
peach_config.dyn_dns_server_address,
|
||||
peach_config.dyn_enabled,
|
||||
peach_config.dyn_nameserver,
|
||||
);
|
||||
if !peach_config.dyn_enabled {
|
||||
info!("dyndns is not enabled, not updating");
|
||||
Ok(false)
|
||||
} else {
|
||||
// call nsupdate passing appropriate configs
|
||||
let mut nsupdate_command = Command::new("/usr/bin/nsupdate")
|
||||
let mut nsupdate_command = Command::new("nsupdate");
|
||||
nsupdate_command
|
||||
.arg("-k")
|
||||
.arg(&peach_config.dyn_tsig_key_path)
|
||||
.arg("-v")
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()?;
|
||||
.arg("-v");
|
||||
// pass nsupdate commands via stdin
|
||||
let public_ip_address = get_public_ip_address()?;
|
||||
info!("found public ip address: {}", public_ip_address);
|
||||
@ -148,20 +142,20 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
|
||||
update delete {DOMAIN} A
|
||||
update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS}
|
||||
send",
|
||||
NAMESERVER = "ns.peachcloud.org",
|
||||
NAMESERVER = peach_config.dyn_nameserver,
|
||||
ZONE = peach_config.dyn_domain,
|
||||
DOMAIN = peach_config.dyn_domain,
|
||||
PUBLIC_IP_ADDRESS = public_ip_address,
|
||||
);
|
||||
let mut nsupdate_stdin = nsupdate_command.stdin.take().ok_or(PeachError::NsUpdate {
|
||||
msg: "unable to capture stdin handle for `nsupdate` command".to_string(),
|
||||
})?;
|
||||
write!(nsupdate_stdin, "{}", ns_commands).map_err(|source| PeachError::Write {
|
||||
source,
|
||||
path: peach_config.dyn_tsig_key_path.to_string(),
|
||||
})?;
|
||||
let nsupdate_output = nsupdate_command.wait_with_output()?;
|
||||
info!("nsupdate output: {:?}", nsupdate_output);
|
||||
info!("ns_commands: {:?}", ns_commands);
|
||||
info!("creating nsupdate temp file");
|
||||
let temp_file_path = "/var/lib/peachcloud/nsupdate.sh";
|
||||
// write ns_commands to temp_file
|
||||
fs::write(temp_file_path, ns_commands)?;
|
||||
nsupdate_command.arg(temp_file_path);
|
||||
let nsupdate_output = nsupdate_command.output()?;
|
||||
let args: Vec<&OsStr> = nsupdate_command.get_args().collect();
|
||||
info!("nsupdate command: {:?}", args);
|
||||
// We only return a successful result if nsupdate was successful
|
||||
if nsupdate_output.status.success() {
|
||||
info!("nsupdate succeeded, returning ok");
|
||||
@ -204,7 +198,7 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
|
||||
})?;
|
||||
// replace newline if found
|
||||
// TODO: maybe we can use `.trim()` instead
|
||||
let contents = contents.replace("\n", "");
|
||||
let contents = contents.replace('\n', "");
|
||||
// TODO: consider adding additional context?
|
||||
let time_ran_dt = DateTime::parse_from_rfc3339(&contents).map_err(|source| {
|
||||
PeachError::ParseDateTime {
|
||||
@ -223,20 +217,15 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
|
||||
/// and has successfully run recently (in the last six minutes)
|
||||
pub fn is_dns_updater_online() -> Result<bool, PeachError> {
|
||||
// first check if it is enabled in peach-config
|
||||
let peach_config = load_peach_config()?;
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
let is_enabled = peach_config.dyn_enabled;
|
||||
// then check if it has successfully run within the last 6 minutes (60*6 seconds)
|
||||
let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?;
|
||||
let ran_recently: bool;
|
||||
match num_seconds_since_successful_update {
|
||||
Some(seconds) => {
|
||||
ran_recently = seconds < (60 * 6);
|
||||
}
|
||||
let ran_recently: bool = match num_seconds_since_successful_update {
|
||||
Some(seconds) => seconds < (60 * 6),
|
||||
// if the value is None, then the last time it ran successfully is unknown
|
||||
None => {
|
||||
ran_recently = false;
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
};
|
||||
// debug log
|
||||
info!("is_dyndns_enabled: {:?}", is_enabled);
|
||||
info!("dyndns_ran_recently: {:?}", ran_recently);
|
||||
@ -258,11 +247,10 @@ pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
// helper function which checks if a dyndns domain is new
|
||||
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> bool {
|
||||
// TODO: return `Result<bool, PeachError>` and replace `unwrap` with `?` operator
|
||||
let peach_config = load_peach_config().unwrap();
|
||||
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> Result<bool, PeachError> {
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
let previous_dyndns_domain = peach_config.dyn_domain;
|
||||
dyndns_full_domain != previous_dyndns_domain
|
||||
Ok(dyndns_full_domain != previous_dyndns_domain)
|
||||
}
|
||||
|
||||
jsonrpc_client!(pub struct PeachDynDnsClient {
|
||||
|
@ -7,6 +7,9 @@ use std::{io, str, string};
|
||||
/// This type represents all possible errors that can occur when interacting with the PeachCloud library.
|
||||
#[derive(Debug)]
|
||||
pub enum PeachError {
|
||||
/// Represents a failure to determine the path of the user's home directory.
|
||||
HomeDir,
|
||||
|
||||
/// Represents all other cases of `std::io::Error`.
|
||||
Io(io::Error),
|
||||
|
||||
@ -58,15 +61,18 @@ pub enum PeachError {
|
||||
/// Represents a failure to parse or compile a regular expression.
|
||||
Regex(regex::Error),
|
||||
|
||||
/// Represents a failure to successfully execute an sbot command.
|
||||
SbotCli {
|
||||
/// The `stderr` output from the sbot command.
|
||||
msg: String,
|
||||
},
|
||||
/// Represents a failure to successfully execute an sbot command (via golgi).
|
||||
Sbot(String),
|
||||
|
||||
/// Represents a failure to serialize or deserialize JSON.
|
||||
SerdeJson(serde_json::error::Error),
|
||||
|
||||
/// Represents a failure to deserialize TOML.
|
||||
TomlDeser(toml::de::Error),
|
||||
|
||||
/// Represents a failure to serialize TOML.
|
||||
TomlSer(toml::ser::Error),
|
||||
|
||||
/// Represents a failure to serialize or deserialize YAML.
|
||||
SerdeYaml(serde_yaml::Error),
|
||||
|
||||
@ -87,7 +93,7 @@ pub enum PeachError {
|
||||
Write {
|
||||
/// The underlying source of the error.
|
||||
source: io::Error,
|
||||
/// The file path for the write attemp.
|
||||
/// The file path for the write attempt.
|
||||
path: String,
|
||||
},
|
||||
}
|
||||
@ -95,6 +101,7 @@ pub enum PeachError {
|
||||
impl std::error::Error for PeachError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match *self {
|
||||
PeachError::HomeDir => None,
|
||||
PeachError::Io(_) => None,
|
||||
PeachError::JsonRpcClientCore(_) => None,
|
||||
PeachError::JsonRpcCore(_) => None,
|
||||
@ -107,10 +114,12 @@ impl std::error::Error for PeachError {
|
||||
PeachError::PasswordNotSet => None,
|
||||
PeachError::Read { ref source, .. } => Some(source),
|
||||
PeachError::Regex(_) => None,
|
||||
PeachError::SbotCli { .. } => None,
|
||||
PeachError::Sbot(_) => None,
|
||||
PeachError::SerdeJson(_) => None,
|
||||
PeachError::SerdeYaml(_) => None,
|
||||
PeachError::SsbAdminIdNotFound { .. } => None,
|
||||
PeachError::TomlDeser(_) => None,
|
||||
PeachError::TomlSer(_) => None,
|
||||
PeachError::Utf8ToStr(_) => None,
|
||||
PeachError::Utf8ToString(_) => None,
|
||||
PeachError::Write { ref source, .. } => Some(source),
|
||||
@ -121,6 +130,12 @@ impl std::error::Error for PeachError {
|
||||
impl std::fmt::Display for PeachError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match *self {
|
||||
PeachError::HomeDir => {
|
||||
write!(
|
||||
f,
|
||||
"Unable to determine the path of the user's home directory"
|
||||
)
|
||||
}
|
||||
PeachError::Io(ref err) => err.fmt(f),
|
||||
PeachError::JsonRpcClientCore(ref err) => err.fmt(f),
|
||||
PeachError::JsonRpcCore(ref err) => {
|
||||
@ -135,22 +150,19 @@ impl std::fmt::Display for PeachError {
|
||||
write!(f, "Date/time parse error: {}", path)
|
||||
}
|
||||
PeachError::PasswordIncorrect => {
|
||||
write!(f, "Password error: user-supplied password is incorrect")
|
||||
write!(f, "password is incorrect")
|
||||
}
|
||||
PeachError::PasswordMismatch => {
|
||||
write!(f, "Password error: user-supplied passwords do not match")
|
||||
write!(f, "passwords do not match")
|
||||
}
|
||||
PeachError::PasswordNotSet => {
|
||||
write!(
|
||||
f,
|
||||
"Password error: hash value in YAML configuration file is empty"
|
||||
)
|
||||
write!(f, "hash value in YAML configuration file is empty")
|
||||
}
|
||||
PeachError::Read { ref path, .. } => {
|
||||
write!(f, "Read error: {}", path)
|
||||
}
|
||||
PeachError::Regex(ref err) => err.fmt(f),
|
||||
PeachError::SbotCli { ref msg } => {
|
||||
PeachError::Sbot(ref msg) => {
|
||||
write!(f, "Sbot error: {}", msg)
|
||||
}
|
||||
PeachError::SerdeJson(ref err) => err.fmt(f),
|
||||
@ -158,6 +170,8 @@ impl std::fmt::Display for PeachError {
|
||||
PeachError::SsbAdminIdNotFound { ref id } => {
|
||||
write!(f, "Config error: SSB admin ID `{}` not found", id)
|
||||
}
|
||||
PeachError::TomlDeser(ref err) => err.fmt(f),
|
||||
PeachError::TomlSer(ref err) => err.fmt(f),
|
||||
PeachError::Utf8ToStr(ref err) => err.fmt(f),
|
||||
PeachError::Utf8ToString(ref err) => err.fmt(f),
|
||||
PeachError::Write { ref path, .. } => {
|
||||
@ -209,6 +223,18 @@ impl From<serde_yaml::Error> for PeachError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::de::Error> for PeachError {
|
||||
fn from(err: toml::de::Error) -> PeachError {
|
||||
PeachError::TomlDeser(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::ser::Error> for PeachError {
|
||||
fn from(err: toml::ser::Error) -> PeachError {
|
||||
PeachError::TomlSer(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<str::Utf8Error> for PeachError {
|
||||
fn from(err: str::Utf8Error) -> PeachError {
|
||||
PeachError::Utf8ToStr(err)
|
||||
|
@ -4,7 +4,7 @@ pub mod error;
|
||||
pub mod network_client;
|
||||
pub mod oled_client;
|
||||
pub mod password_utils;
|
||||
pub mod sbot_client;
|
||||
pub mod sbot;
|
||||
pub mod stats_client;
|
||||
|
||||
// re-export error types
|
||||
|
@ -1,15 +1,16 @@
|
||||
use std::iter;
|
||||
use async_std::task;
|
||||
use golgi::Sbot;
|
||||
use log::debug;
|
||||
use nanorand::{Rng, WyRand};
|
||||
use sha3::{Digest, Sha3_256};
|
||||
|
||||
use crypto::{digest::Digest, sha3::Sha3};
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
|
||||
use crate::{config_manager, error::PeachError, sbot_client};
|
||||
use crate::{config_manager, error::PeachError, sbot::SbotConfig};
|
||||
|
||||
/// Returns Ok(()) if the supplied password is correct,
|
||||
/// and returns Err if the supplied password is incorrect.
|
||||
pub fn verify_password(password: &str) -> Result<(), PeachError> {
|
||||
let real_admin_password_hash = config_manager::get_admin_password_hash()?;
|
||||
let password_hash = hash_password(&password.to_string());
|
||||
let password_hash = hash_password(password);
|
||||
if real_admin_password_hash == password_hash {
|
||||
Ok(())
|
||||
} else {
|
||||
@ -31,7 +32,7 @@ pub fn validate_new_passwords(new_password1: &str, new_password2: &str) -> Resul
|
||||
|
||||
/// Sets a new password for the admin user
|
||||
pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
|
||||
let new_password_hash = hash_password(&new_password.to_string());
|
||||
let new_password_hash = hash_password(new_password);
|
||||
config_manager::set_admin_password_hash(&new_password_hash)?;
|
||||
|
||||
Ok(())
|
||||
@ -39,15 +40,19 @@ pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
|
||||
|
||||
/// Creates a hash from a password string
|
||||
pub fn hash_password(password: &str) -> String {
|
||||
let mut hasher = Sha3::sha3_256();
|
||||
hasher.input_str(password);
|
||||
hasher.result_str()
|
||||
let mut hasher = Sha3_256::new();
|
||||
// write input message
|
||||
hasher.update(password);
|
||||
// read hash digest
|
||||
let result = hasher.finalize();
|
||||
// convert `u8` to `String`
|
||||
result[0].to_string()
|
||||
}
|
||||
|
||||
/// Sets a new temporary password for the admin user
|
||||
/// which can be used to reset the permanent password
|
||||
pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> {
|
||||
let new_password_hash = hash_password(&new_password.to_string());
|
||||
let new_password_hash = hash_password(new_password);
|
||||
config_manager::set_temporary_password_hash(&new_password_hash)?;
|
||||
|
||||
Ok(())
|
||||
@ -57,7 +62,7 @@ pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError>
|
||||
/// and returns Err if the supplied temp_password is incorrect
|
||||
pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> {
|
||||
let temporary_admin_password_hash = config_manager::get_temporary_password_hash()?;
|
||||
let password_hash = hash_password(&password.to_string());
|
||||
let password_hash = hash_password(password);
|
||||
if temporary_admin_password_hash == password_hash {
|
||||
Ok(())
|
||||
} else {
|
||||
@ -68,13 +73,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()?;
|
||||
@ -84,7 +86,7 @@ pub fn send_password_reset() -> Result<(), PeachError> {
|
||||
"Your new temporary password is: {}
|
||||
|
||||
If you are on the same WiFi network as your PeachCloud device you can reset your password \
|
||||
using this link: http://peach.local/reset_password",
|
||||
using this link: http://peach.local/auth/reset",
|
||||
temporary_password
|
||||
);
|
||||
// if there is an external domain, then include remote link in message
|
||||
@ -93,7 +95,7 @@ using this link: http://peach.local/reset_password",
|
||||
Some(domain) => {
|
||||
format!(
|
||||
"\n\nOr if you are on a different WiFi network, you can reset your password \
|
||||
using the the following link: {}/reset_password",
|
||||
using the the following link: {}/auth/reset",
|
||||
domain
|
||||
)
|
||||
}
|
||||
@ -103,7 +105,37 @@ using this link: http://peach.local/reset_password",
|
||||
// finally send the message to the admins
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
for ssb_admin_id in peach_config.ssb_admin_ids {
|
||||
sbot_client::private_message(&msg, &ssb_admin_id)?;
|
||||
// use golgi to send a private message on scuttlebutt
|
||||
match task::block_on(publish_private_msg(&msg, &ssb_admin_id)) {
|
||||
Ok(_) => (),
|
||||
Err(e) => return Err(PeachError::Sbot(e)),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn publish_private_msg(msg: &str, recipient: &str) -> Result<(), String> {
|
||||
// retrieve latest go-sbot configuration parameters
|
||||
let sbot_config = SbotConfig::read().ok();
|
||||
|
||||
let msg = msg.to_string();
|
||||
let recipient = vec![recipient.to_string()];
|
||||
|
||||
// initialise sbot connection with ip:port and shscap from config file
|
||||
let mut sbot_client = match sbot_config {
|
||||
// TODO: panics if we pass `Some(conf.shscap)` as second arg
|
||||
Some(conf) => {
|
||||
let ip_port = conf.lis.clone();
|
||||
Sbot::init(Some(ip_port), None)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
None => Sbot::init(None, None).await.map_err(|e| e.to_string())?,
|
||||
};
|
||||
|
||||
debug!("Publishing a Scuttlebutt private message with temporary password");
|
||||
match sbot_client.publish_private(msg, recipient).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("Failed to publish private message: {}", e)),
|
||||
}
|
||||
}
|
||||
|
235
peach-lib/src/sbot.rs
Normal file
235
peach-lib/src/sbot.rs
Normal file
@ -0,0 +1,235 @@
|
||||
//! Data types and associated methods for monitoring and configuring go-sbot.
|
||||
|
||||
use std::{fs, fs::File, io, io::Write, path::PathBuf, process::Command, str};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::PeachError;
|
||||
|
||||
/* HELPER FUNCTIONS */
|
||||
|
||||
// iterate over the given directory path to determine the size of the directory
|
||||
fn dir_size(path: impl Into<PathBuf>) -> io::Result<u64> {
|
||||
fn dir_size(mut dir: fs::ReadDir) -> io::Result<u64> {
|
||||
dir.try_fold(0, |acc, file| {
|
||||
let file = file?;
|
||||
let size = match file.metadata()? {
|
||||
data if data.is_dir() => dir_size(fs::read_dir(file.path())?)?,
|
||||
data => data.len(),
|
||||
};
|
||||
Ok(acc + size)
|
||||
})
|
||||
}
|
||||
|
||||
dir_size(fs::read_dir(path.into())?)
|
||||
}
|
||||
|
||||
/* SBOT-RELATED TYPES AND METHODS */
|
||||
|
||||
/// go-sbot process status.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SbotStatus {
|
||||
/// Current process state.
|
||||
pub state: Option<String>,
|
||||
/// Current process boot state.
|
||||
pub boot_state: Option<String>,
|
||||
/// Current process memory usage in bytes.
|
||||
pub memory: Option<u32>,
|
||||
/// Uptime for the process (if state is `active`).
|
||||
pub uptime: Option<String>,
|
||||
/// Downtime for the process (if state is `inactive`).
|
||||
pub downtime: Option<String>,
|
||||
/// Size of the blobs directory in bytes.
|
||||
pub blobstore: Option<u64>,
|
||||
}
|
||||
|
||||
/// Default builder for `SbotStatus`.
|
||||
impl Default for SbotStatus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: None,
|
||||
boot_state: None,
|
||||
memory: None,
|
||||
uptime: None,
|
||||
downtime: None,
|
||||
blobstore: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SbotStatus {
|
||||
/// Retrieve statistics for the go-sbot systemd process by querying `systemctl`.
|
||||
pub fn read() -> Result<Self, PeachError> {
|
||||
let mut status = SbotStatus::default();
|
||||
|
||||
let info_output = Command::new("systemctl")
|
||||
.arg("--user")
|
||||
.arg("show")
|
||||
.arg("go-sbot.service")
|
||||
.arg("--no-page")
|
||||
.output()?;
|
||||
|
||||
let service_info = std::str::from_utf8(&info_output.stdout)?;
|
||||
|
||||
for line in service_info.lines() {
|
||||
if line.starts_with("ActiveState=") {
|
||||
if let Some(state) = line.strip_prefix("ActiveState=") {
|
||||
status.state = Some(state.to_string())
|
||||
}
|
||||
} else if line.starts_with("MemoryCurrent=") {
|
||||
if let Some(memory) = line.strip_prefix("MemoryCurrent=") {
|
||||
status.memory = memory.parse().ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let status_output = Command::new("systemctl")
|
||||
.arg("--user")
|
||||
.arg("status")
|
||||
.arg("go-sbot.service")
|
||||
.output()?;
|
||||
|
||||
let service_status = str::from_utf8(&status_output.stdout)?;
|
||||
//.map_err(PeachError::Utf8ToStr)?;
|
||||
|
||||
for line in service_status.lines() {
|
||||
// example of the output line we're looking for:
|
||||
// `Loaded: loaded (/home/glyph/.config/systemd/user/go-sbot.service; enabled; vendor
|
||||
// preset: enabled)`
|
||||
if line.contains("Loaded:") {
|
||||
let before_boot_state = line.find(';');
|
||||
let after_boot_state = line.rfind(';');
|
||||
if let (Some(start), Some(end)) = (before_boot_state, after_boot_state) {
|
||||
// extract the enabled / disabled from the `Loaded: ...` line
|
||||
// using the index of the first ';' + 2 and the last ';'
|
||||
status.boot_state = Some(line[start + 2..end].to_string());
|
||||
}
|
||||
// example of the output line we're looking for here:
|
||||
// `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago`
|
||||
} else if line.contains("Active:") {
|
||||
let before_time = line.find(';');
|
||||
let after_time = line.find(" ago");
|
||||
if let (Some(start), Some(end)) = (before_time, after_time) {
|
||||
// extract the uptime / downtime from the `Active: ...` line
|
||||
// using the index of ';' + 2 and the index of " ago"
|
||||
let time = Some(&line[start + 2..end]);
|
||||
// if service is active then the `time` reading is uptime
|
||||
if status.state == Some("active".to_string()) {
|
||||
status.uptime = time.map(|t| t.to_string())
|
||||
// if service is inactive then the `time` reading is downtime
|
||||
} else if status.state == Some("inactive".to_string()) {
|
||||
status.downtime = time.map(|t| t.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// determine path of user's home directory
|
||||
let mut blobstore_path = dirs::home_dir().ok_or(PeachError::HomeDir)?;
|
||||
|
||||
// append the blobstore path
|
||||
blobstore_path.push(".ssb-go/blobs/sha256");
|
||||
|
||||
// determine the size of the blobstore directory in bytes
|
||||
status.blobstore = dir_size(blobstore_path).ok();
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
}
|
||||
|
||||
/// go-sbot configuration parameters.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct SbotConfig {
|
||||
// TODO: maybe define as a Path type?
|
||||
/// Directory path for the log and indexes.
|
||||
pub repo: String,
|
||||
/// Directory path for writing debug output.
|
||||
pub debugdir: String,
|
||||
/// Secret-handshake app-key (aka. network key).
|
||||
pub shscap: String,
|
||||
/// HMAC hash used to sign messages.
|
||||
pub hmac: String,
|
||||
/// Replication hops (1: friends, 2: friends of friends).
|
||||
pub hops: u8,
|
||||
/// Address to listen on.
|
||||
pub lis: String,
|
||||
/// Address to listen on for WebSocket connections.
|
||||
pub wslis: String,
|
||||
/// Address to for metrics and pprof HTTP server.
|
||||
pub debuglis: String,
|
||||
/// Enable sending local UDP broadcasts.
|
||||
pub localadv: bool,
|
||||
/// Enable listening for UDP broadcasts and connecting.
|
||||
pub localdiscov: bool,
|
||||
/// Enable syncing by using epidemic-broadcast-trees (EBT).
|
||||
#[serde(rename(serialize = "enable_ebt", deserialize = "enable-ebt"))]
|
||||
pub enable_ebt: bool,
|
||||
/// Bypass graph auth and fetch remote's feed (useful for pubs that are restoring their data
|
||||
/// from peer; user beware - caveats about).
|
||||
pub promisc: bool,
|
||||
/// Disable the UNIX socket RPC interface.
|
||||
pub nounixsock: bool,
|
||||
/// Attempt to repair the filesystem before starting.
|
||||
pub repair: bool,
|
||||
}
|
||||
|
||||
/// Default configuration values for go-sbot.
|
||||
impl Default for SbotConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
repo: ".ssb-go".to_string(),
|
||||
debugdir: "".to_string(),
|
||||
shscap: "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=".to_string(),
|
||||
hmac: "".to_string(),
|
||||
hops: 1,
|
||||
lis: ":8008".to_string(),
|
||||
wslis: ":8989".to_string(),
|
||||
debuglis: "localhost:6078".to_string(),
|
||||
localadv: false,
|
||||
localdiscov: false,
|
||||
enable_ebt: false,
|
||||
promisc: false,
|
||||
nounixsock: false,
|
||||
repair: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SbotConfig {
|
||||
/// Read the go-sbot `config.toml` file from file and deserialize into `SbotConfig`.
|
||||
pub fn read() -> Result<Self, PeachError> {
|
||||
// determine path of user's home directory
|
||||
let mut config_path = dirs::home_dir().ok_or(PeachError::HomeDir)?;
|
||||
config_path.push(".ssb-go/config.toml");
|
||||
|
||||
let config_contents = fs::read_to_string(config_path)?;
|
||||
|
||||
let config: SbotConfig = toml::from_str(&config_contents)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Write the given `SbotConfig` to the go-sbot `config.toml` file.
|
||||
pub fn write(config: SbotConfig) -> Result<(), PeachError> {
|
||||
let repo_comment = "# For details about go-sbot configuration, please visit the repo: https://github.com/cryptoscope/ssb\n".to_string();
|
||||
|
||||
// convert the provided `SbotConfig` instance to a string
|
||||
let config_string = toml::to_string(&config)?;
|
||||
|
||||
// determine path of user's home directory
|
||||
let mut config_path = dirs::home_dir().ok_or(PeachError::HomeDir)?;
|
||||
config_path.push(".ssb-go/config.toml");
|
||||
|
||||
// open config file for writing
|
||||
let mut file = File::create(config_path)?;
|
||||
|
||||
// write the repo comment to file
|
||||
write!(file, "{}", repo_comment)?;
|
||||
|
||||
// write the config string to file
|
||||
write!(file, "{}", config_string)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
//! Interfaces for monitoring and configuring go-sbot using sbotcli.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::PeachError;
|
||||
|
||||
pub fn is_sbot_online() -> Result<bool, PeachError> {
|
||||
let output = Command::new("/usr/bin/systemctl")
|
||||
.arg("status")
|
||||
.arg("peach-go-sbot")
|
||||
.output()?;
|
||||
let status = output.status;
|
||||
// returns true if the service had an exist status of 0 (is running)
|
||||
let is_running = status.success();
|
||||
Ok(is_running)
|
||||
}
|
||||
|
||||
/// currently go-sbotcli determines where the working directory is
|
||||
/// using the home directory of th user that invokes it
|
||||
/// this could be changed to be supplied as CLI arg
|
||||
/// but for now all sbotcli commands must first become peach-go-sbot before running
|
||||
/// the sudoers file is configured to allow this to happen without a password
|
||||
pub fn sbotcli_command() -> Command {
|
||||
let mut command = Command::new("sudo");
|
||||
command
|
||||
.arg("-u")
|
||||
.arg("peach-go-sbot")
|
||||
.arg("/usr/bin/sbotcli");
|
||||
command
|
||||
}
|
||||
|
||||
pub fn post(msg: &str) -> Result<(), PeachError> {
|
||||
let mut command = sbotcli_command();
|
||||
let output = command.arg("publish").arg("post").arg(msg).output()?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||
Err(PeachError::SbotCli {
|
||||
msg: format!("Error making ssb post: {}", stderr),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct WhoAmIValue {
|
||||
id: String,
|
||||
}
|
||||
|
||||
pub fn whoami() -> Result<String, PeachError> {
|
||||
let mut command = sbotcli_command();
|
||||
let output = command.arg("call").arg("whoami").output()?;
|
||||
let text_output = std::str::from_utf8(&output.stdout)?;
|
||||
let value: WhoAmIValue = serde_json::from_str(text_output)?;
|
||||
let id = value.id;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn create_invite(uses: i32) -> Result<String, PeachError> {
|
||||
let mut command = sbotcli_command();
|
||||
let output = command
|
||||
.arg("invite")
|
||||
.arg("create")
|
||||
.arg("--uses")
|
||||
.arg(uses.to_string())
|
||||
.output()?;
|
||||
let text_output = std::str::from_utf8(&output.stdout)?;
|
||||
let output = text_output.replace("\n", "");
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub fn update_pub_name(new_name: &str) -> Result<(), PeachError> {
|
||||
let pub_ssb_id = whoami()?;
|
||||
let mut command = sbotcli_command();
|
||||
let output = command
|
||||
.arg("publish")
|
||||
.arg("about")
|
||||
.arg("--name")
|
||||
.arg(new_name)
|
||||
.arg(pub_ssb_id)
|
||||
.output()?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||
Err(PeachError::SbotCli {
|
||||
msg: format!("Error updating pub name: {}", stderr),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn private_message(msg: &str, recipient: &str) -> Result<(), PeachError> {
|
||||
let mut command = sbotcli_command();
|
||||
let output = command
|
||||
.arg("publish")
|
||||
.arg("post")
|
||||
.arg("--recps")
|
||||
.arg(recipient)
|
||||
.arg(msg)
|
||||
.output()?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = std::str::from_utf8(&output.stderr)?;
|
||||
Err(PeachError::SbotCli {
|
||||
msg: format!("Error sending ssb private message: {}", stderr),
|
||||
})
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,44 +1,32 @@
|
||||
[package]
|
||||
name = "peach-network"
|
||||
version = "0.2.12"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
|
||||
edition = "2018"
|
||||
description = "Query and configure network interfaces using JSON-RPC over HTTP."
|
||||
version = "0.4.2"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
edition = "2021"
|
||||
description = "Query and configure network interfaces."
|
||||
homepage = "https://opencollective.com/peachcloud"
|
||||
repository = "https://github.com/peachcloud/peach-network"
|
||||
repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-network"
|
||||
readme = "README.md"
|
||||
license = "AGPL-3.0-only"
|
||||
license = "LGPL-3.0-only"
|
||||
publish = false
|
||||
|
||||
[package.metadata.deb]
|
||||
depends = "$auto"
|
||||
extended-description = """\
|
||||
peach-network is a microservice to query and configure network interfaces \
|
||||
using JSON-RPC over HTTP."""
|
||||
maintainer-scripts="debian"
|
||||
systemd-units = { unit-name = "peach-network" }
|
||||
assets = [
|
||||
["target/release/peach-network", "usr/bin/", "755"],
|
||||
["README.md", "usr/share/doc/peach-network/README", "644"],
|
||||
]
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "peachcloud/peach-network", branch = "master" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.6"
|
||||
failure = "0.1"
|
||||
get_if_addrs = "0.5.3"
|
||||
jsonrpc-core = "11"
|
||||
jsonrpc-http-server = "11"
|
||||
log = "0.4"
|
||||
probes = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
snafu = "0.6"
|
||||
miniserde = { version = "0.1.15", optional = true }
|
||||
probes = "0.4.1"
|
||||
serde = { version = "1.0.130", features = ["derive"], optional = true }
|
||||
regex = "1"
|
||||
wpactrl = "0.3.1"
|
||||
# replace this with crate import once latest changes have been published
|
||||
wpactrl = { git = "https://github.com/sauyon/wpa-ctrl-rs.git", branch = "master" }
|
||||
|
||||
[dev-dependencies]
|
||||
jsonrpc-test = "11"
|
||||
[features]
|
||||
default = []
|
||||
|
||||
# Provide `Serialize` and `Deserialize` traits for library structs using `miniserde`
|
||||
miniserde_support = ["miniserde"]
|
||||
|
||||
# Provide `Serialize` and `Deserialize` traits for library structs using `serde`
|
||||
serde_support = ["serde"]
|
||||
|
@ -1,178 +1,46 @@
|
||||
# peach-network
|
||||
|
||||
[](https://travis-ci.com/peachcloud/peach-network) 
|
||||

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

|
||||
|
||||
## Web Interface for PeachCloud
|
||||
|
||||
**peach-web** provides a web interface for the PeachCloud device. It serves static assets and exposes a JSON API for programmatic interactions.
|
||||
**peach-web** provides a web interface for the PeachCloud device.
|
||||
|
||||
Initial development is focused on administration of the device itself, beginning with networking functionality, with SSB-related administration to be integrated at a later stage.
|
||||
The web interface is primarily designed as a means of managing a Scuttlebutt pub. As such, it exposes the following features:
|
||||
|
||||
The peach-web stack currently consists of [Rocket](https://rocket.rs/) (Rust web framework), [Tera](http://tera.netlify.com/) (Rust template engine), HTML, CSS and JavaScript.
|
||||
- Create a Scuttlebutt profile
|
||||
- Follow, unfollow, block and unblock peers
|
||||
- Generate pub invite codes
|
||||
- Configure the sbot (hops, log directory, LAN discovery etc.)
|
||||
- Send private messages
|
||||
- Stop, start and restart the sbot
|
||||
|
||||
Additional features are focused on administration of the device itself. This includes networking functionality and device statistics.
|
||||
|
||||
The peach-web stack currently consists of [Rouille](https://crates.io/crates/rouille) (Rust web framework), [Maud](https://maud.lambda.xyz/) (Rust template engine), HTML and CSS. Scuttlebutt functionality is provided by [golgi](http://golgi.mycelial.technology).
|
||||
|
||||
_Note: This is a work-in-progress._
|
||||
|
||||
### Setup
|
||||
## Setup
|
||||
|
||||
Clone the `peach-workspace` repo:
|
||||
|
||||
@ -23,27 +32,23 @@ Move into the repo and compile:
|
||||
`cd peach-workspace/peach-web`
|
||||
`cargo build --release`
|
||||
|
||||
Run the tests:
|
||||
|
||||
`cargo test`
|
||||
|
||||
Move back to the `peach-workspace` directory:
|
||||
|
||||
`cd ..`
|
||||
|
||||
Run the binary:
|
||||
|
||||
`./target/release/peach-web`
|
||||
`../target/release/peach-web`
|
||||
|
||||
_Note: Networking functionality requires peach-network microservice to be running._
|
||||
## Environment
|
||||
|
||||
### Environment
|
||||
### Configuration Mode
|
||||
|
||||
The web application deployment mode is configured with the `ROCKET_ENV` environment variable:
|
||||
The web application can be run with a minimal set of routes and functionality (PeachPub - a simple sbot manager) or with the full-suite of capabilities, including network management and access to device statistics (PeachCloud). The mode is enabled by default (as defined in `Rocket.toml`) but can be overwritten using the `STANDALONE_MODE` environment variable: `true` or `false`. If the variable is unset or the value is incorrectly set, the application defaults to standalone mode.
|
||||
|
||||
`export ROCKET_ENV=stage`
|
||||
### Authentication
|
||||
|
||||
Other deployment modes are `dev` and `prod`. Read the [Rocket Environment Configurations docs](https://rocket.rs/v0.5-rc/guide/configuration/#environment-variables) for further information.
|
||||
Authentication is enabled by default when running the application. It can be disabled by setting the `DISABLE_AUTH` environment variable to `true`:
|
||||
|
||||
`export DISABLE_AUTH=true`
|
||||
|
||||
### Logging
|
||||
|
||||
Logging is made available with `env_logger`:
|
||||
|
||||
@ -51,7 +56,7 @@ Logging is made available with `env_logger`:
|
||||
|
||||
Other logging levels include `debug`, `warn` and `error`.
|
||||
|
||||
### Debian Packaging
|
||||
## Debian Packaging
|
||||
|
||||
A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-web` 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.
|
||||
|
||||
@ -83,10 +88,24 @@ Remove configuration files (not removed with `apt-get remove`):
|
||||
|
||||
`sudo apt-get purge peach-web`
|
||||
|
||||
### Design
|
||||
## Configuration
|
||||
|
||||
`peach-web` is built on the Rocket webserver and Tera templating engine. It presents a web interface for interacting with the device. HTML is rendered server-side. Request handlers call JSON-RPC microservices and serve HTML and assets. A JSON API is exposed for remote calls and dynamic client-side content updates (via plain JavaScript following unobstructive design principles). Each Tera template is passed a context object. In the case of Rust, this object is a `struct` and must implement `Serialize`. The fields of the context object are available in the context of the template to be rendered.
|
||||
Configuration variables are stored in /var/lib/peachcloud/config.yml.
|
||||
Peach-web also updates this file when changes are made to configurations via
|
||||
the web interface. peach-web has no database, so all configurations are stored in this file.
|
||||
|
||||
### Licensing
|
||||
### Dynamic DNS Configuration
|
||||
|
||||
Most users will want to use the default PeachCloud dynamic dns server.
|
||||
If the config dyn_use_custom_server=false, then default values will be used.
|
||||
If the config dyn_use_custom_server=true, then a value must also be set for dyn_dns_server_address (e.g. "http://peachdynserver.commoninternet.net").
|
||||
This value is the URL of the instance of peach-dyndns-server that requests will be sent to for domain registration.
|
||||
Using a custom value can here can be useful for testing.
|
||||
|
||||
## Design
|
||||
|
||||
`peach-web` has been designed with simplicity and resource minimalism in mind. Both the dependencies used by the project, as well as the code itself, reflect these design priorities. The Rouille micro-web-framework and Maud templating engine have been used to present a web interface for interacting with the device. HTML is rendered server-side and request handlers call `peach-` libraries and serve HTML and assets. The optimised binary for `peach-web` can be compiled on a RPi 3 B+ in approximately 30 minutes.
|
||||
|
||||
## Licensing
|
||||
|
||||
AGPL-3.0
|
||||
|
@ -1,5 +0,0 @@
|
||||
[development]
|
||||
template_dir = "templates/"
|
||||
|
||||
[production]
|
||||
template_dir = "templates/"
|
@ -5,54 +5,18 @@ set -e
|
||||
adduser --quiet --system peach-web
|
||||
usermod -g peach peach-web
|
||||
|
||||
# create secret passwords folder if it doesn't already exist
|
||||
mkdir -p /var/lib/peachcloud/passwords
|
||||
chown -R peach-web:peach /var/lib/peachcloud/passwords
|
||||
chmod -R u+rwX,go+rX,go-w /var/lib/peachcloud/passwords
|
||||
|
||||
# create nginx config
|
||||
cat <<EOF > /etc/nginx/sites-enabled/default
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name peach.local www.peach.local;
|
||||
|
||||
# nginx authentication
|
||||
auth_basic "If you have forgotten your password visit: http://peach.local/send_password_reset/";
|
||||
auth_basic_user_file /var/lib/peachcloud/passwords/htpasswd;
|
||||
|
||||
# remove trailing slash if found
|
||||
rewrite ^/(.*)/$ /$1 permanent;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
|
||||
# public routes
|
||||
location /send_password_reset {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /reset_password {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /public/ {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /js/ {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /css/ {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /icons/ {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
|
||||
}
|
||||
EOF
|
||||
|
||||
|
53
peach-web/src/config.rs
Normal file
53
peach-web/src/config.rs
Normal file
@ -0,0 +1,53 @@
|
||||
//! Define the configuration parameters for the web application.
|
||||
//!
|
||||
//! Sets default values and updates them if the corresponding environment
|
||||
//! variables have been set.
|
||||
|
||||
use std::env;
|
||||
|
||||
// environment variable keys to check for
|
||||
const ENV_VARS: [&str; 4] = ["STANDALONE_MODE", "DISABLE_AUTH", "ADDR", "PORT"];
|
||||
|
||||
pub struct Config {
|
||||
pub standalone_mode: bool,
|
||||
pub disable_auth: bool,
|
||||
pub addr: String,
|
||||
pub port: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
standalone_mode: true,
|
||||
disable_auth: false,
|
||||
addr: "127.0.0.1".to_string(),
|
||||
port: "8000".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new() -> Config {
|
||||
// define default config values
|
||||
let mut config = Config::default();
|
||||
|
||||
// check for the environment variables in our config
|
||||
for key in ENV_VARS {
|
||||
// if a variable (key) has been set, check the value
|
||||
if let Ok(val) = env::var(key) {
|
||||
// if the value is of the correct type, update the config value
|
||||
match key {
|
||||
"STANDALONE_MODE" if val.as_str() == "true" => config.standalone_mode = true,
|
||||
"STANDALONE_MODE" if val.as_str() == "false" => config.standalone_mode = false,
|
||||
"DISABLE_AUTH" if val.as_str() == "true" => config.disable_auth = true,
|
||||
"DISABLE_AUTH" if val.as_str() == "false" => config.disable_auth = false,
|
||||
"ADDR" => config.addr = val,
|
||||
"PORT" => config.port = val,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
}
|
@ -1,38 +1,93 @@
|
||||
//! Custom error type representing all possible error variants for peach-web.
|
||||
|
||||
use std::io::Error as IoError;
|
||||
|
||||
use golgi::GolgiError;
|
||||
use peach_lib::error::PeachError;
|
||||
use peach_lib::{serde_json, serde_yaml};
|
||||
use snafu::Snafu;
|
||||
use serde_json::error::Error as JsonError;
|
||||
use serde_yaml::Error as YamlError;
|
||||
|
||||
#[derive(Debug, Snafu)]
|
||||
/// Custom error type encapsulating all possible errors for the web application.
|
||||
#[derive(Debug)]
|
||||
pub enum PeachWebError {
|
||||
#[snafu(display("Error loading serde json"))]
|
||||
Serde { source: serde_json::error::Error },
|
||||
#[snafu(display("Error loading peach-config yaml"))]
|
||||
YamlError { source: serde_yaml::Error },
|
||||
#[snafu(display("{}", msg))]
|
||||
FailedToRegisterDynDomain { msg: String },
|
||||
#[snafu(display("{}: {}", source, msg))]
|
||||
PeachLibError { source: PeachError, msg: String },
|
||||
FailedToRegisterDynDomain(String),
|
||||
Golgi(GolgiError),
|
||||
HomeDir,
|
||||
Io(IoError),
|
||||
Json(JsonError),
|
||||
OsString,
|
||||
PeachLib { source: PeachError, msg: String },
|
||||
Yaml(YamlError),
|
||||
}
|
||||
|
||||
impl From<serde_json::error::Error> for PeachWebError {
|
||||
fn from(err: serde_json::error::Error) -> PeachWebError {
|
||||
PeachWebError::Serde { source: err }
|
||||
impl std::error::Error for PeachWebError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match *self {
|
||||
PeachWebError::FailedToRegisterDynDomain(_) => None,
|
||||
PeachWebError::Golgi(ref source) => Some(source),
|
||||
PeachWebError::HomeDir => None,
|
||||
PeachWebError::Io(ref source) => Some(source),
|
||||
PeachWebError::Json(ref source) => Some(source),
|
||||
PeachWebError::OsString => None,
|
||||
PeachWebError::PeachLib { ref source, .. } => Some(source),
|
||||
PeachWebError::Yaml(ref source) => Some(source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_yaml::Error> for PeachWebError {
|
||||
fn from(err: serde_yaml::Error) -> PeachWebError {
|
||||
PeachWebError::YamlError { source: err }
|
||||
impl std::fmt::Display for PeachWebError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match *self {
|
||||
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
|
||||
write!(f, "DYN DNS error: {}", msg)
|
||||
}
|
||||
PeachWebError::Golgi(ref source) => write!(f, "Golgi error: {}", source),
|
||||
PeachWebError::HomeDir => write!(
|
||||
f,
|
||||
"Filesystem error: failed to determine home directory path"
|
||||
),
|
||||
PeachWebError::Io(ref source) => write!(f, "IO error: {}", source),
|
||||
PeachWebError::Json(ref source) => write!(f, "Serde JSON error: {}", source),
|
||||
PeachWebError::OsString => write!(
|
||||
f,
|
||||
"Filesystem error: failed to convert OsString to String for go-ssb directory path"
|
||||
),
|
||||
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
|
||||
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GolgiError> for PeachWebError {
|
||||
fn from(err: GolgiError) -> PeachWebError {
|
||||
PeachWebError::Golgi(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IoError> for PeachWebError {
|
||||
fn from(err: IoError) -> PeachWebError {
|
||||
PeachWebError::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JsonError> for PeachWebError {
|
||||
fn from(err: JsonError) -> PeachWebError {
|
||||
PeachWebError::Json(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PeachError> for PeachWebError {
|
||||
fn from(err: PeachError) -> PeachWebError {
|
||||
PeachWebError::PeachLibError {
|
||||
PeachWebError::PeachLib {
|
||||
source: err,
|
||||
msg: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<YamlError> for PeachWebError {
|
||||
fn from(err: YamlError) -> PeachWebError {
|
||||
PeachWebError::Yaml(err)
|
||||
}
|
||||
}
|
||||
|
@ -8,177 +8,122 @@
|
||||
//! ## Design
|
||||
//!
|
||||
//! `peach-web` is written primarily in Rust and presents a web interface for
|
||||
//! interacting with the device. The stack currently consists of Rocket (Rust
|
||||
//! web framework), Tera (Rust template engine inspired by Jinja2 and the Django
|
||||
//! template language), HTML, CSS and JavaScript. Additional functionality is
|
||||
//! provided by JSON-RPC clients for the `peach-network` and `peach-stats`
|
||||
//! microservices.
|
||||
//!
|
||||
//! HTML is rendered server-side. Request handlers call JSON-RPC microservices
|
||||
//! and serve HTML and assets. A JSON API is exposed for remote calls and
|
||||
//! dynamic client-side content updates via vanilla JavaScript following
|
||||
//! unobstructive design principles. Each Tera template is passed a context
|
||||
//! object. In the case of Rust, this object is a `struct` and must implement
|
||||
//! `Serialize`. The fields of the context object are available in the context
|
||||
//! of the template to be rendered.
|
||||
|
||||
#![feature(proc_macro_hygiene, decl_macro)]
|
||||
//! interacting with the device. The stack currently consists of Rouille (Rust
|
||||
//! micro-web-framework), Maud (an HTML template engine for Rust), HTML and
|
||||
//! CSS.
|
||||
|
||||
mod config;
|
||||
pub mod error;
|
||||
pub mod routes;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod private_router;
|
||||
mod public_router;
|
||||
mod routes;
|
||||
mod templates;
|
||||
pub mod utils;
|
||||
|
||||
use log::{error, info};
|
||||
use std::process;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Mutex, RwLock},
|
||||
};
|
||||
|
||||
use rocket::{catchers, fs::FileServer, routes, Build, Rocket};
|
||||
use rocket_dyn_templates::Template;
|
||||
use lazy_static::lazy_static;
|
||||
use log::{debug, info};
|
||||
use peach_lib::{config_manager, config_manager::YAML_PATH as PEACH_CONFIG};
|
||||
|
||||
use crate::routes::authentication::*;
|
||||
use crate::routes::catchers::*;
|
||||
use crate::routes::index::*;
|
||||
use crate::routes::scuttlebutt::*;
|
||||
use crate::routes::status::device::*;
|
||||
use crate::routes::status::network::*;
|
||||
use crate::routes::status::ping::*;
|
||||
// crate-local dependencies
|
||||
use config::Config;
|
||||
use utils::theme::Theme;
|
||||
|
||||
use crate::routes::settings::admin::*;
|
||||
use crate::routes::settings::dns::*;
|
||||
use crate::routes::settings::menu::*;
|
||||
use crate::routes::settings::network::*;
|
||||
use crate::routes::settings::scuttlebutt::*;
|
||||
|
||||
pub type BoxError = Box<dyn std::error::Error>;
|
||||
|
||||
/// Create rocket instance & mount all routes.
|
||||
fn init_rocket() -> Rocket<Build> {
|
||||
rocket::build()
|
||||
// GENERAL HTML ROUTES
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
help,
|
||||
home,
|
||||
login,
|
||||
login_post,
|
||||
logout,
|
||||
reboot_cmd,
|
||||
shutdown_cmd,
|
||||
power_menu,
|
||||
settings_menu,
|
||||
],
|
||||
)
|
||||
// STATUS HTML ROUTES
|
||||
.mount("/status", routes![device_status, network_status])
|
||||
// ADMIN SETTINGS HTML ROUTES
|
||||
.mount(
|
||||
"/settings/admin",
|
||||
routes![
|
||||
admin_menu,
|
||||
configure_admin,
|
||||
add_admin,
|
||||
add_admin_post,
|
||||
delete_admin_post,
|
||||
change_password,
|
||||
change_password_post,
|
||||
reset_password,
|
||||
reset_password_post,
|
||||
forgot_password_page,
|
||||
send_password_reset_post,
|
||||
],
|
||||
)
|
||||
// NETWORK SETTINGS HTML ROUTES
|
||||
.mount(
|
||||
"/settings/network",
|
||||
routes![
|
||||
add_credentials,
|
||||
connect_wifi,
|
||||
configure_dns,
|
||||
configure_dns_post,
|
||||
disconnect_wifi,
|
||||
deploy_ap,
|
||||
deploy_client,
|
||||
forget_wifi,
|
||||
network_home,
|
||||
add_ssid,
|
||||
add_wifi,
|
||||
network_detail,
|
||||
wifi_list,
|
||||
wifi_password,
|
||||
wifi_set_password,
|
||||
wifi_usage,
|
||||
wifi_usage_alerts,
|
||||
wifi_usage_reset,
|
||||
],
|
||||
)
|
||||
// SCUTTLEBUTT SETTINGS HTML ROUTES
|
||||
.mount("/settings/scuttlebutt", routes![ssb_settings_menu])
|
||||
// SCUTTLEBUTT SOCIAL HTML ROUTES
|
||||
.mount(
|
||||
"/scuttlebutt",
|
||||
routes![
|
||||
peers, friends, follows, followers, blocks, profile, private, follow, unfollow,
|
||||
block, publish,
|
||||
],
|
||||
)
|
||||
// GENERAL JSON API ROUTES
|
||||
.mount(
|
||||
"/api/v1",
|
||||
routes![ping_pong, ping_network, ping_oled, ping_stats,],
|
||||
)
|
||||
// ADMIN JSON API ROUTES
|
||||
.mount(
|
||||
"/api/v1/admin",
|
||||
routes![
|
||||
save_password_form_endpoint,
|
||||
reset_password_form_endpoint,
|
||||
reboot_device,
|
||||
shutdown_device,
|
||||
],
|
||||
)
|
||||
// NETWORK JSON API ROUTES
|
||||
.mount(
|
||||
"/api/v1/network",
|
||||
routes![
|
||||
activate_ap,
|
||||
activate_client,
|
||||
add_wifi_credentials,
|
||||
connect_ap,
|
||||
disconnect_ap,
|
||||
forget_ap,
|
||||
modify_password,
|
||||
reset_data_total,
|
||||
return_ip,
|
||||
return_rssi,
|
||||
return_ssid,
|
||||
return_state,
|
||||
return_status,
|
||||
scan_networks,
|
||||
update_wifi_alerts,
|
||||
save_dns_configuration_endpoint,
|
||||
],
|
||||
)
|
||||
.mount("/", FileServer::from("static"))
|
||||
.register("/", catchers![not_found, internal_error, forbidden])
|
||||
.attach(Template::fairing())
|
||||
// load the application configuration and create the theme switcher
|
||||
lazy_static! {
|
||||
static ref CONFIG: Config = Config::new();
|
||||
static ref THEME: RwLock<Theme> = RwLock::new(Theme::Light);
|
||||
}
|
||||
|
||||
/// Launch the peach-web rocket server.
|
||||
#[rocket::main]
|
||||
async fn main() {
|
||||
/// Session data for each authenticated client.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionData {
|
||||
_login: String,
|
||||
}
|
||||
|
||||
/// Launch the peach-web server.
|
||||
fn main() {
|
||||
// initialize logger
|
||||
env_logger::init();
|
||||
|
||||
// initialize rocket
|
||||
info!("Initializing Rocket");
|
||||
let rocket = init_rocket();
|
||||
// check if /var/lib/peachcloud/config.yml exists
|
||||
if !std::path::Path::new(PEACH_CONFIG).exists() {
|
||||
debug!("PeachCloud configuration file not found; loading default values");
|
||||
// since we're in the intialisation phase, panic if the loading fails
|
||||
let config =
|
||||
config_manager::load_peach_config().expect("peachcloud configuration loading failed");
|
||||
|
||||
// launch rocket
|
||||
info!("Launching Rocket");
|
||||
if let Err(e) = rocket.launch().await {
|
||||
error!("Error in Rocket application: {}", e);
|
||||
process::exit(1);
|
||||
debug!("Saving default PeachCloud configuration values to file");
|
||||
// this ensures a config file is created if it does not already exist
|
||||
config_manager::save_peach_config(config).expect("peachcloud configuration saving failed");
|
||||
}
|
||||
|
||||
// set ip address / hostname and port for the webserver
|
||||
// defaults to "127.0.0.1:8000"
|
||||
let addr_and_port = format!("{}:{}", CONFIG.addr, CONFIG.port);
|
||||
|
||||
// store the session data for each session and a hashmap that associates
|
||||
// each session id with the data
|
||||
// note: we are storing this data in memory. all sessions are erased when
|
||||
// the program is restarted.
|
||||
let sessions_storage: Mutex<HashMap<String, SessionData>> = Mutex::new(HashMap::new());
|
||||
|
||||
info!("Launching web server on {}", addr_and_port);
|
||||
|
||||
// the `start_server` starts listening forever on the given address
|
||||
rouille::start_server(addr_and_port, move |request| {
|
||||
// assign a unique id to each client (appends a cookie to the response
|
||||
// with a name of "SID" and a duration of one hour (3600 seconds)
|
||||
rouille::session::session(request, "SID", 3600, |session| {
|
||||
// if the "DISABLE_AUTH" env var is true, authenticate the session
|
||||
let mut session_data = if CONFIG.disable_auth {
|
||||
Some(SessionData {
|
||||
_login: "success".to_string(),
|
||||
})
|
||||
// if the client already has an identifier from a previous request,
|
||||
// try to load the existing session data. if successful, make a
|
||||
// copy of the data in order to avoid locking the session for too
|
||||
// long
|
||||
} else if session.client_has_sid() {
|
||||
sessions_storage.lock().unwrap().get(session.id()).cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// pass the request to the public router
|
||||
//
|
||||
// the public router includes authentication-related routes which
|
||||
// do not require the user to be authenticated (ie. login and reset
|
||||
// password)
|
||||
//
|
||||
// if the user is already authenticated, their request will be
|
||||
// passed to the private router by public_router::handle_route()
|
||||
//
|
||||
// we pass a mutable reference to the `Option<SessionData>` so that
|
||||
// the function is free to modify it
|
||||
let response = public_router::handle_route(request, &mut session_data);
|
||||
|
||||
// since the function call to `handle_route` can modify the session
|
||||
// data, we have to store it back in the `sessions_storage` after
|
||||
// the request has been handled
|
||||
if let Some(data) = session_data {
|
||||
sessions_storage
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(session.id().to_owned(), data);
|
||||
} else if session.client_has_sid() {
|
||||
// if the content of the `Option` was erased (ie. due to
|
||||
// deauthentication on logout), remove the session from the
|
||||
// storage. this is only done if the client already has an
|
||||
// identifier, otherwise calling `session.id()` will assign one
|
||||
sessions_storage.lock().unwrap().remove(session.id());
|
||||
}
|
||||
|
||||
response
|
||||
})
|
||||
});
|
||||
}
|
||||
|
197
peach-web/src/private_router.rs
Normal file
197
peach-web/src/private_router.rs
Normal file
@ -0,0 +1,197 @@
|
||||
use rouille::{router, Request, Response};
|
||||
|
||||
use crate::{routes, templates, utils::flash::FlashResponse, SessionData};
|
||||
|
||||
// TODO: add mount_peachcloud_routes()
|
||||
// https://github.com/tomaka/rouille/issues/232#issuecomment-919225104
|
||||
|
||||
/// Define the PeachPub router.
|
||||
///
|
||||
/// Takes an incoming request and matches on the defined routes,
|
||||
/// returning either a template or a redirect.
|
||||
///
|
||||
/// All of these routes require the user to be authenticated. See the
|
||||
/// `public_router` for publically-accessible, authentication-related routes.
|
||||
///
|
||||
/// Excludes settings and status routes related to networking and the device
|
||||
/// (memory, hard disk, CPU etc.).
|
||||
pub fn mount_peachpub_routes(
|
||||
request: &Request,
|
||||
session_data: &mut Option<SessionData>,
|
||||
) -> Response {
|
||||
router!(request,
|
||||
(GET) (/) => {
|
||||
Response::html(routes::home::build_template())
|
||||
},
|
||||
|
||||
(GET) (/auth/change) => {
|
||||
// build the html template
|
||||
Response::html(routes::authentication::change::build_template(request))
|
||||
// reset the flash msg cookies in the response object
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/auth/change) => {
|
||||
routes::authentication::change::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/auth/logout) => {
|
||||
routes::authentication::logout::deauthenticate(session_data)
|
||||
},
|
||||
|
||||
(GET) (/guide) => {
|
||||
Response::html(routes::guide::build_template())
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/block) => {
|
||||
routes::scuttlebutt::block::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/blocks) => {
|
||||
Response::html(routes::scuttlebutt::blocks::build_template())
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/follow) => {
|
||||
routes::scuttlebutt::follow::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/follows) => {
|
||||
Response::html(routes::scuttlebutt::follows::build_template())
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/friends) => {
|
||||
Response::html(routes::scuttlebutt::friends::build_template())
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/invites) => {
|
||||
Response::html(routes::scuttlebutt::invites::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/invites) => {
|
||||
routes::scuttlebutt::invites::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/peers) => {
|
||||
Response::html(routes::scuttlebutt::peers::build_template())
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/private) => {
|
||||
Response::html(routes::scuttlebutt::private::build_template(request, None))
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/private) => {
|
||||
routes::scuttlebutt::private::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/private/{ssb_id: String}) => {
|
||||
Response::html(routes::scuttlebutt::private::build_template(request, Some(ssb_id)))
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/profile) => {
|
||||
Response::html(routes::scuttlebutt::profile::build_template(request, None))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/profile/update) => {
|
||||
Response::html(routes::scuttlebutt::profile_update::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/profile/update) => {
|
||||
routes::scuttlebutt::profile_update::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/profile/{ssb_id: String}) => {
|
||||
Response::html(routes::scuttlebutt::profile::build_template(request, Some(ssb_id)))
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/publish) => {
|
||||
routes::scuttlebutt::publish::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/scuttlebutt/search) => {
|
||||
Response::html(routes::scuttlebutt::search::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/search) => {
|
||||
routes::scuttlebutt::search::handle_form(request)
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/unblock) => {
|
||||
routes::scuttlebutt::unblock::handle_form(request)
|
||||
},
|
||||
|
||||
(POST) (/scuttlebutt/unfollow) => {
|
||||
routes::scuttlebutt::unfollow::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/settings) => {
|
||||
Response::html(routes::settings::menu::build_template())
|
||||
},
|
||||
|
||||
(GET) (/settings/admin) => {
|
||||
Response::html(routes::settings::admin::menu::build_template())
|
||||
},
|
||||
|
||||
(POST) (/settings/admin/add) => {
|
||||
routes::settings::admin::add::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/settings/admin/configure) => {
|
||||
Response::html(routes::settings::admin::configure::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/settings/admin/delete) => {
|
||||
routes::settings::admin::delete::handle_form(request)
|
||||
},
|
||||
|
||||
(GET) (/settings/scuttlebutt) => {
|
||||
Response::html(routes::settings::scuttlebutt::menu::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(GET) (/settings/scuttlebutt/restart) => {
|
||||
routes::settings::scuttlebutt::restart::restart_sbot()
|
||||
},
|
||||
|
||||
(GET) (/settings/scuttlebutt/start) => {
|
||||
routes::settings::scuttlebutt::start::start_sbot()
|
||||
},
|
||||
|
||||
(GET) (/settings/scuttlebutt/stop) => {
|
||||
routes::settings::scuttlebutt::stop::stop_sbot()
|
||||
},
|
||||
|
||||
(GET) (/settings/scuttlebutt/configure) => {
|
||||
Response::html(routes::settings::scuttlebutt::configure::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/settings/scuttlebutt/configure) => {
|
||||
routes::settings::scuttlebutt::configure::handle_form(request, false)
|
||||
},
|
||||
|
||||
(POST) (/settings/scuttlebutt/configure/restart) => {
|
||||
routes::settings::scuttlebutt::configure::handle_form(request, true)
|
||||
},
|
||||
|
||||
(GET) (/settings/scuttlebutt/configure/default) => {
|
||||
routes::settings::scuttlebutt::default::write_config()
|
||||
},
|
||||
|
||||
(GET) (/settings/theme/{theme: String}) => {
|
||||
routes::settings::theme::set_theme(theme)
|
||||
},
|
||||
|
||||
(GET) (/status/scuttlebutt) => {
|
||||
Response::html(routes::status::scuttlebutt::build_template())
|
||||
},
|
||||
|
||||
// render the not_found template and set a 404 status code if none of
|
||||
// the other blocks matches the request
|
||||
_ => Response::html(templates::not_found::build_template()).with_status_code(404)
|
||||
)
|
||||
}
|
103
peach-web/src/public_router.rs
Normal file
103
peach-web/src/public_router.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use log::{error, info};
|
||||
use rouille::{router, Request, Response};
|
||||
|
||||
use crate::{
|
||||
private_router, routes,
|
||||
utils::{flash::FlashResponse, sbot},
|
||||
SessionData,
|
||||
};
|
||||
|
||||
/// Request handler.
|
||||
///
|
||||
/// Mount the fileservers for static assets and define the
|
||||
/// publically-accessible routes (including per-route handlers). Includes
|
||||
/// logging of all incoming requests.
|
||||
///
|
||||
/// If the request is for a private route (ie. a route requiring successful
|
||||
/// authentication to view), check the authentication status of the user
|
||||
/// by querying the `session_data`. If the user is authenticated, pass their
|
||||
/// request to the private router. Otherwise, redirect them to the login page.
|
||||
pub fn handle_route(request: &Request, session_data: &mut Option<SessionData>) -> Response {
|
||||
// static file server
|
||||
// matches on assets in the `static` directory
|
||||
let static_response = rouille::match_assets(request, "static");
|
||||
if static_response.is_success() {
|
||||
return static_response;
|
||||
}
|
||||
|
||||
// set the `.ssb-go` path in order to mount the blob fileserver
|
||||
let ssb_path = sbot::get_go_ssb_path().expect("define ssb-go dir path");
|
||||
let blobstore = format!("{}/blobs/sha256", ssb_path);
|
||||
|
||||
// blobstore file server
|
||||
// removes the /blob url prefix and serves blobs from blobstore
|
||||
// matches on assets in the `static` directory
|
||||
if let Some(request) = request.remove_prefix("/blob") {
|
||||
return rouille::match_assets(&request, &blobstore);
|
||||
}
|
||||
|
||||
// get the current time (for logging purposes)
|
||||
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.6f");
|
||||
|
||||
// define the success logger for incoming requests
|
||||
let log_ok = |req: &Request, _resp: &Response, _elap: std::time::Duration| {
|
||||
info!("{} {} {}", now, req.method(), req.raw_url());
|
||||
};
|
||||
|
||||
// define the error logger for incoming requests
|
||||
let log_err = |req: &Request, _elap: std::time::Duration| {
|
||||
error!(
|
||||
"{} Handler panicked: {} {}",
|
||||
now,
|
||||
req.method(),
|
||||
req.raw_url()
|
||||
);
|
||||
};
|
||||
|
||||
// instantiate request logging
|
||||
rouille::log_custom(request, log_ok, log_err, || {
|
||||
// handle the routes which are always accessible (ie. whether logged-in
|
||||
// or not)
|
||||
router!(request,
|
||||
(GET) (/auth/forgot) => {
|
||||
Response::html(routes::authentication::forgot::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(GET) (/auth/login) => {
|
||||
Response::html(routes::authentication::login::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/auth/login) => {
|
||||
routes::authentication::login::handle_form(request, session_data)
|
||||
},
|
||||
|
||||
(GET) (/auth/reset) => {
|
||||
Response::html(routes::authentication::reset::build_template(request))
|
||||
.reset_flash()
|
||||
},
|
||||
|
||||
(POST) (/auth/reset) => {
|
||||
routes::authentication::reset::handle_form(request)
|
||||
},
|
||||
|
||||
(POST) (/auth/temporary) => {
|
||||
routes::authentication::temporary::handle_form()
|
||||
},
|
||||
|
||||
_ => {
|
||||
// now that we handled all the routes that are accessible in all
|
||||
// circumstances, we check that the user is logged in before proceeding
|
||||
if let Some(_session) = session_data.as_ref() {
|
||||
// logged in:
|
||||
// mount the routes which require authentication to view
|
||||
private_router::mount_peachpub_routes(request, session_data)
|
||||
} else {
|
||||
// not logged in:
|
||||
Response::redirect_303("/auth/login")
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
@ -1,424 +0,0 @@
|
||||
use log::info;
|
||||
use rocket::form::{Form, FromForm};
|
||||
use rocket::request::FlashMessage;
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::{get, post};
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use peach_lib::error::PeachError;
|
||||
use peach_lib::password_utils;
|
||||
|
||||
use crate::error::PeachWebError;
|
||||
use crate::utils::{build_json_response, TemplateOrRedirect};
|
||||
use rocket::http::{Cookie, CookieJar, Status};
|
||||
use rocket::request::{self, FromRequest, Request};
|
||||
use rocket::serde::json::Value;
|
||||
|
||||
// HELPERS AND STRUCTS FOR AUTHENTICATION WITH COOKIES
|
||||
|
||||
pub const AUTH_COOKIE_KEY: &str = "peachweb_auth";
|
||||
pub const ADMIN_USERNAME: &str = "admin";
|
||||
|
||||
/// Note: Currently we use an empty struct for the Authenticated request guard
|
||||
/// because there is only one user to be authenticated, and no data needs to be stored here.
|
||||
/// In a multi-user authentication scheme, we would store the user_id in this struct,
|
||||
/// and retrieve the correct user via the user_id stored in the cookie.
|
||||
pub struct Authenticated;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoginError {
|
||||
UserNotLoggedIn,
|
||||
}
|
||||
|
||||
/// Request guard which returns an empty Authenticated struct from the request
|
||||
/// if and only if the user has a cookie which proves they are authenticated with peach-web.
|
||||
///
|
||||
/// Note that cookies.get_private uses encryption, which means that this private cookie
|
||||
/// cannot be inspected, tampered with, or manufactured by clients.
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for Authenticated {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let authenticated = req
|
||||
.cookies()
|
||||
.get_private(AUTH_COOKIE_KEY)
|
||||
.and_then(|cookie| cookie.value().parse().ok())
|
||||
.map(|_value: String| Authenticated {});
|
||||
match authenticated {
|
||||
Some(auth) => request::Outcome::Success(auth),
|
||||
None => request::Outcome::Failure((Status::Forbidden, LoginError::UserNotLoggedIn)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /login
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct LoginContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl LoginContext {
|
||||
pub fn build() -> LoginContext {
|
||||
LoginContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/login")]
|
||||
pub fn login(flash: Option<FlashMessage>) -> Template {
|
||||
let mut context = LoginContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Login".to_string());
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
Template::render("login", &context)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct LoginForm {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
/// Takes in a LoginForm and returns Ok(()) if username and password
|
||||
/// are correct to authenticate with peach-web.
|
||||
///
|
||||
/// Note: currently there is only one user, and the username should always
|
||||
/// be "admin".
|
||||
pub fn verify_login_form(login_form: LoginForm) -> Result<(), PeachError> {
|
||||
password_utils::verify_password(&login_form.password)
|
||||
}
|
||||
|
||||
#[post("/login", data = "<login_form>")]
|
||||
pub fn login_post(login_form: Form<LoginForm>, cookies: &CookieJar<'_>) -> TemplateOrRedirect {
|
||||
let result = verify_login_form(login_form.into_inner());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
// if successful login, add a cookie indicating the user is authenticated
|
||||
// and redirect to home page
|
||||
// NOTE: since we currently have just one user, the value of the cookie
|
||||
// is just admin (this is arbitrary).
|
||||
// If we had multiple users, we could put the user_id here.
|
||||
cookies.add_private(Cookie::new(AUTH_COOKIE_KEY, ADMIN_USERNAME));
|
||||
TemplateOrRedirect::Redirect(Redirect::to("/"))
|
||||
}
|
||||
Err(_) => {
|
||||
// if unsuccessful login, render /login page again
|
||||
let mut context = LoginContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Login".to_string());
|
||||
context.flash_name = Some("error".to_string());
|
||||
let flash_msg = "Invalid password".to_string();
|
||||
context.flash_msg = Some(flash_msg);
|
||||
TemplateOrRedirect::Template(Template::render("login", &context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /logout
|
||||
|
||||
#[get("/logout")]
|
||||
pub fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
|
||||
// logout authenticated user
|
||||
info!("Attempting deauthentication of user.");
|
||||
cookies.remove_private(Cookie::named(AUTH_COOKIE_KEY));
|
||||
Flash::success(Redirect::to("/login"), "Logged out")
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /reset_password
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct ResetPasswordForm {
|
||||
pub temporary_password: String,
|
||||
pub new_password1: String,
|
||||
pub new_password2: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ResetPasswordContext {
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl ResetPasswordContext {
|
||||
pub fn build() -> ResetPasswordContext {
|
||||
ResetPasswordContext {
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ChangePasswordContext {
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl ChangePasswordContext {
|
||||
pub fn build() -> ChangePasswordContext {
|
||||
ChangePasswordContext {
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify, validate and save the submitted password. This function is publicly exposed for users who have forgotten their password.
|
||||
pub fn save_reset_password_form(password_form: ResetPasswordForm) -> Result<(), PeachWebError> {
|
||||
info!(
|
||||
"reset password!: {} {} {}",
|
||||
password_form.temporary_password, password_form.new_password1, password_form.new_password2
|
||||
);
|
||||
password_utils::verify_temporary_password(&password_form.temporary_password)?;
|
||||
// if the previous line did not throw an error, then the secret_link is correct
|
||||
password_utils::validate_new_passwords(
|
||||
&password_form.new_password1,
|
||||
&password_form.new_password2,
|
||||
)?;
|
||||
// if the previous line did not throw an error, then the new password is valid
|
||||
password_utils::set_new_password(&password_form.new_password1)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Password reset request handler. This route is used by a user who is not logged in
|
||||
/// and is specifically for users who have forgotten their password.
|
||||
#[get("/reset_password")]
|
||||
pub fn reset_password(flash: Option<FlashMessage>) -> Template {
|
||||
let mut context = ResetPasswordContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Reset Password".to_string());
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
Template::render("settings/admin/reset_password", &context)
|
||||
}
|
||||
|
||||
/// Password reset form request handler. This route is used by a user who is not logged in
|
||||
/// and is specifically for users who have forgotten their password.
|
||||
#[post("/reset_password", data = "<reset_password_form>")]
|
||||
pub fn reset_password_post(reset_password_form: Form<ResetPasswordForm>) -> Template {
|
||||
let result = save_reset_password_form(reset_password_form.into_inner());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Reset Password".to_string());
|
||||
context.flash_name = Some("success".to_string());
|
||||
let flash_msg = "New password is now saved. Return home to login".to_string();
|
||||
context.flash_msg = Some(flash_msg);
|
||||
Template::render("settings/admin/reset_password", &context)
|
||||
}
|
||||
Err(err) => {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Reset Password".to_string());
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some(format!("Failed to reset password: {}", err));
|
||||
Template::render("settings/admin/reset_password", &context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON password reset form request handler. This route is used by a user who is not logged in
|
||||
/// and is specifically for users who have forgotten their password.
|
||||
#[post("/reset_password", data = "<reset_password_form>")]
|
||||
pub fn reset_password_form_endpoint(reset_password_form: Json<ResetPasswordForm>) -> Value {
|
||||
let result = save_reset_password_form(reset_password_form.into_inner());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let status = "success".to_string();
|
||||
let msg = "New password is now saved. Return home to login.".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
Err(err) => {
|
||||
let status = "error".to_string();
|
||||
let msg = format!("{}", err);
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /send_password_reset
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SendPasswordResetContext {
|
||||
pub back: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
}
|
||||
|
||||
impl SendPasswordResetContext {
|
||||
pub fn build() -> SendPasswordResetContext {
|
||||
SendPasswordResetContext {
|
||||
back: None,
|
||||
title: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Page for users who have forgotten their password.
|
||||
/// This route is used by a user who is not logged in
|
||||
/// to initiate the sending of a new password reset.
|
||||
#[get("/forgot_password")]
|
||||
pub fn forgot_password_page(flash: Option<FlashMessage>) -> Template {
|
||||
let mut context = SendPasswordResetContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Send Password Reset".to_string());
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
Template::render("settings/admin/forgot_password", &context)
|
||||
}
|
||||
|
||||
/// Send password reset request handler. This route is used by a user who is not logged in
|
||||
/// and is specifically for users who have forgotten their password. A successful request results
|
||||
/// in a Scuttlebutt private message being sent to the account of the device admin.
|
||||
#[post("/send_password_reset")]
|
||||
pub fn send_password_reset_post() -> Template {
|
||||
info!("++ send password reset post");
|
||||
let result = password_utils::send_password_reset();
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Send Password Reset".to_string());
|
||||
context.flash_name = Some("success".to_string());
|
||||
let flash_msg =
|
||||
"A password reset link has been sent to the admin of this device".to_string();
|
||||
context.flash_msg = Some(flash_msg);
|
||||
Template::render("settings/admin/forgot_password", &context)
|
||||
}
|
||||
Err(err) => {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Send Password Reset".to_string());
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some(format!("Failed to send password reset link: {}", err));
|
||||
Template::render("settings/admin/forgot_password", &context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /settings/change_password
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct PasswordForm {
|
||||
pub old_password: String,
|
||||
pub new_password1: String,
|
||||
pub new_password2: String,
|
||||
}
|
||||
|
||||
/// 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(password_form: PasswordForm) -> Result<(), PeachWebError> {
|
||||
info!(
|
||||
"change password!: {} {} {}",
|
||||
password_form.old_password, password_form.new_password1, password_form.new_password2
|
||||
);
|
||||
password_utils::verify_password(&password_form.old_password)?;
|
||||
// if the previous line did not throw an error, then the old password is correct
|
||||
password_utils::validate_new_passwords(
|
||||
&password_form.new_password1,
|
||||
&password_form.new_password2,
|
||||
)?;
|
||||
// if the previous line did not throw an error, then the new password is valid
|
||||
password_utils::set_new_password(&password_form.new_password1)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change password request handler. This is used by a user who is already logged in.
|
||||
#[get("/change_password")]
|
||||
pub fn change_password(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/settings/admin".to_string());
|
||||
context.title = Some("Change Password".to_string());
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
Template::render("settings/admin/change_password", &context)
|
||||
}
|
||||
|
||||
/// Change password form request handler. This route is used by a user who is already logged in.
|
||||
#[post("/change_password", data = "<password_form>")]
|
||||
pub fn change_password_post(password_form: Form<PasswordForm>, _auth: Authenticated) -> Template {
|
||||
let result = save_password_form(password_form.into_inner());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/settings/admin".to_string());
|
||||
context.title = Some("Change Password".to_string());
|
||||
context.flash_name = Some("success".to_string());
|
||||
context.flash_msg = Some("New password is now saved".to_string());
|
||||
// template_dir is set in Rocket.toml
|
||||
Template::render("settings/admin/change_password", &context)
|
||||
}
|
||||
Err(err) => {
|
||||
let mut context = ChangePasswordContext::build();
|
||||
// set back icon link to network route
|
||||
context.back = Some("/settings/admin".to_string());
|
||||
context.title = Some("Change Password".to_string());
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some(format!("Failed to save new password: {}", err));
|
||||
Template::render("settings/admin/change_password", &context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON change password form request handler.
|
||||
#[post("/change_password", data = "<password_form>")]
|
||||
pub fn save_password_form_endpoint(
|
||||
password_form: Json<PasswordForm>,
|
||||
_auth: Authenticated,
|
||||
) -> Value {
|
||||
let result = save_password_form(password_form.into_inner());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let status = "success".to_string();
|
||||
let msg = "Your password was successfully changed".to_string();
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
Err(err) => {
|
||||
let status = "error".to_string();
|
||||
let msg = format!("{}", err);
|
||||
build_json_response(status, None, Some(msg))
|
||||
}
|
||||
}
|
||||
}
|
116
peach-web/src/routes/authentication/change.rs
Normal file
116
peach-web/src/routes/authentication/change.rs
Normal file
@ -0,0 +1,116 @@
|
||||
use log::info;
|
||||
use maud::{html, PreEscaped};
|
||||
use peach_lib::password_utils;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
error::PeachWebError,
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
theme,
|
||||
},
|
||||
};
|
||||
|
||||
// HELPER AND ROUTES FOR /auth/change (GET and POST)
|
||||
|
||||
/// Password change form template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let form_template = html! {
|
||||
(PreEscaped("<!-- CHANGE PASSWORD FORM -->"))
|
||||
div class="card center" {
|
||||
form id="changePassword" class="center" action="/auth/change" method="post" {
|
||||
div style="display: flex; flex-direction: column; margin-bottom: 1rem;" {
|
||||
(PreEscaped("<!-- input for current password -->"))
|
||||
label for="currentPassword" class="center label-small font-gray" style="width: 80%;" { "CURRENT PASSWORD" }
|
||||
input id="currentPassword" class="center input" name="current_password" type="password" title="Current password" autofocus;
|
||||
(PreEscaped("<!-- input for new password -->"))
|
||||
label for="newPassword" class="center label-small font-gray" style="width: 80%;" { "NEW PASSWORD" }
|
||||
input id="newPassword" class="center input" name="new_password1" type="password" title="New password";
|
||||
(PreEscaped("<!-- input for duplicate new password -->"))
|
||||
label for="newPasswordDuplicate" class="center label-small font-gray" style="width: 80%;" { "RE-ENTER NEW PASSWORD" }
|
||||
input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" title="New password duplicate";
|
||||
(PreEscaped("<!-- save (form submission) button -->"))
|
||||
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 cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the settings menu template content
|
||||
// parameters are template, title and back url
|
||||
let body =
|
||||
templates::nav::build_template(form_template, "Change Password", Some("/settings/admin"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Verify, validate and set a new password, overwriting the current password.
|
||||
pub fn save_password(
|
||||
current_password: &str,
|
||||
new_password1: &str,
|
||||
new_password2: &str,
|
||||
) -> Result<(), PeachWebError> {
|
||||
info!(
|
||||
"Attempting password change: {} {} {}",
|
||||
current_password, new_password1, new_password2
|
||||
);
|
||||
|
||||
// check that the supplied value matches the actual current password
|
||||
password_utils::verify_password(current_password)?;
|
||||
|
||||
// ensure that both new_password values match
|
||||
password_utils::validate_new_passwords(new_password1, new_password2)?;
|
||||
|
||||
// hash the password and save the hash to file
|
||||
password_utils::set_new_password(new_password1)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse current and new passwords from the submitted form, save the new
|
||||
/// password hash to file (`/var/lib/peachcloud/config.yml`) and redirect
|
||||
/// to the change password form URL.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
current_password: String,
|
||||
new_password1: String,
|
||||
new_password2: String,
|
||||
}));
|
||||
|
||||
// save submitted admin id to file
|
||||
// match on the result and set flash name and msg accordingly
|
||||
let (flash_name, flash_msg) = match save_password(
|
||||
&data.current_password,
|
||||
&data.new_password1,
|
||||
&data.new_password2,
|
||||
) {
|
||||
Ok(_) => (
|
||||
// <cookie-name>=<cookie-value>
|
||||
"flash_name=success".to_string(),
|
||||
"flash_msg=New password has been saved".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg=Failed to save new password: {}", err),
|
||||
),
|
||||
};
|
||||
|
||||
// set the flash cookie headers and redirect to the change password page
|
||||
Response::redirect_303("/auth/change").add_flash(flash_name, flash_msg)
|
||||
}
|
56
peach-web/src/routes/authentication/forgot.rs
Normal file
56
peach-web/src/routes/authentication/forgot.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use maud::{html, PreEscaped};
|
||||
use rouille::Request;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{flash::FlashRequest, theme},
|
||||
};
|
||||
|
||||
// ROUTE: /auth/forgot
|
||||
|
||||
/// Forgot password template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let password_reset_template = html! {
|
||||
(PreEscaped("<!-- PASSWORD RESET REQUEST CARD -->"))
|
||||
div class="card center" {
|
||||
div class="capsule capsule-container border-info" {
|
||||
p class="card-text" {
|
||||
"Click the 'Send Temporary Password' button to send a new temporary password which can be used to change your device password."
|
||||
}
|
||||
p class="card-text" style="margin-top: 1rem;" {
|
||||
"The temporary password will be sent in an SSB private message to the admin of this device."
|
||||
}
|
||||
p class="card-text" style="margin-top: 1rem;" {
|
||||
"Once you have the temporary password, click the 'Set New Password' button to reach the password reset page."
|
||||
}
|
||||
}
|
||||
form id="sendPasswordReset" action="/auth/temporary" method="post" {
|
||||
div id="buttonDiv" {
|
||||
input class="button button-primary center" style="margin-top: 1rem;" type="submit" value="Send Temporary Password" title="Send temporary password to Scuttlebutt admin(s)";
|
||||
a href="/auth/reset_password" class="button button-primary center" title="Set a new password using the temporary password" {
|
||||
"Set New Password"
|
||||
}
|
||||
}
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the settings menu template content
|
||||
// parameters are template, title and back url
|
||||
let body =
|
||||
templates::nav::build_template(password_reset_template, "Send Password Reset", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
87
peach-web/src/routes/authentication/login.rs
Normal file
87
peach-web/src/routes/authentication/login.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use log::debug;
|
||||
use maud::{html, PreEscaped};
|
||||
use peach_lib::password_utils;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
theme,
|
||||
},
|
||||
SessionData,
|
||||
};
|
||||
|
||||
// HELPER AND ROUTES FOR /auth/login (GET and POST)
|
||||
|
||||
/// Login form template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let form_template = html! {
|
||||
(PreEscaped("<!-- LOGIN FORM -->"))
|
||||
div class="card center" {
|
||||
form id="login_form" class="center" action="/auth/login" method="post" {
|
||||
div style="display: flex; flex-direction: column; margin-bottom: 1rem;" {
|
||||
(PreEscaped("<!-- input for password -->"))
|
||||
label for="password" class="center label-small font-gray" style="width: 80%;" { "PASSWORD" }
|
||||
input id="password" name="password" class="center input" type="password" title="Password for given username" autofocus;
|
||||
(PreEscaped("<!-- login (form submission) button -->"))
|
||||
input id="loginUser" class="button button-primary center" title="Login" type="submit" value="Login";
|
||||
div class="center-text" style="margin-top: 1rem;" {
|
||||
a href="/auth/forgot" class="label-small link font-gray" { "Forgot Password?" }
|
||||
}
|
||||
}
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the settings menu template content
|
||||
// parameters are template, title and back url
|
||||
let body = templates::nav::build_template(form_template, "Login", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Parse and verify the submitted password. If verification succeeds, set the
|
||||
/// auth session cookie and redirect to the home page. If not, set a flash
|
||||
/// message and redirect to the login page.
|
||||
pub fn handle_form(request: &Request, session_data: &mut Option<SessionData>) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, { password: String }));
|
||||
|
||||
match password_utils::verify_password(&data.password) {
|
||||
Ok(_) => {
|
||||
debug!("Successful login attempt");
|
||||
// if password verification is successful, write to `session_data`
|
||||
// to authenticate the user
|
||||
*session_data = Some(SessionData {
|
||||
_login: "success".to_string(),
|
||||
});
|
||||
|
||||
Response::redirect_303("/")
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("Unsuccessful login attempt");
|
||||
let err_msg = format!("Invalid password: {}", err);
|
||||
let (flash_name, flash_msg) = (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", err_msg),
|
||||
);
|
||||
|
||||
// if unsuccessful login, render /login page again
|
||||
Response::redirect_303("/auth/login").add_flash(flash_name, flash_msg)
|
||||
}
|
||||
}
|
||||
}
|
23
peach-web/src/routes/authentication/logout.rs
Normal file
23
peach-web/src/routes/authentication/logout.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use log::info;
|
||||
use rouille::Response;
|
||||
|
||||
use crate::{utils::flash::FlashResponse, SessionData};
|
||||
|
||||
// ROUTE: /auth/logout (GET)
|
||||
|
||||
/// Deauthenticate the logged-in user by erasing the session data.
|
||||
/// Redirect to the login page.
|
||||
pub fn deauthenticate(session_data: &mut Option<SessionData>) -> Response {
|
||||
info!("Attempting deauthentication of user.");
|
||||
|
||||
// erase the content of `session_data` to deauthenticate the user
|
||||
*session_data = None;
|
||||
|
||||
let (flash_name, flash_msg) = (
|
||||
"flash_name=success".to_string(),
|
||||
"flash_msg=Logged out".to_string(),
|
||||
);
|
||||
|
||||
// set the flash cookie headers and redirect to the login page
|
||||
Response::redirect_303("/auth/login".to_string()).add_flash(flash_name, flash_msg)
|
||||
}
|
6
peach-web/src/routes/authentication/mod.rs
Normal file
6
peach-web/src/routes/authentication/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub mod change;
|
||||
pub mod forgot;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod reset;
|
||||
pub mod temporary;
|
114
peach-web/src/routes/authentication/reset.rs
Normal file
114
peach-web/src/routes/authentication/reset.rs
Normal file
@ -0,0 +1,114 @@
|
||||
use log::info;
|
||||
use maud::{html, PreEscaped};
|
||||
use peach_lib::password_utils;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
error::PeachWebError,
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
theme,
|
||||
},
|
||||
};
|
||||
|
||||
// HELPER AND ROUTES FOR /auth/reset (GET and POST)
|
||||
|
||||
/// Password reset form template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let form_template = html! {
|
||||
(PreEscaped("<!-- RESET PASSWORD PAGE -->"))
|
||||
div class="card center" {
|
||||
form id="resetPassword" class="center" action="/auth/reset" method="post" {
|
||||
div style="display: flex; flex-direction: column; margin-bottom: 1rem;" {
|
||||
(PreEscaped("<!-- input for temporary password -->"))
|
||||
label for="temporaryPassword" class="center label-small font-gray" style="width: 80%;" { "TEMPORARY PASSWORD" }
|
||||
input id="temporaryPassword" class="center input" name="temporary_password" type="password" title="Temporary password" autofocus;
|
||||
(PreEscaped("<!-- input for new password1 -->"))
|
||||
label for="newPassword" class="center label-small font-gray" style="width: 80%;" { "NEW PASSWORD" }
|
||||
input id="newPassword" class="center input" name="new_password1" type="password" title="New password";
|
||||
(PreEscaped("<!-- input for duplicate new password -->"))
|
||||
label for="newPasswordDuplicate" class="center label-small font-gray" style="width: 80%;" { "RE-ENTER NEW PASSWORD" }
|
||||
input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" title="New password duplicate";
|
||||
(PreEscaped("<!-- save (form submission) button -->"))
|
||||
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 cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the settings menu template content
|
||||
// parameters are template, title and back url
|
||||
let body =
|
||||
templates::nav::build_template(form_template, "Reset Password", Some("/settings/admin"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Verify, validate and set a new password, overwriting the current password.
|
||||
pub fn save_password(
|
||||
temporary_password: &str,
|
||||
new_password1: &str,
|
||||
new_password2: &str,
|
||||
) -> Result<(), PeachWebError> {
|
||||
info!(
|
||||
"Attempting password reset: {} {} {}",
|
||||
temporary_password, new_password1, new_password2
|
||||
);
|
||||
|
||||
// check that the supplied value matches the actual temporary password
|
||||
password_utils::verify_temporary_password(temporary_password)?;
|
||||
|
||||
// ensure that both new_password values match
|
||||
password_utils::validate_new_passwords(new_password1, new_password2)?;
|
||||
|
||||
// hash the password and save the hash to file
|
||||
password_utils::set_new_password(new_password1)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse temporary and new passwords from the submitted form, save the new
|
||||
/// password hash to file (`/var/lib/peachcloud/config.yml`) and redirect
|
||||
/// to the reset password form URL.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
temporary_password: String,
|
||||
new_password1: String,
|
||||
new_password2: String,
|
||||
}));
|
||||
|
||||
// save submitted admin id to file
|
||||
let (flash_name, flash_msg) = match save_password(
|
||||
&data.temporary_password,
|
||||
&data.new_password1,
|
||||
&data.new_password2,
|
||||
) {
|
||||
Ok(_) => (
|
||||
"flash_name=success".to_string(),
|
||||
"flash_msg=New password has been saved. Return home to login".to_string(),
|
||||
),
|
||||
Err(err) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg=Failed to reset password: {}", err),
|
||||
),
|
||||
};
|
||||
|
||||
// redirect to the configure admin page
|
||||
Response::redirect_303("/auth/reset").add_flash(flash_name, flash_msg)
|
||||
}
|
42
peach-web/src/routes/authentication/temporary.rs
Normal file
42
peach-web/src/routes/authentication/temporary.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use log::debug;
|
||||
use peach_lib::password_utils;
|
||||
use rouille::Response;
|
||||
|
||||
use crate::utils::flash::FlashResponse;
|
||||
|
||||
// ROUTE: /auth/temporary (POST)
|
||||
|
||||
/// Send a temporary password as a Scuttlebutt private message to the admin(s).
|
||||
///
|
||||
/// This route is used by a user who is not logged in and is specifically for
|
||||
/// users who have forgotten their password. A successful request results
|
||||
/// in a Scuttlebutt private message being sent to the account of the device
|
||||
/// admin.
|
||||
///
|
||||
/// Redirects to the Send Password Reset page a flash message describing the
|
||||
/// outcome of the action (may be successful or unsuccessful).
|
||||
pub fn handle_form() -> Response {
|
||||
// save submitted admin id to file
|
||||
let (flash_name, flash_msg) = match password_utils::send_password_reset() {
|
||||
Ok(_) => {
|
||||
debug!("Sent temporary password to device admin(s)");
|
||||
(
|
||||
"flash_name=success".to_string(),
|
||||
"flash_msg=A temporary password has been sent to the admin(s) of this device"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
debug!(
|
||||
"Received an error while trying to send temporary password to device admin(s): {}",
|
||||
err
|
||||
);
|
||||
(
|
||||
"error".to_string(),
|
||||
format!("Failed to send temporary password: {}", err),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Response::redirect_303("/auth/forgot").add_flash(flash_name, flash_msg)
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
use log::debug;
|
||||
use rocket::catch;
|
||||
use rocket::response::Redirect;
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::Serialize;
|
||||
|
||||
// HELPERS AND ROUTES FOR 404 ERROR
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ErrorContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl ErrorContext {
|
||||
pub fn build() -> ErrorContext {
|
||||
ErrorContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[catch(404)]
|
||||
pub fn not_found() -> Template {
|
||||
debug!("404 Page Not Found");
|
||||
let mut context = ErrorContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("404: Page Not Found".to_string());
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some("No resource found for given URL".to_string());
|
||||
|
||||
Template::render("catchers/not_found", context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR 500 ERROR
|
||||
|
||||
#[catch(500)]
|
||||
pub fn internal_error() -> Template {
|
||||
debug!("500 Internal Server Error");
|
||||
let mut context = ErrorContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("500: Internal Server Error".to_string());
|
||||
context.flash_name = Some("error".to_string());
|
||||
context.flash_msg = Some("Internal server error".to_string());
|
||||
|
||||
Template::render("catchers/internal_error", context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR 403 FORBIDDEN
|
||||
|
||||
#[catch(403)]
|
||||
pub fn forbidden() -> Redirect {
|
||||
debug!("403 Forbidden");
|
||||
Redirect::to("/login")
|
||||
}
|
106
peach-web/src/routes/guide.rs
Normal file
106
peach-web/src/routes/guide.rs
Normal file
@ -0,0 +1,106 @@
|
||||
use maud::{html, PreEscaped};
|
||||
|
||||
use crate::{templates, utils::theme};
|
||||
|
||||
/// Guide template builder.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
// render the guide template html
|
||||
let guide_template = html! {
|
||||
(PreEscaped("<!-- GUIDE -->"))
|
||||
div class="card card-wide center" {
|
||||
div class="capsule capsule-container border-info" {
|
||||
(PreEscaped("<!-- GETTING STARTED -->"))
|
||||
details {
|
||||
summary class="card-text link" { "Getting started" }
|
||||
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
|
||||
"The Scuttlebutt server (sbot) will be inactive when you first run PeachCloud. This is to allow configuration parameters to be set before it is activated for the first time. Navigate to the "
|
||||
strong {
|
||||
a href="/settings/scuttlebutt/configure" class="link font-gray" {
|
||||
"Sbot Configuration"
|
||||
}
|
||||
}
|
||||
" page to configure your system. The default configuration will be fine for most usecases."
|
||||
}
|
||||
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
|
||||
"Once the configuration is set, navigate to the "
|
||||
strong {
|
||||
a href="/settings/scuttlebutt" class="link font-gray" {
|
||||
"Scuttlebutt settings menu"
|
||||
}
|
||||
}
|
||||
" to start the sbot. If the server starts successfully, you will see a green smiley face on the home page. If the face is orange and sleeping, that means the sbot is still inactive (ie. the process is not running). If the face is red and dead, that means the sbot failed to start - indicated an error. For now, the best way to gain insight into the problem is to check the systemd log. Open a terminal and enter: "
|
||||
code { "systemctl --user status go-sbot.service" }
|
||||
". The log output may give some clues about the source of the error."
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- BUG REPORTS -->"))
|
||||
details {
|
||||
summary class="card-text link" { "Submit a bug report" }
|
||||
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
|
||||
"Bug reports can be submitted by "
|
||||
strong {
|
||||
a href="https://git.coopcloud.tech/PeachCloud/peach-workspace/issues/new?template=BUG_TEMPLATE.md" class="link font-gray" {
|
||||
"filing an issue"
|
||||
}
|
||||
}
|
||||
" on the peach-workspace git repo. Before filing a report, first check to see if an issue already exists for the bug you've encountered. If not, you're invited to submit a new report; the template will guide you through several questions."
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- REQUEST SUPPORT -->"))
|
||||
details {
|
||||
summary class="card-text link" { "Share feedback & request support" }
|
||||
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
|
||||
"You're invited to share your thoughts and experiences of PeachCloud in the #peachcloud channel on Scuttlebutt. The channel is also a good place to ask for help."
|
||||
}
|
||||
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
|
||||
"Alternatively, we have a "
|
||||
strong {
|
||||
a href="https://matrix.to/#/#peachcloud:matrix.org" class="link font-gray" {
|
||||
"Matrix channel"
|
||||
}
|
||||
}
|
||||
" for discussion about PeachCloud and you can also reach out to @glyph "
|
||||
strong {
|
||||
a href="mailto:glyph@mycelial.technology" class="link font-gray" {
|
||||
"via email"
|
||||
}
|
||||
}
|
||||
"."
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- CONTRIBUTE -->"))
|
||||
details {
|
||||
summary class="card-text link" { "Contribute to PeachCloud" }
|
||||
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
|
||||
"PeachCloud is free, open-source software and relies on donations and grants to fund develop. Donations can be made on our "
|
||||
strong {
|
||||
a href="https://opencollective.com/peachcloud" class="link font-gray" {
|
||||
"Open Collective"
|
||||
}
|
||||
}
|
||||
" page."
|
||||
}
|
||||
p class="card-text" style="margin-top: 1rem; margin-bottom: 1rem;" {
|
||||
"Programmers, designers, artists and writers are also welcome to contribute to the project. Please visit the "
|
||||
strong {
|
||||
a href="https://git.coopcloud.tech/PeachCloud/peach-workspace" class="link font-gray" {
|
||||
"main PeachCloud git repository"
|
||||
}
|
||||
}
|
||||
" to find out more details or contact the team via Scuttlebutt, Matrix or email."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the home template content
|
||||
// title is "" and back button link is `None` because this is the homepage
|
||||
let body = templates::nav::build_template(guide_template, "Guide", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
105
peach-web/src/routes/home.rs
Normal file
105
peach-web/src/routes/home.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use maud::{html, PreEscaped};
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
|
||||
use crate::{templates, utils::theme};
|
||||
|
||||
/// Read the state of the go-sbot process and define status-related
|
||||
/// elements accordingly.
|
||||
fn render_status_elements<'a>() -> (&'a str, &'a str, &'a str) {
|
||||
// retrieve go-sbot systemd process status
|
||||
let sbot_status = SbotStatus::read();
|
||||
|
||||
// conditionally render the center circle class, center circle text and
|
||||
// status circle class color based on the go-sbot process state
|
||||
if let Ok(status) = sbot_status {
|
||||
if status.state == Some("active".to_string()) {
|
||||
("circle-success", "^_^", "border-success")
|
||||
} else if status.state == Some("inactive".to_string()) {
|
||||
("circle-warning", "z_z", "border-warning")
|
||||
} else {
|
||||
("circle-error", "x_x", "border-danger")
|
||||
}
|
||||
} else {
|
||||
("circle-error", "x_x", "border-danger")
|
||||
}
|
||||
}
|
||||
|
||||
/// Home template builder.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
let (circle_color, center_circle_text, circle_border) = render_status_elements();
|
||||
|
||||
// render the home template html
|
||||
let home_template = 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 border-circle-small border-ssb" {
|
||||
img class="icon-medium" src="/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 border-circle-small border-ssb" {
|
||||
img class="icon-medium" src="/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 border-circle-small border-ssb" {
|
||||
img class="icon-medium" src="/icons/envelope.svg";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- middle -->"))
|
||||
a class="middle" {
|
||||
div class={ "circle circle-large " (circle_color) } {
|
||||
p style="font-size: 4rem; color: var(--near-black);" {
|
||||
(center_circle_text)
|
||||
}
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- bottom-left -->"))
|
||||
(PreEscaped("<!-- SYSTEM STATUS LINK AND ICON -->"))
|
||||
a class="bottom-left" href="/status/scuttlebutt" title="Status" {
|
||||
div class={ "circle circle-small border-circle-small " (circle_border) } {
|
||||
img class="icon-medium" src="/icons/heart-pulse.svg";
|
||||
}
|
||||
}
|
||||
/*
|
||||
TODO: render the path of the status circle button based on the mode
|
||||
{%- if standalone_mode == true -%}
|
||||
<a class="bottom-left" href="/status/scuttlebutt" title="Status">
|
||||
{% else -%}
|
||||
<a class="bottom-left" href="/status" title="Status">
|
||||
{%- endif -%}
|
||||
*/
|
||||
(PreEscaped("<!-- bottom-middle -->"))
|
||||
(PreEscaped("<!-- PEACHCLOUD GUIDEBOOK LINK AND ICON -->"))
|
||||
a class="bottom-middle" href="/guide" title="Guide" {
|
||||
div class="circle circle-small border-circle-small border-info" {
|
||||
img class="icon-medium" src="/icons/book.svg";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- bottom-right -->"))
|
||||
(PreEscaped("<!-- SYSTEM SETTINGS LINK AND ICON -->"))
|
||||
a class="bottom-right" href="/settings" title="Settings" {
|
||||
div class="circle circle-small border-circle-small border-settings" {
|
||||
img class="icon-medium" src="/icons/cog.svg";
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the home template content
|
||||
// title is "" and back button link is `None` because this is the homepage
|
||||
let body = templates::nav::build_template(home_template, "", None);
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
use rocket::{get, request::FlashMessage};
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
|
||||
// HELPERS AND ROUTES FOR / (HOME PAGE)
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct HomeContext {
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl HomeContext {
|
||||
pub fn build() -> HomeContext {
|
||||
HomeContext {
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub fn home(_auth: Authenticated) -> Template {
|
||||
let context = HomeContext {
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
};
|
||||
Template::render("home", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /help
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct HelpContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl HelpContext {
|
||||
pub fn build() -> HelpContext {
|
||||
HelpContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/help")]
|
||||
pub fn help(flash: Option<FlashMessage>) -> Template {
|
||||
let mut context = HelpContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Help".to_string());
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
Template::render("help", &context)
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
pub mod authentication;
|
||||
pub mod catchers;
|
||||
pub mod index;
|
||||
//pub mod catchers;
|
||||
//pub mod index;
|
||||
pub mod guide;
|
||||
pub mod home;
|
||||
pub mod scuttlebutt;
|
||||
pub mod settings;
|
||||
pub mod status;
|
||||
|
@ -1,365 +0,0 @@
|
||||
//! Routes for Scuttlebutt related functionality.
|
||||
|
||||
use rocket::{
|
||||
form::{Form, FromForm},
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
serde::{Deserialize, Serialize},
|
||||
uri,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
|
||||
// HELPERS AND ROUTES FOR /private
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PrivateContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl PrivateContext {
|
||||
pub fn build() -> PrivateContext {
|
||||
PrivateContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A private message composition and publication page.
|
||||
#[get("/private")]
|
||||
pub fn private(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = PrivateContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Private Messages".to_string());
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
Template::render("scuttlebutt/messages", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /peers
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PeerContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl PeerContext {
|
||||
pub fn build() -> PeerContext {
|
||||
PeerContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A peer menu which allows navigating to lists of friends, follows, followers and blocks.
|
||||
#[get("/peers")]
|
||||
pub fn peers(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = PeerContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Scuttlebutt Peers".to_string());
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
Template::render("scuttlebutt/peers", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /post/publish
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct Post {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// Publish a public Scuttlebutt post. Redirects to profile page of the PeachCloud local identity with a flash message describing the outcome of the action (may be successful or unsuccessful).
|
||||
#[post("/publish", data = "<post>")]
|
||||
pub fn publish(
|
||||
post: Form<Post>,
|
||||
flash: Option<FlashMessage>,
|
||||
_auth: Authenticated,
|
||||
) -> Flash<Redirect> {
|
||||
let post_text = &post.text;
|
||||
// perform the sbotcli publish action using post_text
|
||||
// if successful, redirect to home profile page and flash "success"
|
||||
// if error, redirect to home profile page and flash "error"
|
||||
// redirect to the profile template without public key ("home" / local profile)
|
||||
let pub_key: std::option::Option<&str> = None;
|
||||
let profile_url = uri!(profile(pub_key));
|
||||
// consider adding the message reference to the flash message (or render it in the template for
|
||||
// `profile`
|
||||
Flash::success(Redirect::to(profile_url), "Published public post")
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /follow
|
||||
|
||||
#[derive(Debug, Deserialize, FromForm)]
|
||||
pub struct PublicKey {
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
/// Follow a Scuttlebutt profile specified by the given public key. Redirects to the appropriate profile page with a flash message describing the outcome of the action (may be successful or unsuccessful).
|
||||
#[post("/follow", data = "<pub_key>")]
|
||||
pub fn follow(
|
||||
pub_key: Form<PublicKey>,
|
||||
flash: Option<FlashMessage>,
|
||||
_auth: Authenticated,
|
||||
) -> Flash<Redirect> {
|
||||
let public_key = &pub_key.key;
|
||||
// perform the sbotcli follow action using &pub_key.0
|
||||
// if successful, redirect to profile page with provided public key and flash "success"
|
||||
// if error, redirect to profile page with provided public key and flash "error"
|
||||
// redirect to the profile template with provided public key
|
||||
let profile_url = uri!(profile(Some(public_key)));
|
||||
let success_msg = format!("Followed {}", public_key);
|
||||
Flash::success(Redirect::to(profile_url), success_msg)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /unfollow
|
||||
|
||||
/// Unfollow a Scuttlebutt profile specified by the given public key. Redirects to the appropriate profile page with a flash message describing the outcome of the action (may be successful or unsuccessful).
|
||||
#[post("/unfollow", data = "<pub_key>")]
|
||||
pub fn unfollow(
|
||||
pub_key: Form<PublicKey>,
|
||||
flash: Option<FlashMessage>,
|
||||
_auth: Authenticated,
|
||||
) -> Flash<Redirect> {
|
||||
let public_key = &pub_key.key;
|
||||
// perform the sbotcli unfollow action using &pub_key.0
|
||||
// if successful, redirect to profile page with provided public key and flash "success"
|
||||
// if error, redirect to profile page with provided public key and flash "error"
|
||||
// redirect to the profile template with provided public key
|
||||
let profile_url = uri!(profile(Some(public_key)));
|
||||
let success_msg = format!("Unfollowed {}", public_key);
|
||||
Flash::success(Redirect::to(profile_url), success_msg)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /block
|
||||
|
||||
/// Block a Scuttlebutt profile specified by the given public key. Redirects to the appropriate profile page with a flash message describing the outcome of the action (may be successful or unsuccessful).
|
||||
#[post("/block", data = "<pub_key>")]
|
||||
pub fn block(
|
||||
pub_key: Form<PublicKey>,
|
||||
flash: Option<FlashMessage>,
|
||||
_auth: Authenticated,
|
||||
) -> Flash<Redirect> {
|
||||
let public_key = &pub_key.key;
|
||||
// perform the sbotcli block action using &pub_key.0
|
||||
// if successful, redirect to profile page with provided public key and flash "success"
|
||||
// if error, redirect to profile page with provided public key and flash "error"
|
||||
// redirect to the profile template with provided public key
|
||||
let profile_url = uri!(profile(Some(public_key)));
|
||||
let success_msg = format!("Blocked {}", public_key);
|
||||
Flash::success(Redirect::to(profile_url), success_msg)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /profile
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ProfileContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl ProfileContext {
|
||||
pub fn build() -> ProfileContext {
|
||||
ProfileContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Scuttlebutt profile, specified by a public key. It may be our own profile or the profile of a peer. If not public key query parameter is provided, the local profile is displayed (ie. the profile of the public key associated with the local PeachCloud device).
|
||||
#[get("/profile?<pub_key>")]
|
||||
pub fn profile(
|
||||
pub_key: Option<&str>,
|
||||
flash: Option<FlashMessage>,
|
||||
_auth: Authenticated,
|
||||
) -> Template {
|
||||
let mut context = ProfileContext::build();
|
||||
context.back = Some("/".to_string());
|
||||
context.title = Some("Profile".to_string());
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
Template::render("scuttlebutt/profile", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /friends
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FriendsContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl FriendsContext {
|
||||
pub fn build() -> FriendsContext {
|
||||
FriendsContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of friends (mutual follows), with each list item displaying the name, image and public
|
||||
/// key of the peer.
|
||||
#[get("/friends")]
|
||||
pub fn friends(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = FriendsContext::build();
|
||||
context.back = Some("/scuttlebutt/peers".to_string());
|
||||
context.title = Some("Friends".to_string());
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
Template::render("scuttlebutt/peers_list", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /follows
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FollowsContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl FollowsContext {
|
||||
pub fn build() -> FollowsContext {
|
||||
FollowsContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of follows (peers we follow who do not follow us), with each list item displaying the name, image and public
|
||||
/// key of the peer.
|
||||
#[get("/follows")]
|
||||
pub fn follows(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = FollowsContext::build();
|
||||
context.back = Some("/scuttlebutt/peers".to_string());
|
||||
context.title = Some("Follows".to_string());
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
Template::render("scuttlebutt/peers_list", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /followers
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FollowersContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl FollowersContext {
|
||||
pub fn build() -> FollowersContext {
|
||||
FollowersContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of followers (peers who follow us but who we do not follow), with each list item displaying the name, image and public
|
||||
/// key of the peer.
|
||||
#[get("/followers")]
|
||||
pub fn followers(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = FollowersContext::build();
|
||||
context.back = Some("/scuttlebutt/peers".to_string());
|
||||
context.title = Some("Followers".to_string());
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
Template::render("scuttlebutt/peers_list", &context)
|
||||
}
|
||||
|
||||
// HELPERS AND ROUTES FOR /blocks
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BlocksContext {
|
||||
pub back: Option<String>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl BlocksContext {
|
||||
pub fn build() -> BlocksContext {
|
||||
BlocksContext {
|
||||
back: None,
|
||||
flash_name: None,
|
||||
flash_msg: None,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of blocks (peers we've blocked previously), with each list item displaying the name, image and public
|
||||
/// key of the peer.
|
||||
#[get("/blocks")]
|
||||
pub fn blocks(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
||||
let mut context = BlocksContext::build();
|
||||
context.back = Some("/scuttlebutt/peers".to_string());
|
||||
context.title = Some("Blocks".to_string());
|
||||
|
||||
// check to see if there is a flash message to display
|
||||
if let Some(flash) = flash {
|
||||
// add flash message contents to the context object
|
||||
context.flash_name = Some(flash.kind().to_string());
|
||||
context.flash_msg = Some(flash.message().to_string());
|
||||
};
|
||||
Template::render("scuttlebutt/peers_list", &context)
|
||||
}
|
42
peach-web/src/routes/scuttlebutt/block.rs
Normal file
42
peach-web/src/routes/scuttlebutt/block.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::utils::{flash::FlashResponse, sbot};
|
||||
|
||||
// ROUTE: /scuttlebutt/block
|
||||
|
||||
/// Block a Scuttlebutt profile specified by the given public key.
|
||||
///
|
||||
/// Parse the public key from the submitted form and publish a contact message.
|
||||
/// Redirect to the appropriate profile page with a flash message describing
|
||||
/// the outcome of the action (may be successful or unsuccessful).
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
public_key: String,
|
||||
}));
|
||||
|
||||
let (flash_name, flash_msg) = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
match sbot::block_peer(&data.public_key) {
|
||||
Ok(success_msg) => (
|
||||
"flash_name=success".to_string(),
|
||||
format!("flash_msg={}", success_msg),
|
||||
),
|
||||
Err(error_msg) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", error_msg),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
"flash_name=warning".to_string(),
|
||||
"Social interactions are unavailable.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
let url = format!("/scuttlebutt/profile/{}", data.public_key);
|
||||
|
||||
Response::redirect_303(url).add_flash(flash_name, flash_msg)
|
||||
}
|
29
peach-web/src/routes/scuttlebutt/blocks.rs
Normal file
29
peach-web/src/routes/scuttlebutt/blocks.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use maud::PreEscaped;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{sbot, theme},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/blocks
|
||||
|
||||
/// Scuttlebutt blocks list template builder.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
// retrieve the list of blocked peers
|
||||
match sbot::get_blocks_list() {
|
||||
// populate the peers_list template with blocks and render it
|
||||
Ok(blocks) => templates::peers_list::build_template(blocks, "Blocks"),
|
||||
Err(e) => {
|
||||
// render the sbot error template with the error message
|
||||
let error_template = templates::error::build_template(e.to_string());
|
||||
// wrap the nav bars around the error template content
|
||||
let body = templates::nav::build_template(error_template, "Blocks", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
}
|
||||
}
|
42
peach-web/src/routes/scuttlebutt/follow.rs
Normal file
42
peach-web/src/routes/scuttlebutt/follow.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::utils::{flash::FlashResponse, sbot};
|
||||
|
||||
// ROUTE: /scuttlebutt/follow
|
||||
|
||||
/// Follow a Scuttlebutt profile specified by the given public key.
|
||||
///
|
||||
/// Parse the public key from the submitted form and publish a contact message.
|
||||
/// Redirect to the appropriate profile page with a flash message describing
|
||||
/// the outcome of the action (may be successful or unsuccessful).
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
public_key: String,
|
||||
}));
|
||||
|
||||
let (flash_name, flash_msg) = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
match sbot::follow_peer(&data.public_key) {
|
||||
Ok(success_msg) => (
|
||||
"flash_name=success".to_string(),
|
||||
format!("flash_msg={}", success_msg),
|
||||
),
|
||||
Err(error_msg) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", error_msg),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
"flash_name=warning".to_string(),
|
||||
"Social interactions are unavailable.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
let url = format!("/scuttlebutt/profile/{}", data.public_key);
|
||||
|
||||
Response::redirect_303(url).add_flash(flash_name, flash_msg)
|
||||
}
|
29
peach-web/src/routes/scuttlebutt/follows.rs
Normal file
29
peach-web/src/routes/scuttlebutt/follows.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use maud::PreEscaped;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{sbot, theme},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/follows
|
||||
|
||||
/// Scuttlebutt follows list template builder.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
// retrieve the list of follows
|
||||
match sbot::get_follows_list() {
|
||||
// populate the peers_list template with follows
|
||||
Ok(follows) => templates::peers_list::build_template(follows, "Follows"),
|
||||
Err(e) => {
|
||||
// render the sbot error template with the error message
|
||||
let error_template = templates::error::build_template(e.to_string());
|
||||
// wrap the nav bars around the error template content
|
||||
let body = templates::nav::build_template(error_template, "Follows", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
}
|
||||
}
|
29
peach-web/src/routes/scuttlebutt/friends.rs
Normal file
29
peach-web/src/routes/scuttlebutt/friends.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use maud::PreEscaped;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{sbot, theme},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/friends
|
||||
|
||||
/// Scuttlebutt friends list template builder.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
// retrieve the list of friends
|
||||
match sbot::get_friends_list() {
|
||||
// populate the peers_list template with friends and render it
|
||||
Ok(friends) => templates::peers_list::build_template(friends, "Friends"),
|
||||
Err(e) => {
|
||||
// render the sbot error template with the error message
|
||||
let error_template = templates::error::build_template(e.to_string());
|
||||
// wrap the nav bars around the error template content
|
||||
let body = templates::nav::build_template(error_template, "Friends", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
}
|
||||
}
|
97
peach-web/src/routes/scuttlebutt/invites.rs
Normal file
97
peach-web/src/routes/scuttlebutt/invites.rs
Normal file
@ -0,0 +1,97 @@
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
sbot, theme,
|
||||
},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/invites
|
||||
|
||||
/// Render the invite form template.
|
||||
fn invite_form_template(
|
||||
flash_name: Option<&str>,
|
||||
flash_msg: Option<&str>,
|
||||
invite_code: Option<&str>,
|
||||
) -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- SCUTTLEBUTT INVITE FORM -->"))
|
||||
div class="card center" {
|
||||
form id="invites" class="center" action="/scuttlebutt/invites" method="post" {
|
||||
div class="center" style="width: 80%;" {
|
||||
label for="inviteUses" class="label-small font-gray" title="Number of times the invite code can be reused" { "USES" }
|
||||
input type="number" id="inviteUses" name="uses" min="1" max="150" size="3" value="1";
|
||||
@if let Some(code) = invite_code {
|
||||
p class="card-text" style="margin-top: 1rem; user-select: all;" title="Invite code" {
|
||||
(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
input id="createInvite" class="button button-primary center" style="margin-top: 1rem;" type="submit" title="Create a new invite code" value="Create";
|
||||
a id="cancel" class="button button-secondary center" href="/scuttlebutt/peers" title="Cancel" { "Cancel" }
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
// avoid displaying the invite code-containing flash msg
|
||||
@if name != "code" {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scuttlebutt invite template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
// if flash_name is "code" then flash_msg will be an invite code
|
||||
let invite_code = if flash_name == Some("code") {
|
||||
flash_msg
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let invite_form_template = match SbotStatus::read() {
|
||||
// only render the invite form template if the sbot is active
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
html! { (invite_form_template(flash_name, flash_msg, invite_code)) }
|
||||
}
|
||||
_ => {
|
||||
// the sbot is not active; render a message instead of the invite form
|
||||
templates::inactive::build_template("Invite creation is unavailable.")
|
||||
}
|
||||
};
|
||||
|
||||
let body =
|
||||
templates::nav::build_template(invite_form_template, "Invites", Some("/scuttlebutt/peers"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Parse the invite uses data and attempt to generate an invite code.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
// the number of times the invite code can be used
|
||||
uses: u16,
|
||||
}));
|
||||
|
||||
let (flash_name, flash_msg) = match sbot::create_invite(data.uses) {
|
||||
Ok(code) => ("flash_name=code".to_string(), format!("flash_msg={}", code)),
|
||||
Err(e) => ("flash_name=error".to_string(), format!("flash_msg={}", e)),
|
||||
};
|
||||
|
||||
Response::redirect_303("/scuttlebutt/invites").add_flash(flash_name, flash_msg)
|
||||
}
|
14
peach-web/src/routes/scuttlebutt/mod.rs
Normal file
14
peach-web/src/routes/scuttlebutt/mod.rs
Normal file
@ -0,0 +1,14 @@
|
||||
pub mod block;
|
||||
pub mod blocks;
|
||||
pub mod follow;
|
||||
pub mod follows;
|
||||
pub mod friends;
|
||||
pub mod invites;
|
||||
pub mod peers;
|
||||
pub mod private;
|
||||
pub mod profile;
|
||||
pub mod profile_update;
|
||||
pub mod publish;
|
||||
pub mod search;
|
||||
pub mod unblock;
|
||||
pub mod unfollow;
|
45
peach-web/src/routes/scuttlebutt/peers.rs
Normal file
45
peach-web/src/routes/scuttlebutt/peers.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use maud::{html, PreEscaped};
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
|
||||
use crate::{templates, utils::theme};
|
||||
|
||||
/// Scuttlebutt peer menu template builder.
|
||||
///
|
||||
/// A peer menu which allows navigating to lists of friends, follows, followers
|
||||
/// and blocks, as well as accessing the invite creation form.
|
||||
pub fn build_template() -> PreEscaped<String> {
|
||||
let menu_template = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
// render the scuttlebutt peers menu
|
||||
html! {
|
||||
(PreEscaped("<!-- SCUTTLEBUTT PEERS -->"))
|
||||
div class="card center" {
|
||||
div class="card-container" {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="buttons" {
|
||||
a id="search" class="button button-primary center" href="/scuttlebutt/search" title="Search for a peer" { "Search" }
|
||||
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="blocks" class="button button-primary center" href="/scuttlebutt/blocks" title="List blocks" { "Blocks" }
|
||||
a id="invites" class="button button-primary center" href="/scuttlebutt/invites" title="Create invites" { "Invites" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// the sbot is not active; render a message instead of the menu
|
||||
templates::inactive::build_template("Social lists and interactions are unavailable.")
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the nav bars around the settings menu template content
|
||||
// parameters are template, title and back url
|
||||
let body = templates::nav::build_template(menu_template, "Scuttlebutt Peers", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
// render the base template with the provided body
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
131
peach-web/src/routes/scuttlebutt/private.rs
Normal file
131
peach-web/src/routes/scuttlebutt/private.rs
Normal file
@ -0,0 +1,131 @@
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
sbot, theme,
|
||||
},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/private
|
||||
|
||||
fn public_key_input_template(ssb_id: &Option<String>) -> Markup {
|
||||
match ssb_id {
|
||||
Some(id) => {
|
||||
html! { input type="text" id="publicKey" name="recipient" placeholder="@xYz...=.ed25519" value=(id); }
|
||||
}
|
||||
// render the input with autofocus if no ssb_id has been provided
|
||||
None => {
|
||||
html! { input type="text" id="publicKey" name="recipient" placeholder="@xYz...=.ed25519" autofocus; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn private_message_textarea_template(ssb_id: &Option<String>) -> Markup {
|
||||
match ssb_id {
|
||||
Some(_) => {
|
||||
html! { textarea id="privatePost" class="center input message-input" name="text" title="Compose a private message" placeholder="Write a private message..." autofocus { "" } }
|
||||
}
|
||||
// render the textarea with autofocus if an ssb_id has been provided
|
||||
None => {
|
||||
html! { textarea id="privatePost" class="center input message-input" name="text" title="Compose a private message" placeholder="Write a private message..." { "" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scuttlebutt private message template builder.
|
||||
///
|
||||
/// Render a form for publishing a provate message. The recipient input field
|
||||
/// is populated with the provided ssb_id. If no recipient is provided, the
|
||||
/// template autofocuses on the recipient input field.
|
||||
pub fn build_template(request: &Request, ssb_id: Option<String>) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let profile_template = match SbotStatus::read() {
|
||||
// only render the private message elements if the sbot is active
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
// retrieve the local public key (set to blank if an error is returned)
|
||||
let local_id = match sbot::get_local_id() {
|
||||
Ok(id) => id,
|
||||
Err(_) => "".to_string(),
|
||||
};
|
||||
|
||||
html! {
|
||||
(PreEscaped("<!-- SCUTTLEBUTT PRIVATE MESSAGE FORM -->"))
|
||||
div class="card card-wide center" {
|
||||
form id="sbotConfig" class="center" action="/scuttlebutt/private" method="post" {
|
||||
div class="center" style="display: flex; flex-direction: column; margin-bottom: 1rem;" title="Public key (ID) of the peer being written to" {
|
||||
label for="publicKey" class="label-small font-gray" {
|
||||
"PUBLIC KEY"
|
||||
}
|
||||
(public_key_input_template(&ssb_id))
|
||||
}
|
||||
(PreEscaped("<!-- input for message contents -->"))
|
||||
(private_message_textarea_template(&ssb_id))
|
||||
(PreEscaped("<!-- hidden input field to pass the public key of the local peer -->"))
|
||||
input type="hidden" id="localId" name="id" value=(local_id);
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
input id="publish" class="button button-primary center" type="submit" style="margin-top: 1rem;" title="Publish private message to peer" value="Publish";
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => templates::inactive::build_template("Private messaging is unavailable."),
|
||||
};
|
||||
|
||||
let body = templates::nav::build_template(profile_template, "Profile", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Publish a private message.
|
||||
///
|
||||
/// Parse the public key and private message text from the submitted form
|
||||
/// and publish the message. Set a flash message communicating the outcome
|
||||
/// of the publishing attempt and redirect to the private message page.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
id: String,
|
||||
text: String,
|
||||
recipient: String
|
||||
}));
|
||||
|
||||
// now we need to add the local id to the recipients vector,
|
||||
// otherwise the local id will not be able to read the message.
|
||||
let recipients = vec![data.id, data.recipient];
|
||||
|
||||
let (flash_name, flash_msg) = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
match sbot::publish_private_msg(data.text, recipients) {
|
||||
Ok(success_msg) => (
|
||||
"flash_name=success".to_string(),
|
||||
format!("flash_msg={}", success_msg),
|
||||
),
|
||||
Err(error_msg) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", error_msg),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
"flash_name=warning".to_string(),
|
||||
"Private messaging is unavailable.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
Response::redirect_303("/scuttlebutt/private").add_flash(flash_name, flash_msg)
|
||||
}
|
178
peach-web/src/routes/scuttlebutt/profile.rs
Normal file
178
peach-web/src/routes/scuttlebutt/profile.rs
Normal file
@ -0,0 +1,178 @@
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::Request;
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{flash::FlashRequest, sbot, sbot::Profile, theme},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/profile
|
||||
|
||||
fn public_post_form_template() -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- PUBLIC POST FORM -->"))
|
||||
form id="postForm" class="center" action="/scuttlebutt/publish" method="post" {
|
||||
(PreEscaped("<!-- input for message contents -->"))
|
||||
textarea id="publicPost" class="center input message-input" name="text" title="Compose Public Post" placeholder="Write a public post..." { }
|
||||
input id="publishPost" class="button button-primary center" title="Publish" type="submit" value="Publish";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_info_box_template(profile: &Profile) -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- PROFILE INFO BOX -->"))
|
||||
div class="capsule capsule-profile border-ssb" title="Scuttlebutt account profile information" {
|
||||
@if profile.is_local_profile {
|
||||
(PreEscaped("<!-- edit profile button -->"))
|
||||
a class="nav-icon-right" href="/scuttlebutt/profile/update" title="Edit your profile" {
|
||||
img id="editProfile" class="icon-small icon-active" src="/icons/pencil.svg" alt="Edit";
|
||||
}
|
||||
}
|
||||
// render the profile bio: picture, id, name, image & description
|
||||
(profile_bio_template(profile))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_bio_template(profile: &Profile) -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- PROFILE BIO -->"))
|
||||
(PreEscaped("<!-- profile picture -->"))
|
||||
// only try to render profile pic if we have the blob
|
||||
@match &profile.blob_path {
|
||||
Some(blob_path) if profile.blob_exists => {
|
||||
img id="profilePicture" class="icon-large" src={ "/blob/" (blob_path) } title="Profile picture" alt="Profile picture";
|
||||
},
|
||||
_ => {
|
||||
// use a placeholder image if we don't have the blob
|
||||
img id="peerImage" class="icon icon-active list-icon" src="/icons/user.svg" alt="Placeholder profile image";
|
||||
}
|
||||
}
|
||||
(PreEscaped("<!-- name, public key & description -->"))
|
||||
p id="profileName" class="card-text" title="Name" {
|
||||
@if let Some(name) = &profile.name {
|
||||
(name)
|
||||
} @else {
|
||||
i { "Name is unavailable or has not been set" }
|
||||
}
|
||||
}
|
||||
label class="label-small label-ellipsis font-gray" style="user-select: all;" for="profileName" title="Public Key" {
|
||||
@if let Some(id) = &profile.id {
|
||||
(id)
|
||||
} @else {
|
||||
"Public key unavailable"
|
||||
}
|
||||
}
|
||||
p id="profileDescription" style="margin-top: 1rem" class="card-text" title="Description" {
|
||||
@if let Some(description) = &profile.description {
|
||||
(description)
|
||||
} @else {
|
||||
i { "Description is unavailable or has not been set" }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fn social_interaction_buttons_template(profile: &Profile) -> Markup {
|
||||
html! {
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
div id="buttons" style="margin-top: 2rem;" {
|
||||
@match (profile.following, &profile.id) {
|
||||
(Some(false), Some(ssb_id)) => {
|
||||
form id="followForm" class="center" action="/scuttlebutt/follow" method="post" {
|
||||
input type="hidden" id="publicKey" name="public_key" value=(ssb_id);
|
||||
input id="followPeer" class="button button-primary center" type="submit" title="Follow Peer" value="Follow";
|
||||
}
|
||||
},
|
||||
(Some(true), Some(ssb_id)) => {
|
||||
form id="unfollowForm" class="center" action="/scuttlebutt/unfollow" method="post" {
|
||||
input type="hidden" id="publicKey" name="public_key" value=(ssb_id);
|
||||
input id="unfollowPeer" class="button button-primary center" type="submit" title="Unfollow Peer" value="Unfollow";
|
||||
}
|
||||
},
|
||||
_ => p { "Unable to determine follow state" }
|
||||
}
|
||||
@match (profile.blocking, &profile.id) {
|
||||
(Some(false), Some(ssb_id)) => {
|
||||
form id="blockForm" class="center" action="/scuttlebutt/block" method="post" {
|
||||
input type="hidden" id="publicKey" name="public_key" value=(ssb_id);
|
||||
input id="blockPeer" class="button button-primary center" type="submit" title="Block Peer" value="Block";
|
||||
}
|
||||
},
|
||||
(Some(true), Some(ssb_id)) => {
|
||||
form id="unblockForm" class="center" action="/scuttlebutt/unblock" method="post" {
|
||||
input type="hidden" id="publicKey" name="public_key" value=(ssb_id);
|
||||
input id="unblockPeer" class="button button-primary center" type="submit" title="Unblock Peer" value="Unblock";
|
||||
}
|
||||
},
|
||||
_ => p { "Unable to determine block state" }
|
||||
}
|
||||
@if let Some(ssb_id) = &profile.id {
|
||||
form class="center" {
|
||||
a id="privateMessage" class="button button-primary center" href={ "/scuttlebutt/private/" (ssb_id) } title="Private Message" {
|
||||
"Send Private Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scuttlebutt profile template builder.
|
||||
///
|
||||
/// Render a Scuttlebutt profile, either for the local profile or for a peer
|
||||
/// specified by a public key. If the public key query parameter is not
|
||||
/// provided, the local profile is displayed (ie. the profile of the public key
|
||||
/// associated with the local PeachCloud device).
|
||||
pub fn build_template(request: &Request, ssb_id: Option<String>) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let profile_template = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
// TODO: validate ssb_id and return error template
|
||||
|
||||
// retrieve the profile info
|
||||
match sbot::get_profile_info(ssb_id) {
|
||||
Ok(profile) => {
|
||||
// render the profile template
|
||||
html! {
|
||||
(PreEscaped("<!-- SSB PROFILE -->"))
|
||||
div class="card card-wide center" {
|
||||
// render profile info box
|
||||
(profile_info_box_template(&profile))
|
||||
@if profile.is_local_profile {
|
||||
// render the public post form template
|
||||
(public_post_form_template())
|
||||
} @else {
|
||||
// render follow / unfollow, block / unblock and
|
||||
// private message buttons
|
||||
(social_interaction_buttons_template(&profile))
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// render the sbot error template with the error message
|
||||
templates::error::build_template(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => templates::inactive::build_template("Profile is unavailable."),
|
||||
};
|
||||
|
||||
let body = templates::nav::build_template(profile_template, "Profile", Some("/"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
173
peach-web/src/routes/scuttlebutt/profile_update.rs
Normal file
173
peach-web/src/routes/scuttlebutt/profile_update.rs
Normal file
@ -0,0 +1,173 @@
|
||||
use maud::{html, PreEscaped};
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{input::post::BufferedFile, post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
sbot,
|
||||
sbot::Profile,
|
||||
theme,
|
||||
},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/profile/update
|
||||
|
||||
fn parse_profile_info(profile: Profile) -> (String, String, String) {
|
||||
let id = match profile.id {
|
||||
Some(id) => id,
|
||||
_ => "Public key unavailable".to_string(),
|
||||
};
|
||||
|
||||
let name = match profile.name {
|
||||
Some(name) => name,
|
||||
_ => "Name unavailable".to_string(),
|
||||
};
|
||||
|
||||
let description = match profile.description {
|
||||
Some(description) => description,
|
||||
_ => "Description unavailable".to_string(),
|
||||
};
|
||||
|
||||
(id, name, description)
|
||||
}
|
||||
|
||||
/// Scuttlebutt profile update template builder.
|
||||
///
|
||||
/// Serve a form for the purpose of updating the name, description and picture
|
||||
/// for the local Scuttlebutt profile.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let profile_update_template = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
// retrieve the local profile info
|
||||
match sbot::get_profile_info(None) {
|
||||
Ok(profile) => {
|
||||
let (id, name, description) = parse_profile_info(profile);
|
||||
|
||||
// render the scuttlebutt profile update form
|
||||
html! {
|
||||
(PreEscaped("<!-- SSB PROFILE UPDATE FORM -->"))
|
||||
div class="card card-wide center" {
|
||||
form id="profileInfo" class="center" enctype="multipart/form-data" action="/scuttlebutt/profile/update" method="post" {
|
||||
div style="display: flex; flex-direction: column" {
|
||||
label for="name" class="label-small font-gray" {
|
||||
"NAME"
|
||||
}
|
||||
input style="margin-bottom: 1rem;" type="text" id="name" name="new_name" placeholder="Choose a name for your profile..." value=(name);
|
||||
label for="description" class="label-small font-gray" {
|
||||
"DESCRIPTION"
|
||||
}
|
||||
textarea id="description" class="message-input" style="margin-bottom: 1rem;" name="new_description" placeholder="Write a description for your profile..." {
|
||||
(description)
|
||||
}
|
||||
label for="image" class="label-small font-gray" {
|
||||
"IMAGE"
|
||||
}
|
||||
input type="file" id="fileInput" class="font-normal" name="image";
|
||||
}
|
||||
input type="hidden" name="id" value=(id);
|
||||
input type="hidden" name="current_name" value=(name);
|
||||
input type="hidden" name="current_description" value=(description);
|
||||
div id="buttonDiv" style="margin-top: 2rem;" {
|
||||
input id="updateProfile" class="button button-primary center" title="Publish" type="submit" value="Publish";
|
||||
a class="button button-secondary center" href="/scuttlebutt/profile" title="Cancel" { "Cancel" }
|
||||
|
||||
}
|
||||
}
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// render the sbot error template with the error message
|
||||
templates::error::build_template(e.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// the sbot is not active; render a message instead of the form
|
||||
templates::inactive::build_template("Profile is unavailable.")
|
||||
}
|
||||
};
|
||||
|
||||
let body = templates::nav::build_template(
|
||||
profile_update_template,
|
||||
"Profile",
|
||||
Some("/scuttlebutt/profile"),
|
||||
);
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Update the name, description and picture for the local Scuttlebutt profile.
|
||||
///
|
||||
/// Redirects to profile page of the PeachCloud local identity with a flash
|
||||
/// message describing the outcome of the action (may be successful or
|
||||
/// unsuccessful).
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
id: String,
|
||||
current_name: String,
|
||||
current_description: String,
|
||||
new_name: Option<String>,
|
||||
new_description: Option<String>,
|
||||
image: Option<BufferedFile>,
|
||||
}));
|
||||
|
||||
let (flash_name, flash_msg) = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
// we can't pass `data` into the function (due to macro creation)
|
||||
// so we pass in each individual value instead
|
||||
match sbot::update_profile_info(
|
||||
data.current_name,
|
||||
data.current_description,
|
||||
data.new_name,
|
||||
data.new_description,
|
||||
data.image,
|
||||
) {
|
||||
Ok(success_msg) => (
|
||||
"flash_name=success".to_string(),
|
||||
format!("flash_msg={}", success_msg),
|
||||
),
|
||||
Err(error_msg) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", error_msg),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
"flash_name=warning".to_string(),
|
||||
"Profile is unavailable.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
Response::redirect_303("/scuttlebutt/profile/update").add_flash(flash_name, flash_msg)
|
||||
}
|
||||
|
||||
/*
|
||||
match sbot::validate_public_key(&data.public_key) {
|
||||
Ok(_) => {
|
||||
let url = format!("/scuttlebutt/profile?={}", &data.public_key);
|
||||
Response::redirect_303(url)
|
||||
}
|
||||
Err(err) => {
|
||||
let (flash_name, flash_msg) =
|
||||
("flash_name=error".to_string(), format!("flash_msg={}", err));
|
||||
Response::redirect_303("/scuttlebutt/search").add_flash(flash_name, flash_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
41
peach-web/src/routes/scuttlebutt/publish.rs
Normal file
41
peach-web/src/routes/scuttlebutt/publish.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use peach_lib::sbot::SbotStatus;
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::utils::{flash::FlashResponse, sbot};
|
||||
|
||||
// ROUTE: /scuttlebutt/publish
|
||||
|
||||
/// Publish a public Scuttlebutt post.
|
||||
///
|
||||
/// Parse the post text from the submitted form and publish the message.
|
||||
/// Redirect to the profile page of the PeachCloud local identity with a flash
|
||||
/// message describing the outcome of the action (may be successful or
|
||||
/// unsuccessful).
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
text: String,
|
||||
}));
|
||||
|
||||
let (flash_name, flash_msg) = match SbotStatus::read() {
|
||||
Ok(status) if status.state == Some("active".to_string()) => {
|
||||
match sbot::publish_public_post(data.text) {
|
||||
Ok(success_msg) => (
|
||||
"flash_name=success".to_string(),
|
||||
format!("flash_msg={}", success_msg),
|
||||
),
|
||||
Err(error_msg) => (
|
||||
"flash_name=error".to_string(),
|
||||
format!("flash_msg={}", error_msg),
|
||||
),
|
||||
}
|
||||
}
|
||||
_ => (
|
||||
"flash_name=warning".to_string(),
|
||||
"Public posting is unavailable.".to_string(),
|
||||
),
|
||||
};
|
||||
|
||||
Response::redirect_303("/scuttlebutt/profile").add_flash(flash_name, flash_msg)
|
||||
}
|
69
peach-web/src/routes/scuttlebutt/search.rs
Normal file
69
peach-web/src/routes/scuttlebutt/search.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use maud::{html, PreEscaped};
|
||||
use rouille::{post_input, try_or_400, Request, Response};
|
||||
|
||||
use crate::{
|
||||
templates,
|
||||
utils::{
|
||||
flash::{FlashRequest, FlashResponse},
|
||||
sbot, theme,
|
||||
},
|
||||
};
|
||||
|
||||
// ROUTE: /scuttlebutt/search
|
||||
|
||||
/// Scuttlebutt peer search template builder.
|
||||
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||
|
||||
let search_template = html! {
|
||||
(PreEscaped("<!-- PEER SEARCH FORM -->"))
|
||||
div class="card center" {
|
||||
form id="sbotConfig" class="center" action="/scuttlebutt/search" method="post" {
|
||||
div class="center" style="display: flex; flex-direction: column; margin-bottom: 2rem;" title="Public key (ID) of a peer" {
|
||||
label for="publicKey" class="label-small font-gray" { "PUBLIC KEY" }
|
||||
input type="text" id="publicKey" name="public_key" placeholder="@xYz...=.ed25519" autofocus;
|
||||
}
|
||||
(PreEscaped("<!-- BUTTONS -->"))
|
||||
input id="search" class="button button-primary center" type="submit" title="Search for peer" value="Search";
|
||||
// render flash message if cookies were found in the request
|
||||
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||
(templates::flash::build_template(name, msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let body =
|
||||
templates::nav::build_template(search_template, "Search", Some("/scuttlebutt/peers"));
|
||||
|
||||
// query the current theme so we can pass it into the base template builder
|
||||
let theme = theme::get_theme();
|
||||
|
||||
templates::base::build_template(body, theme)
|
||||
}
|
||||
|
||||
/// Parse the public key, verify that it's valid and then redirect to the
|
||||
/// profile of the given key.
|
||||
///
|
||||
/// If the public key is invalid, set an error flash message and redirect.
|
||||
pub fn handle_form(request: &Request) -> Response {
|
||||
// query the request body for form data
|
||||
// return a 400 error if the admin_id field is missing
|
||||
let data = try_or_400!(post_input!(request, {
|
||||
public_key: String,
|
||||
}));
|
||||
|
||||
match sbot::validate_public_key(&data.public_key) {
|
||||
Ok(_) => {
|
||||
let url = format!("/scuttlebutt/profile/{}", &data.public_key);
|
||||
Response::redirect_303(url)
|
||||
}
|
||||
Err(err) => {
|
||||
let (flash_name, flash_msg) =
|
||||
("flash_name=error".to_string(), format!("flash_msg={}", err));
|
||||
Response::redirect_303("/scuttlebutt/search").add_flash(flash_name, flash_msg)
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user