Compare commits
181 Commits
fix_clippy
...
tilde-inte
Author | SHA1 | Date | |
---|---|---|---|
22e32a5715 | |||
6bbfb454de | |||
b4e2dd2683 | |||
2c26200867 | |||
6cc8faa0c3 | |||
6028e07bde | |||
ebc7b9d417 | |||
b8ff944377 | |||
8cbb295c3a | |||
7d5d6bcc1f | |||
8c3a92aa88 | |||
cfe270a995 | |||
2eca779208 | |||
a1b16f8d38 | |||
3bf095e148 | |||
d9167a2cd6 | |||
4e7fbd5fdf | |||
0fab57d94f | |||
441d2a6a3b | |||
52e0aff4d1 | |||
24ceedbb9d | |||
d3ab490c05 | |||
1e7a54b728 | |||
3eab3e3687 | |||
8b0381ead1 | |||
e91c40355a | |||
8cd8ee5dd6 | |||
24deb4601a | |||
fedf2855ed | |||
0814eedf13 | |||
4fb4ea2f9c | |||
8e283fbc6e | |||
bdd3b7ab9b | |||
4f36f61128 | |||
acab30acce | |||
61ef909ed3 | |||
97030fbfbf | |||
b6cd54142c | |||
67f33385e5 | |||
a9bcc267a2 | |||
a513b7aa5b | |||
1a7bd7987b | |||
c5c0bb91e4 | |||
5a50730435 | |||
86b4714274 | |||
d5a2390e29 | |||
c83a22461d | |||
40bd1e48f1 | |||
6407495292 | |||
03ac890793 | |||
bc0c0fca7f | |||
05c1577f2a | |||
add169db07 | |||
fcb17d6802 | |||
fc50bb5ee5 | |||
29f5ad0e84 | |||
cb09d6c3e9 | |||
01138eef35 | |||
2637b28380 | |||
03a0a51f4d | |||
eddb167c4c | |||
9704269c8a | |||
466db8ceea | |||
90badbfe30 | |||
7489916d5f | |||
7daab74b37 | |||
58bf306d3b | |||
bdac23092a | |||
f1ab2caa08 | |||
1fab4f3c43 | |||
8dcd594dd7 | |||
fcaa9e29c4 | |||
c6fc5c2992 | |||
1258a3697d | |||
7e94135839 | |||
f002f5cf3e | |||
fba1e91d8b | |||
6621a09ec9 | |||
170b037248 | |||
251aaf9237 | |||
123ebc06cc | |||
9ce27d17c5 | |||
2a8cf4ecfb | |||
d1a55e29d7 | |||
4568577f81 | |||
ab0e27c14d | |||
65b5f95a90 | |||
a60d892e95 | |||
5bd8a68ddf | |||
a6f52ce384 | |||
c71cc3992d | |||
56fafc8d67 | |||
414508f8ff | |||
6ad5c620c1 | |||
76d5e6a355 | |||
11e94fa421 | |||
216b60b86a | |||
a70f5e227d | |||
cddcb8f9bd | |||
8b33f8c174 | |||
1b43dc8b18 | |||
2d7b74d377 | |||
6b34864289 | |||
87ad2439b9 | |||
5838faf128 | |||
9c6fa00ec7 | |||
a81b8b42cf | |||
cdcff3475c | |||
077c2a9178 | |||
8b0b872d21 | |||
218a70b8f8 | |||
50dcb2cf9e | |||
e1877b5024 | |||
0923c24693 | |||
f3d4ba9fe5 | |||
16e6d42f87 | |||
3493e5adb9 | |||
5147eed497 | |||
9f6ba14123 | |||
21fb29c322 | |||
6c9e5fd3fd | |||
3adb226969 | |||
92f516b161 | |||
543470b949 | |||
6434471599 | |||
56c142a387 | |||
7deaa00d6e | |||
bf7f2c8e31 | |||
dc79833e2b | |||
039cbcdfc4 | |||
4cd2692b9a | |||
3e5e7e0f7c | |||
b0b79fef24 | |||
98497fa5ae | |||
e6fd9a48cf | |||
8960df6635 | |||
781af460ae | |||
4a08e4ed6d | |||
908d265de6 | |||
8202d4af5f | |||
5ea6a86700 | |||
99fd3be4ad | |||
e041e1c7f9 | |||
e10777a5a5 | |||
288941e8a3 | |||
f96c950aa6 | |||
827ccbd4dc | |||
c21e2d090c | |||
bab33b602a | |||
b84e470a42 | |||
97680f9010 | |||
ab0401e555 | |||
810a97db8a | |||
610d60d989 | |||
f4c1bc1169 | |||
3ae182caa9 | |||
8a6ad4ad61 | |||
600f9c58bf | |||
2540a77af1 | |||
5b86f754f4 | |||
29804b0dce | |||
e2ac5de6e4 | |||
d03de8cf5d | |||
03720a7338 | |||
cf9c0c7eca | |||
f764acc2df | |||
a347e4726d | |||
3d3006049b | |||
2adb3006fe | |||
64b5929e5c | |||
5629a048a1 | |||
713c3da4cc | |||
92c7d7daa9 | |||
5a95ade8b9 | |||
315b04a63e | |||
1866e289a6 | |||
bff86a490b | |||
65d5352c85 | |||
df3b4b8858 | |||
2f1535fbee | |||
b75aadd62d |
@ -30,5 +30,4 @@ steps:
|
|||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
event:
|
event:
|
||||||
- push
|
|
||||||
- pull_request
|
- pull_request
|
||||||
|
1557
Cargo.lock
generated
1557
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
"peach-buttons",
|
|
||||||
"peach-oled",
|
"peach-oled",
|
||||||
"peach-lib",
|
"peach-lib",
|
||||||
"peach-config",
|
"peach-config",
|
||||||
@ -11,5 +9,7 @@ members = [
|
|||||||
"peach-monitor",
|
"peach-monitor",
|
||||||
"peach-stats",
|
"peach-stats",
|
||||||
"peach-jsonrpc-server",
|
"peach-jsonrpc-server",
|
||||||
"peach-dyndns-updater"
|
"peach-dyndns-updater",
|
||||||
|
"tilde-client"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
13
README.md
13
README.md
@ -45,6 +45,19 @@ _Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware
|
|||||||
- [peach-patterns](https://github.com/peachcloud/peach-patterns) - Pattern library for the PeachCloud UI design system
|
- [peach-patterns](https://github.com/peachcloud/peach-patterns) - Pattern library for the PeachCloud UI design system
|
||||||
- [peach-web](https://github.com/peachcloud/peach-web) - A web interface for monitoring and interacting with the PeachCloud device
|
- [peach-web](https://github.com/peachcloud/peach-web) - A web interface for monitoring and interacting with the PeachCloud device
|
||||||
|
|
||||||
|
## Continuous Integration
|
||||||
|
|
||||||
|
[Drone CI](https://docs.drone.io/) is used to provide continuous integration for this workspace. The configuration file can be found in `.drone.yml` in the root of this repository. It is currently configured to run `cargo fmt`, `cargo clippy`, `cargo test` and `cargo build` on every `pull request` event. The pipeline runs on the AMD64 Debian Buster image from the official Rust Docker image repository.
|
||||||
|
|
||||||
|
The status of the current and previous CI builds can be viewed via the [Drone CI Build UI](https://build.coopcloud.tech/PeachCloud/peach-workspace) (kindly hosted by Co-op Cloud).
|
||||||
|
|
||||||
|
Adding `[CI SKIP]` to the end of a commit message results in the CI checks being skipped for the next event. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
git commit -m "update readme [CI SKIP]"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
## Developer Diaries
|
## Developer Diaries
|
||||||
|
|
||||||
- [@ahdinosaur](https://github.com/ahdinosaur): `@6ilZq3kN0F+dXFHAPjAwMm87JEb/VdB+LC9eIMW3sa0=.ed25519`
|
- [@ahdinosaur](https://github.com/ahdinosaur): `@6ilZq3kN0F+dXFHAPjAwMm87JEb/VdB+LC9eIMW3sa0=.ed25519`
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "peach-config"
|
name = "peach-config"
|
||||||
version = "0.1.17"
|
version = "0.1.27"
|
||||||
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
|
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "Command line tool for installing, updating and configuring PeachCloud"
|
description = "Command line tool for installing, updating and configuring PeachCloud"
|
||||||
@ -37,3 +37,5 @@ log = "0.4"
|
|||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
peach-lib = { path = "../peach-lib" }
|
peach-lib = { path = "../peach-lib" }
|
||||||
rpassword = "5.0"
|
rpassword = "5.0"
|
||||||
|
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
|
||||||
|
async-std = "1.10.0"
|
||||||
|
@ -10,7 +10,7 @@ pub const SERVICES: [&str; 8] = [
|
|||||||
"peach-buttons",
|
"peach-buttons",
|
||||||
"peach-oled",
|
"peach-oled",
|
||||||
"peach-dyndns-updater",
|
"peach-dyndns-updater",
|
||||||
"peach-go-sbot",
|
"go-sbot",
|
||||||
"peach-config",
|
"peach-config",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#![allow(clippy::nonstandard_macro_braces)]
|
#![allow(clippy::nonstandard_macro_braces)]
|
||||||
|
use golgi::error::GolgiError;
|
||||||
use peach_lib::error::PeachError;
|
use peach_lib::error::PeachError;
|
||||||
pub use snafu::ResultExt;
|
pub use snafu::ResultExt;
|
||||||
use snafu::Snafu;
|
use snafu::Snafu;
|
||||||
@ -35,6 +36,14 @@ pub enum PeachConfigError {
|
|||||||
ChangePasswordError { source: PeachError },
|
ChangePasswordError { source: PeachError },
|
||||||
#[snafu(display("Entered passwords did not match. Please try again."))]
|
#[snafu(display("Entered passwords did not match. Please try again."))]
|
||||||
InvalidPassword,
|
InvalidPassword,
|
||||||
|
#[snafu(display("Error in peach lib: {}", source))]
|
||||||
|
PeachLibError { source: PeachError },
|
||||||
|
#[snafu(display("Error in golgi: {}", source))]
|
||||||
|
Golgi { source: GolgiError },
|
||||||
|
#[snafu(display("{}", message))]
|
||||||
|
CmdInputError { message: String },
|
||||||
|
#[snafu(display("{}", message))]
|
||||||
|
WaitForSbotError { message: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::io::Error> for PeachConfigError {
|
impl From<std::io::Error> for PeachConfigError {
|
||||||
@ -51,3 +60,15 @@ impl From<serde_json::Error> for PeachConfigError {
|
|||||||
PeachConfigError::SerdeError { source: err }
|
PeachConfigError::SerdeError { source: err }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<PeachError> for PeachConfigError {
|
||||||
|
fn from(err: PeachError) -> PeachConfigError {
|
||||||
|
PeachConfigError::PeachLibError { source: err }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GolgiError> for PeachConfigError {
|
||||||
|
fn from(err: GolgiError) -> PeachConfigError {
|
||||||
|
PeachConfigError::Golgi { source: err }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,40 +1,32 @@
|
|||||||
use regex::Regex;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use snafu::ResultExt;
|
use snafu::ResultExt;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use crate::constants::HARDWARE_CONFIG_FILE;
|
use crate::constants::{HARDWARE_CONFIG_FILE, SERVICES};
|
||||||
use crate::error::{FileReadError, FileWriteError, PeachConfigError};
|
use crate::error::{FileReadError, FileWriteError, PeachConfigError};
|
||||||
use crate::utils::get_output;
|
use crate::utils::get_output;
|
||||||
use crate::RtcOption;
|
use crate::RtcOption;
|
||||||
|
|
||||||
|
/// Helper function which returns the version of a package currently installed,
|
||||||
|
/// as an Ok(String) if found, and as an Err if not found
|
||||||
|
pub fn get_package_version_number(package: &str) -> Result<String, PeachConfigError> {
|
||||||
|
let version = get_output(&["dpkg-query", "--showformat=${Version}", "--show", package])?;
|
||||||
|
Ok(version)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a HashMap<String, String> of all the peach-packages which are currently installed
|
/// Returns a HashMap<String, String> of all the peach-packages which are currently installed
|
||||||
/// mapped to their version number e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" }
|
/// mapped to their version number e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" }
|
||||||
pub fn get_currently_installed_microservices() -> Result<HashMap<String, String>, PeachConfigError>
|
pub fn get_currently_installed_microservices() -> Result<HashMap<String, String>, PeachConfigError>
|
||||||
{
|
{
|
||||||
// gets a list of all packages currently installed with dpkg
|
// gets a list of all packages currently installed with dpkg-query
|
||||||
let packages = get_output(&["dpkg", "-l"])?;
|
let peach_packages: HashMap<String, String> = SERVICES
|
||||||
|
.iter()
|
||||||
// this regex matches packages which contain the word peach in them
|
.filter_map(|service| {
|
||||||
// and has two match groups
|
let version = get_package_version_number(service);
|
||||||
// 1. the first match group gets the package name
|
match version {
|
||||||
// 2. the second match group gets the version number of the package
|
Ok(v) => Some((service.to_string(), v)),
|
||||||
let re: Regex = Regex::new(r"\S+\s+(\S*peach\S+)\s+(\S+).*\n").unwrap();
|
Err(_) => None,
|
||||||
|
|
||||||
// the following iterator, iterates through the captures matched via the regex
|
|
||||||
// and for each capture, creates a value in the hash map,
|
|
||||||
// which maps the name of the package, to its version number
|
|
||||||
// e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" }
|
|
||||||
let peach_packages: HashMap<String, String> = re
|
|
||||||
.captures_iter(&packages)
|
|
||||||
.filter_map(|cap| {
|
|
||||||
let groups = (cap.get(1), cap.get(2));
|
|
||||||
match groups {
|
|
||||||
(Some(package), Some(version)) => {
|
|
||||||
Some((package.as_str().to_string(), version.as_str().to_string()))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -2,12 +2,15 @@ mod change_password;
|
|||||||
mod constants;
|
mod constants;
|
||||||
mod error;
|
mod error;
|
||||||
mod generate_manifest;
|
mod generate_manifest;
|
||||||
|
mod publish_address;
|
||||||
mod set_permissions;
|
mod set_permissions;
|
||||||
mod setup_networking;
|
mod setup_networking;
|
||||||
mod setup_peach;
|
mod setup_peach;
|
||||||
mod setup_peach_deb;
|
mod setup_peach_deb;
|
||||||
|
mod status;
|
||||||
mod update;
|
mod update;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
mod wait_for_sbot;
|
||||||
|
|
||||||
use clap::arg_enum;
|
use clap::arg_enum;
|
||||||
use log::error;
|
use log::error;
|
||||||
@ -44,12 +47,25 @@ enum PeachConfig {
|
|||||||
Update(UpdateOpts),
|
Update(UpdateOpts),
|
||||||
|
|
||||||
/// Changes the password for the peach-web interface
|
/// Changes the password for the peach-web interface
|
||||||
#[structopt(name = "changepassword")]
|
#[structopt(name = "change-password")]
|
||||||
ChangePassword(ChangePasswordOpts),
|
ChangePassword(ChangePasswordOpts),
|
||||||
|
|
||||||
/// Updates file permissions on PeachCloud device
|
/// Updates file permissions on PeachCloud device
|
||||||
#[structopt(name = "permissions")]
|
#[structopt(name = "permissions")]
|
||||||
SetPermissions,
|
SetPermissions,
|
||||||
|
|
||||||
|
/// Returns sbot id if sbot is running
|
||||||
|
#[structopt(name = "whoami")]
|
||||||
|
WhoAmI,
|
||||||
|
|
||||||
|
/// Publish domain and port.
|
||||||
|
/// It takes an address argument of the form host:port
|
||||||
|
#[structopt(name = "publish-address")]
|
||||||
|
PublishAddress(PublishAddressOpts),
|
||||||
|
|
||||||
|
/// Wait for a successful connection to sbot
|
||||||
|
#[structopt(name = "wait-for-sbot")]
|
||||||
|
WaitForSbot,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
@ -90,6 +106,13 @@ pub struct ChangePasswordOpts {
|
|||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)]
|
||||||
|
pub struct PublishAddressOpts {
|
||||||
|
/// Specify address in the form domain:port
|
||||||
|
#[structopt(short, long)]
|
||||||
|
address: String,
|
||||||
|
}
|
||||||
|
|
||||||
arg_enum! {
|
arg_enum! {
|
||||||
/// enum options for real-time clock choices
|
/// enum options for real-time clock choices
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -102,7 +125,7 @@ arg_enum! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
async fn run() {
|
||||||
// initialize the logger
|
// initialize the logger
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
@ -155,6 +178,42 @@ fn main() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
PeachConfig::WhoAmI => match status::whoami().await {
|
||||||
|
Ok(sbot_id) => {
|
||||||
|
println!("{:?}", sbot_id);
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("sbot whoami encountered an error: {}", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PeachConfig::PublishAddress(opts) => {
|
||||||
|
match publish_address::publish_address(opts.address).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"peach-config encountered an error during publish address: {}",
|
||||||
|
err
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PeachConfig::WaitForSbot => match wait_for_sbot::wait_for_sbot().await {
|
||||||
|
Ok(sbot_id) => {
|
||||||
|
println!("connected with sbot and found sbot_id: {:?}", sbot_id)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("peach-config did not successfully connect to sbot: {}", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable an async main function and execute the `run()` function,
|
||||||
|
// catching any errors and printing them to `stderr` before exiting the
|
||||||
|
// process.
|
||||||
|
#[async_std::main]
|
||||||
|
async fn main() {
|
||||||
|
run().await;
|
||||||
|
}
|
||||||
|
37
peach-config/src/publish_address.rs
Normal file
37
peach-config/src/publish_address.rs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
use crate::error::PeachConfigError;
|
||||||
|
use golgi::kuska_ssb::api::dto::content::PubAddress;
|
||||||
|
use golgi::messages::SsbMessageContent;
|
||||||
|
use peach_lib::sbot::init_sbot;
|
||||||
|
|
||||||
|
/// Utility function to publish the address (domain:port) of the pub
|
||||||
|
/// publishing the address causes the domain and port to be used for invite generation,
|
||||||
|
/// and also gossips this pub address to their peers
|
||||||
|
pub async fn publish_address(address: String) -> Result<(), PeachConfigError> {
|
||||||
|
// split address into domain:port
|
||||||
|
let split: Vec<&str> = address.split(':').collect();
|
||||||
|
let (domain, port): (&str, &str) = (split[0], split[1]);
|
||||||
|
// convert port to u16
|
||||||
|
let port_as_u16: u16 = port
|
||||||
|
.parse()
|
||||||
|
.map_err(|_err| PeachConfigError::CmdInputError {
|
||||||
|
message: "Failure to parse domain and port. Address must be of the format host:port."
|
||||||
|
.to_string(),
|
||||||
|
})?;
|
||||||
|
// publish address
|
||||||
|
let mut sbot = init_sbot().await?;
|
||||||
|
let pub_id = sbot.whoami().await?;
|
||||||
|
// Compose a `pub` address type message.
|
||||||
|
let pub_address_msg = SsbMessageContent::Pub {
|
||||||
|
address: Some(PubAddress {
|
||||||
|
// Host name (can be an IP address if onboarding over WiFi).
|
||||||
|
host: Some(domain.to_string()),
|
||||||
|
// Port.
|
||||||
|
port: port_as_u16,
|
||||||
|
// Public key.
|
||||||
|
key: pub_id,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
// Publish the `pub` address message.
|
||||||
|
let _pub_msg_ref = sbot.publish(pub_address_msg).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,21 +1,30 @@
|
|||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
use peach_lib::config_manager;
|
||||||
|
|
||||||
use crate::error::PeachConfigError;
|
use crate::error::PeachConfigError;
|
||||||
use crate::utils::cmd;
|
use crate::utils::cmd;
|
||||||
|
|
||||||
/// All configs are stored in this folder, and should be read/writeable by peach group
|
lazy_static! {
|
||||||
/// so they can be read and written by all PeachCloud services.
|
pub static ref PEACH_CONFIGDIR: String = config_manager::get_config_value("PEACH_CONFIGDIR")
|
||||||
pub const CONFIGS_DIR: &str = "/var/lib/peachcloud";
|
.expect("Failed to load config value for PEACH_CONFIGDIR");
|
||||||
pub const PEACH_WEB_DIR: &str = "/usr/share/peach-web";
|
pub static ref PEACH_WEBDIR: String = config_manager::get_config_value("PEACH_WEBDIR")
|
||||||
|
.expect("Failed to load config value for PEACH_WEBDIR");
|
||||||
|
pub static ref PEACH_HOMEDIR: String = config_manager::get_config_value("PEACH_HOMEDIR")
|
||||||
|
.expect("Failed to load config value for PEACH_HOMEDIR");
|
||||||
|
}
|
||||||
|
|
||||||
/// Utility function to set correct file permissions on the PeachCloud device.
|
/// Utility function to set correct file permissions on the PeachCloud device.
|
||||||
/// Accidentally changing file permissions is a fairly common thing to happen,
|
/// 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.
|
/// so this is a useful CLI function for quickly correcting anything that may be out of order.
|
||||||
pub fn set_permissions() -> Result<(), PeachConfigError> {
|
pub fn set_permissions() -> Result<(), PeachConfigError> {
|
||||||
println!("[ UPDATING FILE PERMISSIONS ON PEACHCLOUD DEVICE ]");
|
println!("[ UPDATING FILE PERMISSIONS ON PEACHCLOUD DEVICE ]");
|
||||||
cmd(&["chmod", "-R", "u+rwX,g+rwX", CONFIGS_DIR])?;
|
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_CONFIGDIR])?;
|
||||||
cmd(&["chown", "-R", "peach", CONFIGS_DIR])?;
|
cmd(&["chown", "-R", "peach:peach", &PEACH_CONFIGDIR])?;
|
||||||
cmd(&["chgrp", "-R", "peach", CONFIGS_DIR])?;
|
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_WEBDIR])?;
|
||||||
cmd(&["chmod", "-R", "u+rwX,g+rwX", PEACH_WEB_DIR])?;
|
cmd(&["chown", "-R", "peach:peach", &PEACH_WEBDIR])?;
|
||||||
cmd(&["chown", "-R", "peach-web:peach", PEACH_WEB_DIR])?;
|
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_HOMEDIR])?;
|
||||||
|
cmd(&["chown", "-R", "peach:peach", &PEACH_HOMEDIR])?;
|
||||||
println!("[ PERMISSIONS SUCCESSFULLY UPDATED ]");
|
println!("[ PERMISSIONS SUCCESSFULLY UPDATED ]");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ use std::fs;
|
|||||||
|
|
||||||
use crate::error::{FileWriteError, PeachConfigError};
|
use crate::error::{FileWriteError, PeachConfigError};
|
||||||
use crate::generate_manifest::save_hardware_config;
|
use crate::generate_manifest::save_hardware_config;
|
||||||
|
use crate::set_permissions::set_permissions;
|
||||||
use crate::setup_networking::configure_networking;
|
use crate::setup_networking::configure_networking;
|
||||||
use crate::setup_peach_deb::setup_peach_deb;
|
use crate::setup_peach_deb::setup_peach_deb;
|
||||||
use crate::update::update_microservices;
|
use crate::update::update_microservices;
|
||||||
@ -239,6 +240,9 @@ pub fn setup_peach(
|
|||||||
info!("[ SAVING LOG OF HARDWARE CONFIGURATIONS ]");
|
info!("[ SAVING LOG OF HARDWARE CONFIGURATIONS ]");
|
||||||
save_hardware_config(i2c, rtc)?;
|
save_hardware_config(i2c, rtc)?;
|
||||||
|
|
||||||
|
info!("[ SETTING FILE PERMISSIONS ]");
|
||||||
|
set_permissions()?;
|
||||||
|
|
||||||
info!("[ PEACHCLOUD SETUP COMPLETE ]");
|
info!("[ PEACHCLOUD SETUP COMPLETE ]");
|
||||||
info!("[ ------------------------- ]");
|
info!("[ ------------------------- ]");
|
||||||
info!("[ please reboot your device ]");
|
info!("[ please reboot your device ]");
|
||||||
|
9
peach-config/src/status.rs
Normal file
9
peach-config/src/status.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
use crate::error::PeachConfigError;
|
||||||
|
use peach_lib::sbot::init_sbot;
|
||||||
|
|
||||||
|
/// Utility function to check if sbot is running via the whoami method
|
||||||
|
pub async fn whoami() -> Result<String, PeachConfigError> {
|
||||||
|
let mut sbot = init_sbot().await?;
|
||||||
|
let sbot_id = sbot.whoami().await?;
|
||||||
|
Ok(sbot_id)
|
||||||
|
}
|
52
peach-config/src/wait_for_sbot.rs
Normal file
52
peach-config/src/wait_for_sbot.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use std::{thread, time};
|
||||||
|
|
||||||
|
use crate::error::PeachConfigError;
|
||||||
|
use peach_lib::sbot::init_sbot;
|
||||||
|
|
||||||
|
static MAX_NUM_ATTEMPTS: u8 = 10;
|
||||||
|
|
||||||
|
/// Utility function to wait for a successful whoami call with sbot
|
||||||
|
/// After each attempt to call whoami it waits 2 seconds,
|
||||||
|
/// and if after MAX_NUM_ATTEMPTS (10) there is no successful whoami call
|
||||||
|
/// it returns an Error. Otherwise it returns Ok(sbot_id).
|
||||||
|
pub async fn wait_for_sbot() -> Result<String, PeachConfigError> {
|
||||||
|
let mut num_attempts = 0;
|
||||||
|
let mut whoami = None;
|
||||||
|
|
||||||
|
while num_attempts < MAX_NUM_ATTEMPTS {
|
||||||
|
let mut sbot = None;
|
||||||
|
|
||||||
|
let sbot_res = init_sbot().await;
|
||||||
|
match sbot_res {
|
||||||
|
Ok(sbot_instance) => {
|
||||||
|
sbot = Some(sbot_instance);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("failed to connect to sbot: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sbot.is_some() {
|
||||||
|
let sbot_id_res = sbot.unwrap().whoami().await;
|
||||||
|
match sbot_id_res {
|
||||||
|
Ok(sbot_id) => {
|
||||||
|
whoami = Some(sbot_id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("whoami failed: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("trying to connect to sbot again {:?}", num_attempts);
|
||||||
|
num_attempts += 1;
|
||||||
|
|
||||||
|
let sleep_duration = time::Duration::from_secs(2);
|
||||||
|
thread::sleep(sleep_duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
whoami.ok_or(PeachConfigError::WaitForSbotError {
|
||||||
|
message: "Failed to find sbot_id after 10 attempts".to_string(),
|
||||||
|
})
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "peach-lib"
|
name = "peach-lib"
|
||||||
version = "1.3.2"
|
version = "1.3.4"
|
||||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
@ -9,15 +9,19 @@ async-std = "1.10"
|
|||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
dirs = "4.0"
|
dirs = "4.0"
|
||||||
fslock="0.1"
|
fslock="0.1"
|
||||||
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi" }
|
kuska-ssb = { git = "https://github.com/Kuska-ssb/ssb" }
|
||||||
|
tilde-client = { path = "../tilde-client" }
|
||||||
jsonrpc-client-core = "0.5"
|
jsonrpc-client-core = "0.5"
|
||||||
jsonrpc-client-http = "0.5"
|
jsonrpc-client-http = "0.5"
|
||||||
jsonrpc-core = "8.0"
|
jsonrpc-core = "8.0"
|
||||||
|
jsonrpc_client = "0.7"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
nanorand = "0.6"
|
nanorand = { version = "0.6", features = ["getrandom"] }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.8"
|
serde_yaml = "0.8"
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
sha3 = "0.10"
|
sha3 = "0.10"
|
||||||
|
lazy_static = "1.4"
|
||||||
|
anyhow = "1.0.86"
|
||||||
|
@ -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,12 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "debug"
|
|
||||||
version = "0.1.0"
|
|
||||||
authors = ["notplants <mfowler.email@gmail.com>"]
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
peach-lib = { path = "../" }
|
|
||||||
env_logger = "0.6"
|
|
||||||
chrono = "0.4.19"
|
|
@ -1,65 +0,0 @@
|
|||||||
use peach_lib::dyndns_client::{dyndns_update_ip, register_domain, is_dns_updater_online, log_successful_nsupdate, get_num_seconds_since_successful_dns_update };
|
|
||||||
use peach_lib::password_utils::{verify_password, set_new_password, verify_temporary_password, set_new_temporary_password, send_password_reset};
|
|
||||||
use peach_lib::config_manager::{add_ssb_admin_id, delete_ssb_admin_id};
|
|
||||||
use peach_lib::sbot_client;
|
|
||||||
use std::process;
|
|
||||||
use chrono::prelude::*;
|
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// initalize the logger
|
|
||||||
env_logger::init();
|
|
||||||
//
|
|
||||||
// println!("Hello, world its debug!");
|
|
||||||
// let result = set_new_password("password3");
|
|
||||||
// println!("result: {:?}", result);
|
|
||||||
//
|
|
||||||
// let result = verify_password("password1");
|
|
||||||
// println!("result should be error: {:?}", result);
|
|
||||||
//
|
|
||||||
// let result = verify_password("password3");
|
|
||||||
// println!("result should be ok: {:?}", result);
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// println!("Testing temporary passwords");
|
|
||||||
// let result = set_new_temporary_password("abcd");
|
|
||||||
// println!("result: {:?}", result);
|
|
||||||
//
|
|
||||||
// let result = verify_temporary_password("password1");
|
|
||||||
// println!("result should be error: {:?}", result);
|
|
||||||
//
|
|
||||||
// let result = verify_temporary_password("abcd");
|
|
||||||
// println!("result should be ok: {:?}", result);
|
|
||||||
//
|
|
||||||
let result = send_password_reset();
|
|
||||||
println!("send password reset result should be ok: {:?}", result);
|
|
||||||
|
|
||||||
// sbot_client::post("hi cat");
|
|
||||||
// let result = sbot_client::whoami();
|
|
||||||
// let result = sbot_client::create_invite(50);
|
|
||||||
// let result = sbot_client::post("is this working");
|
|
||||||
// println!("result: {:?}", result);
|
|
||||||
// let result = sbot_client::post("nice we have contact");
|
|
||||||
// let result = sbot_client::update_pub_name("vermont-pub");
|
|
||||||
// let result = sbot_client::private_message("this is a private message", "@LZx+HP6/fcjUm7vef2eaBKAQ9gAKfzmrMVGzzdJiQtA=.ed25519");
|
|
||||||
// println!("result: {:?}", result);
|
|
||||||
|
|
||||||
// let result = send_password_reset();
|
|
||||||
// let result = add_ssb_admin_id("xyzdab");
|
|
||||||
// println!("result: {:?}", result);
|
|
||||||
// let result = delete_ssb_admin_id("xyzdab");
|
|
||||||
// println!("result: {:?}", result);
|
|
||||||
// let result = delete_ssb_admin_id("ab");
|
|
||||||
// println!("result: {:?}", result);
|
|
||||||
|
|
||||||
//// let result = log_successful_nsupdate();
|
|
||||||
//// let result = get_num_seconds_since_successful_dns_update();
|
|
||||||
// let is_online = is_dns_updater_online();
|
|
||||||
// println!("is online: {:?}", is_online);
|
|
||||||
//
|
|
||||||
//// let result = get_last_successful_dns_update();
|
|
||||||
//// println!("result: {:?}", result);
|
|
||||||
//// register_domain("newquarter299.dyn.peachcloud.org");
|
|
||||||
// let result = dyndns_update_ip();
|
|
||||||
// println!("result: {:?}", result);
|
|
||||||
}
|
|
11
peach-lib/examples/config.rs
Normal file
11
peach-lib/examples/config.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
use peach_lib::config_manager::{get_config_value, save_config_value};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("Running example of PeachCloud configuration management");
|
||||||
|
let v = get_config_value("ADDR").unwrap();
|
||||||
|
println!("ADDR: {}", v);
|
||||||
|
|
||||||
|
save_config_value("ADDR", "1.1.1.1");
|
||||||
|
let v = get_config_value("ADDR").unwrap();
|
||||||
|
println!("ADDR: {}", v);
|
||||||
|
}
|
@ -3,170 +3,278 @@
|
|||||||
//! Different PeachCloud microservices import peach-lib, so that they can share
|
//! Different PeachCloud microservices import peach-lib, so that they can share
|
||||||
//! this interface.
|
//! this interface.
|
||||||
//!
|
//!
|
||||||
|
//! Config values are looked up from three locations in this order by key name:
|
||||||
|
//! 1. from environmental variables
|
||||||
|
//! 2. from a configuration file
|
||||||
|
//! 3. from default values
|
||||||
|
//!
|
||||||
//! The configuration file is located at: "/var/lib/peachcloud/config.yml"
|
//! The configuration file is located at: "/var/lib/peachcloud/config.yml"
|
||||||
|
//! unless its path is configured by setting PEACH_CONFIG_PATH env variable.
|
||||||
|
|
||||||
use std::fs;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use std::{env, fs};
|
||||||
|
|
||||||
use fslock::LockFile;
|
use fslock::LockFile;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::error::PeachError;
|
use crate::error::PeachError;
|
||||||
|
|
||||||
// main configuration file
|
// load path to main configuration file
|
||||||
pub const YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
|
// from PEACH_CONFIG_PATH if that environment variable is set
|
||||||
|
// or using the default value if not set
|
||||||
// lock file (used to avoid race conditions during config reading & writing)
|
pub const DEFAULT_YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
|
||||||
pub const LOCK_FILE_PATH: &str = "/var/lib/peachcloud/config.lock";
|
lazy_static! {
|
||||||
|
static ref CONFIG_PATH: String = {
|
||||||
// default values
|
if let Ok(val) = env::var("PEACH_CONFIG_PATH") {
|
||||||
pub const DEFAULT_DYN_SERVER_ADDRESS: &str = "http://dynserver.dyn.peachcloud.org";
|
val
|
||||||
pub const DEFAULT_DYN_NAMESERVER: &str = "ns.peachcloud.org";
|
}
|
||||||
|
else {
|
||||||
// we make use of Serde default values in order to make PeachCloud
|
DEFAULT_YAML_PATH.to_string()
|
||||||
// robust and keep running even with a not fully complete config.yml
|
}
|
||||||
// main type which represents all peachcloud configurations
|
};
|
||||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
// lock file (used to avoid race conditions during config reading & writing)
|
||||||
pub struct PeachConfig {
|
// the lock file path is the config file path + ".lock"
|
||||||
#[serde(default)]
|
static ref LOCK_FILE_PATH: String = format!("{}.lock", *CONFIG_PATH);
|
||||||
pub external_domain: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub dyn_domain: String,
|
|
||||||
#[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,
|
|
||||||
#[serde(default)] // default is empty vector
|
|
||||||
pub ssb_admin_ids: Vec<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub admin_password_hash: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub temporary_password_hash: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper functions for serializing and deserializing PeachConfig from disc
|
// Default values for PeachCloud configs which are used for any key which is not set
|
||||||
pub fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachError> {
|
// via an environment variable or in a saved configuration file.
|
||||||
|
pub fn get_peach_config_defaults() -> HashMap<String, String> {
|
||||||
|
let peach_config_defaults: HashMap<&str, &str> = HashMap::from([
|
||||||
|
("STANDALONE_MODE", "true"),
|
||||||
|
("DISABLE_AUTH", "false"),
|
||||||
|
("ADDR", "127.0.0.1"),
|
||||||
|
("PORT", "8000"),
|
||||||
|
("EXTERNAL_DOMAIN", ""),
|
||||||
|
("DYN_DOMAIN", ""),
|
||||||
|
(
|
||||||
|
"DYN_DNS_SERVER_ADDRESS",
|
||||||
|
"http://dynserver.dyn.peachcloud.org",
|
||||||
|
),
|
||||||
|
("DYN_USE_CUSTOM_SERVER", "true"),
|
||||||
|
("DYN_TSIG_KEY_PATH", ""),
|
||||||
|
("DYN_NAMESERVER", "ns.peachcloud.org"),
|
||||||
|
("DYN_ENABLED", "false"),
|
||||||
|
("SSB_ADMIN_IDS", ""),
|
||||||
|
("ADMIN_PASSWORD_HASH", "47"),
|
||||||
|
("TEMPORARY_PASSWORD_HASH", ""),
|
||||||
|
("TILDE_SBOT_DATADIR", "/home/notplants/.local/share/tildefriends/"),
|
||||||
|
("TILDE_SBOT_SERVICE", "tilde-sbot.service"),
|
||||||
|
("PEACH_CONFIGDIR", "/var/lib/peachcloud"),
|
||||||
|
("PEACH_HOMEDIR", "/home/peach"),
|
||||||
|
("PEACH_WEBDIR", "/usr/share/peach-web"),
|
||||||
|
]);
|
||||||
|
// convert HashMap<&str, &str> to HashMap<String, String> and return
|
||||||
|
let pc_defaults: HashMap<String, String> = peach_config_defaults
|
||||||
|
.iter()
|
||||||
|
.map(|(key, val)| (key.to_string(), val.to_string()))
|
||||||
|
.collect();
|
||||||
|
pc_defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
// primary interface for getting config values
|
||||||
|
// Config values are looked up from three locations in this order by key name:
|
||||||
|
// 1. from environmental variables
|
||||||
|
// 2. from a configuration file
|
||||||
|
// 3. from default values
|
||||||
|
pub fn get_config_value(key: &str) -> Result<String, PeachError> {
|
||||||
|
// first check if there is an environmental variable set
|
||||||
|
if let Ok(val) = env::var(key) {
|
||||||
|
Ok(val)
|
||||||
|
} else {
|
||||||
|
// then check if a value is set in the config file
|
||||||
|
let peach_config_on_disc = load_peach_config_from_disc()?;
|
||||||
|
let val = peach_config_on_disc.get(key);
|
||||||
|
// if no value is found in the config file, then get the default value
|
||||||
|
match val {
|
||||||
|
// return config value
|
||||||
|
Some(v) => Ok(v.to_string()),
|
||||||
|
// get default value
|
||||||
|
None => {
|
||||||
|
match get_peach_config_defaults().get(key) {
|
||||||
|
Some(v) => Ok(v.to_string()),
|
||||||
|
// if this key was not found in the defaults, then it was an invalid key
|
||||||
|
None => Err(PeachError::InvalidKey {
|
||||||
|
key: key.to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to load PeachCloud configuration file saved to disc
|
||||||
|
pub fn load_peach_config_from_disc() -> Result<HashMap<String, String>, PeachError> {
|
||||||
|
let peach_config_exists = std::path::Path::new(CONFIG_PATH.as_str()).exists();
|
||||||
|
// if config file does not exist, return an emtpy HashMap
|
||||||
|
if !peach_config_exists {
|
||||||
|
let peach_config: HashMap<String, String> = HashMap::new();
|
||||||
|
Ok(peach_config)
|
||||||
|
}
|
||||||
|
// otherwise we load peach config from disk
|
||||||
|
else {
|
||||||
|
debug!("Loading peach config: {} exists", CONFIG_PATH.as_str());
|
||||||
|
let contents =
|
||||||
|
fs::read_to_string(CONFIG_PATH.as_str()).map_err(|source| PeachError::Read {
|
||||||
|
source,
|
||||||
|
path: CONFIG_PATH.to_string(),
|
||||||
|
})?;
|
||||||
|
let peach_config: HashMap<String, String> = serde_yaml::from_str(&contents)?;
|
||||||
|
Ok(peach_config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to save PeachCloud configuration file to disc
|
||||||
|
// takes in a Hashmap<String, String> and saves the whole HashMap as a yaml file
|
||||||
|
// with the keys in alphabetical order
|
||||||
|
pub fn save_peach_config_to_disc(
|
||||||
|
peach_config: HashMap<String, String>,
|
||||||
|
) -> Result<HashMap<String, String>, PeachError> {
|
||||||
// use a file lock to avoid race conditions while saving config
|
// use a file lock to avoid race conditions while saving config
|
||||||
let mut lock = LockFile::open(LOCK_FILE_PATH)?;
|
let mut lock = LockFile::open(&*LOCK_FILE_PATH).map_err(|source| PeachError::Read {
|
||||||
|
source,
|
||||||
|
path: LOCK_FILE_PATH.to_string(),
|
||||||
|
})?;
|
||||||
lock.lock()?;
|
lock.lock()?;
|
||||||
|
|
||||||
let yaml_str = serde_yaml::to_string(&peach_config)?;
|
// first convert Hashmap to BTreeMap (so that keys are saved in deterministic alphabetical order)
|
||||||
|
let ordered: BTreeMap<_, _> = peach_config.iter().collect();
|
||||||
|
// then serialize BTreeMap as yaml
|
||||||
|
let yaml_str = serde_yaml::to_string(&ordered)?;
|
||||||
|
|
||||||
fs::write(YAML_PATH, yaml_str).map_err(|source| PeachError::Write {
|
// write yaml to file
|
||||||
|
fs::write(CONFIG_PATH.as_str(), yaml_str).map_err(|source| PeachError::Write {
|
||||||
source,
|
source,
|
||||||
path: YAML_PATH.to_string(),
|
path: CONFIG_PATH.to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// unlock file lock
|
// unlock file lock
|
||||||
lock.unlock()?;
|
lock.unlock()?;
|
||||||
|
|
||||||
// return peach_config
|
// return modified HashMap
|
||||||
Ok(peach_config)
|
Ok(peach_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
|
// helper functions for serializing and deserializing PeachConfig values from disc
|
||||||
let peach_config_exists = std::path::Path::new(YAML_PATH).exists();
|
pub fn save_config_value(key: &str, value: &str) -> Result<HashMap<String, String>, PeachError> {
|
||||||
|
// get current config from disc
|
||||||
|
let mut peach_config = load_peach_config_from_disc()?;
|
||||||
|
|
||||||
let peach_config: PeachConfig = if !peach_config_exists {
|
// insert new key/value
|
||||||
debug!("Loading peach config: {} does not exist", YAML_PATH);
|
peach_config.insert(key.to_string(), value.to_string());
|
||||||
PeachConfig {
|
|
||||||
external_domain: "".to_string(),
|
|
||||||
dyn_domain: "".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(),
|
|
||||||
// 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(),
|
|
||||||
})?;
|
|
||||||
serde_yaml::from_str(&contents)?
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(peach_config)
|
// save the modified hashmap to disc
|
||||||
|
save_peach_config_to_disc(peach_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// interfaces for setting specific config values
|
// set all dyn configuration values at once
|
||||||
pub fn set_peach_dyndns_config(
|
pub fn set_peach_dyndns_config(
|
||||||
dyn_domain: &str,
|
dyn_domain: &str,
|
||||||
dyn_dns_server_address: &str,
|
dyn_dns_server_address: &str,
|
||||||
dyn_tsig_key_path: &str,
|
dyn_tsig_key_path: &str,
|
||||||
dyn_enabled: bool,
|
dyn_enabled: bool,
|
||||||
) -> Result<PeachConfig, PeachError> {
|
) -> Result<HashMap<String, String>, PeachError> {
|
||||||
let mut peach_config = load_peach_config()?;
|
let mut peach_config = load_peach_config_from_disc()?;
|
||||||
peach_config.dyn_domain = dyn_domain.to_string();
|
let dyn_enabled_str = match dyn_enabled {
|
||||||
peach_config.dyn_dns_server_address = dyn_dns_server_address.to_string();
|
true => "true",
|
||||||
peach_config.dyn_tsig_key_path = dyn_tsig_key_path.to_string();
|
false => "false",
|
||||||
peach_config.dyn_enabled = dyn_enabled;
|
};
|
||||||
save_peach_config(peach_config)
|
peach_config.insert("DYN_DOMAIN".to_string(), dyn_domain.to_string());
|
||||||
|
peach_config.insert(
|
||||||
|
"DYN_DNS_SERVER_ADDRESS".to_string(),
|
||||||
|
dyn_dns_server_address.to_string(),
|
||||||
|
);
|
||||||
|
peach_config.insert(
|
||||||
|
"DYN_TSIG_KEY_PATH".to_string(),
|
||||||
|
dyn_tsig_key_path.to_string(),
|
||||||
|
);
|
||||||
|
peach_config.insert("DYN_ENABLED".to_string(), dyn_enabled_str.to_string());
|
||||||
|
save_peach_config_to_disc(peach_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_external_domain(new_external_domain: &str) -> Result<PeachConfig, PeachError> {
|
pub fn set_external_domain(
|
||||||
let mut peach_config = load_peach_config()?;
|
new_external_domain: &str,
|
||||||
peach_config.external_domain = new_external_domain.to_string();
|
) -> Result<HashMap<String, String>, PeachError> {
|
||||||
save_peach_config(peach_config)
|
save_config_value("EXTERNAL_DOMAIN", new_external_domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> {
|
pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> {
|
||||||
let peach_config = load_peach_config()?;
|
let external_domain = get_config_value("EXTERNAL_DOMAIN")?;
|
||||||
if !peach_config.external_domain.is_empty() {
|
let dyn_domain = get_config_value("DYN_DOMAIN")?;
|
||||||
Ok(Some(peach_config.external_domain))
|
if !external_domain.is_empty() {
|
||||||
} else if !peach_config.dyn_domain.is_empty() {
|
Ok(Some(external_domain))
|
||||||
Ok(Some(peach_config.dyn_domain))
|
} else if !dyn_domain.is_empty() {
|
||||||
|
Ok(Some(dyn_domain))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_dyndns_server_address() -> Result<String, PeachError> {
|
pub fn get_dyndns_server_address() -> Result<String, PeachError> {
|
||||||
let peach_config = load_peach_config()?;
|
get_config_value("DYN_DNS_SERVER_ADDRESS")
|
||||||
// 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)
|
pub fn set_dyndns_enabled_value(
|
||||||
}
|
enabled_value: bool,
|
||||||
// otherwise hardcode the address
|
) -> Result<HashMap<String, String>, PeachError> {
|
||||||
else {
|
match enabled_value {
|
||||||
Ok(DEFAULT_DYN_SERVER_ADDRESS.to_string())
|
true => save_config_value("DYN_ENABLED", "true"),
|
||||||
|
false => save_config_value("DYN_ENABLED", "false"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result<PeachConfig, PeachError> {
|
pub fn get_dyndns_enabled_value() -> Result<bool, PeachError> {
|
||||||
let mut peach_config = load_peach_config()?;
|
let val = get_config_value("DYN_ENABLED")?;
|
||||||
peach_config.dyn_enabled = enabled_value;
|
Ok(val == "true")
|
||||||
save_peach_config(peach_config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
|
pub fn set_admin_password_hash(
|
||||||
let mut peach_config = load_peach_config()?;
|
password_hash: String,
|
||||||
peach_config.ssb_admin_ids.push(ssb_id.to_string());
|
) -> Result<HashMap<String, String>, PeachError> {
|
||||||
save_peach_config(peach_config)
|
save_config_value("ADMIN_PASSWORD_HASH", &password_hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
|
pub fn get_admin_password_hash() -> Result<String, PeachError> {
|
||||||
let mut peach_config = load_peach_config()?;
|
let admin_password_hash = get_config_value("ADMIN_PASSWORD_HASH")?;
|
||||||
let mut ssb_admin_ids = peach_config.ssb_admin_ids;
|
if !admin_password_hash.is_empty() {
|
||||||
|
Ok(admin_password_hash)
|
||||||
|
} else {
|
||||||
|
Err(PeachError::PasswordNotSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_temporary_password_hash(
|
||||||
|
password_hash: String,
|
||||||
|
) -> Result<HashMap<String, String>, PeachError> {
|
||||||
|
save_config_value("TEMPORARY_PASSWORD_HASH", &password_hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_temporary_password_hash() -> Result<String, PeachError> {
|
||||||
|
let admin_password_hash = get_config_value("TEMPORARY_PASSWORD_HASH")?;
|
||||||
|
if !admin_password_hash.is_empty() {
|
||||||
|
Ok(admin_password_hash)
|
||||||
|
} else {
|
||||||
|
Err(PeachError::PasswordNotSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add ssb_id to vector of admin ids and save new value for SSB_ADMIN_IDS
|
||||||
|
pub fn add_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
|
||||||
|
let mut ssb_admin_ids = get_ssb_admin_ids()?;
|
||||||
|
ssb_admin_ids.push(ssb_id.to_string());
|
||||||
|
save_ssb_admin_ids(ssb_admin_ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove ssb_id from vector of admin ids if found and save new value for SSB_ADMIN_IDS
|
||||||
|
// if value is not found then return an error
|
||||||
|
pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
|
||||||
|
let mut ssb_admin_ids = get_ssb_admin_ids()?;
|
||||||
let index_result = ssb_admin_ids.iter().position(|x| *x == ssb_id);
|
let index_result = ssb_admin_ids.iter().position(|x| *x == ssb_id);
|
||||||
match index_result {
|
match index_result {
|
||||||
Some(index) => {
|
Some(index) => {
|
||||||
ssb_admin_ids.remove(index);
|
ssb_admin_ids.remove(index);
|
||||||
peach_config.ssb_admin_ids = ssb_admin_ids;
|
save_ssb_admin_ids(ssb_admin_ids)
|
||||||
save_peach_config(peach_config)
|
|
||||||
}
|
}
|
||||||
None => Err(PeachError::SsbAdminIdNotFound {
|
None => Err(PeachError::SsbAdminIdNotFound {
|
||||||
id: ssb_id.to_string(),
|
id: ssb_id.to_string(),
|
||||||
@ -174,32 +282,16 @@ pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_admin_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> {
|
// looks up the String value for SSB_ADMIN_IDS and converts it into a Vec<String>
|
||||||
let mut peach_config = load_peach_config()?;
|
pub fn get_ssb_admin_ids() -> Result<Vec<String>, PeachError> {
|
||||||
peach_config.admin_password_hash = password_hash.to_string();
|
let ssb_admin_ids_str = get_config_value("SSB_ADMIN_IDS")?;
|
||||||
save_peach_config(peach_config)
|
let ssb_admin_ids: Vec<String> = serde_json::from_str(&ssb_admin_ids_str)?;
|
||||||
|
Ok(ssb_admin_ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_admin_password_hash() -> Result<String, PeachError> {
|
// takes in a Vec<String> and saves SSB_ADMIN_IDS as a json string representation of this vec
|
||||||
let peach_config = load_peach_config()?;
|
pub fn save_ssb_admin_ids(ssb_admin_ids: Vec<String>) -> Result<Vec<String>, PeachError> {
|
||||||
if !peach_config.admin_password_hash.is_empty() {
|
let ssb_admin_ids_as_json_str = serde_json::to_string(&ssb_admin_ids)?;
|
||||||
Ok(peach_config.admin_password_hash)
|
save_config_value("SSB_ADMIN_IDS", &ssb_admin_ids_as_json_str)?;
|
||||||
} else {
|
Ok(ssb_admin_ids)
|
||||||
Err(PeachError::PasswordNotSet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_temporary_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> {
|
|
||||||
let mut peach_config = load_peach_config()?;
|
|
||||||
peach_config.temporary_password_hash = password_hash.to_string();
|
|
||||||
save_peach_config(peach_config)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_temporary_password_hash() -> Result<String, PeachError> {
|
|
||||||
let peach_config = load_peach_config()?;
|
|
||||||
if !peach_config.temporary_password_hash.is_empty() {
|
|
||||||
Ok(peach_config.temporary_password_hash)
|
|
||||||
} else {
|
|
||||||
Err(PeachError::PasswordNotSet)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,9 @@ use jsonrpc_client_http::HttpTransport;
|
|||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use crate::config_manager::get_dyndns_server_address;
|
use crate::config_manager::{
|
||||||
|
get_config_value, get_dyndns_enabled_value, get_dyndns_server_address,
|
||||||
|
};
|
||||||
use crate::{config_manager, error::PeachError};
|
use crate::{config_manager, error::PeachError};
|
||||||
|
|
||||||
/// constants for dyndns configuration
|
/// constants for dyndns configuration
|
||||||
@ -107,7 +109,11 @@ fn get_public_ip_address() -> Result<String, PeachError> {
|
|||||||
/// Reads dyndns configurations from config.yml
|
/// Reads dyndns configurations from config.yml
|
||||||
/// and then uses nsupdate to update the IP address for the configured domain
|
/// and then uses nsupdate to update the IP address for the configured domain
|
||||||
pub fn dyndns_update_ip() -> Result<bool, PeachError> {
|
pub fn dyndns_update_ip() -> Result<bool, PeachError> {
|
||||||
let peach_config = config_manager::load_peach_config()?;
|
let dyn_tsig_key_path = get_config_value("DYN_TSIG_KEY_PATH")?;
|
||||||
|
let dyn_enabled = get_dyndns_enabled_value()?;
|
||||||
|
let dyn_domain = get_config_value("DYN_DOMAIN")?;
|
||||||
|
let dyn_dns_server_address = get_config_value("DYN_DNS_SERVER_ADDRESS")?;
|
||||||
|
let dyn_nameserver = get_config_value("DYN_NAMESERVER")?;
|
||||||
info!(
|
info!(
|
||||||
"Using config:
|
"Using config:
|
||||||
dyn_tsig_key_path: {:?}
|
dyn_tsig_key_path: {:?}
|
||||||
@ -116,22 +122,15 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
|
|||||||
dyn_enabled: {:?}
|
dyn_enabled: {:?}
|
||||||
dyn_nameserver: {:?}
|
dyn_nameserver: {:?}
|
||||||
",
|
",
|
||||||
peach_config.dyn_tsig_key_path,
|
dyn_tsig_key_path, dyn_domain, dyn_dns_server_address, dyn_enabled, dyn_nameserver,
|
||||||
peach_config.dyn_domain,
|
|
||||||
peach_config.dyn_dns_server_address,
|
|
||||||
peach_config.dyn_enabled,
|
|
||||||
peach_config.dyn_nameserver,
|
|
||||||
);
|
);
|
||||||
if !peach_config.dyn_enabled {
|
if !dyn_enabled {
|
||||||
info!("dyndns is not enabled, not updating");
|
info!("dyndns is not enabled, not updating");
|
||||||
Ok(false)
|
Ok(false)
|
||||||
} else {
|
} else {
|
||||||
// call nsupdate passing appropriate configs
|
// call nsupdate passing appropriate configs
|
||||||
let mut nsupdate_command = Command::new("nsupdate");
|
let mut nsupdate_command = Command::new("nsupdate");
|
||||||
nsupdate_command
|
nsupdate_command.arg("-k").arg(&dyn_tsig_key_path).arg("-v");
|
||||||
.arg("-k")
|
|
||||||
.arg(&peach_config.dyn_tsig_key_path)
|
|
||||||
.arg("-v");
|
|
||||||
// pass nsupdate commands via stdin
|
// pass nsupdate commands via stdin
|
||||||
let public_ip_address = get_public_ip_address()?;
|
let public_ip_address = get_public_ip_address()?;
|
||||||
info!("found public ip address: {}", public_ip_address);
|
info!("found public ip address: {}", public_ip_address);
|
||||||
@ -142,9 +141,9 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
|
|||||||
update delete {DOMAIN} A
|
update delete {DOMAIN} A
|
||||||
update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS}
|
update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS}
|
||||||
send",
|
send",
|
||||||
NAMESERVER = peach_config.dyn_nameserver,
|
NAMESERVER = dyn_nameserver,
|
||||||
ZONE = peach_config.dyn_domain,
|
ZONE = dyn_domain,
|
||||||
DOMAIN = peach_config.dyn_domain,
|
DOMAIN = dyn_domain,
|
||||||
PUBLIC_IP_ADDRESS = public_ip_address,
|
PUBLIC_IP_ADDRESS = public_ip_address,
|
||||||
);
|
);
|
||||||
info!("ns_commands: {:?}", ns_commands);
|
info!("ns_commands: {:?}", ns_commands);
|
||||||
@ -217,8 +216,7 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
|
|||||||
/// and has successfully run recently (in the last six minutes)
|
/// and has successfully run recently (in the last six minutes)
|
||||||
pub fn is_dns_updater_online() -> Result<bool, PeachError> {
|
pub fn is_dns_updater_online() -> Result<bool, PeachError> {
|
||||||
// first check if it is enabled in peach-config
|
// first check if it is enabled in peach-config
|
||||||
let peach_config = config_manager::load_peach_config()?;
|
let is_enabled = get_dyndns_enabled_value()?;
|
||||||
let is_enabled = peach_config.dyn_enabled;
|
|
||||||
// then check if it has successfully run within the last 6 minutes (60*6 seconds)
|
// then check if it has successfully run within the last 6 minutes (60*6 seconds)
|
||||||
let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?;
|
let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?;
|
||||||
let ran_recently: bool = match num_seconds_since_successful_update {
|
let ran_recently: bool = match num_seconds_since_successful_update {
|
||||||
@ -248,8 +246,7 @@ pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option<String> {
|
|||||||
|
|
||||||
// helper function which checks if a dyndns domain is new
|
// helper function which checks if a dyndns domain is new
|
||||||
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> Result<bool, PeachError> {
|
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 = get_config_value("DYN_DOMAIN")?;
|
||||||
let previous_dyndns_domain = peach_config.dyn_domain;
|
|
||||||
Ok(dyndns_full_domain != previous_dyndns_domain)
|
Ok(dyndns_full_domain != previous_dyndns_domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
//! Error handling for various aspects of the PeachCloud system, including the network, OLED, stats and dyndns JSON-RPC clients, as well as the configuration manager, sbot client and password utilities.
|
//! Error handling for various aspects of the PeachCloud system, including the network, OLED, stats and dyndns JSON-RPC clients, as well as the configuration manager, sbot client and password utilities.
|
||||||
|
|
||||||
use std::{io, str, string};
|
use std::{io, str, string};
|
||||||
|
use jsonrpc_client::JsonRpcError;
|
||||||
|
use anyhow::Error; // Add the anyhow crate for errors
|
||||||
|
|
||||||
/// This type represents all possible errors that can occur when interacting with the PeachCloud library.
|
/// This type represents all possible errors that can occur when interacting with the PeachCloud library.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PeachError {
|
pub enum PeachError {
|
||||||
|
/// Represents looking up a Config value with a non-existent key
|
||||||
|
InvalidKey {
|
||||||
|
/// the key value which was invalid
|
||||||
|
key: String,
|
||||||
|
},
|
||||||
|
|
||||||
/// Represents a failure to determine the path of the user's home directory.
|
/// Represents a failure to determine the path of the user's home directory.
|
||||||
HomeDir,
|
HomeDir,
|
||||||
|
|
||||||
@ -96,12 +103,23 @@ pub enum PeachError {
|
|||||||
/// The file path for the write attempt.
|
/// The file path for the write attempt.
|
||||||
path: String,
|
path: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Represents a JsonRpcError with Solar
|
||||||
|
JsonRpcError(JsonRpcError),
|
||||||
|
|
||||||
|
/// Represents an Anyhow error with Solar
|
||||||
|
SolarClientError(String),
|
||||||
|
|
||||||
|
/// Represents an error with encoding or decoding an SsbMessage
|
||||||
|
SsbMessageError(String),
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for PeachError {
|
impl std::error::Error for PeachError {
|
||||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
match *self {
|
match *self {
|
||||||
PeachError::HomeDir => None,
|
PeachError::HomeDir => None,
|
||||||
|
PeachError::InvalidKey { .. } => None,
|
||||||
PeachError::Io(_) => None,
|
PeachError::Io(_) => None,
|
||||||
PeachError::JsonRpcClientCore(_) => None,
|
PeachError::JsonRpcClientCore(_) => None,
|
||||||
PeachError::JsonRpcCore(_) => None,
|
PeachError::JsonRpcCore(_) => None,
|
||||||
@ -123,6 +141,9 @@ impl std::error::Error for PeachError {
|
|||||||
PeachError::Utf8ToStr(_) => None,
|
PeachError::Utf8ToStr(_) => None,
|
||||||
PeachError::Utf8ToString(_) => None,
|
PeachError::Utf8ToString(_) => None,
|
||||||
PeachError::Write { ref source, .. } => Some(source),
|
PeachError::Write { ref source, .. } => Some(source),
|
||||||
|
PeachError::JsonRpcError(_) => None,
|
||||||
|
PeachError::SolarClientError(_) => None,
|
||||||
|
PeachError::SsbMessageError(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -130,6 +151,9 @@ impl std::error::Error for PeachError {
|
|||||||
impl std::fmt::Display for PeachError {
|
impl std::fmt::Display for PeachError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
|
PeachError::InvalidKey { ref key } => {
|
||||||
|
write!(f, "Invalid key in config lookup for key: {}", key)
|
||||||
|
}
|
||||||
PeachError::HomeDir => {
|
PeachError::HomeDir => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
@ -177,6 +201,9 @@ impl std::fmt::Display for PeachError {
|
|||||||
PeachError::Write { ref path, .. } => {
|
PeachError::Write { ref path, .. } => {
|
||||||
write!(f, "Write error: {}", path)
|
write!(f, "Write error: {}", path)
|
||||||
}
|
}
|
||||||
|
PeachError::JsonRpcError(ref err) => err.fmt(f),
|
||||||
|
PeachError::SolarClientError(ref err) => err.fmt(f),
|
||||||
|
PeachError::SsbMessageError(ref err) => err.fmt(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -246,3 +273,16 @@ impl From<string::FromUtf8Error> for PeachError {
|
|||||||
PeachError::Utf8ToString(err)
|
PeachError::Utf8ToString(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<JsonRpcError> for PeachError {
|
||||||
|
fn from(err: JsonRpcError) -> PeachError {
|
||||||
|
PeachError::JsonRpcError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<anyhow::Error> for PeachError {
|
||||||
|
fn from(error: anyhow::Error) -> Self {
|
||||||
|
// TODO: include whole error somehow?
|
||||||
|
PeachError::SolarClientError(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
@ -6,9 +6,11 @@ pub mod oled_client;
|
|||||||
pub mod password_utils;
|
pub mod password_utils;
|
||||||
pub mod sbot;
|
pub mod sbot;
|
||||||
pub mod stats_client;
|
pub mod stats_client;
|
||||||
|
pub mod ssb_messages;
|
||||||
|
|
||||||
// re-export error types
|
// re-export error types
|
||||||
pub use jsonrpc_client_core;
|
pub use jsonrpc_client_core;
|
||||||
pub use jsonrpc_core;
|
pub use jsonrpc_core;
|
||||||
pub use serde_json;
|
pub use serde_json;
|
||||||
pub use serde_yaml;
|
pub use serde_yaml;
|
||||||
|
pub use tilde_client;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use async_std::task;
|
use async_std::task;
|
||||||
use golgi::Sbot;
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use nanorand::{Rng, WyRand};
|
use nanorand::{Rng, WyRand};
|
||||||
use sha3::{Digest, Sha3_256};
|
use sha3::{Digest, Sha3_256};
|
||||||
|
|
||||||
|
use crate::sbot::init_sbot;
|
||||||
use crate::{config_manager, error::PeachError, sbot::SbotConfig};
|
use crate::{config_manager, error::PeachError, sbot::SbotConfig};
|
||||||
|
|
||||||
/// Returns Ok(()) if the supplied password is correct,
|
/// Returns Ok(()) if the supplied password is correct,
|
||||||
@ -33,7 +33,7 @@ pub fn validate_new_passwords(new_password1: &str, new_password2: &str) -> Resul
|
|||||||
/// Sets a new password for the admin user
|
/// Sets a new password for the admin user
|
||||||
pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
|
pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
|
||||||
let new_password_hash = hash_password(new_password);
|
let new_password_hash = hash_password(new_password);
|
||||||
config_manager::set_admin_password_hash(&new_password_hash)?;
|
config_manager::set_admin_password_hash(new_password_hash)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -53,7 +53,7 @@ pub fn hash_password(password: &str) -> String {
|
|||||||
/// which can be used to reset the permanent password
|
/// which can be used to reset the permanent password
|
||||||
pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> {
|
pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> {
|
||||||
let new_password_hash = hash_password(new_password);
|
let new_password_hash = hash_password(new_password);
|
||||||
config_manager::set_temporary_password_hash(&new_password_hash)?;
|
config_manager::set_temporary_password_hash(new_password_hash)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -103,8 +103,8 @@ using this link: http://peach.local/auth/reset",
|
|||||||
};
|
};
|
||||||
msg += &remote_link;
|
msg += &remote_link;
|
||||||
// finally send the message to the admins
|
// finally send the message to the admins
|
||||||
let peach_config = config_manager::load_peach_config()?;
|
let ssb_admin_ids = config_manager::get_ssb_admin_ids()?;
|
||||||
for ssb_admin_id in peach_config.ssb_admin_ids {
|
for ssb_admin_id in ssb_admin_ids {
|
||||||
// use golgi to send a private message on scuttlebutt
|
// use golgi to send a private message on scuttlebutt
|
||||||
match task::block_on(publish_private_msg(&msg, &ssb_admin_id)) {
|
match task::block_on(publish_private_msg(&msg, &ssb_admin_id)) {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
@ -122,20 +122,15 @@ async fn publish_private_msg(msg: &str, recipient: &str) -> Result<(), String> {
|
|||||||
let recipient = vec![recipient.to_string()];
|
let recipient = vec![recipient.to_string()];
|
||||||
|
|
||||||
// initialise sbot connection with ip:port and shscap from config file
|
// initialise sbot connection with ip:port and shscap from config file
|
||||||
let mut sbot_client = match sbot_config {
|
let mut sbot_client = init_sbot();
|
||||||
// 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");
|
debug!("Publishing a Scuttlebutt private message with temporary password");
|
||||||
match sbot_client.publish_private(msg, recipient).await {
|
// TODO: implement publish private message in solar, and then implement this
|
||||||
Ok(_) => Ok(()),
|
Err(format!("Failed to publish private message: \
|
||||||
Err(e) => Err(format!("Failed to publish private message: {}", e)),
|
private publishing is not yet implemented in solar_client: \
|
||||||
}
|
the message meant to be sent was: {}", msg))
|
||||||
|
// match sbot_client.publish_private(msg, recipient).await {
|
||||||
|
// Ok(_) => Ok(()),
|
||||||
|
// Err(e) => Err(format!("Failed to publish private message: {}", e)),
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
//! Data types and associated methods for monitoring and configuring go-sbot.
|
//! Data types and associated methods for monitoring and configuring solar-sbot.
|
||||||
|
|
||||||
use std::{fs, fs::File, io, io::Write, path::PathBuf, process::Command, str};
|
use std::{fs, fs::File, io, io::Write, path::PathBuf, process::Command, str};
|
||||||
|
use std::os::linux::raw::ino_t;
|
||||||
|
use tilde_client::{TildeClient, get_sbot_client};
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
|
use crate::config_manager;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::error::PeachError;
|
use crate::error::PeachError;
|
||||||
@ -26,7 +30,7 @@ fn dir_size(path: impl Into<PathBuf>) -> io::Result<u64> {
|
|||||||
|
|
||||||
/* SBOT-RELATED TYPES AND METHODS */
|
/* SBOT-RELATED TYPES AND METHODS */
|
||||||
|
|
||||||
/// go-sbot process status.
|
/// solar-sbot process status.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct SbotStatus {
|
pub struct SbotStatus {
|
||||||
/// Current process state.
|
/// Current process state.
|
||||||
@ -58,14 +62,16 @@ impl Default for SbotStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SbotStatus {
|
impl SbotStatus {
|
||||||
/// Retrieve statistics for the go-sbot systemd process by querying `systemctl`.
|
/// Retrieve statistics for the solar-sbot systemd process by querying `systemctl`.
|
||||||
pub fn read() -> Result<Self, PeachError> {
|
pub fn read() -> Result<Self, PeachError> {
|
||||||
let mut status = SbotStatus::default();
|
let mut status = SbotStatus::default();
|
||||||
|
|
||||||
|
// note this command does not need to be run as sudo
|
||||||
|
// because non-privileged users are able to run systemctl show
|
||||||
|
let service_name = config_manager::get_config_value("TILDE_SBOT_SERVICE")?;
|
||||||
let info_output = Command::new("systemctl")
|
let info_output = Command::new("systemctl")
|
||||||
.arg("--user")
|
|
||||||
.arg("show")
|
.arg("show")
|
||||||
.arg("go-sbot.service")
|
.arg(service_name)
|
||||||
.arg("--no-page")
|
.arg("--no-page")
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
@ -83,10 +89,11 @@ impl SbotStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// note this command does not need to be run as sudo
|
||||||
|
// because non-privileged users are able to run systemctl status
|
||||||
let status_output = Command::new("systemctl")
|
let status_output = Command::new("systemctl")
|
||||||
.arg("--user")
|
|
||||||
.arg("status")
|
.arg("status")
|
||||||
.arg("go-sbot.service")
|
.arg(config_manager::get_config_value("TILDE_SBOT_SERVICE")?)
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
let service_status = str::from_utf8(&status_output.stdout)?;
|
let service_status = str::from_utf8(&status_output.stdout)?;
|
||||||
@ -94,7 +101,7 @@ impl SbotStatus {
|
|||||||
|
|
||||||
for line in service_status.lines() {
|
for line in service_status.lines() {
|
||||||
// example of the output line we're looking for:
|
// example of the output line we're looking for:
|
||||||
// `Loaded: loaded (/home/glyph/.config/systemd/user/go-sbot.service; enabled; vendor
|
// `Loaded: loaded (/home/glyph/.config/systemd/user/solar-sbot.service; enabled; vendor
|
||||||
// preset: enabled)`
|
// preset: enabled)`
|
||||||
if line.contains("Loaded:") {
|
if line.contains("Loaded:") {
|
||||||
let before_boot_state = line.find(';');
|
let before_boot_state = line.find(';');
|
||||||
@ -124,11 +131,16 @@ impl SbotStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine path of user's home directory
|
// TOOD restore this
|
||||||
let mut blobstore_path = dirs::home_dir().ok_or(PeachError::HomeDir)?;
|
// get path to blobstore
|
||||||
|
// let blobstore_path = format!(
|
||||||
// append the blobstore path
|
// "{}/blobs/sha256",
|
||||||
blobstore_path.push(".ssb-go/blobs/sha256");
|
// config_manager::get_config_value("TILDE_SBOT_DATADIR")?
|
||||||
|
// );
|
||||||
|
let blobstore_path = format!(
|
||||||
|
"{}",
|
||||||
|
config_manager::get_config_value("TILDE_SBOT_DATADIR")?
|
||||||
|
);
|
||||||
|
|
||||||
// determine the size of the blobstore directory in bytes
|
// determine the size of the blobstore directory in bytes
|
||||||
status.blobstore = dir_size(blobstore_path).ok();
|
status.blobstore = dir_size(blobstore_path).ok();
|
||||||
@ -137,10 +149,10 @@ impl SbotStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// go-sbot configuration parameters.
|
/// solar-sbot configuration parameters.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct SbotConfig {
|
pub struct Config {
|
||||||
// TODO: maybe define as a Path type?
|
// TODO: maybe define as a Path type?
|
||||||
/// Directory path for the log and indexes.
|
/// Directory path for the log and indexes.
|
||||||
pub repo: String,
|
pub repo: String,
|
||||||
@ -174,7 +186,27 @@ pub struct SbotConfig {
|
|||||||
pub repair: bool,
|
pub repair: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default configuration values for go-sbot.
|
// TODO: make this real
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct SbotConfig {
|
||||||
|
pub repo: String,
|
||||||
|
pub debugdir: String,
|
||||||
|
pub shscap: String,
|
||||||
|
pub hmac: String,
|
||||||
|
pub hops: i8,
|
||||||
|
pub lis: String,
|
||||||
|
pub wslis: String,
|
||||||
|
pub debuglis: String,
|
||||||
|
pub localadv: bool,
|
||||||
|
pub localdiscov: bool,
|
||||||
|
pub enable_ebt: bool,
|
||||||
|
pub promisc: bool,
|
||||||
|
pub nounixsock: bool,
|
||||||
|
pub repair: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default configuration values for solar-sbot.
|
||||||
impl Default for SbotConfig {
|
impl Default for SbotConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -197,11 +229,13 @@ impl Default for SbotConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SbotConfig {
|
impl SbotConfig {
|
||||||
/// Read the go-sbot `config.toml` file from file and deserialize into `SbotConfig`.
|
/// Read the solar-sbot `config.toml` file from file and deserialize into `SbotConfig`.
|
||||||
pub fn read() -> Result<Self, PeachError> {
|
pub fn read() -> Result<Self, PeachError> {
|
||||||
// determine path of user's home directory
|
// determine path of user's solar-sbot config.toml
|
||||||
let mut config_path = dirs::home_dir().ok_or(PeachError::HomeDir)?;
|
let config_path = format!(
|
||||||
config_path.push(".ssb-go/config.toml");
|
"{}/config.toml",
|
||||||
|
config_manager::get_config_value("SOLAR_SBOT_DATADIR")?
|
||||||
|
);
|
||||||
|
|
||||||
let config_contents = fs::read_to_string(config_path)?;
|
let config_contents = fs::read_to_string(config_path)?;
|
||||||
|
|
||||||
@ -210,16 +244,18 @@ impl SbotConfig {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the given `SbotConfig` to the go-sbot `config.toml` file.
|
/// Write the given `SbotConfig` to the solar-sbot `config.toml` file.
|
||||||
pub fn write(config: SbotConfig) -> Result<(), PeachError> {
|
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();
|
let repo_comment = "# For details about solar-sbot configuration, please visit the repo: https://github.com/cryptoscope/ssb\n".to_string();
|
||||||
|
|
||||||
// convert the provided `SbotConfig` instance to a string
|
// convert the provided `SbotConfig` instance to a string
|
||||||
let config_string = toml::to_string(&config)?;
|
let config_string = toml::to_string(&config)?;
|
||||||
|
|
||||||
// determine path of user's home directory
|
// determine path of user's solar-sbot config.toml
|
||||||
let mut config_path = dirs::home_dir().ok_or(PeachError::HomeDir)?;
|
let config_path = format!(
|
||||||
config_path.push(".ssb-go/config.toml");
|
"{}/config.toml",
|
||||||
|
config_manager::get_config_value("SOLAR_SBOT_DATADIR")?
|
||||||
|
);
|
||||||
|
|
||||||
// open config file for writing
|
// open config file for writing
|
||||||
let mut file = File::create(config_path)?;
|
let mut file = File::create(config_path)?;
|
||||||
@ -233,3 +269,20 @@ impl SbotConfig {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialise an sbot client
|
||||||
|
pub async fn init_sbot() -> Result<TildeClient, PeachError> {
|
||||||
|
// read sbot config from config.toml
|
||||||
|
let sbot_config = SbotConfig::read().ok();
|
||||||
|
|
||||||
|
debug!("Initialising an sbot client with configuration parameters");
|
||||||
|
// initialise sbot connection with ip:port and shscap from config file
|
||||||
|
let key_path = format!(
|
||||||
|
"{}/secret",
|
||||||
|
config_manager::get_config_value("SOLAR_SBOT_DATADIR")?
|
||||||
|
);
|
||||||
|
// TODO: read this from config
|
||||||
|
const SERVER_ADDR: &str = "http://127.0.0.1:3030";
|
||||||
|
let sbot_client = get_sbot_client();
|
||||||
|
Ok(sbot_client)
|
||||||
|
}
|
||||||
|
104
peach-lib/src/ssb_messages.rs
Normal file
104
peach-lib/src/ssb_messages.rs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
//! Message types and conversion methods.
|
||||||
|
|
||||||
|
use kuska_ssb::api::dto::content::TypedMessage;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use crate::error::PeachError;
|
||||||
|
use crate::error::PeachError::SsbMessageError;
|
||||||
|
|
||||||
|
/// `SsbMessageContent` is a type alias for `TypedMessage` from the `kuska_ssb` library.
|
||||||
|
/// It is aliased in golgi to fit the naming convention of the other message
|
||||||
|
/// types: `SsbMessageKVT` and `SsbMessageValue`.
|
||||||
|
///
|
||||||
|
/// See the [kuska source code](https://github.com/Kuska-ssb/ssb/blob/master/src/api/dto/content.rs#L103) for the type definition of `TypedMessage`.
|
||||||
|
pub type SsbMessageContent = TypedMessage;
|
||||||
|
|
||||||
|
/// The `value` of an SSB message (the `V` in `KVT`).
|
||||||
|
///
|
||||||
|
/// More information concerning the data model can be found in the
|
||||||
|
/// [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata).
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub struct SsbMessageValue {
|
||||||
|
pub previous: Option<String>,
|
||||||
|
pub author: String,
|
||||||
|
pub sequence: u64,
|
||||||
|
pub timestamp: f64,
|
||||||
|
pub hash: String,
|
||||||
|
pub content: Value,
|
||||||
|
pub signature: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Message content types.
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum SsbMessageContentType {
|
||||||
|
About,
|
||||||
|
Vote,
|
||||||
|
Post,
|
||||||
|
Contact,
|
||||||
|
Unrecognized,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SsbMessageValue {
|
||||||
|
/// Get the type field of the message content as an enum, if found.
|
||||||
|
///
|
||||||
|
/// If no `type` field is found or the `type` field is not a string,
|
||||||
|
/// it returns an `Err(GolgiError::ContentType)`.
|
||||||
|
///
|
||||||
|
/// If a `type` field is found but with an unknown string,
|
||||||
|
/// it returns an `Ok(SsbMessageContentType::Unrecognized)`.
|
||||||
|
pub fn get_message_type(&self) -> Result<SsbMessageContentType, PeachError> {
|
||||||
|
let msg_type = self
|
||||||
|
.content
|
||||||
|
.get("type")
|
||||||
|
.ok_or_else(|| SsbMessageError("type field not found".to_string()))?;
|
||||||
|
let mtype_str: &str = msg_type.as_str().ok_or_else(|| {
|
||||||
|
SsbMessageError("type field value is not a string as expected".to_string())
|
||||||
|
})?;
|
||||||
|
let enum_type = match mtype_str {
|
||||||
|
"about" => SsbMessageContentType::About,
|
||||||
|
"post" => SsbMessageContentType::Post,
|
||||||
|
"vote" => SsbMessageContentType::Vote,
|
||||||
|
"contact" => SsbMessageContentType::Contact,
|
||||||
|
_ => SsbMessageContentType::Unrecognized,
|
||||||
|
};
|
||||||
|
Ok(enum_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function which returns `true` if this message is of the given type,
|
||||||
|
/// and `false` if the type does not match or is not found.
|
||||||
|
pub fn is_message_type(&self, message_type: SsbMessageContentType) -> bool {
|
||||||
|
let self_message_type = self.get_message_type();
|
||||||
|
match self_message_type {
|
||||||
|
Ok(mtype) => mtype == message_type,
|
||||||
|
Err(_err) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the content JSON value into an `SsbMessageContent` `enum`,
|
||||||
|
/// using the `type` field as a tag to select which variant of the `enum`
|
||||||
|
/// to deserialize into.
|
||||||
|
///
|
||||||
|
/// See the [Serde docs on internally-tagged enum representations](https://serde.rs/enum-representations.html#internally-tagged) for further details.
|
||||||
|
pub fn into_ssb_message_content(self) -> Result<SsbMessageContent, PeachError> {
|
||||||
|
let m: SsbMessageContent = serde_json::from_value(self.content)?;
|
||||||
|
Ok(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An SSB message represented as a key-value-timestamp (`KVT`).
|
||||||
|
///
|
||||||
|
/// More information concerning the data model can be found in the
|
||||||
|
/// [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata).
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub struct SsbMessageKVT {
|
||||||
|
pub key: String,
|
||||||
|
pub value: SsbMessageValue,
|
||||||
|
pub timestamp: Option<f64>,
|
||||||
|
pub rts: Option<f64>,
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "peach-network"
|
name = "peach-network"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Query and configure network interfaces."
|
description = "Query and configure network interfaces."
|
||||||
|
@ -6,7 +6,7 @@ use std::num::ParseIntError;
|
|||||||
use io::Error as IoError;
|
use io::Error as IoError;
|
||||||
use probes::ProbeError;
|
use probes::ProbeError;
|
||||||
use regex::Error as RegexError;
|
use regex::Error as RegexError;
|
||||||
use wpactrl::WpaError;
|
use wpactrl::Error as WpaError;
|
||||||
|
|
||||||
/// Custom error type encapsulating all possible errors when querying
|
/// Custom error type encapsulating all possible errors when querying
|
||||||
/// network interfaces and modifying their state.
|
/// network interfaces and modifying their state.
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
//! access point credentials to `wpa_supplicant-<wlan_iface>.conf`.
|
//! access point credentials to `wpa_supplicant-<wlan_iface>.conf`.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
fs::OpenOptions,
|
fs::OpenOptions,
|
||||||
io::prelude::*,
|
io::prelude::*,
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
@ -22,6 +23,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use probes::network;
|
use probes::network;
|
||||||
|
use wpactrl::Client as WpaClient;
|
||||||
|
|
||||||
#[cfg(feature = "miniserde_support")]
|
#[cfg(feature = "miniserde_support")]
|
||||||
use miniserde::{Deserialize, Serialize};
|
use miniserde::{Deserialize, Serialize};
|
||||||
@ -105,8 +107,86 @@ pub struct Traffic {
|
|||||||
pub transmitted: u64,
|
pub transmitted: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access point data including state and signal strength.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||||
|
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||||
|
pub struct AccessPoint {
|
||||||
|
/// Access point data retrieved via scan.
|
||||||
|
pub detail: Option<Scan>,
|
||||||
|
/// Current state of the access point (e.g. "Available" or "Out of range").
|
||||||
|
pub state: String,
|
||||||
|
/// Signal strength of the access point as a percentage.
|
||||||
|
pub signal: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccessPoint {
|
||||||
|
fn available(detail: Option<Scan>, signal: Option<i32>) -> AccessPoint {
|
||||||
|
AccessPoint {
|
||||||
|
detail,
|
||||||
|
state: String::from("Available"),
|
||||||
|
signal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn saved() -> AccessPoint {
|
||||||
|
AccessPoint {
|
||||||
|
detail: None,
|
||||||
|
state: String::from("Out of range"),
|
||||||
|
signal: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* GET - Methods for retrieving data */
|
/* GET - Methods for retrieving data */
|
||||||
|
|
||||||
|
/// Retrieve combined list of available (in-range) and saved wireless access
|
||||||
|
/// points for a given network interface.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `iface` - A string slice holding the name of a wireless network interface
|
||||||
|
///
|
||||||
|
/// If the list results include one or more access points for the given network
|
||||||
|
/// interface, an `Ok` `Result` type is returned containing `HashMap<String,
|
||||||
|
/// AccessPoint>`.
|
||||||
|
///
|
||||||
|
/// Each entry in the returned `HashMap` contains an SSID (`String`) and
|
||||||
|
/// `AccessPoint` `struct`. If no access points are found, an empty `HashMap`
|
||||||
|
/// is returned in the `Result`. In the event of an error, a `NetworkError`
|
||||||
|
/// is returned in the `Result`.
|
||||||
|
pub fn all_networks(iface: &str) -> Result<HashMap<String, AccessPoint>, NetworkError> {
|
||||||
|
let mut wlan_networks = HashMap::new();
|
||||||
|
|
||||||
|
if let Ok(Some(networks)) = available_networks(iface) {
|
||||||
|
for ap in networks {
|
||||||
|
let ssid = ap.ssid.clone();
|
||||||
|
|
||||||
|
let rssi = ap.signal_level.clone();
|
||||||
|
// parse the string to a signed integer (for math)
|
||||||
|
let rssi_parsed = rssi.parse::<i32>().unwrap();
|
||||||
|
// perform rssi (dBm) to quality (%) conversion
|
||||||
|
let quality_percent = 2 * (rssi_parsed + 100);
|
||||||
|
|
||||||
|
let ap_detail = AccessPoint::available(Some(ap), Some(quality_percent));
|
||||||
|
wlan_networks.insert(ssid, ap_detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(Some(networks)) = saved_networks() {
|
||||||
|
for saved_ssid in networks {
|
||||||
|
if !wlan_networks.contains_key(&saved_ssid) {
|
||||||
|
let ssid = saved_ssid.clone();
|
||||||
|
|
||||||
|
let ap_detail = AccessPoint::saved();
|
||||||
|
wlan_networks.insert(ssid, ap_detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(wlan_networks)
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieve list of available wireless access points for a given network
|
/// Retrieve list of available wireless access points for a given network
|
||||||
/// interface.
|
/// interface.
|
||||||
///
|
///
|
||||||
@ -121,7 +201,7 @@ pub struct Traffic {
|
|||||||
/// In the event of an error, a `NetworkError` is returned in the `Result`.
|
/// In the event of an error, a `NetworkError` is returned in the `Result`.
|
||||||
pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError> {
|
pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError> {
|
||||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||||
wpa.request("SCAN")?;
|
wpa.request("SCAN")?;
|
||||||
let networks = wpa.request("SCAN_RESULTS")?;
|
let networks = wpa.request("SCAN_RESULTS")?;
|
||||||
let mut scan = Vec::new();
|
let mut scan = Vec::new();
|
||||||
@ -173,7 +253,7 @@ pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError
|
|||||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||||
pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
|
pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
|
||||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||||
let networks = wpa.request("LIST_NETWORKS")?;
|
let networks = wpa.request("LIST_NETWORKS")?;
|
||||||
let mut id = Vec::new();
|
let mut id = Vec::new();
|
||||||
for network in networks.lines() {
|
for network in networks.lines() {
|
||||||
@ -232,7 +312,7 @@ pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
|
|||||||
/// `Result`.
|
/// `Result`.
|
||||||
pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
|
pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||||
let status = wpa.request("SIGNAL_POLL")?;
|
let status = wpa.request("SIGNAL_POLL")?;
|
||||||
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
|
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
|
||||||
|
|
||||||
@ -259,7 +339,7 @@ pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
|
|||||||
/// the `Result`.
|
/// the `Result`.
|
||||||
pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
|
pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||||
let status = wpa.request("SIGNAL_POLL")?;
|
let status = wpa.request("SIGNAL_POLL")?;
|
||||||
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
|
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
|
||||||
|
|
||||||
@ -291,7 +371,7 @@ pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
|
|||||||
/// is returned in the `Result`. In the event of an error, a `NetworkError` is
|
/// is returned in the `Result`. In the event of an error, a `NetworkError` is
|
||||||
/// returned in the `Result`.
|
/// returned in the `Result`.
|
||||||
pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
|
pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
let mut wpa = WpaClient::builder().open()?;
|
||||||
let networks = wpa.request("LIST_NETWORKS")?;
|
let networks = wpa.request("LIST_NETWORKS")?;
|
||||||
let mut ssids = Vec::new();
|
let mut ssids = Vec::new();
|
||||||
for network in networks.lines() {
|
for network in networks.lines() {
|
||||||
@ -323,7 +403,7 @@ pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
|
|||||||
/// returned in the `Result`.
|
/// returned in the `Result`.
|
||||||
pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
|
pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||||
let status = wpa.request("STATUS")?;
|
let status = wpa.request("STATUS")?;
|
||||||
|
|
||||||
// pass the regex pattern and status output to the regex finder
|
// pass the regex pattern and status output to the regex finder
|
||||||
@ -379,7 +459,7 @@ pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
|
|||||||
/// a `NetworkError` is returned in the `Result`.
|
/// a `NetworkError` is returned in the `Result`.
|
||||||
pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
|
pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
|
||||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||||
let wpa_status = wpa.request("STATUS")?;
|
let wpa_status = wpa.request("STATUS")?;
|
||||||
|
|
||||||
// pass the regex pattern and status output to the regex finder
|
// pass the regex pattern and status output to the regex finder
|
||||||
@ -579,7 +659,7 @@ pub fn check_iface(wlan_iface: &str, ap_iface: &str) -> Result<(), NetworkError>
|
|||||||
/// is returned in the `Result`.
|
/// is returned in the `Result`.
|
||||||
pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
|
pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||||
let select = format!("SELECT {}", id);
|
let select = format!("SELECT {}", id);
|
||||||
wpa.request(&select)?;
|
wpa.request(&select)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -598,7 +678,7 @@ pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
|
|||||||
/// returned in the `Result`.
|
/// returned in the `Result`.
|
||||||
pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
|
pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||||
let remove = format!("REMOVE_NETWORK {}", id);
|
let remove = format!("REMOVE_NETWORK {}", id);
|
||||||
wpa.request(&remove)?;
|
wpa.request(&remove)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -617,7 +697,7 @@ pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
|
|||||||
/// `Result`.
|
/// `Result`.
|
||||||
pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
|
pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||||
let disable = format!("DISABLE_NETWORK {}", id);
|
let disable = format!("DISABLE_NETWORK {}", id);
|
||||||
wpa.request(&disable)?;
|
wpa.request(&disable)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -634,7 +714,7 @@ pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
|
|||||||
/// error, a `NetworkError` is returned in the `Result`.
|
/// error, a `NetworkError` is returned in the `Result`.
|
||||||
pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
|
pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
|
||||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||||
let disconnect = "DISCONNECT".to_string();
|
let disconnect = "DISCONNECT".to_string();
|
||||||
wpa.request(&disconnect)?;
|
wpa.request(&disconnect)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -685,7 +765,7 @@ pub fn forget(iface: &str, ssid: &str) -> Result<(), NetworkError> {
|
|||||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||||
pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
|
pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
|
||||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||||
let new_pass = format!("NEW_PASSWORD {} {}", id, pass);
|
let new_pass = format!("NEW_PASSWORD {} {}", id, pass);
|
||||||
wpa.request(&new_pass)?;
|
wpa.request(&new_pass)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -702,7 +782,7 @@ pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
|
|||||||
/// error, a `NetworkError` is returned in the `Result`.
|
/// error, a `NetworkError` is returned in the `Result`.
|
||||||
pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
|
pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
|
||||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||||
wpa.request("REASSOCIATE")?;
|
wpa.request("REASSOCIATE")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -714,7 +794,7 @@ pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
|
|||||||
/// `Result` type is returned. In the event of an error, a `NetworkError` is
|
/// `Result` type is returned. In the event of an error, a `NetworkError` is
|
||||||
/// returned in the `Result`.
|
/// returned in the `Result`.
|
||||||
pub fn reconfigure() -> Result<(), NetworkError> {
|
pub fn reconfigure() -> Result<(), NetworkError> {
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
let mut wpa = WpaClient::builder().open()?;
|
||||||
wpa.request("RECONFIGURE")?;
|
wpa.request("RECONFIGURE")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -730,7 +810,7 @@ pub fn reconfigure() -> Result<(), NetworkError> {
|
|||||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||||
pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
|
pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
|
||||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?;
|
||||||
wpa.request("DISCONNECT")?;
|
wpa.request("DISCONNECT")?;
|
||||||
wpa.request("RECONNECT")?;
|
wpa.request("RECONNECT")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -742,7 +822,7 @@ pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
|
|||||||
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
|
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
|
||||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||||
pub fn save() -> Result<(), NetworkError> {
|
pub fn save() -> Result<(), NetworkError> {
|
||||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
let mut wpa = WpaClient::builder().open()?;
|
||||||
wpa.request("SAVE_CONFIG")?;
|
wpa.request("SAVE_CONFIG")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "peach-stats"
|
name = "peach-stats"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "Query system statistics. Provides a wrapper around the probes and systemstat crates."
|
description = "Query system statistics. Provides a wrapper around the probes and systemstat crates."
|
||||||
|
@ -44,8 +44,8 @@ impl SbotStat {
|
|||||||
pub fn sbot_stats() -> Result<SbotStat, StatsError> {
|
pub fn sbot_stats() -> Result<SbotStat, StatsError> {
|
||||||
let mut status = SbotStat::default();
|
let mut status = SbotStat::default();
|
||||||
|
|
||||||
let info_output = Command::new("/usr/bin/systemctl")
|
let info_output = Command::new("sudo")
|
||||||
.arg("--user")
|
.arg("systemctl")
|
||||||
.arg("show")
|
.arg("show")
|
||||||
.arg("go-sbot.service")
|
.arg("go-sbot.service")
|
||||||
.arg("--no-page")
|
.arg("--no-page")
|
||||||
@ -66,8 +66,8 @@ pub fn sbot_stats() -> Result<SbotStat, StatsError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let status_output = Command::new("/usr/bin/systemctl")
|
let status_output = Command::new("sudo")
|
||||||
.arg("--user")
|
.arg("systemctl")
|
||||||
.arg("status")
|
.arg("status")
|
||||||
.arg("go-sbot.service")
|
.arg("go-sbot.service")
|
||||||
.output()
|
.output()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "peach-web"
|
name = "peach-web"
|
||||||
version = "0.6.0"
|
version = "0.6.21"
|
||||||
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
|
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
description = "peach-web is a web application which provides a web interface for monitoring and interacting with the PeachCloud device. This allows administration of the single-board computer (ie. Raspberry Pi) running PeachCloud, as well as the ssb-server and related plugins."
|
description = "peach-web is a web application which provides a web interface for monitoring and interacting with the PeachCloud device. This allows administration of the single-board computer (ie. Raspberry Pi) running PeachCloud, as well as the ssb-server and related plugins."
|
||||||
homepage = "https://opencollective.com/peachcloud"
|
homepage = "https://opencollective.com/peachcloud"
|
||||||
@ -33,20 +33,21 @@ travis-ci = { repository = "peachcloud/peach-web", branch = "master" }
|
|||||||
maintenance = { status = "actively-developed" }
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-std = "1.10"
|
async-std = { version = "1", features=["attributes", "tokio1"] }
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
dirs = "4.0"
|
dirs = "4.0"
|
||||||
env_logger = "0.8"
|
env_logger = "0.8"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
|
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
maud = "0.23"
|
maud = "0.23"
|
||||||
peach-lib = { path = "../peach-lib" }
|
peach-lib = { path = "../peach-lib" }
|
||||||
# these will be reintroduced when the full peachcloud mode is added
|
peach-network = { path = "../peach-network" }
|
||||||
#peach-network = { path = "../peach-network" }
|
peach-stats = { path = "../peach-stats" }
|
||||||
#peach-stats = { path = "../peach-stats" }
|
|
||||||
rouille = { version = "3.5", default-features = false }
|
rouille = { version = "3.5", default-features = false }
|
||||||
temporary = "0.6"
|
temporary = "0.6"
|
||||||
|
vnstat_parse = "0.1.0"
|
||||||
xdg = "2.2"
|
xdg = "2.2"
|
||||||
|
jsonrpc_client = { version = "0.7", features = ["macros", "reqwest"] }
|
||||||
|
reqwest = "0.11.24"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# peach-web
|
# peach-web
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Web Interface for PeachCloud
|
## Web Interface for PeachCloud
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ The web interface is primarily designed as a means of managing a Scuttlebutt pub
|
|||||||
|
|
||||||
Additional features are focused on administration of the device itself. This includes networking functionality and device statistics.
|
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).
|
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, CSS and a tiny bit of JS. Scuttlebutt functionality is provided by [golgi](http://golgi.mycelial.technology).
|
||||||
|
|
||||||
_Note: This is a work-in-progress._
|
_Note: This is a work-in-progress._
|
||||||
|
|
||||||
@ -36,11 +36,46 @@ Run the binary:
|
|||||||
|
|
||||||
`../target/release/peach-web`
|
`../target/release/peach-web`
|
||||||
|
|
||||||
## Environment
|
## Development Setup
|
||||||
|
|
||||||
|
In order to test `peach-web` on a development machine you will need to have a running instance of `go-sbot` (please see the [go-sbot README](https://github.com/cryptoscope/ssb) for installation details). The `GO_SBOT_DATADIR` environment variable or corresponding config variable must be set to `/home/<user>/.ssb-go` and the `PEACH_HOMEDIR` variable must be set to `/home/<user>`. See the Configuration section below for more details.
|
||||||
|
|
||||||
|
The `go-sbot` process must be managed by `systemd` in order for it to be controlled via the `peach-web` web interface. Here is a basic `go-sbot.service` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description=GoSSB server.
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/go-sbot
|
||||||
|
Environment="LIBRARIAN_WRITEALL=0"
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
And a `sudoers` rule must be created to allow the `go-sbot.service` state to be modified without requiring a password. Here is an example `/etc/sudoers.d/peach-web` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Control go-sbot service without sudo passworkd
|
||||||
|
|
||||||
|
<user> ALL=(ALL) NOPASSWD: /bin/systemctl start go-sbot.service, /bin/systemctl restart go-sbot.service, /bin/systemctl stop go-sbot.service, /bin/systemctl enable go-sbot.service, /bin/systemctl disable go-sbot.service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
By default, configuration variables are stored in `/var/lib/peachcloud/config.yml`. The variables in the file are updated by `peach-web` when changes are made to configurations via the web interface. Since `peach-web` has no database, all configurations are stored in this file.
|
||||||
|
|
||||||
|
A non-default configuration directory can be defined via the `PEACH_CONFIGDIR` environment variable or corresponding key in the `config.yml` file.
|
||||||
|
|
||||||
### Configuration Mode
|
### Configuration Mode
|
||||||
|
|
||||||
The web application can be run with a minimal set of routes and functionality (PeachPub - a simple sbot manager) or with the full-suite of capabilities, including network management and access to device statistics (PeachCloud). The mode is enabled by default (as defined in `Rocket.toml`) but can be overwritten using the `STANDALONE_MODE` environment variable: `true` or `false`. If the variable is unset or the value is incorrectly set, the application defaults to standalone mode.
|
The web application can be run with a minimal set of routes and functionality (PeachPub - a simple sbot manager) or with the full-suite of capabilities, including network management and access to device statistics (PeachCloud).
|
||||||
|
|
||||||
|
The application runs in PeachPub mode by default. The complete PeachCloud mode will be available once a large refactor is complete; it is not currently in working order so it's best to stick with PeachPub for now.
|
||||||
|
|
||||||
|
The running mode can be defined by setting the `STANDALONE_MODE` environment variable (`true` for PeachPub or `false` for PeachCloud). Alternatively, the desired mode can be set by modifying the PeachCloud configuration file.
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
@ -56,6 +91,14 @@ Logging is made available with `env_logger`:
|
|||||||
|
|
||||||
Other logging levels include `debug`, `warn` and `error`.
|
Other logging levels include `debug`, `warn` and `error`.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
## 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.
|
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.
|
||||||
@ -88,20 +131,6 @@ Remove configuration files (not removed with `apt-get remove`):
|
|||||||
|
|
||||||
`sudo apt-get purge peach-web`
|
`sudo apt-get purge peach-web`
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Configuration variables are stored in /var/lib/peachcloud/config.yml.
|
|
||||||
Peach-web also updates this file when changes are made to configurations via
|
|
||||||
the web interface. peach-web has no database, so all configurations are stored in this file.
|
|
||||||
|
|
||||||
### Dynamic DNS Configuration
|
|
||||||
|
|
||||||
Most users will want to use the default PeachCloud dynamic dns server.
|
|
||||||
If the config dyn_use_custom_server=false, then default values will be used.
|
|
||||||
If the config dyn_use_custom_server=true, then a value must also be set for dyn_dns_server_address (e.g. "http://peachdynserver.commoninternet.net").
|
|
||||||
This value is the URL of the instance of peach-dyndns-server that requests will be sent to for domain registration.
|
|
||||||
Using a custom value can here can be useful for testing.
|
|
||||||
|
|
||||||
## Design
|
## 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.
|
`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.
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Rocket web application for serving the PeachCloud web interface.
|
Description=Rouille web application for serving the PeachCloud web interface.
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
User=peach-web
|
User=peach
|
||||||
Group=www-data
|
Group=peach
|
||||||
WorkingDirectory=/usr/share/peach-web
|
WorkingDirectory=/usr/share/peach-web
|
||||||
Environment="ROCKET_ENV=prod"
|
|
||||||
Environment="ROCKET_ADDRESS=127.0.0.1"
|
|
||||||
Environment="ROCKET_PORT=3000"
|
|
||||||
Environment="ROCKET_LOG=critical"
|
|
||||||
Environment="RUST_LOG=info"
|
Environment="RUST_LOG=info"
|
||||||
ExecStart=/usr/bin/peach-web
|
ExecStart=/usr/bin/peach-web
|
||||||
Restart=always
|
Restart=always
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
# create user which peach-web runs as
|
# create user which peach-web runs as
|
||||||
adduser --quiet --system peach-web
|
id -u peach &>/dev/null || adduser --quiet peach
|
||||||
usermod -g peach peach-web
|
|
||||||
|
|
||||||
# create nginx config
|
# create nginx config
|
||||||
cat <<EOF > /etc/nginx/sites-enabled/default
|
cat <<EOF > /etc/nginx/sites-enabled/default
|
||||||
@ -15,16 +14,25 @@ server {
|
|||||||
rewrite ^/(.*)/$ /$1 permanent;
|
rewrite ^/(.*)/$ /$1 permanent;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:8000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat <<EOF > /etc/sudoers.d/peach-web
|
# update sudoers to allow peach-web to stop and restart go-sbot.service
|
||||||
# allow peach-web to run commands as peach-go-sbot without a password
|
mkdir -p /etc/sudoers.d/
|
||||||
peach-web ALL=(peach-go-sbot) NOPASSWD:ALL
|
|
||||||
|
|
||||||
|
SYSTEMCTL=/bin/systemctl
|
||||||
|
START="${SYSTEMCTL} start go-sbot.service"
|
||||||
|
RESTART="${SYSTEMCTL} restart go-sbot.service"
|
||||||
|
STOP="${SYSTEMCTL} stop go-sbot.service"
|
||||||
|
ENABLE="${SYSTEMCTL} enable go-sbot.service"
|
||||||
|
DISABLE="${SYSTEMCTL} disable go-sbot.service"
|
||||||
|
|
||||||
|
cat <<EOF > /etc/sudoers.d/peach-web
|
||||||
|
peach ALL=(ALL) NOPASSWD: $START, $STOP, $RESTART, $ENABLE, $DISABLE
|
||||||
EOF
|
EOF
|
||||||
|
chmod 0440 /etc/sudoers.d/peach-web
|
||||||
|
|
||||||
# cargo deb automatically replaces this token below, see https://github.com/mmstick/cargo-deb/blob/master/systemd.md
|
# cargo deb automatically replaces this token below, see https://github.com/mmstick/cargo-deb/blob/master/systemd.md
|
||||||
#DEBHELPER#
|
#DEBHELPER#
|
@ -1,53 +1,31 @@
|
|||||||
//! Define the configuration parameters for the web application.
|
//! Define the configuration parameters for the web application.
|
||||||
//!
|
//!
|
||||||
//! Sets default values and updates them if the corresponding environment
|
//! These configs are loaded using peach-lib::config_manager which checks config keys from
|
||||||
//! variables have been set.
|
//! three sources:
|
||||||
|
//! 1. from environmental variables
|
||||||
|
//! 2. from a configuration file
|
||||||
|
//! 3. from default values
|
||||||
|
|
||||||
use std::env;
|
use crate::error::PeachWebError;
|
||||||
|
use peach_lib::config_manager::get_config_value;
|
||||||
|
|
||||||
// environment variable keys to check for
|
pub struct ServerConfig {
|
||||||
const ENV_VARS: [&str; 4] = ["STANDALONE_MODE", "DISABLE_AUTH", "ADDR", "PORT"];
|
|
||||||
|
|
||||||
pub struct Config {
|
|
||||||
pub standalone_mode: bool,
|
pub standalone_mode: bool,
|
||||||
pub disable_auth: bool,
|
pub disable_auth: bool,
|
||||||
pub addr: String,
|
pub addr: String,
|
||||||
pub port: String,
|
pub port: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl ServerConfig {
|
||||||
fn default() -> Self {
|
pub fn new() -> Result<ServerConfig, PeachWebError> {
|
||||||
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
|
// define default config values
|
||||||
let mut config = Config::default();
|
let config = ServerConfig {
|
||||||
|
standalone_mode: get_config_value("STANDALONE_MODE")?.as_str() == "true",
|
||||||
|
disable_auth: get_config_value("DISABLE_AUTH")?.as_str() == "true",
|
||||||
|
addr: get_config_value("ADDR")?,
|
||||||
|
port: get_config_value("PORT")?,
|
||||||
|
};
|
||||||
|
|
||||||
// check for the environment variables in our config
|
Ok(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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
use std::io::Error as IoError;
|
use std::io::Error as IoError;
|
||||||
|
|
||||||
use golgi::GolgiError;
|
|
||||||
use peach_lib::error::PeachError;
|
use peach_lib::error::PeachError;
|
||||||
use peach_lib::{serde_json, serde_yaml};
|
use peach_lib::{serde_json, serde_yaml};
|
||||||
use serde_json::error::Error as JsonError;
|
use serde_json::error::Error as JsonError;
|
||||||
@ -12,26 +11,26 @@ use serde_yaml::Error as YamlError;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PeachWebError {
|
pub enum PeachWebError {
|
||||||
FailedToRegisterDynDomain(String),
|
FailedToRegisterDynDomain(String),
|
||||||
Golgi(GolgiError),
|
|
||||||
HomeDir,
|
HomeDir,
|
||||||
Io(IoError),
|
Io(IoError),
|
||||||
Json(JsonError),
|
Json(JsonError),
|
||||||
OsString,
|
OsString,
|
||||||
PeachLib { source: PeachError, msg: String },
|
PeachLib { source: PeachError, msg: String },
|
||||||
Yaml(YamlError),
|
Yaml(YamlError),
|
||||||
|
NotYetImplemented,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for PeachWebError {
|
impl std::error::Error for PeachWebError {
|
||||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
match *self {
|
match *self {
|
||||||
PeachWebError::FailedToRegisterDynDomain(_) => None,
|
PeachWebError::FailedToRegisterDynDomain(_) => None,
|
||||||
PeachWebError::Golgi(ref source) => Some(source),
|
|
||||||
PeachWebError::HomeDir => None,
|
PeachWebError::HomeDir => None,
|
||||||
PeachWebError::Io(ref source) => Some(source),
|
PeachWebError::Io(ref source) => Some(source),
|
||||||
PeachWebError::Json(ref source) => Some(source),
|
PeachWebError::Json(ref source) => Some(source),
|
||||||
PeachWebError::OsString => None,
|
PeachWebError::OsString => None,
|
||||||
PeachWebError::PeachLib { ref source, .. } => Some(source),
|
PeachWebError::PeachLib { ref source, .. } => Some(source),
|
||||||
PeachWebError::Yaml(ref source) => Some(source),
|
PeachWebError::Yaml(ref source) => Some(source),
|
||||||
|
PeachWebError::NotYetImplemented => None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,7 +41,6 @@ impl std::fmt::Display for PeachWebError {
|
|||||||
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
|
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
|
||||||
write!(f, "DYN DNS error: {}", msg)
|
write!(f, "DYN DNS error: {}", msg)
|
||||||
}
|
}
|
||||||
PeachWebError::Golgi(ref source) => write!(f, "Golgi error: {}", source),
|
|
||||||
PeachWebError::HomeDir => write!(
|
PeachWebError::HomeDir => write!(
|
||||||
f,
|
f,
|
||||||
"Filesystem error: failed to determine home directory path"
|
"Filesystem error: failed to determine home directory path"
|
||||||
@ -55,16 +53,11 @@ impl std::fmt::Display for PeachWebError {
|
|||||||
),
|
),
|
||||||
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
|
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
|
||||||
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
|
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
|
||||||
|
PeachWebError::NotYetImplemented => write!(f, "Not yet implemented"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<GolgiError> for PeachWebError {
|
|
||||||
fn from(err: GolgiError) -> PeachWebError {
|
|
||||||
PeachWebError::Golgi(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<IoError> for PeachWebError {
|
impl From<IoError> for PeachWebError {
|
||||||
fn from(err: IoError) -> PeachWebError {
|
fn from(err: IoError) -> PeachWebError {
|
||||||
PeachWebError::Io(err)
|
PeachWebError::Io(err)
|
||||||
|
@ -26,19 +26,25 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use log::{debug, info};
|
use log::info;
|
||||||
use peach_lib::{config_manager, config_manager::YAML_PATH as PEACH_CONFIG};
|
|
||||||
|
|
||||||
// crate-local dependencies
|
// crate-local dependencies
|
||||||
use config::Config;
|
use config::ServerConfig;
|
||||||
use utils::theme::Theme;
|
use utils::theme::Theme;
|
||||||
|
|
||||||
// load the application configuration and create the theme switcher
|
// load the application configuration and create the theme switcher
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CONFIG: Config = Config::new();
|
static ref SERVER_CONFIG: ServerConfig =
|
||||||
|
ServerConfig::new().expect("Failed to load rouille configuration values on server startup");
|
||||||
static ref THEME: RwLock<Theme> = RwLock::new(Theme::Light);
|
static ref THEME: RwLock<Theme> = RwLock::new(Theme::Light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wireless interface identifier.
|
||||||
|
pub const WLAN_IFACE: &str = "wlan0";
|
||||||
|
|
||||||
|
/// Access point interface identifier.
|
||||||
|
pub const AP_IFACE: &str = "ap0";
|
||||||
|
|
||||||
/// Session data for each authenticated client.
|
/// Session data for each authenticated client.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SessionData {
|
pub struct SessionData {
|
||||||
@ -50,21 +56,9 @@ fn main() {
|
|||||||
// initialize logger
|
// initialize logger
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
// 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");
|
|
||||||
|
|
||||||
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
|
// set ip address / hostname and port for the webserver
|
||||||
// defaults to "127.0.0.1:8000"
|
// defaults to "127.0.0.1:8000"
|
||||||
let addr_and_port = format!("{}:{}", CONFIG.addr, CONFIG.port);
|
let addr_and_port = format!("{}:{}", SERVER_CONFIG.addr, SERVER_CONFIG.port);
|
||||||
|
|
||||||
// store the session data for each session and a hashmap that associates
|
// store the session data for each session and a hashmap that associates
|
||||||
// each session id with the data
|
// each session id with the data
|
||||||
@ -80,7 +74,7 @@ fn main() {
|
|||||||
// with a name of "SID" and a duration of one hour (3600 seconds)
|
// with a name of "SID" and a duration of one hour (3600 seconds)
|
||||||
rouille::session::session(request, "SID", 3600, |session| {
|
rouille::session::session(request, "SID", 3600, |session| {
|
||||||
// if the "DISABLE_AUTH" env var is true, authenticate the session
|
// if the "DISABLE_AUTH" env var is true, authenticate the session
|
||||||
let mut session_data = if CONFIG.disable_auth {
|
let mut session_data = if SERVER_CONFIG.disable_auth {
|
||||||
Some(SessionData {
|
Some(SessionData {
|
||||||
_login: "success".to_string(),
|
_login: "success".to_string(),
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
use rouille::{router, Request, Response};
|
use rouille::{router, Request, Response};
|
||||||
|
|
||||||
use crate::{routes, templates, utils::flash::FlashResponse, SessionData};
|
use crate::{
|
||||||
|
routes, templates,
|
||||||
|
utils::{cookie::CookieResponse, flash::FlashResponse},
|
||||||
|
SessionData,
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: add mount_peachcloud_routes()
|
// TODO: add mount_peachcloud_routes()
|
||||||
// https://github.com/tomaka/rouille/issues/232#issuecomment-919225104
|
// https://github.com/tomaka/rouille/issues/232#issuecomment-919225104
|
||||||
@ -22,6 +26,8 @@ pub fn mount_peachpub_routes(
|
|||||||
router!(request,
|
router!(request,
|
||||||
(GET) (/) => {
|
(GET) (/) => {
|
||||||
Response::html(routes::home::build_template())
|
Response::html(routes::home::build_template())
|
||||||
|
// reset the back_url cookie each time we visit the homepage
|
||||||
|
.reset_cookie("back_url")
|
||||||
},
|
},
|
||||||
|
|
||||||
(GET) (/auth/change) => {
|
(GET) (/auth/change) => {
|
||||||
@ -49,6 +55,9 @@ pub fn mount_peachpub_routes(
|
|||||||
|
|
||||||
(GET) (/scuttlebutt/blocks) => {
|
(GET) (/scuttlebutt/blocks) => {
|
||||||
Response::html(routes::scuttlebutt::blocks::build_template())
|
Response::html(routes::scuttlebutt::blocks::build_template())
|
||||||
|
// add a back_url cookie to allow the path of the back button
|
||||||
|
// to be set correctly on the /scuttlebutt/profile page
|
||||||
|
.add_cookie("back_url=/scuttlebutt/blocks")
|
||||||
},
|
},
|
||||||
|
|
||||||
(POST) (/scuttlebutt/follow) => {
|
(POST) (/scuttlebutt/follow) => {
|
||||||
@ -57,10 +66,16 @@ pub fn mount_peachpub_routes(
|
|||||||
|
|
||||||
(GET) (/scuttlebutt/follows) => {
|
(GET) (/scuttlebutt/follows) => {
|
||||||
Response::html(routes::scuttlebutt::follows::build_template())
|
Response::html(routes::scuttlebutt::follows::build_template())
|
||||||
|
// add a back_url cookie to allow the path of the back button
|
||||||
|
// to be set correctly on the /scuttlebutt/profile page
|
||||||
|
.add_cookie("back_url=/scuttlebutt/follows")
|
||||||
},
|
},
|
||||||
|
|
||||||
(GET) (/scuttlebutt/friends) => {
|
(GET) (/scuttlebutt/friends) => {
|
||||||
Response::html(routes::scuttlebutt::friends::build_template())
|
Response::html(routes::scuttlebutt::friends::build_template())
|
||||||
|
// add a back_url cookie to allow the path of the back button
|
||||||
|
// to be set correctly on the /scuttlebutt/profile page
|
||||||
|
.add_cookie("back_url=/scuttlebutt/friends")
|
||||||
},
|
},
|
||||||
|
|
||||||
(GET) (/scuttlebutt/invites) => {
|
(GET) (/scuttlebutt/invites) => {
|
||||||
@ -117,6 +132,9 @@ pub fn mount_peachpub_routes(
|
|||||||
|
|
||||||
(POST) (/scuttlebutt/search) => {
|
(POST) (/scuttlebutt/search) => {
|
||||||
routes::scuttlebutt::search::handle_form(request)
|
routes::scuttlebutt::search::handle_form(request)
|
||||||
|
// add a back_url cookie to allow the path of the back button
|
||||||
|
// to be set correctly on the /scuttlebutt/profile page
|
||||||
|
.add_cookie("back_url=/scuttlebutt/search")
|
||||||
},
|
},
|
||||||
|
|
||||||
(POST) (/scuttlebutt/unblock) => {
|
(POST) (/scuttlebutt/unblock) => {
|
||||||
@ -148,6 +166,18 @@ pub fn mount_peachpub_routes(
|
|||||||
routes::settings::admin::delete::handle_form(request)
|
routes::settings::admin::delete::handle_form(request)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
(GET) (/settings/power) => {
|
||||||
|
Response::html(routes::settings::power::menu::build_template(request))
|
||||||
|
},
|
||||||
|
|
||||||
|
(GET) (/settings/power/reboot) => {
|
||||||
|
routes::settings::power::reboot::handle_reboot()
|
||||||
|
},
|
||||||
|
|
||||||
|
(GET) (/settings/power/shutdown) => {
|
||||||
|
routes::settings::power::shutdown::handle_shutdown()
|
||||||
|
},
|
||||||
|
|
||||||
(GET) (/settings/scuttlebutt) => {
|
(GET) (/settings/scuttlebutt) => {
|
||||||
Response::html(routes::settings::scuttlebutt::menu::build_template(request))
|
Response::html(routes::settings::scuttlebutt::menu::build_template(request))
|
||||||
.reset_flash()
|
.reset_flash()
|
||||||
@ -182,12 +212,64 @@ pub fn mount_peachpub_routes(
|
|||||||
routes::settings::scuttlebutt::default::write_config()
|
routes::settings::scuttlebutt::default::write_config()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
(GET) (/settings/network) => {
|
||||||
|
Response::html(routes::settings::network::menu::build_template(request)).reset_flash()
|
||||||
|
},
|
||||||
|
|
||||||
|
(GET) (/settings/network/dns) => {
|
||||||
|
Response::html(routes::settings::network::configure_dns::build_template(request)).reset_flash()
|
||||||
|
},
|
||||||
|
|
||||||
|
(POST) (/settings/network/dns) => {
|
||||||
|
routes::settings::network::configure_dns::handle_form(request)
|
||||||
|
},
|
||||||
|
|
||||||
|
(GET) (/settings/network/wifi) => {
|
||||||
|
Response::html(routes::settings::network::list_aps::build_template())
|
||||||
|
},
|
||||||
|
|
||||||
|
(GET) (/settings/network/wifi/add) => {
|
||||||
|
Response::html(routes::settings::network::add_ap::build_template(request, None)).reset_flash()
|
||||||
|
},
|
||||||
|
|
||||||
|
(POST) (/settings/network/wifi/add) => {
|
||||||
|
routes::settings::network::add_ap::handle_form(request)
|
||||||
|
},
|
||||||
|
|
||||||
|
(GET) (/settings/network/wifi/add/{ssid: String}) => {
|
||||||
|
Response::html(routes::settings::network::add_ap::build_template(request, Some(ssid))).reset_flash()
|
||||||
|
},
|
||||||
|
|
||||||
|
(GET) (/settings/network/wifi/modify) => {
|
||||||
|
Response::html(routes::settings::network::modify_ap::build_template(request, None)).reset_flash()
|
||||||
|
},
|
||||||
|
|
||||||
|
(POST) (/settings/network/wifi/modify) => {
|
||||||
|
routes::settings::network::modify_ap::handle_form(request)
|
||||||
|
},
|
||||||
|
|
||||||
|
(GET) (/settings/network/wifi/modify/{ssid: String}) => {
|
||||||
|
Response::html(routes::settings::network::modify_ap::build_template(request, Some(ssid))).reset_flash()
|
||||||
|
},
|
||||||
|
|
||||||
|
(GET) (/settings/network/wifi/{ssid: String}) => {
|
||||||
|
Response::html(routes::settings::network::ap_details::build_template(request, ssid))
|
||||||
|
},
|
||||||
|
|
||||||
(GET) (/settings/theme/{theme: String}) => {
|
(GET) (/settings/theme/{theme: String}) => {
|
||||||
routes::settings::theme::set_theme(theme)
|
routes::settings::theme::set_theme(theme)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
(GET) (/status) => {
|
||||||
|
Response::html(routes::status::device::build_template())
|
||||||
|
},
|
||||||
|
|
||||||
(GET) (/status/scuttlebutt) => {
|
(GET) (/status/scuttlebutt) => {
|
||||||
Response::html(routes::status::scuttlebutt::build_template())
|
Response::html(routes::status::scuttlebutt::build_template()).add_cookie("back_url=/status/scuttlebutt")
|
||||||
|
},
|
||||||
|
|
||||||
|
(GET) (/status/network) => {
|
||||||
|
Response::html(routes::status::network::build_template())
|
||||||
},
|
},
|
||||||
|
|
||||||
// render the not_found template and set a 404 status code if none of
|
// render the not_found template and set a 404 status code if none of
|
||||||
|
@ -30,7 +30,7 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
|
|||||||
form id="sendPasswordReset" action="/auth/temporary" method="post" {
|
form id="sendPasswordReset" action="/auth/temporary" method="post" {
|
||||||
div id="buttonDiv" {
|
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)";
|
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" {
|
a href="/auth/reset" class="button button-primary center" title="Set a new password using the temporary password" {
|
||||||
"Set New Password"
|
"Set New Password"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ pub fn build_template() -> PreEscaped<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
" 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: "
|
" 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" }
|
code { "systemctl status go-sbot.service" }
|
||||||
". The log output may give some clues about the source of the error."
|
". The log output may give some clues about the source of the error."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use maud::{html, PreEscaped};
|
use maud::{html, PreEscaped};
|
||||||
use peach_lib::sbot::SbotStatus;
|
use peach_lib::sbot::SbotStatus;
|
||||||
|
|
||||||
use crate::{templates, utils::theme};
|
use crate::{templates, utils::theme, SERVER_CONFIG};
|
||||||
|
|
||||||
/// Read the state of the go-sbot process and define status-related
|
/// Read the state of the go-sbot process and define status-related
|
||||||
/// elements accordingly.
|
/// elements accordingly.
|
||||||
@ -24,9 +24,23 @@ fn render_status_elements<'a>() -> (&'a str, &'a str, &'a str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render the URL for the status element (icon / link).
|
||||||
|
///
|
||||||
|
/// If the application is running in standalone mode then the element links
|
||||||
|
/// directly to the Scuttlebutt status page. If not, it links to the device
|
||||||
|
/// status page.
|
||||||
|
fn render_status_url<'a>() -> &'a str {
|
||||||
|
if SERVER_CONFIG.standalone_mode {
|
||||||
|
"/status/scuttlebutt"
|
||||||
|
} else {
|
||||||
|
"/status"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Home template builder.
|
/// Home template builder.
|
||||||
pub fn build_template() -> PreEscaped<String> {
|
pub fn build_template() -> PreEscaped<String> {
|
||||||
let (circle_color, center_circle_text, circle_border) = render_status_elements();
|
let (circle_color, center_circle_text, circle_border) = render_status_elements();
|
||||||
|
let status_url = render_status_url();
|
||||||
|
|
||||||
// render the home template html
|
// render the home template html
|
||||||
let home_template = html! {
|
let home_template = html! {
|
||||||
@ -63,7 +77,7 @@ pub fn build_template() -> PreEscaped<String> {
|
|||||||
}
|
}
|
||||||
(PreEscaped("<!-- bottom-left -->"))
|
(PreEscaped("<!-- bottom-left -->"))
|
||||||
(PreEscaped("<!-- SYSTEM STATUS LINK AND ICON -->"))
|
(PreEscaped("<!-- SYSTEM STATUS LINK AND ICON -->"))
|
||||||
a class="bottom-left" href="/status/scuttlebutt" title="Status" {
|
a class="bottom-left" href=(status_url) title="Status" {
|
||||||
div class={ "circle circle-small border-circle-small " (circle_border) } {
|
div class={ "circle circle-small border-circle-small " (circle_border) } {
|
||||||
img class="icon-medium" src="/icons/heart-pulse.svg";
|
img class="icon-medium" src="/icons/heart-pulse.svg";
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ use rouille::Request;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
templates,
|
templates,
|
||||||
utils::{flash::FlashRequest, sbot, sbot::Profile, theme},
|
utils::{cookie::CookieRequest, flash::FlashRequest, sbot, sbot::Profile, theme},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ROUTE: /scuttlebutt/profile
|
// ROUTE: /scuttlebutt/profile
|
||||||
@ -83,13 +83,15 @@ fn social_interaction_buttons_template(profile: &Profile) -> Markup {
|
|||||||
@match (profile.following, &profile.id) {
|
@match (profile.following, &profile.id) {
|
||||||
(Some(false), Some(ssb_id)) => {
|
(Some(false), Some(ssb_id)) => {
|
||||||
form id="followForm" class="center" action="/scuttlebutt/follow" method="post" {
|
form id="followForm" class="center" action="/scuttlebutt/follow" method="post" {
|
||||||
input type="hidden" id="publicKey" name="public_key" value=(ssb_id);
|
// url encode the ssb_id value
|
||||||
|
input type="hidden" id="publicKey" name="public_key" value=(ssb_id.replace('/', "%2F"));
|
||||||
input id="followPeer" class="button button-primary center" type="submit" title="Follow Peer" value="Follow";
|
input id="followPeer" class="button button-primary center" type="submit" title="Follow Peer" value="Follow";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(Some(true), Some(ssb_id)) => {
|
(Some(true), Some(ssb_id)) => {
|
||||||
form id="unfollowForm" class="center" action="/scuttlebutt/unfollow" method="post" {
|
form id="unfollowForm" class="center" action="/scuttlebutt/unfollow" method="post" {
|
||||||
input type="hidden" id="publicKey" name="public_key" value=(ssb_id);
|
// url encode the ssb_id value
|
||||||
|
input type="hidden" id="publicKey" name="public_key" value=(ssb_id.replace('/', "%2F"));
|
||||||
input id="unfollowPeer" class="button button-primary center" type="submit" title="Unfollow Peer" value="Unfollow";
|
input id="unfollowPeer" class="button button-primary center" type="submit" title="Unfollow Peer" value="Unfollow";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -98,13 +100,15 @@ fn social_interaction_buttons_template(profile: &Profile) -> Markup {
|
|||||||
@match (profile.blocking, &profile.id) {
|
@match (profile.blocking, &profile.id) {
|
||||||
(Some(false), Some(ssb_id)) => {
|
(Some(false), Some(ssb_id)) => {
|
||||||
form id="blockForm" class="center" action="/scuttlebutt/block" method="post" {
|
form id="blockForm" class="center" action="/scuttlebutt/block" method="post" {
|
||||||
input type="hidden" id="publicKey" name="public_key" value=(ssb_id);
|
// url encode the ssb_id value
|
||||||
|
input type="hidden" id="publicKey" name="public_key" value=(ssb_id.replace('/', "%2F"));
|
||||||
input id="blockPeer" class="button button-primary center" type="submit" title="Block Peer" value="Block";
|
input id="blockPeer" class="button button-primary center" type="submit" title="Block Peer" value="Block";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(Some(true), Some(ssb_id)) => {
|
(Some(true), Some(ssb_id)) => {
|
||||||
form id="unblockForm" class="center" action="/scuttlebutt/unblock" method="post" {
|
form id="unblockForm" class="center" action="/scuttlebutt/unblock" method="post" {
|
||||||
input type="hidden" id="publicKey" name="public_key" value=(ssb_id);
|
// url encode the ssb_id value
|
||||||
|
input type="hidden" id="publicKey" name="public_key" value=(ssb_id.replace('/', "%2F"));
|
||||||
input id="unblockPeer" class="button button-primary center" type="submit" title="Unblock Peer" value="Unblock";
|
input id="unblockPeer" class="button button-primary center" type="submit" title="Unblock Peer" value="Unblock";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -112,7 +116,8 @@ fn social_interaction_buttons_template(profile: &Profile) -> Markup {
|
|||||||
}
|
}
|
||||||
@if let Some(ssb_id) = &profile.id {
|
@if let Some(ssb_id) = &profile.id {
|
||||||
form class="center" {
|
form class="center" {
|
||||||
a id="privateMessage" class="button button-primary center" href={ "/scuttlebutt/private/" (ssb_id) } title="Private Message" {
|
// url encode the ssb_id
|
||||||
|
a id="privateMessage" class="button button-primary center" href={ "/scuttlebutt/private/" (ssb_id.replace('/', "%2F")) } title="Private Message" {
|
||||||
"Send Private Message"
|
"Send Private Message"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -169,7 +174,15 @@ pub fn build_template(request: &Request, ssb_id: Option<String>) -> PreEscaped<S
|
|||||||
_ => templates::inactive::build_template("Profile is unavailable."),
|
_ => templates::inactive::build_template("Profile is unavailable."),
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = templates::nav::build_template(profile_template, "Profile", Some("/"));
|
// a request to /scuttlebutt/profile can originate via the Friends,
|
||||||
|
// Follows or Blocks menu - as well as the Search page and Homepage.
|
||||||
|
// therefore, we check to see if the `back_url` cookie has been set
|
||||||
|
// and assign the path of the back button accordingly.
|
||||||
|
// for example, if the request has come via the Friends menu then the
|
||||||
|
// `back_url` cookie will be set with a value of "/scuttlebutt/friends".
|
||||||
|
let back_url = request.retrieve_cookie("back_url").or(Some("/"));
|
||||||
|
|
||||||
|
let body = templates::nav::build_template(profile_template, "Profile", back_url);
|
||||||
|
|
||||||
// query the current theme so we can pass it into the base template builder
|
// query the current theme so we can pass it into the base template builder
|
||||||
let theme = theme::get_theme();
|
let theme = theme::get_theme();
|
||||||
|
@ -13,8 +13,8 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
|
|||||||
let (mut flash_name, mut flash_msg) = request.retrieve_flash();
|
let (mut flash_name, mut flash_msg) = request.retrieve_flash();
|
||||||
|
|
||||||
// attempt to load peachcloud config file
|
// attempt to load peachcloud config file
|
||||||
let ssb_admins = match config_manager::load_peach_config() {
|
let ssb_admins = match config_manager::get_ssb_admin_ids() {
|
||||||
Ok(config) => Some(config.ssb_admin_ids),
|
Ok(ssb_admin_ids) => Some(ssb_admin_ids),
|
||||||
// note: this will overwrite any received flash cookie values
|
// note: this will overwrite any received flash cookie values
|
||||||
// TODO: find a way to include the `err` in the flash_msg
|
// TODO: find a way to include the `err` in the flash_msg
|
||||||
// currently produces an error because we end up with Some(String)
|
// currently produces an error because we end up with Some(String)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use maud::{html, PreEscaped};
|
use maud::{html, PreEscaped};
|
||||||
|
|
||||||
use crate::{templates, utils::theme, CONFIG};
|
use crate::{templates, utils::theme, SERVER_CONFIG};
|
||||||
|
|
||||||
// ROUTE: /settings
|
// ROUTE: /settings
|
||||||
|
|
||||||
@ -11,8 +11,10 @@ pub fn build_template() -> PreEscaped<String> {
|
|||||||
div class="card center" {
|
div class="card center" {
|
||||||
(PreEscaped("<!-- BUTTONS -->"))
|
(PreEscaped("<!-- BUTTONS -->"))
|
||||||
div id="settingsButtons" {
|
div id="settingsButtons" {
|
||||||
// render the network settings button if we're not in standalone mode
|
// render the network settings and power menu buttons if we're
|
||||||
@if !CONFIG.standalone_mode {
|
// not in standalone mode
|
||||||
|
@if !SERVER_CONFIG.standalone_mode {
|
||||||
|
a id="power" class="button button-primary center" href="/settings/power" title="Power Menu" { "Power" }
|
||||||
a id="network" class="button button-primary center" href="/settings/network" title="Network Settings" { "Network" }
|
a id="network" class="button button-primary center" href="/settings/network" title="Network Settings" { "Network" }
|
||||||
}
|
}
|
||||||
a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" }
|
a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" }
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
pub mod admin;
|
pub mod admin;
|
||||||
//pub mod dns;
|
//pub mod dns;
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
//pub mod network;
|
pub mod network;
|
||||||
|
pub mod power;
|
||||||
pub mod scuttlebutt;
|
pub mod scuttlebutt;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
|
@ -1,322 +0,0 @@
|
|||||||
use log::{debug, warn};
|
|
||||||
use rocket::{
|
|
||||||
form::{Form, FromForm},
|
|
||||||
get, post,
|
|
||||||
request::FlashMessage,
|
|
||||||
response::{Flash, Redirect},
|
|
||||||
uri, UriDisplayQuery,
|
|
||||||
};
|
|
||||||
use rocket_dyn_templates::{tera::Context, Template};
|
|
||||||
|
|
||||||
use peach_network::network;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
context,
|
|
||||||
context::network::{NetworkAlertContext, NetworkDetailContext, NetworkListContext},
|
|
||||||
routes::authentication::Authenticated,
|
|
||||||
utils::{monitor, monitor::Threshold},
|
|
||||||
AP_IFACE, WLAN_IFACE,
|
|
||||||
};
|
|
||||||
|
|
||||||
// STRUCTS USED BY NETWORK ROUTES
|
|
||||||
|
|
||||||
#[derive(Debug, FromForm, UriDisplayQuery)]
|
|
||||||
pub struct Ssid {
|
|
||||||
pub ssid: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, FromForm)]
|
|
||||||
pub struct WiFi {
|
|
||||||
pub ssid: String,
|
|
||||||
pub pass: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /settings/network/wifi/usage/reset
|
|
||||||
|
|
||||||
#[get("/wifi/usage/reset")]
|
|
||||||
pub fn wifi_usage_reset(_auth: Authenticated) -> Flash<Redirect> {
|
|
||||||
let url = uri!(wifi_usage);
|
|
||||||
match monitor::reset_data() {
|
|
||||||
Ok(_) => Flash::success(Redirect::to(url), "Reset stored network traffic total"),
|
|
||||||
Err(_) => Flash::error(
|
|
||||||
Redirect::to(url),
|
|
||||||
"Failed to reset stored network traffic total",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/wifi/connect", data = "<network>")]
|
|
||||||
pub fn connect_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
|
|
||||||
let ssid = &network.ssid;
|
|
||||||
let url = uri!(network_detail(ssid = ssid));
|
|
||||||
match network::id(&*WLAN_IFACE, ssid) {
|
|
||||||
Ok(Some(id)) => match network::connect(&id, &*WLAN_IFACE) {
|
|
||||||
Ok(_) => Flash::success(Redirect::to(url), "Connected to chosen network"),
|
|
||||||
Err(_) => Flash::error(Redirect::to(url), "Failed to connect to chosen network"),
|
|
||||||
},
|
|
||||||
_ => Flash::error(Redirect::to(url), "Failed to retrieve the network ID"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/wifi/disconnect", data = "<network>")]
|
|
||||||
pub fn disconnect_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
|
|
||||||
let ssid = &network.ssid;
|
|
||||||
let url = uri!(network_home);
|
|
||||||
match network::disable(&*WLAN_IFACE, ssid) {
|
|
||||||
Ok(_) => Flash::success(Redirect::to(url), "Disconnected from WiFi network"),
|
|
||||||
Err(_) => Flash::error(Redirect::to(url), "Failed to disconnect from WiFi network"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/wifi/forget", data = "<network>")]
|
|
||||||
pub fn forget_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
|
|
||||||
let ssid = &network.ssid;
|
|
||||||
let url = uri!(network_home);
|
|
||||||
match network::forget(&*WLAN_IFACE, ssid) {
|
|
||||||
Ok(_) => Flash::success(Redirect::to(url), "WiFi credentials removed"),
|
|
||||||
Err(_) => Flash::error(
|
|
||||||
Redirect::to(url),
|
|
||||||
"Failed to remove WiFi credentials".to_string(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/wifi/modify?<ssid>")]
|
|
||||||
pub fn wifi_password(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
|
||||||
let mut context = Context::new();
|
|
||||||
context.insert("back", &Some("/settings/network/wifi".to_string()));
|
|
||||||
context.insert("title", &Some("Update WiFi Password".to_string()));
|
|
||||||
context.insert("selected", &Some(ssid.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.insert("flash_name", &Some(flash.kind().to_string()));
|
|
||||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
|
||||||
};
|
|
||||||
|
|
||||||
Template::render("settings/network/modify_ap", &context.into_json())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/wifi/modify", data = "<wifi>")]
|
|
||||||
pub fn wifi_set_password(wifi: Form<WiFi>, _auth: Authenticated) -> Flash<Redirect> {
|
|
||||||
let ssid = &wifi.ssid;
|
|
||||||
let pass = &wifi.pass;
|
|
||||||
let url = uri!(network_detail(ssid = ssid));
|
|
||||||
match network::update(&*WLAN_IFACE, ssid, pass) {
|
|
||||||
Ok(_) => Flash::success(Redirect::to(url), "WiFi password updated".to_string()),
|
|
||||||
Err(_) => Flash::error(
|
|
||||||
Redirect::to(url),
|
|
||||||
"Failed to update WiFi password".to_string(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /settings/network
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
pub fn network_home(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
|
||||||
// assign context
|
|
||||||
let mut context = Context::new();
|
|
||||||
context.insert("back", &Some("/settings"));
|
|
||||||
context.insert("title", &Some("Network Configuration"));
|
|
||||||
context.insert("ap_state", &context::network::ap_state());
|
|
||||||
|
|
||||||
// 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.insert("flash_name", &Some(flash.kind().to_string()));
|
|
||||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
|
||||||
};
|
|
||||||
|
|
||||||
// template_dir is set in Rocket.toml
|
|
||||||
Template::render("settings/network/menu", &context.into_json())
|
|
||||||
}
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /settings/network/ap/activate
|
|
||||||
|
|
||||||
#[get("/ap/activate")]
|
|
||||||
pub fn deploy_ap(_auth: Authenticated) -> Flash<Redirect> {
|
|
||||||
// activate the wireless access point
|
|
||||||
debug!("Activating WiFi access point.");
|
|
||||||
match network::start_iface_service(&*AP_IFACE) {
|
|
||||||
Ok(_) => Flash::success(
|
|
||||||
Redirect::to("/settings/network"),
|
|
||||||
"Activated WiFi access point",
|
|
||||||
),
|
|
||||||
Err(_) => Flash::error(
|
|
||||||
Redirect::to("/settings/network"),
|
|
||||||
"Failed to activate WiFi access point",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /settings/network/wifi
|
|
||||||
|
|
||||||
#[get("/wifi")]
|
|
||||||
pub fn wifi_list(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
|
||||||
// assign context through context_builder call
|
|
||||||
let mut context = NetworkListContext::build();
|
|
||||||
context.back = Some("/settings/network".to_string());
|
|
||||||
context.title = Some("WiFi Networks".to_string());
|
|
||||||
|
|
||||||
// check to see if there is a flash message to display
|
|
||||||
if let Some(flash) = flash {
|
|
||||||
// add flash message contents to the context object
|
|
||||||
context.flash_name = Some(flash.kind().to_string());
|
|
||||||
context.flash_msg = Some(flash.message().to_string());
|
|
||||||
};
|
|
||||||
|
|
||||||
Template::render("settings/network/list_aps", &context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /settings/network/wifi<ssid>
|
|
||||||
|
|
||||||
#[get("/wifi?<ssid>")]
|
|
||||||
pub fn network_detail(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
|
||||||
let mut context = NetworkDetailContext::build();
|
|
||||||
context.back = Some("/settings/network/wifi".to_string());
|
|
||||||
context.title = Some("WiFi Network".to_string());
|
|
||||||
context.selected = Some(ssid.to_string());
|
|
||||||
|
|
||||||
if let Some(flash) = flash {
|
|
||||||
context.flash_name = Some(flash.kind().to_string());
|
|
||||||
context.flash_msg = Some(flash.message().to_string());
|
|
||||||
};
|
|
||||||
|
|
||||||
Template::render("settings/network/ap_details", &context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /settings/network/wifi/activate
|
|
||||||
|
|
||||||
#[get("/wifi/activate")]
|
|
||||||
pub fn deploy_client(_auth: Authenticated) -> Flash<Redirect> {
|
|
||||||
// activate the wireless client
|
|
||||||
debug!("Activating WiFi client mode.");
|
|
||||||
match network::start_iface_service(&*WLAN_IFACE) {
|
|
||||||
Ok(_) => Flash::success(Redirect::to("/settings/network"), "Activated WiFi client"),
|
|
||||||
Err(_) => Flash::error(
|
|
||||||
Redirect::to("/settings/network"),
|
|
||||||
"Failed to activate WiFi client",
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /settings/network/wifi/add
|
|
||||||
|
|
||||||
#[get("/wifi/add")]
|
|
||||||
pub fn add_wifi(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
|
||||||
let mut context = Context::new();
|
|
||||||
context.insert("back", &Some("/settings/network".to_string()));
|
|
||||||
context.insert("title", &Some("Add WiFi Network".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.insert("flash_name", &Some(flash.kind().to_string()));
|
|
||||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
|
||||||
};
|
|
||||||
|
|
||||||
Template::render("settings/network/add_ap", &context.into_json())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/wifi/add?<ssid>")]
|
|
||||||
pub fn add_ssid(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
|
||||||
let mut context = Context::new();
|
|
||||||
context.insert("back", &Some("/settings/network".to_string()));
|
|
||||||
context.insert("title", &Some("Add WiFi Network".to_string()));
|
|
||||||
context.insert("selected", &Some(ssid.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.insert("flash_name", &Some(flash.kind().to_string()));
|
|
||||||
context.insert("flash_msg", &Some(flash.message().to_string()));
|
|
||||||
};
|
|
||||||
|
|
||||||
Template::render("settings/network/add_ap", &context.into_json())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/wifi/add", data = "<wifi>")]
|
|
||||||
pub fn add_credentials(wifi: Form<WiFi>, _auth: Authenticated) -> Template {
|
|
||||||
let mut context = Context::new();
|
|
||||||
context.insert("back", &Some("/settings/network".to_string()));
|
|
||||||
context.insert("title", &Some("Add WiFi Network".to_string()));
|
|
||||||
|
|
||||||
// check if the credentials already exist for this access point
|
|
||||||
// note: this is nicer but it's an unstable feature:
|
|
||||||
// if check_saved_aps(&wifi.ssid).contains(true)
|
|
||||||
// use unwrap_or instead, set value to false if err is returned
|
|
||||||
//let creds_exist = network::saved_networks(&wifi.ssid).unwrap_or(false);
|
|
||||||
let creds_exist = match network::saved_networks() {
|
|
||||||
Ok(Some(networks)) => networks.contains(&wifi.ssid),
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// if credentials not found, generate and write wifi config to wpa_supplicant
|
|
||||||
let (flash_name, flash_msg) = if creds_exist {
|
|
||||||
(
|
|
||||||
"error".to_string(),
|
|
||||||
"Network credentials already exist for this access point".to_string(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
match network::add(&*WLAN_IFACE, &wifi.ssid, &wifi.pass) {
|
|
||||||
Ok(_) => {
|
|
||||||
debug!("Added WiFi credentials.");
|
|
||||||
// force reread of wpa_supplicant.conf file with new credentials
|
|
||||||
match network::reconfigure() {
|
|
||||||
Ok(_) => debug!("Successfully reconfigured wpa_supplicant"),
|
|
||||||
Err(_) => warn!("Failed to reconfigure wpa_supplicant"),
|
|
||||||
}
|
|
||||||
("success".to_string(), "Added WiFi credentials".to_string())
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
debug!("Failed to add WiFi credentials.");
|
|
||||||
("error".to_string(), format!("{}", e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
context.insert("flash_name", &Some(flash_name));
|
|
||||||
context.insert("flash_msg", &Some(flash_msg));
|
|
||||||
|
|
||||||
Template::render("settings/network/add_ap", &context.into_json())
|
|
||||||
}
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR WIFI USAGE
|
|
||||||
|
|
||||||
#[get("/wifi/usage")]
|
|
||||||
pub fn wifi_usage(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
|
||||||
let mut context = NetworkAlertContext::build();
|
|
||||||
// set back icon link to network route
|
|
||||||
context.back = Some("/settings/network".to_string());
|
|
||||||
context.title = Some("Network Data Usage".to_string());
|
|
||||||
// check to see if there is a flash message to display
|
|
||||||
if let Some(flash) = flash {
|
|
||||||
// add flash message contents to the context object
|
|
||||||
context.flash_name = Some(flash.kind().to_string());
|
|
||||||
context.flash_msg = Some(flash.message().to_string());
|
|
||||||
};
|
|
||||||
// template_dir is set in Rocket.toml
|
|
||||||
Template::render("settings/network/data_usage_limits", &context)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/wifi/usage", data = "<thresholds>")]
|
|
||||||
pub fn wifi_usage_alerts(thresholds: Form<Threshold>, _auth: Authenticated) -> Flash<Redirect> {
|
|
||||||
match monitor::update_store(thresholds.into_inner()) {
|
|
||||||
Ok(_) => {
|
|
||||||
debug!("WiFi data usage thresholds updated.");
|
|
||||||
Flash::success(
|
|
||||||
Redirect::to("/settings/network/wifi/usage"),
|
|
||||||
"Updated alert thresholds and flags",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
warn!("Failed to update WiFi data usage thresholds.");
|
|
||||||
Flash::error(
|
|
||||||
Redirect::to("/settings/network/wifi/usage"),
|
|
||||||
"Failed to update alert thresholds and flags",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
97
peach-web/src/routes/settings/network/add_ap.rs
Normal file
97
peach-web/src/routes/settings/network/add_ap.rs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
use maud::{html, Markup, PreEscaped};
|
||||||
|
use peach_network::network;
|
||||||
|
use rouille::{post_input, try_or_400, Request, Response};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
templates,
|
||||||
|
utils::{
|
||||||
|
flash::{FlashRequest, FlashResponse},
|
||||||
|
theme,
|
||||||
|
},
|
||||||
|
WLAN_IFACE,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ROUTE: /settings/network/wifi/add
|
||||||
|
|
||||||
|
fn render_ssid_input(selected_ap: Option<String>) -> Markup {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- input for network ssid -->"))
|
||||||
|
input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value=[selected_ap] autofocus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_password_input() -> Markup {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- input for network password -->"))
|
||||||
|
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_buttons() -> Markup {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- BUTTONS -->"))
|
||||||
|
div id="buttons" {
|
||||||
|
input id="addWifi" class="button button-primary center" title="Add" type="submit" value="Add";
|
||||||
|
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WiFi access point credentials form template builder.
|
||||||
|
pub fn build_template(request: &Request, selected_ap: Option<String>) -> PreEscaped<String> {
|
||||||
|
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||||
|
|
||||||
|
let form_template = html! {
|
||||||
|
(PreEscaped("<!-- WIFI ADD CREDENTIALS FORM -->"))
|
||||||
|
div class="card center" {
|
||||||
|
form id="wifiCreds" action="/settings/network/wifi/add" method="post" {
|
||||||
|
(render_ssid_input(selected_ap))
|
||||||
|
(render_password_input())
|
||||||
|
(render_buttons())
|
||||||
|
}
|
||||||
|
@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(
|
||||||
|
form_template,
|
||||||
|
"Add WiFi Network",
|
||||||
|
Some("/settings/network"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let theme = theme::get_theme();
|
||||||
|
|
||||||
|
templates::base::build_template(body, theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the SSID and password for an access point and save the new credentials.
|
||||||
|
pub fn handle_form(request: &Request) -> Response {
|
||||||
|
let data = try_or_400!(post_input!(request, {
|
||||||
|
ssid: String,
|
||||||
|
pass: String,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let (name, msg) = match network::add(WLAN_IFACE, &data.ssid, &data.pass) {
|
||||||
|
Ok(_) => match network::reconfigure() {
|
||||||
|
Ok(_) => ("success".to_string(), "Added WiFi credentials".to_string()),
|
||||||
|
Err(err) => (
|
||||||
|
"error".to_string(),
|
||||||
|
format!(
|
||||||
|
"Added WiFi credentials but failed to reconfigure interface: {}",
|
||||||
|
err
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Err(err) => (
|
||||||
|
"error".to_string(),
|
||||||
|
format!("Failed to add WiFi credentials for {}: {}", &data.ssid, err),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
|
||||||
|
|
||||||
|
Response::redirect_303("/settings/network/wifi/add").add_flash(flash_name, flash_msg)
|
||||||
|
}
|
197
peach-web/src/routes/settings/network/ap_details.rs
Normal file
197
peach-web/src/routes/settings/network/ap_details.rs
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use maud::{html, Markup, PreEscaped};
|
||||||
|
use peach_network::{network, network::AccessPoint, NetworkError};
|
||||||
|
use rouille::Request;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
templates,
|
||||||
|
utils::{flash::FlashRequest, theme},
|
||||||
|
WLAN_IFACE,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ROUTE: /settings/network/wifi?<ssid>
|
||||||
|
|
||||||
|
fn render_network_status_icon(ssid: &str, wlan_ssid: &str, ap_state: &str) -> Markup {
|
||||||
|
let status_label_value = if ssid == wlan_ssid {
|
||||||
|
"CONNECTED"
|
||||||
|
} else if ap_state == "Available" {
|
||||||
|
"AVAILABLE"
|
||||||
|
} else {
|
||||||
|
"NOT IN RANGE"
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- NETWORK STATUS ICON -->"))
|
||||||
|
div class="grid-column-1" {
|
||||||
|
img id="wifiIcon" class="center icon" src="/icons/wifi.svg" alt="WiFi icon";
|
||||||
|
label class="center label-small font-gray" for="wifiIcon" title="Access Point Status" { (status_label_value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_network_detailed_info(ssid: &str, ap_protocol: &str, ap_signal: Option<i32>) -> Markup {
|
||||||
|
let ap_signal_value = match ap_signal {
|
||||||
|
Some(signal) => signal.to_string(),
|
||||||
|
None => "Unknown".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- NETWORK DETAILED INFO -->"))
|
||||||
|
div class="grid-column-2" {
|
||||||
|
label class="label-small font-gray" for="netSsid" title="WiFi network SSID" { "SSID" };
|
||||||
|
p id="netSsid" class="card-text" title="SSID" { (ssid) }
|
||||||
|
label class="label-small font-gray" for="netSec" title="Security protocol" { "SECURITY" };
|
||||||
|
p id="netSec" class="card-text" title={ "Security protocol in use by " (ssid) } { (ap_protocol) }
|
||||||
|
label class="label-small font-gray" for="netSig" title="Signal Strength" { "SIGNAL" };
|
||||||
|
p id="netSig" class="card-text" title="Signal strength of WiFi access point" { (ap_signal_value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_disconnect_form(ssid: &str) -> Markup {
|
||||||
|
html! {
|
||||||
|
form id="wifiDisconnect" action="/settings/network/wifi/disconnect" method="post" {
|
||||||
|
(PreEscaped("<!-- hidden element: allows ssid to be sent in request -->"))
|
||||||
|
input id="disconnectSsid" name="ssid" type="text" value=(ssid) style="display: none;";
|
||||||
|
input id="disconnectWifi" class="button button-warning center" title="Disconnect from Network" type="submit" value="Disconnect";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_connect_form(ssid: &str) -> Markup {
|
||||||
|
html! {
|
||||||
|
form id="wifiConnect" action="/settings/network/wifi/connect" method="post" {
|
||||||
|
(PreEscaped("<!-- hidden element: allows ssid to be sent in request -->"))
|
||||||
|
input id="connectSsid" name="ssid" type="text" value=(ssid) style="display: none;";
|
||||||
|
input id="connectWifi" class="button button-primary center" title="Connect to Network" type="submit" value="Connect";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_forget_form(ssid: &str) -> Markup {
|
||||||
|
html! {
|
||||||
|
form id="wifiForget" action="/settings/network/wifi/forget" method="post" {
|
||||||
|
(PreEscaped("<!-- hidden element: allows ssid to be sent in request -->"))
|
||||||
|
input id="forgetSsid" name="ssid" type="text" value=(ssid) style="display: none;";
|
||||||
|
input id="forgetWifi" class="button button-warning center" title="Forget Network" type="submit" value="Forget";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_buttons(
|
||||||
|
selected_ap: &str,
|
||||||
|
wlan_ssid: &str,
|
||||||
|
ap: &AccessPoint,
|
||||||
|
saved_wifi_networks: Vec<String>,
|
||||||
|
) -> Markup {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- BUTTONS -->"))
|
||||||
|
div id="buttons" {
|
||||||
|
@if wlan_ssid == selected_ap {
|
||||||
|
(render_disconnect_form(selected_ap))
|
||||||
|
}
|
||||||
|
@if saved_wifi_networks.contains(&selected_ap.to_string()) {
|
||||||
|
@if wlan_ssid != selected_ap && ap.state == "Available" {
|
||||||
|
(render_connect_form(selected_ap))
|
||||||
|
}
|
||||||
|
a class="button button-primary center" href={ "/settings/network/wifi/modify?ssid=" (selected_ap) } { "Modify" }
|
||||||
|
(render_forget_form(selected_ap))
|
||||||
|
} @else {
|
||||||
|
// display the Add button if AP creds not already in saved
|
||||||
|
// networks list
|
||||||
|
a class="button button-primary center" href={ "/settings/network/wifi/add?ssid=" (selected_ap) } { "Add" }
|
||||||
|
}
|
||||||
|
a class="button button-secondary center" href="/settings/network/wifi" title="Cancel" { "Cancel" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve the list of all saved and in-range networks (including SSID and
|
||||||
|
/// AP details for each network), the list of all saved networks (SSIDs only)
|
||||||
|
/// and the SSID for the WiFi interface.
|
||||||
|
fn retrieve_network_data() -> (
|
||||||
|
Result<HashMap<String, AccessPoint>, NetworkError>,
|
||||||
|
Vec<String>,
|
||||||
|
String,
|
||||||
|
) {
|
||||||
|
let all_wifi_networks = network::all_networks(WLAN_IFACE);
|
||||||
|
let saved_wifi_networks = match network::saved_networks() {
|
||||||
|
Ok(Some(ssids)) => ssids,
|
||||||
|
_ => Vec::new(),
|
||||||
|
};
|
||||||
|
let wlan_ssid = match network::ssid(WLAN_IFACE) {
|
||||||
|
Ok(Some(ssid)) => ssid,
|
||||||
|
_ => String::from("Not connected"),
|
||||||
|
};
|
||||||
|
|
||||||
|
(all_wifi_networks, saved_wifi_networks, wlan_ssid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WiFi access point (AP) template builder.
|
||||||
|
///
|
||||||
|
/// Render a UI card with details about the selected access point, including
|
||||||
|
/// the connection state, security protocol being used, the SSID and the
|
||||||
|
/// signal strength. Buttons are also rendering based on the state of the
|
||||||
|
/// access point and whether or not credentials for the AP have previously
|
||||||
|
/// been saved.
|
||||||
|
///
|
||||||
|
/// If the AP is available (ie. in-range) then a Connect button is rendered.
|
||||||
|
/// A Disconnect button is rendered if the WiFi client is currently
|
||||||
|
/// connected to the AP.
|
||||||
|
///
|
||||||
|
/// If credentials have not previously been saved for the AP, an Add button is
|
||||||
|
/// rendered. Forget and Modify buttons are rendered if credentials for the AP
|
||||||
|
/// have previously been saved.
|
||||||
|
pub fn build_template(request: &Request, selected_ap: String) -> PreEscaped<String> {
|
||||||
|
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||||
|
|
||||||
|
let (all_wifi_networks, saved_wifi_networks, wlan_ssid) = retrieve_network_data();
|
||||||
|
|
||||||
|
let network_info_box_class = if selected_ap == wlan_ssid {
|
||||||
|
"two-grid capsule success-border"
|
||||||
|
} else {
|
||||||
|
"two-grid capsule"
|
||||||
|
};
|
||||||
|
|
||||||
|
let network_list_template = html! {
|
||||||
|
(PreEscaped("<!-- NETWORK CARD -->"))
|
||||||
|
div class="card center" {
|
||||||
|
@if let Ok(wlan_networks) = all_wifi_networks {
|
||||||
|
// select only the access point we are interested in displaying
|
||||||
|
@if let Some((ssid, ap)) = wlan_networks.get_key_value(&selected_ap) {
|
||||||
|
@let ap_protocol = match &ap.detail {
|
||||||
|
Some(detail) => detail.protocol.clone(),
|
||||||
|
None => "None".to_string()
|
||||||
|
};
|
||||||
|
(PreEscaped("<!-- NETWORK INFO BOX -->"))
|
||||||
|
div class=(network_info_box_class) title="PeachCloud network mode and status" {
|
||||||
|
(PreEscaped("<!-- left column -->"))
|
||||||
|
(render_network_status_icon(ssid, &wlan_ssid, &ap.state))
|
||||||
|
(PreEscaped("<!-- right column -->"))
|
||||||
|
(render_network_detailed_info(ssid, &ap_protocol, ap.signal))
|
||||||
|
}
|
||||||
|
(render_buttons(ssid, &wlan_ssid, ap, saved_wifi_networks))
|
||||||
|
} @else {
|
||||||
|
p class="card-text list-item" { (selected_ap) " not found in saved or in-range networks" }
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
p class="card-text list-item" { "No saved or in-range networks found" }
|
||||||
|
}
|
||||||
|
@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(
|
||||||
|
network_list_template,
|
||||||
|
"WiFi Networks",
|
||||||
|
Some("/settings/network"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let theme = theme::get_theme();
|
||||||
|
|
||||||
|
templates::base::build_template(body, theme)
|
||||||
|
}
|
201
peach-web/src/routes/settings/network/configure_dns.rs
Normal file
201
peach-web/src/routes/settings/network/configure_dns.rs
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
use log::info;
|
||||||
|
use maud::{html, Markup, PreEscaped};
|
||||||
|
use peach_lib::{
|
||||||
|
config_manager, dyndns_client,
|
||||||
|
error::PeachError,
|
||||||
|
jsonrpc_client_core::{Error, ErrorKind},
|
||||||
|
jsonrpc_core::types::error::ErrorCode,
|
||||||
|
};
|
||||||
|
use rouille::{post_input, try_or_400, Request, Response};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::PeachWebError,
|
||||||
|
templates,
|
||||||
|
utils::{
|
||||||
|
flash::{FlashRequest, FlashResponse},
|
||||||
|
theme,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ROUTE: /settings/network/dns
|
||||||
|
|
||||||
|
fn render_dyndns_status_indicator() -> Markup {
|
||||||
|
let (indicator_class, indicator_label) = match dyndns_client::is_dns_updater_online() {
|
||||||
|
Ok(true) => ("success-border", "Dynamic DNS is currently online."),
|
||||||
|
_ => (
|
||||||
|
"warning-border",
|
||||||
|
"Dynamic DNS is enabled but may be offline.",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- DYNDNS STATUS INDICATOR -->"))
|
||||||
|
div id="dyndns-status-indicator" class={ "stack capsule " (indicator_class) } {
|
||||||
|
div class="stack" {
|
||||||
|
label class="label-small font-near-black" { (indicator_label) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_external_domain_input() -> Markup {
|
||||||
|
let external_domain = config_manager::get_config_value("EXTERNAL_DOMAIN").ok();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div class="input-wrapper" {
|
||||||
|
(PreEscaped("<!-- input for externaldomain -->"))
|
||||||
|
label id="external_domain" class="label-small input-label font-near-black" {
|
||||||
|
label class="label-small input-label font-gray" for="external_domain" style="padding-top: 0.25rem;" { "External Domain (optional)" }
|
||||||
|
input id="external_domain" class="form-input" style="margin-bottom: 0;" name="external_domain" type="text" title="external domain" value=[external_domain];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_dyndns_enabled_checkbox() -> Markup {
|
||||||
|
let dyndns_enabled = config_manager::get_dyndns_enabled_value().unwrap_or(false);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div class="input-wrapper" {
|
||||||
|
div {
|
||||||
|
(PreEscaped("<!-- checkbox for dyndns flag -->"))
|
||||||
|
label class="label-small input-label font-gray" { "Enable Dynamic DNS" }
|
||||||
|
input style="margin-left: 0px;" id="enable_dyndns" name="enable_dyndns" title="Activate dynamic DNS" type="checkbox" checked[dyndns_enabled];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_dynamic_domain_input() -> Markup {
|
||||||
|
let dyndns_domain =
|
||||||
|
config_manager::get_config_value("DYN_DOMAIN").unwrap_or_else(|_| String::from(""));
|
||||||
|
let dyndns_subdomain =
|
||||||
|
dyndns_client::get_dyndns_subdomain(&dyndns_domain).unwrap_or(dyndns_domain);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div class="input-wrapper" {
|
||||||
|
(PreEscaped("<!-- input for dyndns domain -->"))
|
||||||
|
label id="cut" class="label-small input-label font-near-black" {
|
||||||
|
label class="label-small input-label font-gray" for="cut" style="padding-top: 0.25rem;" { "Dynamic DNS Domain" }
|
||||||
|
input id="dyndns_domain" class="alert-input" name="dynamic_domain" placeholder="" type="text" title="dyndns_domain" value=(dyndns_subdomain);
|
||||||
|
{ ".dyn.peachcloud.org" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_save_button() -> Markup {
|
||||||
|
html! {
|
||||||
|
div id="buttonDiv" style="margin-top: 2rem;" {
|
||||||
|
input id="configureDNSButton" class="button button-primary center" title="Add" type="submit" value="Save";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DNS configuration form template builder.
|
||||||
|
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||||
|
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||||
|
|
||||||
|
let dyndns_enabled = config_manager::get_dyndns_enabled_value().unwrap_or(false);
|
||||||
|
|
||||||
|
let form_template = html! {
|
||||||
|
(PreEscaped("<!-- CONFIGURE DNS FORM -->"))
|
||||||
|
div class="card center" {
|
||||||
|
@if dyndns_enabled {
|
||||||
|
(render_dyndns_status_indicator())
|
||||||
|
}
|
||||||
|
form id="configureDNS" class="center" action="/settings/network/dns" method="post" {
|
||||||
|
(render_external_domain_input())
|
||||||
|
(render_dyndns_enabled_checkbox())
|
||||||
|
(render_dynamic_domain_input())
|
||||||
|
(render_save_button())
|
||||||
|
@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(
|
||||||
|
form_template,
|
||||||
|
"Configure Dynamic DNS",
|
||||||
|
Some("/settings/network"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let theme = theme::get_theme();
|
||||||
|
|
||||||
|
templates::base::build_template(body, theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_dns_configuration(
|
||||||
|
external_domain: String,
|
||||||
|
enable_dyndns: bool,
|
||||||
|
dynamic_domain: String,
|
||||||
|
) -> Result<(), PeachWebError> {
|
||||||
|
// first save local configurations
|
||||||
|
config_manager::set_external_domain(&external_domain)?;
|
||||||
|
config_manager::set_dyndns_enabled_value(enable_dyndns)?;
|
||||||
|
|
||||||
|
let full_dynamic_domain = dyndns_client::get_full_dynamic_domain(&dynamic_domain);
|
||||||
|
|
||||||
|
// if dynamic dns is enabled and this is a new domain name, then register it
|
||||||
|
if enable_dyndns && dyndns_client::check_is_new_dyndns_domain(&full_dynamic_domain)? {
|
||||||
|
if let Err(registration_err) = dyndns_client::register_domain(&full_dynamic_domain) {
|
||||||
|
info!("Failed to register dyndns domain: {:?}", registration_err);
|
||||||
|
|
||||||
|
// error message describing the failed update
|
||||||
|
let err_msg = match registration_err {
|
||||||
|
PeachError::JsonRpcClientCore(Error(ErrorKind::JsonRpcError(rpc_err), _)) => {
|
||||||
|
if let ErrorCode::ServerError(-32030) = rpc_err.code {
|
||||||
|
format!(
|
||||||
|
"Error registering domain: {} was previously registered",
|
||||||
|
full_dynamic_domain
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("Failed to register dyndns domain: {:?}", rpc_err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => "Failed to register dyndns domain".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Err(PeachWebError::FailedToRegisterDynDomain(err_msg))
|
||||||
|
} else {
|
||||||
|
info!("Registered new dyndns domain");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info!("Domain {} already registered", dynamic_domain);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the DNS configuration parameters and apply them.
|
||||||
|
pub fn handle_form(request: &Request) -> Response {
|
||||||
|
let data = try_or_400!(post_input!(request, {
|
||||||
|
external_domain: String,
|
||||||
|
enable_dyndns: bool,
|
||||||
|
dynamic_domain: String,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let (name, msg) = match save_dns_configuration(
|
||||||
|
data.external_domain,
|
||||||
|
data.enable_dyndns,
|
||||||
|
data.dynamic_domain,
|
||||||
|
) {
|
||||||
|
Ok(_) => (
|
||||||
|
"success".to_string(),
|
||||||
|
"New dynamic DNS configuration is now enabled".to_string(),
|
||||||
|
),
|
||||||
|
Err(err) => (
|
||||||
|
"error".to_string(),
|
||||||
|
format!("Failed to save DNS configuration: {}", err),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
|
||||||
|
|
||||||
|
Response::redirect_303("/settings/network/dns").add_flash(flash_name, flash_msg)
|
||||||
|
}
|
164
peach-web/src/routes/settings/network/data_usage_limits.rs
Normal file
164
peach-web/src/routes/settings/network/data_usage_limits.rs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
// TODO:
|
||||||
|
//
|
||||||
|
// This template and associated feature set requires vnstat_parse.
|
||||||
|
// - https://crates.io/crates/vnstat_parse
|
||||||
|
//
|
||||||
|
// Use the PeachCloud config system to store warning and cutoff flags,
|
||||||
|
// as well as the associated totals (thresholds):
|
||||||
|
//
|
||||||
|
// - DATA_WARNING_ENABLED
|
||||||
|
// - DATA_WARNING_LIMIT
|
||||||
|
// - DATA_CUTOFF_ENABLED
|
||||||
|
// - DATA_CUTOFF_LIMIT
|
||||||
|
|
||||||
|
use maud::{html, Markup, PreEscaped};
|
||||||
|
use peach_network::network;
|
||||||
|
use rouille::Request;
|
||||||
|
use vnstat_parse::Vnstat;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
templates,
|
||||||
|
utils::{flash::FlashRequest, theme},
|
||||||
|
WLAN_IFACE,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ROUTE: /settings/network/wifi/usage
|
||||||
|
|
||||||
|
fn render_data_usage_total_capsule() -> Markup {
|
||||||
|
html! {
|
||||||
|
div class="stack capsule" style="margin-left: 2rem; margin-right: 2rem;" {
|
||||||
|
div class="flex-grid" {
|
||||||
|
label id="dataTotal" class="label-large" title="Data download total in MB" {
|
||||||
|
data_total.total / 1024 / 1024 | round
|
||||||
|
}
|
||||||
|
label class="label-small font-near-black" { "MB" }
|
||||||
|
}
|
||||||
|
label class="center-text label-small font-gray" { "USAGE TOTAL" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_warning_threshold_icon() -> Markup {
|
||||||
|
// threshold.warn_flag
|
||||||
|
let warning_enabled = true;
|
||||||
|
|
||||||
|
let icon_class = match warning_enabled {
|
||||||
|
true => "icon",
|
||||||
|
false => "icon icon-inactive",
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div class="card-container container" {
|
||||||
|
div {
|
||||||
|
img id="warnIcon" class=(icon_class) alt="Warning" title="Warning threshold" src="/icons/alert.svg";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_warning_threshold_input() -> Markup {
|
||||||
|
// TODO: source threshold.warn value and replace below
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div {
|
||||||
|
(PreEscaped("<!-- input for warning threshold -->"))
|
||||||
|
label id="warn" class="label-small font-near-black" {
|
||||||
|
input id="warnInput" class="alert-input" name="warn" placeholder="0" type="text" title="Warning threshold value" value="{{ threshold.warn }}" { "MB" }
|
||||||
|
}
|
||||||
|
label class="label-small font-gray" for="warn" style="padding-top: 0.25rem;" { "WARNING THRESHOLD" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_warning_threshold_checkbox() -> Markup {
|
||||||
|
let warning_enabled = true;
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div {
|
||||||
|
(PreEscaped("<!-- checkbox for warning threshold flag -->"))
|
||||||
|
input id="warnCheck" name="warn_flag" title="Activate warning" type="checkbox" checked[warning_enabled];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_critical_threshold_icon() -> Markup {
|
||||||
|
// threshold.cut_flag
|
||||||
|
let cutoff_enabled = true;
|
||||||
|
|
||||||
|
let icon_class = match cutoff_enabled {
|
||||||
|
true => "icon",
|
||||||
|
false => "icon icon-inactive",
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div {
|
||||||
|
img id="cutIcon"
|
||||||
|
class=(icon_class)
|
||||||
|
alt="Cutoff"
|
||||||
|
title="Cutoff threshold"
|
||||||
|
src="/icons/scissor.svg";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_critical_threshold_input() -> Markup {
|
||||||
|
// TODO: source threshold.cut value and replace below
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div {
|
||||||
|
(PreEscaped("<!-- input for cutoff threshold -->"))
|
||||||
|
label id="cut" class="label-small font-near-black"><input id="cutInput" class="alert-input" name="cut" placeholder="0" type="text" title="Critical threshold value" value="{{ threshold.cut }}" { "MB" }
|
||||||
|
label class="label-small font-gray" for="cut" style="padding-top: 0.25rem;" { "CUTOFF THRESHOLD" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_critical_threshold_checkbox() -> Markup {
|
||||||
|
// threshold.cut_flag
|
||||||
|
let cutoff_enabled = true;
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div {
|
||||||
|
(PreEscaped("<!-- checkbox for cutoff threshold flag -->"))
|
||||||
|
input id="cutCheck" name="cut_flag" title="Activate cutoff" type="checkbox" checked[cutoff_enabled];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_buttons() -> Markup {
|
||||||
|
html! {
|
||||||
|
div id="buttonDiv" class="button-div" {
|
||||||
|
input id="updateAlerts" class="button button-primary center" title="Update" type="submit" value="Update";
|
||||||
|
a id="resetTotal" class="button button-warning center" href="/settings/network/wifi/usage/reset" title="Reset stored usage total to zero" { "Reset" }
|
||||||
|
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WiFi data usage form template builder.
|
||||||
|
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||||
|
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||||
|
|
||||||
|
let wlan_data = Vnstat::get(WLAN_IFACE);
|
||||||
|
|
||||||
|
// wlan_data.all_time_total
|
||||||
|
// wlan_data.all_time_total_unit
|
||||||
|
|
||||||
|
let form_template = html! {
|
||||||
|
(PreEscaped("<!-- NETWORK DATA ALERTS FORM -->"))
|
||||||
|
form id="wifiAlerts" action="/network/wifi/usage" class="card center" method="post" {
|
||||||
|
(render_data_usage_total_capsule())
|
||||||
|
(render_warning_threshold_icon())
|
||||||
|
(render_warning_threshold_input())
|
||||||
|
(render_warning_threshold_checkbox())
|
||||||
|
(render_critical_threshold_icon())
|
||||||
|
(render_critical_threshold_input())
|
||||||
|
(render_critical_threshold_checkbox())
|
||||||
|
(render_buttons())
|
||||||
|
}
|
||||||
|
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||||
|
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||||
|
(templates::flash::build_template(name, msg))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
106
peach-web/src/routes/settings/network/list_aps.rs
Normal file
106
peach-web/src/routes/settings/network/list_aps.rs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use maud::{html, Markup, PreEscaped};
|
||||||
|
use peach_network::{network, network::AccessPoint};
|
||||||
|
|
||||||
|
use crate::{templates, utils::theme, AP_IFACE, WLAN_IFACE};
|
||||||
|
|
||||||
|
// ROUTE: /settings/network/wifi
|
||||||
|
|
||||||
|
/// Retrieve network state data required by the WiFi network list template.
|
||||||
|
fn get_network_state_data(ap: &str, wlan: &str) -> (String, String, HashMap<String, AccessPoint>) {
|
||||||
|
let ap_state = match network::state(ap) {
|
||||||
|
Ok(Some(state)) => state,
|
||||||
|
_ => "Interface unavailable".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let wlan_ssid = match network::ssid(wlan) {
|
||||||
|
Ok(Some(ssid)) => ssid,
|
||||||
|
_ => "Not connected".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let network_list = match network::all_networks(wlan) {
|
||||||
|
Ok(networks) => networks,
|
||||||
|
Err(_) => HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
(ap_state, wlan_ssid, network_list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_network_connected_elements(ssid: String) -> Markup {
|
||||||
|
let ap_detail_url = format!("/network/wifi?ssid={}", ssid);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
a class="list-item link primary-bg" href=(ap_detail_url) {
|
||||||
|
img id="netStatus" class="icon icon-active icon-medium list-icon" src="/icons/wifi.svg" alt="WiFi online";
|
||||||
|
p class="list-text" { (ssid) }
|
||||||
|
label class="label-small list-label font-gray" for="netStatus" title="Status" { "Connected" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_network_available_elements(ssid: String, ap_state: String) -> Markup {
|
||||||
|
let ap_detail_url = format!("/network/wifi?ssid={}", ssid);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
a class="list-item link light-bg" href=(ap_detail_url) {
|
||||||
|
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/icons/wifi.svg" alt="WiFi offline";
|
||||||
|
p class="list-text" { (ssid) }
|
||||||
|
label class="label-small list-label font-gray" for="netStatus" title="Status" { (ap_state) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_network_unavailable_elements(ssid: String, ap_state: String) -> Markup {
|
||||||
|
let ap_detail_url = format!("/network/wifi?ssid={}", ssid);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
a class="list-item link" href=(ap_detail_url) {
|
||||||
|
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/icons/wifi.svg" alt="WiFi offline";
|
||||||
|
p class="list-text" { (ssid) }
|
||||||
|
label class="label-small list-label font-gray" for="netStatus" title="Status" { (ap_state) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WiFi network list template builder.
|
||||||
|
pub fn build_template() -> PreEscaped<String> {
|
||||||
|
let (ap_state, wlan_ssid, network_list) = get_network_state_data(AP_IFACE, WLAN_IFACE);
|
||||||
|
|
||||||
|
let list_template = html! {
|
||||||
|
div class="card center" {
|
||||||
|
div class="center list-container" {
|
||||||
|
ul class="list" {
|
||||||
|
@if ap_state == "up" {
|
||||||
|
li class="list-item light-bg warning-border" {
|
||||||
|
"Enable WiFi client mode to view saved and available networks."
|
||||||
|
}
|
||||||
|
} @else if network_list.is_empty() {
|
||||||
|
li class="list-item light-bg" {
|
||||||
|
"No saved or available networks found."
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
@for (ssid, ap) in network_list {
|
||||||
|
li {
|
||||||
|
@if ssid == wlan_ssid {
|
||||||
|
(render_network_connected_elements(ssid))
|
||||||
|
} @else if ap.state == "Available" {
|
||||||
|
(render_network_available_elements(ssid, ap.state))
|
||||||
|
} @else {
|
||||||
|
(render_network_unavailable_elements(ssid, ap.state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let body =
|
||||||
|
templates::nav::build_template(list_template, "WiFi Networks", Some("/settings/network"));
|
||||||
|
|
||||||
|
let theme = theme::get_theme();
|
||||||
|
|
||||||
|
templates::base::build_template(body, theme)
|
||||||
|
}
|
65
peach-web/src/routes/settings/network/menu.rs
Normal file
65
peach-web/src/routes/settings/network/menu.rs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
use maud::{html, Markup, PreEscaped};
|
||||||
|
use peach_network::network;
|
||||||
|
use rouille::Request;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
templates,
|
||||||
|
utils::{flash::FlashRequest, theme},
|
||||||
|
AP_IFACE,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ROUTE: /settings/network
|
||||||
|
|
||||||
|
/// Read the wireless interface mode (WiFi AP or client) and selectively render
|
||||||
|
/// the activation button for the deactivated mode.
|
||||||
|
fn render_mode_toggle_button() -> Markup {
|
||||||
|
match network::state(AP_IFACE) {
|
||||||
|
Ok(Some(state)) if state == "up" => {
|
||||||
|
html! {
|
||||||
|
a id="connectWifi" class="button button-primary center" href="/settings/network/wifi/activate" title="Enable WiFi" { "Enable WiFi" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => html! {
|
||||||
|
a id="deployAccessPoint" class="button button-primary center" href="/settings/network/ap/activate" title="Deploy Access Point" { "Deploy Access Point" }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_buttons() -> Markup {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- BUTTONS -->"))
|
||||||
|
div id="buttons" {
|
||||||
|
a class="button button-primary center" href="/settings/network/wifi/add" title="Add WiFi Network" { "Add WiFi Network" }
|
||||||
|
a id="configureDNS" class="button button-primary center" href="/settings/network/dns" title="Configure DNS" { "Configure DNS" }
|
||||||
|
(PreEscaped("<!-- if ap is up, show 'Enable WiFi' button, else show 'Deplay Access Point' -->"))
|
||||||
|
(render_mode_toggle_button())
|
||||||
|
a id="listWifi" class="button button-primary center" href="/settings/network/wifi" title="List WiFi Networks" { "List WiFi Networks" }
|
||||||
|
// TODO: uncomment this once data usage feature is in place
|
||||||
|
// a id="viewUsage" class="button button-primary center" href="/settings/network/wifi/usage" title="View Data Usage" { "View Data Usage" }
|
||||||
|
a id="viewStatus" class="button button-primary center" href="/status/network" title="View Network Status" { "View Network Status" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Network settings menu template builder.
|
||||||
|
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||||
|
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||||
|
|
||||||
|
let menu_template = html! {
|
||||||
|
(PreEscaped("<!-- NETWORK SETTINGS MENU -->"))
|
||||||
|
div class="card center" {
|
||||||
|
(render_buttons())
|
||||||
|
// 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(menu_template, "Network Settings", Some("/settings"));
|
||||||
|
|
||||||
|
let theme = theme::get_theme();
|
||||||
|
|
||||||
|
templates::base::build_template(body, theme)
|
||||||
|
}
|
8
peach-web/src/routes/settings/network/mod.rs
Normal file
8
peach-web/src/routes/settings/network/mod.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
pub mod add_ap;
|
||||||
|
pub mod ap_details;
|
||||||
|
pub mod configure_dns;
|
||||||
|
// TODO: uncomment this once data usage feature is in place
|
||||||
|
// pub mod data_usage_limits;
|
||||||
|
pub mod list_aps;
|
||||||
|
pub mod menu;
|
||||||
|
pub mod modify_ap;
|
105
peach-web/src/routes/settings/network/modify_ap.rs
Normal file
105
peach-web/src/routes/settings/network/modify_ap.rs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
use maud::{html, Markup, PreEscaped};
|
||||||
|
use peach_network::network;
|
||||||
|
use rouille::{post_input, try_or_400, Request, Response};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
templates,
|
||||||
|
utils::{
|
||||||
|
flash::{FlashRequest, FlashResponse},
|
||||||
|
theme,
|
||||||
|
},
|
||||||
|
WLAN_IFACE,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ROUTE: /settings/network/wifi/modify?<ssid>
|
||||||
|
|
||||||
|
fn render_ssid_input(selected_ap: Option<String>) -> Markup {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- input for network ssid -->"))
|
||||||
|
input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value=[selected_ap] autofocus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_password_input() -> Markup {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- input for network password -->"))
|
||||||
|
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_buttons() -> Markup {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- BUTTONS -->"))
|
||||||
|
div id="buttons" {
|
||||||
|
input id="savePassword" class="button button-primary center" title="Save" type="submit" value="Save";
|
||||||
|
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WiFi access point password modification form template builder.
|
||||||
|
pub fn build_template(request: &Request, selected_ap: Option<String>) -> PreEscaped<String> {
|
||||||
|
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||||
|
|
||||||
|
let form_template = html! {
|
||||||
|
(PreEscaped("<!-- NETWORK MODIFY AP PASSWORD FORM -->"))
|
||||||
|
div class="card center" {
|
||||||
|
form id="wifiModify" action="/settings/network/wifi/modify" method="post" {
|
||||||
|
(render_ssid_input(selected_ap))
|
||||||
|
(render_password_input())
|
||||||
|
(render_buttons())
|
||||||
|
}
|
||||||
|
// 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(
|
||||||
|
form_template,
|
||||||
|
"Change WiFi Password",
|
||||||
|
Some("/settings/network"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let theme = theme::get_theme();
|
||||||
|
|
||||||
|
templates::base::build_template(body, theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the SSID and password for an access point and save the new password.
|
||||||
|
pub fn handle_form(request: &Request) -> Response {
|
||||||
|
let data = try_or_400!(post_input!(request, {
|
||||||
|
ssid: String,
|
||||||
|
pass: String,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let (name, msg) = match network::id(WLAN_IFACE, &data.ssid) {
|
||||||
|
Ok(Some(id)) => match network::modify(&id, &data.ssid, &data.pass) {
|
||||||
|
Ok(_) => ("success".to_string(), "WiFi password updated".to_string()),
|
||||||
|
Err(err) => (
|
||||||
|
"error".to_string(),
|
||||||
|
format!("Failed to update WiFi password: {}", err),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Ok(None) => (
|
||||||
|
"error".to_string(),
|
||||||
|
format!(
|
||||||
|
"Failed to update WiFi password: no saved credentials found for network {}",
|
||||||
|
&data.ssid
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Err(err) => (
|
||||||
|
"error".to_string(),
|
||||||
|
format!(
|
||||||
|
"Failed to update WiFi password: no ID found for network {}: {}",
|
||||||
|
&data.ssid, err
|
||||||
|
),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
|
||||||
|
|
||||||
|
Response::redirect_303("/settings/network/wifi/modify").add_flash(flash_name, flash_msg)
|
||||||
|
}
|
37
peach-web/src/routes/settings/power/menu.rs
Normal file
37
peach-web/src/routes/settings/power/menu.rs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
use maud::{html, PreEscaped};
|
||||||
|
use rouille::Request;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
templates,
|
||||||
|
utils::{flash::FlashRequest, theme},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Power menu template builder.
|
||||||
|
///
|
||||||
|
/// Presents options for rebooting or shutting down the device.
|
||||||
|
pub fn build_template(request: &Request) -> PreEscaped<String> {
|
||||||
|
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||||
|
|
||||||
|
let power_menu_template = html! {
|
||||||
|
(PreEscaped("<!-- POWER MENU -->"))
|
||||||
|
div class="card center" {
|
||||||
|
div class="card-container" {
|
||||||
|
div id="buttons" {
|
||||||
|
a id="rebootBtn" class="button button-primary center" href="/reboot" title="Reboot Device" { "Reboot" }
|
||||||
|
a id="shutdownBtn" class="button button-warning center" href="/shutdown" title="Shutdown Device" { "Shutdown" }
|
||||||
|
a id="cancelBtn" class="button button-secondary center" href="/settings" title="Cancel" { "Cancel" }
|
||||||
|
}
|
||||||
|
@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(power_menu_template, "Power Menu", Some("/"));
|
||||||
|
|
||||||
|
let theme = theme::get_theme();
|
||||||
|
|
||||||
|
templates::base::build_template(body, theme)
|
||||||
|
}
|
3
peach-web/src/routes/settings/power/mod.rs
Normal file
3
peach-web/src/routes/settings/power/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod menu;
|
||||||
|
pub mod reboot;
|
||||||
|
pub mod shutdown;
|
36
peach-web/src/routes/settings/power/reboot.rs
Normal file
36
peach-web/src/routes/settings/power/reboot.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
use log::info;
|
||||||
|
use rouille::Response;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io::Result,
|
||||||
|
process::{Command, Output},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::utils::flash::FlashResponse;
|
||||||
|
|
||||||
|
/// Executes a system command to reboot the device immediately.
|
||||||
|
fn reboot() -> Result<Output> {
|
||||||
|
info!("Rebooting the device");
|
||||||
|
// ideally, we'd like to reboot after 5 seconds to allow time for JSON
|
||||||
|
// response but this is not possible with the `shutdown` command alone.
|
||||||
|
// TODO: send "rebooting..." message to `peach-oled` for display
|
||||||
|
Command::new("sudo")
|
||||||
|
.arg("shutdown")
|
||||||
|
.arg("-r")
|
||||||
|
.arg("now")
|
||||||
|
.output()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_reboot() -> Response {
|
||||||
|
let (name, msg) = match reboot() {
|
||||||
|
Ok(_) => ("success".to_string(), "Rebooting the device".to_string()),
|
||||||
|
Err(err) => (
|
||||||
|
"error".to_string(),
|
||||||
|
format!("Failed to reboot the device: {}", err),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
|
||||||
|
|
||||||
|
Response::redirect_303("/power").add_flash(flash_name, flash_msg)
|
||||||
|
}
|
35
peach-web/src/routes/settings/power/shutdown.rs
Normal file
35
peach-web/src/routes/settings/power/shutdown.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use log::info;
|
||||||
|
use rouille::Response;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io::Result,
|
||||||
|
process::{Command, Output},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::utils::flash::FlashResponse;
|
||||||
|
|
||||||
|
/// Executes a system command to shutdown the device immediately.
|
||||||
|
fn shutdown() -> Result<Output> {
|
||||||
|
info!("Shutting down the device");
|
||||||
|
// ideally, we'd like to shutdown after 5 seconds to allow time for JSON
|
||||||
|
// response but this is not possible with the `shutdown` command alone.
|
||||||
|
// TODO: send "shutting down..." message to `peach-oled` for display
|
||||||
|
Command::new("sudo").arg("shutdown").arg("now").output()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_shutdown() -> Response {
|
||||||
|
let (name, msg) = match shutdown() {
|
||||||
|
Ok(_) => (
|
||||||
|
"success".to_string(),
|
||||||
|
"Shutting down the device".to_string(),
|
||||||
|
),
|
||||||
|
Err(err) => (
|
||||||
|
"error".to_string(),
|
||||||
|
format!("Failed to shutdown the device: {}", err),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
|
||||||
|
|
||||||
|
Response::redirect_303("/power").add_flash(flash_name, flash_msg)
|
||||||
|
}
|
@ -122,6 +122,15 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
|
|||||||
input type="text" id="database_dir" name="repo" value=(sbot_config.repo);
|
input type="text" id="database_dir" name="repo" value=(sbot_config.repo);
|
||||||
}
|
}
|
||||||
div class="center" {
|
div class="center" {
|
||||||
|
@if sbot_config.enable_ebt {
|
||||||
|
input type="checkbox" id="ebtReplication" style="margin-bottom: 1rem;" name="enable_ebt" checked;
|
||||||
|
} @else {
|
||||||
|
input type="checkbox" id="ebtReplication" style="margin-bottom: 1rem;" name="enable_ebt";
|
||||||
|
}
|
||||||
|
label class="font-normal" for="ebtReplication" title="Enable Epidemic Broadcast Tree (EBT) replication instead of legacy replication" {
|
||||||
|
"Enable EBT Replication"
|
||||||
|
}
|
||||||
|
br;
|
||||||
@if sbot_config.localadv {
|
@if sbot_config.localadv {
|
||||||
input type="checkbox" id="lanBroadcast" style="margin-bottom: 1rem;" name="localadv" checked;
|
input type="checkbox" id="lanBroadcast" style="margin-bottom: 1rem;" name="localadv" checked;
|
||||||
} @else {
|
} @else {
|
||||||
@ -157,7 +166,6 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
|
|||||||
input type="hidden" id="hmac" name="hmac" value=(sbot_config.hmac);
|
input type="hidden" id="hmac" name="hmac" value=(sbot_config.hmac);
|
||||||
input type="hidden" id="wslis" name="wslis" value=(sbot_config.wslis);
|
input type="hidden" id="wslis" name="wslis" value=(sbot_config.wslis);
|
||||||
input type="hidden" id="debuglis" name="debuglis" value=(sbot_config.debuglis);
|
input type="hidden" id="debuglis" name="debuglis" value=(sbot_config.debuglis);
|
||||||
input type="hidden" id="enable_ebt" name="enable_ebt" value=(sbot_config.enable_ebt);
|
|
||||||
input type="hidden" id="promisc" name="promisc" value=(sbot_config.promisc);
|
input type="hidden" id="promisc" name="promisc" value=(sbot_config.promisc);
|
||||||
input type="hidden" id="nounixsock" name="nounixsock" value=(sbot_config.nounixsock);
|
input type="hidden" id="nounixsock" name="nounixsock" value=(sbot_config.nounixsock);
|
||||||
(PreEscaped("<!-- BUTTONS -->"))
|
(PreEscaped("<!-- BUTTONS -->"))
|
||||||
@ -175,8 +183,11 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
|
|||||||
|
|
||||||
// wrap the nav bars around the settings menu template content
|
// wrap the nav bars around the settings menu template content
|
||||||
// parameters are template, title and back url
|
// parameters are template, title and back url
|
||||||
let body =
|
let body = templates::nav::build_template(
|
||||||
templates::nav::build_template(menu_template, "Scuttlebutt Settings", Some("/settings"));
|
menu_template,
|
||||||
|
"Scuttlebutt Settings",
|
||||||
|
Some("/settings/scuttlebutt"),
|
||||||
|
);
|
||||||
|
|
||||||
// query the current theme so we can pass it into the base template builder
|
// query the current theme so we can pass it into the base template builder
|
||||||
let theme = theme::get_theme();
|
let theme = theme::get_theme();
|
||||||
@ -194,7 +205,7 @@ pub fn handle_form(request: &Request, restart: bool) -> Response {
|
|||||||
debugdir: String,
|
debugdir: String,
|
||||||
shscap: String,
|
shscap: String,
|
||||||
hmac: String,
|
hmac: String,
|
||||||
hops: u8,
|
hops: i8,
|
||||||
lis_ip: String,
|
lis_ip: String,
|
||||||
lis_port: String,
|
lis_port: String,
|
||||||
wslis: String,
|
wslis: String,
|
||||||
|
@ -4,7 +4,7 @@ use rouille::Request;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
templates,
|
templates,
|
||||||
utils::{flash::FlashRequest, theme},
|
utils::{cookie::CookieRequest, flash::FlashRequest, theme},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Read the status of the go-sbot service and render buttons accordingly.
|
/// Read the status of the go-sbot service and render buttons accordingly.
|
||||||
@ -53,10 +53,13 @@ pub fn build_template(request: &Request) -> PreEscaped<String> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// retrieve the value of the "back_url" cookie
|
||||||
|
// if the cookie value is not found then set a hardcoded fallback value
|
||||||
|
let back_url = request.retrieve_cookie("back_url").or(Some("/settings"));
|
||||||
|
|
||||||
// wrap the nav bars around the settings menu template content
|
// wrap the nav bars around the settings menu template content
|
||||||
// parameters are template, title and back url
|
// parameters are template, title and back url
|
||||||
let body =
|
let body = templates::nav::build_template(menu_template, "Scuttlebutt Settings", back_url);
|
||||||
templates::nav::build_template(menu_template, "Scuttlebutt Settings", Some("/settings"));
|
|
||||||
|
|
||||||
// query the current theme so we can pass it into the base template builder
|
// query the current theme so we can pass it into the base template builder
|
||||||
let theme = theme::get_theme();
|
let theme = theme::get_theme();
|
||||||
|
@ -1,238 +1,381 @@
|
|||||||
use log::info;
|
use std::process::Command;
|
||||||
use rocket::{
|
|
||||||
get,
|
|
||||||
request::FlashMessage,
|
|
||||||
response::{Flash, Redirect},
|
|
||||||
};
|
|
||||||
use rocket_dyn_templates::Template;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::{
|
|
||||||
io,
|
|
||||||
process::{Command, Output},
|
|
||||||
};
|
|
||||||
|
|
||||||
use peach_lib::{
|
use maud::{html, Markup, PreEscaped};
|
||||||
config_manager::load_peach_config, dyndns_client, network_client, oled_client, sbot::SbotStatus,
|
use peach_lib::{config_manager, dyndns_client, oled_client};
|
||||||
};
|
|
||||||
use peach_stats::{
|
use peach_stats::{
|
||||||
stats,
|
stats,
|
||||||
stats::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat},
|
stats::{CpuStatPercentages, MemStat},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::routes::authentication::Authenticated;
|
use crate::{templates, utils::theme};
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /status
|
// ROUTE: /status
|
||||||
|
|
||||||
/// System statistics data.
|
/// Query systemd to determine the state of the networking service.
|
||||||
#[derive(Debug, Serialize)]
|
fn retrieve_networking_state() -> Option<String> {
|
||||||
pub struct StatusContext {
|
// call: `systemctl show networking.service --no-page`
|
||||||
pub back: Option<String>,
|
let networking_service_output = Command::new("systemctl")
|
||||||
pub cpu_stat_percent: Option<CpuStatPercentages>,
|
.arg("show")
|
||||||
pub disk_stats: Vec<DiskUsage>,
|
.arg("networking.service")
|
||||||
pub flash_name: Option<String>,
|
.arg("--no-page")
|
||||||
pub flash_msg: Option<String>,
|
.output()
|
||||||
pub load_average: Option<LoadAverage>,
|
.ok()?;
|
||||||
pub mem_stats: Option<MemStat>,
|
|
||||||
pub network_ping: String,
|
let service_info = std::str::from_utf8(&networking_service_output.stdout).ok()?;
|
||||||
pub oled_ping: String,
|
|
||||||
pub dyndns_enabled: bool,
|
// find the line starting with "ActiveState=" and return the value
|
||||||
pub dyndns_is_online: bool,
|
service_info
|
||||||
pub config_is_valid: bool,
|
.lines()
|
||||||
pub sbot_is_online: bool,
|
.find(|line| line.starts_with("ActiveState="))
|
||||||
pub title: Option<String>,
|
.and_then(|line| line.strip_prefix("ActiveState="))
|
||||||
pub uptime: Option<i32>,
|
.map(|state| state.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusContext {
|
/// Query systemd to determine the state of the sbot service.
|
||||||
pub fn build() -> StatusContext {
|
fn retrieve_sbot_state() -> Option<String> {
|
||||||
// convert result to Option<CpuStatPercentages>, discard any error
|
// retrieve the name of the go-sbot service or set default
|
||||||
let cpu_stat_percent = stats::cpu_stats_percent().ok();
|
let go_sbot_service = config_manager::get_config_value("GO_SBOT_SERVICE")
|
||||||
let load_average = stats::load_average().ok();
|
.unwrap_or_else(|_| "go-sbot.service".to_string());
|
||||||
let mem_stats = stats::mem_stats().ok();
|
|
||||||
// TODO: add `wpa_supplicant_status` to peach_network to replace this ping call
|
|
||||||
// instead of: "is the network json-rpc server running?", we want to ask:
|
|
||||||
// "is the wpa_supplicant systemd service functioning correctly?"
|
|
||||||
let network_ping = match network_client::ping() {
|
|
||||||
Ok(_) => "ONLINE".to_string(),
|
|
||||||
Err(_) => "OFFLINE".to_string(),
|
|
||||||
};
|
|
||||||
let oled_ping = match oled_client::ping() {
|
|
||||||
Ok(_) => "ONLINE".to_string(),
|
|
||||||
Err(_) => "OFFLINE".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let uptime = match stats::uptime() {
|
let sbot_service_output = Command::new("systemctl")
|
||||||
Ok(secs) => {
|
.arg("show")
|
||||||
let uptime_mins = secs / 60;
|
.arg(go_sbot_service)
|
||||||
uptime_mins.to_string()
|
.arg("--no-page")
|
||||||
}
|
.output()
|
||||||
Err(_) => "Unavailable".to_string(),
|
.ok()?;
|
||||||
};
|
|
||||||
|
|
||||||
// parse the uptime string to a signed integer (for math)
|
let service_info = std::str::from_utf8(&sbot_service_output.stdout).ok()?;
|
||||||
let uptime_parsed = uptime.parse::<i32>().ok();
|
|
||||||
|
|
||||||
// serialize disk usage data into Vec<DiskUsage>
|
// find the line starting with "ActiveState=" and return the value
|
||||||
let disk_usage_stats = match stats::disk_usage() {
|
service_info
|
||||||
Ok(disks) => disks,
|
.lines()
|
||||||
Err(_) => Vec::new(),
|
.find(|line| line.starts_with("ActiveState="))
|
||||||
};
|
.and_then(|line| line.strip_prefix("ActiveState="))
|
||||||
|
.map(|state| state.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
let mut disk_stats = Vec::new();
|
fn retrieve_device_status_data() -> (Option<u64>, String) {
|
||||||
// select only the partition we're interested in: /dev/mmcblk0p2 ("/")
|
let uptime = stats::uptime().ok();
|
||||||
for disk in disk_usage_stats {
|
|
||||||
if disk.mountpoint == "/" {
|
let oled_ping = match oled_client::ping() {
|
||||||
disk_stats.push(disk);
|
Ok(_) => "ONLINE".to_string(),
|
||||||
|
Err(_) => "OFFLINE".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
(uptime, oled_ping)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn retrieve_device_usage_data() -> (Option<CpuStatPercentages>, Option<MemStat>) {
|
||||||
|
// convert result to Option<CpuStatPercentages>, discard any error
|
||||||
|
let cpu_stat_percent = stats::cpu_stats_percent().ok();
|
||||||
|
let mem_stats = stats::mem_stats().ok();
|
||||||
|
|
||||||
|
(cpu_stat_percent, mem_stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_network_capsule() -> Markup {
|
||||||
|
let (state, stack_class, img_class) = match retrieve_networking_state() {
|
||||||
|
Some(state) if state.as_str() == "active" => {
|
||||||
|
("active", "stack capsule border-success", "icon icon-medium")
|
||||||
|
}
|
||||||
|
Some(state) if state.as_str() == "inactive" => (
|
||||||
|
"inactive",
|
||||||
|
"stack capsule border-warning",
|
||||||
|
"icon icon-inactive icon-medium",
|
||||||
|
),
|
||||||
|
Some(state) if state.as_str() == "failed" => (
|
||||||
|
"failed",
|
||||||
|
"stack capsule border-danger",
|
||||||
|
"icon icon-inactive icon-medium",
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
"error",
|
||||||
|
"stack capsule border-danger",
|
||||||
|
"icon icon-inactive icon-medium",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- PEACH-NETWORK STATUS STACK -->"))
|
||||||
|
div class=(stack_class) {
|
||||||
|
img id="networkIcon" class=(img_class) alt="Network" title="Networking service status" src="icons/wifi.svg";
|
||||||
|
div class="stack" style="padding-top: 0.5rem;" {
|
||||||
|
label class="label-small font-near-black" { "Networking" }
|
||||||
|
label class="label-small font-near-black" { (state.to_uppercase()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// dyndns_is_online & config_is_valid
|
fn render_oled_capsule(state: String) -> Markup {
|
||||||
let dyndns_enabled: bool;
|
let (stack_class, img_class) = match state.as_str() {
|
||||||
let dyndns_is_online: bool;
|
"ONLINE" => ("stack capsule border-success", "icon icon-medium"),
|
||||||
let config_is_valid: bool;
|
_ => (
|
||||||
let load_peach_config_result = load_peach_config();
|
"stack capsule border-warning",
|
||||||
match load_peach_config_result {
|
"icon icon-inactive icon-medium",
|
||||||
Ok(peach_config) => {
|
),
|
||||||
dyndns_enabled = peach_config.dyn_enabled;
|
};
|
||||||
config_is_valid = true;
|
|
||||||
if dyndns_enabled {
|
html! {
|
||||||
let is_dyndns_online_result = dyndns_client::is_dns_updater_online();
|
(PreEscaped("<!-- PEACH-OLED STATUS STACK -->"))
|
||||||
match is_dyndns_online_result {
|
div class=(stack_class) {
|
||||||
Ok(is_online) => {
|
img id="oledIcon" class=(img_class) alt="Display" title="OLED display microservice status" src="icons/lcd.svg";
|
||||||
dyndns_is_online = is_online;
|
div class="stack" style="padding-top: 0.5rem;" {
|
||||||
}
|
label class="label-small font-near-black" { "Display" }
|
||||||
Err(_err) => {
|
label class="label-small font-near-black" { (state) }
|
||||||
dyndns_is_online = false;
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_diagnostics_capsule() -> Markup {
|
||||||
|
// TODO: write a diagnostics module (maybe in peach-lib)
|
||||||
|
|
||||||
|
let diagnostics_state = "CLEAR";
|
||||||
|
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- DIAGNOSTICS AND LOGS STACK -->"))
|
||||||
|
div class="stack capsule border-success" {
|
||||||
|
img id="statsIcon" class="icon icon-medium" alt="Line chart" title="System diagnostics and logs" src="icons/chart.svg";
|
||||||
|
div class="stack" style="padding-top: 0.5rem;" {
|
||||||
|
label class="label-small font-near-black" { "Diagnostics" }
|
||||||
|
label class="label-small font-near-black" { (diagnostics_state) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_dyndns_capsule() -> Markup {
|
||||||
|
let (state, stack_class, img_class) = match dyndns_client::is_dns_updater_online() {
|
||||||
|
Ok(true) => ("ONLINE", "stack capsule border-success", "icon icon-medium"),
|
||||||
|
Ok(false) => (
|
||||||
|
"OFFLINE",
|
||||||
|
"stack capsule border-warning",
|
||||||
|
"icon icon-inactive icon-medium",
|
||||||
|
),
|
||||||
|
Err(_) => (
|
||||||
|
"ERROR",
|
||||||
|
"stack capsule border-danger",
|
||||||
|
"icon icon-inactive icon-medium",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- DYNDNS STATUS STACK -->"))
|
||||||
|
div class=(stack_class) {
|
||||||
|
img id="dnsIcon" class=(img_class) alt="Dyndns" title="Dyndns status" src="icons/dns.png";
|
||||||
|
div class="stack" style="padding-top: 0.5rem;" {
|
||||||
|
label class="label-small font-near-black" { "Dyn DNS" }
|
||||||
|
label class="label-small font-near-black" { (state) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_config_capsule() -> Markup {
|
||||||
|
let (state, stack_class, img_class) =
|
||||||
|
match config_manager::load_peach_config_from_disc().is_ok() {
|
||||||
|
true => ("LOADED", "stack capsule border-success", "icon icon-medium"),
|
||||||
|
false => (
|
||||||
|
"INVALID",
|
||||||
|
"stack capsule border-warning",
|
||||||
|
"icon icon-inactive icon-medium",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- CONFIG STATUS STACK -->"))
|
||||||
|
div class=(stack_class) {
|
||||||
|
img id="configIcon" class=(img_class) alt="Config" title="Config status" src="icons/clipboard.png";
|
||||||
|
div class="stack" style="padding-top: 0.5rem;" {
|
||||||
|
label class="label-small font-near-black" { "Config" }
|
||||||
|
label class="label-small font-near-black" { (state) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_sbot_capsule() -> Markup {
|
||||||
|
let (state, stack_class, img_class) = match retrieve_sbot_state() {
|
||||||
|
Some(state) if state.as_str() == "active" => {
|
||||||
|
("active", "stack capsule border-success", "icon icon-medium")
|
||||||
|
}
|
||||||
|
Some(state) if state.as_str() == "inactive" => (
|
||||||
|
"inactive",
|
||||||
|
"stack capsule border-warning",
|
||||||
|
"icon icon-inactive icon-medium",
|
||||||
|
),
|
||||||
|
Some(state) if state.as_str() == "failed" => (
|
||||||
|
"failed",
|
||||||
|
"stack capsule border-danger",
|
||||||
|
"icon icon-inactive icon-medium",
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
"error",
|
||||||
|
"stack capsule border-danger",
|
||||||
|
"icon icon-inactive icon-medium",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- SBOT STATUS STACK -->"))
|
||||||
|
div class=(stack_class) {
|
||||||
|
img id="sbotIcon" class=(img_class) alt="Sbot" title="Sbot status" src="icons/hermies.svg";
|
||||||
|
div class="stack" style="padding-top: 0.5rem;" {
|
||||||
|
label class="label-small font-near-black" { "Sbot" }
|
||||||
|
label class="label-small font-near-black" { (state.to_uppercase()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_cpu_usage_meter(cpu_usage_percent: Option<CpuStatPercentages>) -> Markup {
|
||||||
|
html! {
|
||||||
|
@if let Some(cpu_usage) = cpu_usage_percent {
|
||||||
|
@let cpu_usage_total = (cpu_usage.nice + cpu_usage.system + cpu_usage.user).round();
|
||||||
|
div class="flex-grid" {
|
||||||
|
span class="card-text" { "CPU" }
|
||||||
|
span class="label-small push-right" { (cpu_usage_total) "%" }
|
||||||
|
}
|
||||||
|
meter value=(cpu_usage_total) min="0" max="100" title="CPU usage" {
|
||||||
|
div class="meter-gauge" {
|
||||||
|
span style={ "width: " (cpu_usage_total) "%;" } {
|
||||||
|
"CPU Usage"
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
dyndns_is_online = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_err) => {
|
} @else {
|
||||||
dyndns_enabled = false;
|
p class="card-text" { "CPU usage data unavailable" }
|
||||||
dyndns_is_online = false;
|
}
|
||||||
config_is_valid = false;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_mem_usage_meter(mem_stats: Option<MemStat>) -> Markup {
|
||||||
|
html! {
|
||||||
|
@if let Some(mem) = mem_stats {
|
||||||
|
// convert kilobyte values to megabyte values
|
||||||
|
@let mem_free_mb = mem.free / 1024;
|
||||||
|
@let mem_total_mb = mem.total / 1024;
|
||||||
|
@let mem_used_mb = mem.used / 1024;
|
||||||
|
// calculate memory usage as a percentage
|
||||||
|
@let mem_used_percent = mem_used_mb * 100 / mem_total_mb;
|
||||||
|
// render disk free value as megabytes or gigabytes based on size
|
||||||
|
@let mem_free_value = if mem_free_mb > 1024 {
|
||||||
|
format!("{} GB", (mem_free_mb / 1024))
|
||||||
|
} else {
|
||||||
|
format!("{} MB", mem_free_mb)
|
||||||
|
};
|
||||||
|
|
||||||
|
div class="flex-grid" {
|
||||||
|
span class="card-text" { "Memory" }
|
||||||
|
span class="label-small push-right" { (mem_used_percent) "% (" (mem_free_value) " free)" }
|
||||||
|
}
|
||||||
|
meter value=(mem_used_mb) min="0" max=(mem_total_mb) title="Memory usage" {
|
||||||
|
div class="meter-gauge" {
|
||||||
|
span style={ "width: " (mem_used_percent) "%;" } { "Memory Usage" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
p class="card-text" { "Memory usage data unavailable" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_disk_usage_meter() -> Markup {
|
||||||
|
let disk_usage_stats = match stats::disk_usage() {
|
||||||
|
Ok(disks) => disks,
|
||||||
|
Err(_) => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// select only the partition we're interested in: /dev/mmcblk0p2 ("/")
|
||||||
|
let disk_usage = disk_usage_stats.iter().find(|disk| disk.mountpoint == "/");
|
||||||
|
|
||||||
|
html! {
|
||||||
|
@if let Some(disk) = disk_usage {
|
||||||
|
// calculate free disk space in megabytes
|
||||||
|
@let disk_free_mb = disk.one_k_blocks_free / 1024;
|
||||||
|
// calculate free disk space in gigabytes
|
||||||
|
@let disk_free_gb = disk_free_mb / 1024;
|
||||||
|
// render disk free value as megabytes or gigabytes based on size
|
||||||
|
@let disk_free_value = if disk_free_mb > 1024 {
|
||||||
|
format!("{} GB", disk_free_gb)
|
||||||
|
} else {
|
||||||
|
format!("{} MB", disk_free_mb)
|
||||||
|
};
|
||||||
|
|
||||||
|
div class="flex-grid" {
|
||||||
|
span class="card-text" { "Disk" }
|
||||||
|
span class="label-small push-right" { (disk.used_percentage) "% (" (disk_free_value) " free)" }
|
||||||
|
}
|
||||||
|
meter value=(disk.used_percentage) min="0" max="100" title="Disk usage" {
|
||||||
|
div class="meter-gauge" {
|
||||||
|
span style={ "width: " (disk.used_percentage) "%;" } {
|
||||||
|
"Disk Usage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
p class="card-text" { "Disk usage data unavailable" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display system uptime in hours and minutes.
|
||||||
|
fn render_uptime_capsule(uptime: Option<u64>) -> Markup {
|
||||||
|
html! {
|
||||||
|
@if let Some(uptime_secs) = uptime {
|
||||||
|
@let uptime_mins = uptime_secs / 60;
|
||||||
|
@if uptime_mins < 60 {
|
||||||
|
// display system uptime in minutes
|
||||||
|
p class="capsule center-text" {
|
||||||
|
"Uptime: " (uptime_mins) " minutes"
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
// display system uptime in hours and minutes
|
||||||
|
@let hours = uptime_mins / 60;
|
||||||
|
@let mins = uptime_mins % 60;
|
||||||
|
p class="capsule center-text" {
|
||||||
|
"Uptime: " (hours) " hours, " (mins) " minutes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
p class="card-text" { "Uptime data unavailable" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device status template builder.
|
||||||
|
pub fn build_template() -> PreEscaped<String> {
|
||||||
|
let (uptime, oled_state) = retrieve_device_status_data();
|
||||||
|
let (cpu_usage, mem_usage) = retrieve_device_usage_data();
|
||||||
|
|
||||||
|
let device_status_template = html! {
|
||||||
|
(PreEscaped("<!-- DEVICE STATUS CARD -->"))
|
||||||
|
div class="card center" {
|
||||||
|
div class="card-container" {
|
||||||
|
// display status capsules for network, oled and diagnostics
|
||||||
|
div class="three-grid" {
|
||||||
|
(render_network_capsule())
|
||||||
|
(render_oled_capsule(oled_state))
|
||||||
|
(render_diagnostics_capsule())
|
||||||
|
}
|
||||||
|
// display status capsules for dyndns, config and sbot
|
||||||
|
div class="three-grid" style="padding-top: 1rem; padding-bottom: 1rem;" {
|
||||||
|
(render_dyndns_capsule())
|
||||||
|
(render_config_capsule())
|
||||||
|
(render_sbot_capsule())
|
||||||
|
}
|
||||||
|
(render_cpu_usage_meter(cpu_usage))
|
||||||
|
(render_mem_usage_meter(mem_usage))
|
||||||
|
(render_disk_usage_meter())
|
||||||
|
(render_uptime_capsule(uptime))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// test if go-sbot is running
|
|
||||||
let sbot_status = SbotStatus::read();
|
|
||||||
let sbot_is_online: bool = match sbot_status {
|
|
||||||
// return true if state is active
|
|
||||||
Ok(status) => matches!(status.state == Some("active".to_string()), true),
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
StatusContext {
|
|
||||||
back: None,
|
|
||||||
cpu_stat_percent,
|
|
||||||
disk_stats,
|
|
||||||
flash_name: None,
|
|
||||||
flash_msg: None,
|
|
||||||
load_average,
|
|
||||||
mem_stats,
|
|
||||||
network_ping,
|
|
||||||
oled_ping,
|
|
||||||
dyndns_enabled,
|
|
||||||
dyndns_is_online,
|
|
||||||
config_is_valid,
|
|
||||||
sbot_is_online,
|
|
||||||
title: None,
|
|
||||||
uptime: uptime_parsed,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
pub fn device_status(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
|
||||||
// assign context through context_builder call
|
|
||||||
let mut context = StatusContext::build();
|
|
||||||
context.back = Some("/".to_string());
|
|
||||||
context.title = Some("Device Status".to_string());
|
|
||||||
// check to see if there is a flash message to display
|
|
||||||
if let Some(flash) = flash {
|
|
||||||
// add flash message contents to the context object
|
|
||||||
context.flash_name = Some(flash.kind().to_string());
|
|
||||||
context.flash_msg = Some(flash.message().to_string());
|
|
||||||
};
|
};
|
||||||
// template_dir is set in Rocket.toml
|
|
||||||
Template::render("status/device", &context)
|
let body = templates::nav::build_template(device_status_template, "Device Status", Some("/"));
|
||||||
}
|
|
||||||
|
let theme = theme::get_theme();
|
||||||
// HELPERS AND ROUTES FOR /power/reboot
|
|
||||||
|
templates::base::build_template(body, theme)
|
||||||
/// Executes a system command to reboot the device immediately.
|
|
||||||
pub fn reboot() -> io::Result<Output> {
|
|
||||||
info!("Rebooting the device");
|
|
||||||
// ideally, we'd like to reboot after 5 seconds to allow time for JSON
|
|
||||||
// response but this is not possible with the `shutdown` command alone.
|
|
||||||
// TODO: send "rebooting..." message to `peach-oled` for display
|
|
||||||
Command::new("sudo")
|
|
||||||
.arg("shutdown")
|
|
||||||
.arg("-r")
|
|
||||||
.arg("now")
|
|
||||||
.output()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/power/reboot")]
|
|
||||||
pub fn reboot_cmd(_auth: Authenticated) -> Flash<Redirect> {
|
|
||||||
match reboot() {
|
|
||||||
Ok(_) => Flash::success(Redirect::to("/power"), "Rebooting the device"),
|
|
||||||
Err(_) => Flash::error(Redirect::to("/power"), "Failed to reboot the device"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /power/shutdown
|
|
||||||
|
|
||||||
/// Executes a system command to shutdown the device immediately.
|
|
||||||
pub fn shutdown() -> io::Result<Output> {
|
|
||||||
info!("Shutting down the device");
|
|
||||||
// ideally, we'd like to reboot after 5 seconds to allow time for JSON
|
|
||||||
// response but this is not possible with the `shutdown` command alone.
|
|
||||||
// TODO: send "shutting down..." message to `peach-oled` for display
|
|
||||||
Command::new("sudo").arg("shutdown").arg("now").output()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/power/shutdown")]
|
|
||||||
pub fn shutdown_cmd(_auth: Authenticated) -> Flash<Redirect> {
|
|
||||||
match shutdown() {
|
|
||||||
Ok(_) => Flash::success(Redirect::to("/power"), "Shutting down the device"),
|
|
||||||
Err(_) => Flash::error(Redirect::to("/power"), "Failed to shutdown the device"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /power
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct PowerContext {
|
|
||||||
pub back: Option<String>,
|
|
||||||
pub flash_name: Option<String>,
|
|
||||||
pub flash_msg: Option<String>,
|
|
||||||
pub title: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PowerContext {
|
|
||||||
pub fn build() -> PowerContext {
|
|
||||||
PowerContext {
|
|
||||||
back: None,
|
|
||||||
flash_name: None,
|
|
||||||
flash_msg: None,
|
|
||||||
title: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/power")]
|
|
||||||
pub fn power_menu(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
|
||||||
let mut context = PowerContext::build();
|
|
||||||
context.back = Some("/".to_string());
|
|
||||||
context.title = Some("Power Menu".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("power", &context)
|
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
//pub mod device;
|
pub mod device;
|
||||||
//pub mod network;
|
pub mod network;
|
||||||
pub mod scuttlebutt;
|
pub mod scuttlebutt;
|
||||||
|
@ -1,21 +1,285 @@
|
|||||||
use rocket::{get, request::FlashMessage};
|
use maud::{html, Markup, PreEscaped};
|
||||||
use rocket_dyn_templates::Template;
|
use peach_network::network;
|
||||||
|
use vnstat_parse::Vnstat;
|
||||||
|
|
||||||
use crate::context::network::NetworkStatusContext;
|
use crate::{templates, utils::theme, AP_IFACE, WLAN_IFACE};
|
||||||
use crate::routes::authentication::Authenticated;
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /status/network
|
enum NetworkState {
|
||||||
|
AccessPoint,
|
||||||
|
WiFiClient,
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/network")]
|
// ROUTE: /status/network
|
||||||
pub fn network_status(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
|
||||||
let mut context = NetworkStatusContext::build();
|
|
||||||
context.back = Some("/status".to_string());
|
|
||||||
context.title = Some("Network Status".to_string());
|
|
||||||
|
|
||||||
if let Some(flash) = flash {
|
/// Render the cog icon which is used as a link to the network settings page.
|
||||||
context.flash_name = Some(flash.kind().to_string());
|
fn render_network_config_icon() -> Markup {
|
||||||
context.flash_msg = Some(flash.message().to_string());
|
html! {
|
||||||
|
(PreEscaped("<!-- top-right config icon -->"))
|
||||||
|
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
|
||||||
|
img id="configureNetworking" class="icon-small" src="/icons/cog.svg" alt="Configure";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the network mode icon, either a WiFi signal or router, based
|
||||||
|
/// on the state of the AP and WiFi interfaces.
|
||||||
|
///
|
||||||
|
/// A router icon is shown if the AP is online (interface is "up").
|
||||||
|
///
|
||||||
|
/// A WiFi signal icon is shown if the AP interface is down. The colour of
|
||||||
|
/// the icon is black if the WLAN interface is up and gray if it's down.
|
||||||
|
fn render_network_mode_icon(state: &NetworkState) -> Markup {
|
||||||
|
// TODO: make this DRYer
|
||||||
|
let (icon_class, icon_src, icon_alt, label_title, label_value) = match state {
|
||||||
|
NetworkState::AccessPoint => (
|
||||||
|
"center icon icon-active",
|
||||||
|
"/icons/router.svg",
|
||||||
|
"WiFi router",
|
||||||
|
"Access Point Online",
|
||||||
|
"ONLINE",
|
||||||
|
),
|
||||||
|
NetworkState::WiFiClient => match network::state(WLAN_IFACE) {
|
||||||
|
Ok(Some(state)) if state == "up" => (
|
||||||
|
"center icon icon-active",
|
||||||
|
"/icons/wifi.svg",
|
||||||
|
"WiFi signal",
|
||||||
|
"WiFi Client Online",
|
||||||
|
"ONLINE",
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
"center icon icon-inactive",
|
||||||
|
"/icons/wifi.svg",
|
||||||
|
"WiFi signal",
|
||||||
|
"WiFi Client Offline",
|
||||||
|
"OFFLINE",
|
||||||
|
),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Template::render("status/network", &context)
|
html! {
|
||||||
|
(PreEscaped("<!-- network mode icon with label -->"))
|
||||||
|
div class="grid-column-1" {
|
||||||
|
img id="netModeIcon" class=(icon_class) src=(icon_src) alt=(icon_alt);
|
||||||
|
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title=(label_title) { (label_value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the network data associated with the deployed access point or
|
||||||
|
/// connected WiFi client depending on active mode.
|
||||||
|
///
|
||||||
|
/// Data includes the network mode (access point or WiFi client), SSID and IP
|
||||||
|
/// address.
|
||||||
|
fn render_network_data(state: &NetworkState, ssid: String, ip: String) -> Markup {
|
||||||
|
let (mode_value, mode_title, ssid_value, ip_title) = match state {
|
||||||
|
NetworkState::AccessPoint => (
|
||||||
|
"Access Point",
|
||||||
|
"Access Point SSID",
|
||||||
|
// TODO: remove hardcoding of this value (query interface instead)
|
||||||
|
"peach",
|
||||||
|
"Access Point IP Address",
|
||||||
|
),
|
||||||
|
NetworkState::WiFiClient => (
|
||||||
|
"WiFi Client",
|
||||||
|
"WiFi SSID",
|
||||||
|
ssid.as_str(),
|
||||||
|
"WiFi Client IP Address",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
|
||||||
|
div class="grid-column-2" {
|
||||||
|
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
|
||||||
|
p id="netMode" class="card-text" title="Network Mode" { (mode_value) }
|
||||||
|
label class="label-small font-gray" for="netSsid" title=(mode_title) { "SSID" }
|
||||||
|
p id="netSsid" class="card-text" title="SSID" { (ssid_value) }
|
||||||
|
label class="label-small font-gray" for="netIp" title=(ip_title) { "IP" }
|
||||||
|
p id="netIp" class="card-text" title="IP" { (ip) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the network status grid comprised of the network config icon,
|
||||||
|
/// network mode icon and network data text.
|
||||||
|
fn render_network_status_grid(state: &NetworkState, ssid: String, ip: String) -> Markup {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- NETWORK STATUS GRID -->"))
|
||||||
|
div class="two-grid" title="PeachCloud network mode and status" {
|
||||||
|
(render_network_config_icon())
|
||||||
|
(PreEscaped("<!-- left column -->"))
|
||||||
|
(render_network_mode_icon(state))
|
||||||
|
(PreEscaped("<!-- right column -->"))
|
||||||
|
(render_network_data(state, ssid, ip))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the signal strength stack comprised of a signal icon, RSSI value
|
||||||
|
/// and label.
|
||||||
|
///
|
||||||
|
/// This stack is displayed when the network mode is set to WiFi
|
||||||
|
/// client (ie. the value reported is the strength of the connection of the
|
||||||
|
/// local WiFi interface to a remote access point).
|
||||||
|
fn render_signal_strength_stack() -> Markup {
|
||||||
|
let wlan_rssi = match network::rssi(WLAN_IFACE) {
|
||||||
|
Ok(Some(rssi)) => rssi,
|
||||||
|
_ => 0.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
div class="stack" {
|
||||||
|
img id="netSignal" class="icon icon-medium" alt="Signal" title="WiFi Signal (%)" src="/icons/low-signal.svg";
|
||||||
|
div class="flex-grid" style="padding-top: 0.5rem;" {
|
||||||
|
label class="label-medium" for="netSignal" style="padding-right: 3px;" title="Signal strength of WiFi connection (%)" { (wlan_rssi) }
|
||||||
|
}
|
||||||
|
label class="label-small font-gray" { "SIGNAL" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the connected devices stack comprised of a devices icon, value
|
||||||
|
/// of connected devices and label.
|
||||||
|
///
|
||||||
|
/// This stack is displayed when the network mode is set to access point
|
||||||
|
/// (ie. the value reported is the number of remote devices connected to the
|
||||||
|
/// local access point).
|
||||||
|
fn render_connected_devices_stack() -> Markup {
|
||||||
|
html! {
|
||||||
|
div class="stack" {
|
||||||
|
img id="devices" class="icon icon-medium" title="Connected devices" src="/icons/devices.svg" alt="Digital devices";
|
||||||
|
div class="flex-grid" style="padding-top: 0.5rem;" {
|
||||||
|
label class="label-medium" for="devices" style="padding-right: 3px;" title="Number of connected devices";
|
||||||
|
}
|
||||||
|
label class="label-small font-gray" { "DEVICES" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the data download stack comprised of a download icon, traffic value
|
||||||
|
/// and label.
|
||||||
|
///
|
||||||
|
/// A zero value is displayed if no interface traffic is available for the
|
||||||
|
/// WLAN interface.
|
||||||
|
fn render_data_download_stack(iface_traffic: &Option<Vnstat>) -> Markup {
|
||||||
|
html! {
|
||||||
|
div class="stack" {
|
||||||
|
img id="dataDownload" class="icon icon-medium" title="Download" src="/icons/down-arrow.svg" alt="Download";
|
||||||
|
div class="flex-grid" style="padding-top: 0.5rem;" {
|
||||||
|
@if let Some(traffic) = iface_traffic {
|
||||||
|
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title={ "Data download total in " (traffic.all_time_rx_unit) } { (traffic.all_time_rx) }
|
||||||
|
label class="label-small font-near-black" { (traffic.all_time_rx_unit) }
|
||||||
|
} @else {
|
||||||
|
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total" { "0" }
|
||||||
|
label class="label-small font-near-black";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label class="label-small font-gray" { "DOWNLOAD" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the data upload stack comprised of an upload icon, traffic value
|
||||||
|
/// and label.
|
||||||
|
///
|
||||||
|
/// A zero value is displayed if no interface traffic is available for the
|
||||||
|
/// WLAN interface.
|
||||||
|
fn render_data_upload_stack(iface_traffic: Option<Vnstat>) -> Markup {
|
||||||
|
html! {
|
||||||
|
div class="stack" {
|
||||||
|
img id="dataUpload" class="icon icon-medium" title="Upload" src="/icons/up-arrow.svg" alt="Upload";
|
||||||
|
div class="flex-grid" style="padding-top: 0.5rem;" {
|
||||||
|
@if let Some(traffic) = iface_traffic {
|
||||||
|
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title={ "Data upload total in " (traffic.all_time_tx_unit) } { (traffic.all_time_tx) }
|
||||||
|
label class="label-small font-near-black" { (traffic.all_time_tx_unit) }
|
||||||
|
} @else {
|
||||||
|
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total" { "0" }
|
||||||
|
label class="label-small font-near-black";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label class="label-small font-gray" { "UPLOAD" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the device / signal and traffic grid.
|
||||||
|
///
|
||||||
|
/// The connected devices stack is displayed if the network mode is set to
|
||||||
|
/// access point and the signal strength stack is displayed if the network
|
||||||
|
/// mode is set to WiFi client.
|
||||||
|
fn render_device_and_traffic_grid(state: NetworkState, iface_traffic: Option<Vnstat>) -> Markup {
|
||||||
|
html! {
|
||||||
|
div class="three-grid card-container" {
|
||||||
|
@match state {
|
||||||
|
NetworkState::AccessPoint => (render_connected_devices_stack()),
|
||||||
|
NetworkState::WiFiClient => (render_signal_strength_stack()),
|
||||||
|
}
|
||||||
|
(render_data_download_stack(&iface_traffic))
|
||||||
|
(render_data_upload_stack(iface_traffic))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Network state data retrieval.
|
||||||
|
///
|
||||||
|
/// This data is injected into the template rendering functions.
|
||||||
|
fn retrieve_network_data() -> (NetworkState, Option<Vnstat>, String, String) {
|
||||||
|
// if the access point interface is "up",
|
||||||
|
// retrieve the traffic stats, ip and ssidfor the ap interface.
|
||||||
|
// otherwise retrieve the stats and ip for the wlan interface.
|
||||||
|
let (state, traffic, ip, ssid) = match network::state(AP_IFACE) {
|
||||||
|
Ok(Some(state)) if state == "up" => {
|
||||||
|
let ap_traffic = Vnstat::get(AP_IFACE).ok();
|
||||||
|
|
||||||
|
let ap_ip = match network::ip(AP_IFACE) {
|
||||||
|
Ok(Some(ip)) => ip,
|
||||||
|
_ => String::from("x.x.x.x"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let ap_ssid = String::from("peach");
|
||||||
|
|
||||||
|
(NetworkState::AccessPoint, ap_traffic, ap_ip, ap_ssid)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let wlan_traffic = Vnstat::get(WLAN_IFACE).ok();
|
||||||
|
|
||||||
|
let wlan_ip = match network::ip(WLAN_IFACE) {
|
||||||
|
Ok(Some(ip)) => ip,
|
||||||
|
_ => String::from("x.x.x.x"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let wlan_ssid = match network::ssid(WLAN_IFACE) {
|
||||||
|
Ok(Some(ssid)) => ssid,
|
||||||
|
_ => String::from("Not connected"),
|
||||||
|
};
|
||||||
|
|
||||||
|
(NetworkState::WiFiClient, wlan_traffic, wlan_ip, wlan_ssid)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(state, traffic, ip, ssid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Network status template builder.
|
||||||
|
pub fn build_template() -> PreEscaped<String> {
|
||||||
|
let (state, traffic, ip, ssid) = retrieve_network_data();
|
||||||
|
|
||||||
|
let network_status_template = html! {
|
||||||
|
(PreEscaped("<!-- NETWORK STATUS CARD -->"))
|
||||||
|
div class="card center" {
|
||||||
|
(PreEscaped("<!-- NETWORK INFO BOX -->"))
|
||||||
|
div class="capsule capsule-container success-border" {
|
||||||
|
(render_network_status_grid(&state, ssid, ip))
|
||||||
|
hr style="color: var(--light-gray);";
|
||||||
|
(render_device_and_traffic_grid(state, traffic))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let body =
|
||||||
|
templates::nav::build_template(network_status_template, "Network Status", Some("/status"));
|
||||||
|
|
||||||
|
let theme = theme::get_theme();
|
||||||
|
|
||||||
|
templates::base::build_template(body, theme)
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,6 @@ fn run_on_startup_element(boot_state: &Option<String>) -> Markup {
|
|||||||
fn database_element(state: &str) -> Markup {
|
fn database_element(state: &str) -> Markup {
|
||||||
// retrieve the sequence number of the latest message in the sbot database
|
// retrieve the sequence number of the latest message in the sbot database
|
||||||
let sequence_num = sbot::latest_sequence_number();
|
let sequence_num = sbot::latest_sequence_number();
|
||||||
|
|
||||||
match (state, sequence_num) {
|
match (state, sequence_num) {
|
||||||
// if the state is "active" and latest_sequence_number() was successful
|
// if the state is "active" and latest_sequence_number() was successful
|
||||||
("active", Ok(number)) => {
|
("active", Ok(number)) => {
|
||||||
@ -62,7 +61,9 @@ fn database_element(state: &str) -> Markup {
|
|||||||
label class="label-small font-gray" { "MESSAGES IN LOCAL DATABASE" }
|
label class="label-small font-gray" { "MESSAGES IN LOCAL DATABASE" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(_, _) => html! { label class="label-small font-gray" { "DATABASE UNAVAILABLE" } },
|
(_, _) => {
|
||||||
|
html! { label class="label-small font-gray" { "DATABASE UNAVAILABLE" } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,25 @@
|
|||||||
use maud::{html, PreEscaped, DOCTYPE};
|
use maud::{html, PreEscaped, DOCTYPE};
|
||||||
|
|
||||||
|
/// JavaScript event listener for the back button on the top navigation bar of
|
||||||
|
/// the UI.
|
||||||
|
///
|
||||||
|
/// When the button is clicked, prevent the default behaviour and invoke
|
||||||
|
/// the history API to load the previous URL (page) in the history list.
|
||||||
|
fn js_back_button_script() -> PreEscaped<String> {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
document.getElementById('backButton').onclick = function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
history.back();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Base template builder.
|
/// Base template builder.
|
||||||
///
|
///
|
||||||
/// Takes an HTML body as input and splices it into the base template.
|
/// Takes an HTML body as input and splices it into the base template.
|
||||||
@ -14,6 +34,7 @@ pub fn build_template(body: PreEscaped<String>, theme: String) -> PreEscaped<Str
|
|||||||
meta name="viewport" content="width=devide-width, initial-scale=1.0";
|
meta name="viewport" content="width=devide-width, initial-scale=1.0";
|
||||||
link rel="stylesheet" href="/css/peachcloud.css";
|
link rel="stylesheet" href="/css/peachcloud.css";
|
||||||
link rel="stylesheet" href="/css/_variables.css";
|
link rel="stylesheet" href="/css/_variables.css";
|
||||||
|
(js_back_button_script())
|
||||||
title { "PeachCloud" }
|
title { "PeachCloud" }
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
|
@ -14,7 +14,7 @@ pub fn build_template(
|
|||||||
let theme = theme::get_theme();
|
let theme = theme::get_theme();
|
||||||
|
|
||||||
// conditionally render the hermies icon and theme-switcher icon with correct link
|
// conditionally render the hermies icon and theme-switcher icon with correct link
|
||||||
let (hermies, switcher) = match theme.as_str() {
|
let (hermies, theme_switcher) = match theme.as_str() {
|
||||||
// if we're using the dark theme, render light icons and "light" query param
|
// if we're using the dark theme, render light icons and "light" query param
|
||||||
"dark" => (
|
"dark" => (
|
||||||
"/icons/hermies_hex_light.svg",
|
"/icons/hermies_hex_light.svg",
|
||||||
@ -38,7 +38,7 @@ pub fn build_template(
|
|||||||
html! {
|
html! {
|
||||||
(PreEscaped("<!-- Top navigation bar -->"))
|
(PreEscaped("<!-- Top navigation bar -->"))
|
||||||
nav class="nav-bar" {
|
nav class="nav-bar" {
|
||||||
a class="nav-item" href=[back] title="Back" {
|
a id="backButton" class="nav-item" href=[back] title="Back" {
|
||||||
img class="icon-medium nav-icon-left icon-active" src="/icons/back.svg" alt="Back";
|
img class="icon-medium nav-icon-left icon-active" src="/icons/back.svg" alt="Back";
|
||||||
}
|
}
|
||||||
h1 class="nav-title" { (title) }
|
h1 class="nav-title" { (title) }
|
||||||
@ -56,8 +56,7 @@ pub fn build_template(
|
|||||||
a class="nav-item" href="/" {
|
a class="nav-item" href="/" {
|
||||||
img class="icon nav-icon-left" src="/icons/peach-icon.png" alt="PeachCloud" title="Home";
|
img class="icon nav-icon-left" src="/icons/peach-icon.png" alt="PeachCloud" title="Home";
|
||||||
}
|
}
|
||||||
// render the pre-defined theme-switcher icon
|
(theme_switcher)
|
||||||
(switcher)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
64
peach-web/src/utils/cookie.rs
Normal file
64
peach-web/src/utils/cookie.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use rouille::{input, Request, Response};
|
||||||
|
|
||||||
|
// The CookieRequest and CookieResponse traits are currently only used
|
||||||
|
// to add, retrieve and reset the `back_url` cookie. That cookie is
|
||||||
|
// used to set the URL of the in-UI back button when visiting a page
|
||||||
|
// which can be arrived at via several paths.
|
||||||
|
//
|
||||||
|
// An example of this is the Scuttlebutt Settings menu (/settings/scuttlebutt),
|
||||||
|
// which can be accessed via the Settings menu (/settings) or the Scuttlebutt
|
||||||
|
// Status page (/status/scuttlebutt). We need to be able to set the path of
|
||||||
|
// the back button to point to the correct page (ie. the one from which we've
|
||||||
|
// come).
|
||||||
|
//
|
||||||
|
// The `back_url` cookie is also used on the Profile page
|
||||||
|
// (/scuttlebutt/profile).
|
||||||
|
|
||||||
|
/// Cookie trait for `Request`.
|
||||||
|
pub trait CookieRequest {
|
||||||
|
/// Retrieve a cookie value from a `Request`.
|
||||||
|
fn retrieve_cookie(&self, cookie_name: &str) -> Option<&str>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CookieRequest for Request {
|
||||||
|
fn retrieve_cookie(&self, cookie_name: &str) -> Option<&str> {
|
||||||
|
// check for cookie using given name
|
||||||
|
let cookie_val = input::cookies(self)
|
||||||
|
.find(|&(n, _)| n == cookie_name)
|
||||||
|
// return the value of the cookie (key is already known)
|
||||||
|
.map(|key_val| key_val.1);
|
||||||
|
|
||||||
|
cookie_val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cookie trait for `Response`.
|
||||||
|
pub trait CookieResponse {
|
||||||
|
/// Add a cookie containing the given data to a `Response`. Data should be
|
||||||
|
/// in the form of `cookie_name=cookie_val`.
|
||||||
|
fn add_cookie(self, cookie_name_val: &str) -> Response;
|
||||||
|
/// Reset a cookie value for a `Response`.
|
||||||
|
fn reset_cookie(self, cookie_name: &str) -> Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CookieResponse for Response {
|
||||||
|
fn add_cookie(self, cookie_name_val: &str) -> Response {
|
||||||
|
// set the cookie header
|
||||||
|
// max-age is currently set to 3600 seconds (1 hour)
|
||||||
|
self.with_additional_header(
|
||||||
|
"Set-Cookie",
|
||||||
|
format!("{}; Max-Age=3600; SameSite=Lax; Path=/", cookie_name_val),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_cookie(self, cookie_name: &str) -> Response {
|
||||||
|
// set a blank cookie to clear the cookie from the previous request
|
||||||
|
self.with_additional_header(
|
||||||
|
"Set-Cookie",
|
||||||
|
format!(
|
||||||
|
"{}=; Max-Age=0; SameSite=Lax; Path=/; Expires=Fri, 21 Aug 1987 12:00:00 UTC",
|
||||||
|
cookie_name
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
pub mod cookie;
|
||||||
pub mod flash;
|
pub mod flash;
|
||||||
pub mod sbot;
|
pub mod sbot;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
|
@ -3,7 +3,6 @@ use std::{
|
|||||||
error::Error,
|
error::Error,
|
||||||
fs,
|
fs,
|
||||||
fs::File,
|
fs::File,
|
||||||
io,
|
|
||||||
io::prelude::*,
|
io::prelude::*,
|
||||||
path::Path,
|
path::Path,
|
||||||
process::{Command, Output},
|
process::{Command, Output},
|
||||||
@ -12,29 +11,33 @@ use std::{
|
|||||||
use async_std::task;
|
use async_std::task;
|
||||||
use dirs;
|
use dirs;
|
||||||
use futures::stream::TryStreamExt;
|
use futures::stream::TryStreamExt;
|
||||||
use golgi::{api::friends::RelationshipQuery, blobs, messages::SsbMessageValue, Sbot};
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
use peach_lib::config_manager;
|
||||||
use peach_lib::sbot::SbotConfig;
|
use peach_lib::sbot::SbotConfig;
|
||||||
|
use peach_lib::sbot::init_sbot;
|
||||||
|
use peach_lib::ssb_messages::SsbMessageKVT;
|
||||||
use rouille::input::post::BufferedFile;
|
use rouille::input::post::BufferedFile;
|
||||||
use temporary::Directory;
|
use temporary::Directory;
|
||||||
|
use peach_lib::serde_json::json;
|
||||||
|
use peach_lib::tilde_client::TildeClient;
|
||||||
use crate::{error::PeachWebError, utils::sbot};
|
use crate::{error::PeachWebError, utils::sbot};
|
||||||
|
|
||||||
// SBOT HELPER FUNCTIONS
|
// SBOT HELPER FUNCTIONS
|
||||||
|
|
||||||
/// Executes a systemctl command for the go-sbot.service process.
|
/// Executes a systemctl command for the solar-sbot.service process.
|
||||||
pub fn systemctl_sbot_cmd(cmd: &str) -> io::Result<Output> {
|
pub fn systemctl_sbot_cmd(cmd: &str) -> Result<Output, PeachWebError> {
|
||||||
Command::new("systemctl")
|
let output = Command::new("sudo")
|
||||||
.arg("--user")
|
.arg("systemctl")
|
||||||
.arg(cmd)
|
.arg(cmd)
|
||||||
.arg("go-sbot.service")
|
.arg(config_manager::get_config_value("TILDE_SBOT_SERVICE")?)
|
||||||
.output()
|
.output()?;
|
||||||
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes a systemctl stop command followed by start command.
|
/// Executes a systemctl stop command followed by start command.
|
||||||
/// Returns a redirect with a flash message stating the output of the restart attempt.
|
/// Returns a redirect with a flash message stating the output of the restart attempt.
|
||||||
pub fn restart_sbot_process() -> (String, String) {
|
pub fn restart_sbot_process() -> (String, String) {
|
||||||
debug!("Restarting go-sbot.service");
|
debug!("Restarting solar-sbot.service");
|
||||||
match systemctl_sbot_cmd("stop") {
|
match systemctl_sbot_cmd("stop") {
|
||||||
// if stop was successful, try to start the process
|
// if stop was successful, try to start the process
|
||||||
Ok(_) => match systemctl_sbot_cmd("start") {
|
Ok(_) => match systemctl_sbot_cmd("start") {
|
||||||
@ -61,20 +64,14 @@ pub fn restart_sbot_process() -> (String, String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Initialise an sbot client with the given configuration parameters.
|
/// Initialise an sbot client with the given configuration parameters.
|
||||||
pub async fn init_sbot_with_config(
|
pub async fn init_sbot_client() -> Result<TildeClient, PeachWebError> {
|
||||||
sbot_config: &Option<SbotConfig>,
|
|
||||||
) -> Result<Sbot, PeachWebError> {
|
|
||||||
debug!("Initialising an sbot client with configuration parameters");
|
debug!("Initialising an sbot client with configuration parameters");
|
||||||
// initialise sbot connection with ip:port and shscap from config file
|
// initialise sbot connection with ip:port and shscap from config file
|
||||||
let sbot_client = match sbot_config {
|
let key_path = format!(
|
||||||
// TODO: panics if we pass `Some(conf.shscap)` as second arg
|
"{}/secret.toml",
|
||||||
Some(conf) => {
|
config_manager::get_config_value("TILDE_SBOT_DATADIR")?
|
||||||
let ip_port = conf.lis.clone();
|
);
|
||||||
Sbot::init(Some(ip_port), None).await?
|
let sbot_client = init_sbot().await?;
|
||||||
}
|
|
||||||
None => Sbot::init(None, None).await?,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(sbot_client)
|
Ok(sbot_client)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,43 +114,53 @@ pub fn validate_public_key(public_key: &str) -> Result<(), String> {
|
|||||||
/// reverses the list and reads the sequence number of the most recently
|
/// reverses the list and reads the sequence number of the most recently
|
||||||
/// authored message. This gives us the size of the database in terms of
|
/// authored message. This gives us the size of the database in terms of
|
||||||
/// the total number of locally-authored messages.
|
/// the total number of locally-authored messages.
|
||||||
pub fn latest_sequence_number() -> Result<u64, Box<dyn Error>> {
|
pub fn latest_sequence_number() -> Result<u64, PeachWebError> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
// retrieve latest solar-sbot configuration parameters
|
||||||
let sbot_config = SbotConfig::read().ok();
|
let sbot_config = SbotConfig::read().ok();
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
|
let mut sbot_client = init_sbot_client().await?;
|
||||||
|
|
||||||
|
Err(PeachWebError::NotYetImplemented)
|
||||||
|
|
||||||
// retrieve the local id
|
// retrieve the local id
|
||||||
let id = sbot_client.whoami().await?;
|
// let id = sbot_client.whoami().await?;
|
||||||
|
|
||||||
let history_stream = sbot_client.create_history_stream(id).await?;
|
// let history_stream = sbot_client.feed(&id).await?;
|
||||||
let mut msgs: Vec<SsbMessageValue> = history_stream.try_collect().await?;
|
|
||||||
|
|
||||||
// there will be zero messages when the sbot is run for the first time
|
// let mut msgs: Vec<SsbMessageKVT> = history_stream.try_collect().await?;
|
||||||
if msgs.is_empty() {
|
//
|
||||||
Ok(0)
|
// // there will be zero messages when the sbot is run for the first time
|
||||||
} else {
|
// if msgs.is_empty() {
|
||||||
// reverse the list of messages so we can easily reference the latest one
|
// Ok(0)
|
||||||
msgs.reverse();
|
// } else {
|
||||||
|
// // reverse the list of messages so we can easily reference the latest one
|
||||||
// return the sequence number of the latest msg
|
// msgs.reverse();
|
||||||
Ok(msgs[0].sequence)
|
//
|
||||||
}
|
// // return the sequence number of the latest msg
|
||||||
|
// Ok(msgs[0].value.sequence)
|
||||||
|
// }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_invite(uses: u16) -> Result<String, Box<dyn Error>> {
|
pub fn create_invite(uses: u16) -> Result<String, PeachWebError> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
// retrieve latest solar-sbot configuration parameters
|
||||||
let sbot_config = SbotConfig::read().ok();
|
let sbot_config = SbotConfig::read().ok();
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
|
let mut sbot_client = init_sbot_client().await?;
|
||||||
|
|
||||||
debug!("Generating Scuttlebutt invite code");
|
debug!("Generating Scuttlebutt invite code");
|
||||||
let invite_code = sbot_client.invite_create(uses).await?;
|
Err(PeachWebError::NotYetImplemented)
|
||||||
|
// let mut invite_code = sbot_client.invite_create(uses).await?;
|
||||||
Ok(invite_code)
|
//
|
||||||
|
// // insert domain into invite if one is configured
|
||||||
|
// let domain = config_manager::get_config_value("EXTERNAL_DOMAIN")?;
|
||||||
|
// if !domain.is_empty() {
|
||||||
|
// invite_code = domain + &invite_code[4..];
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Ok(invite_code)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,11 +200,9 @@ impl Profile {
|
|||||||
|
|
||||||
/// Retrieve the profile info for the given public key.
|
/// Retrieve the profile info for the given public key.
|
||||||
pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error>> {
|
pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error>> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
|
||||||
let sbot_config = SbotConfig::read().ok();
|
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
|
let sbot_client = init_sbot_client().await?;
|
||||||
|
|
||||||
let local_id = sbot_client.whoami().await?;
|
let local_id = sbot_client.whoami().await?;
|
||||||
|
|
||||||
@ -209,32 +214,11 @@ pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error
|
|||||||
// we are not dealing with the local profile
|
// we are not dealing with the local profile
|
||||||
profile.is_local_profile = false;
|
profile.is_local_profile = false;
|
||||||
|
|
||||||
// determine relationship between peer and local id
|
|
||||||
let follow_query = RelationshipQuery {
|
|
||||||
source: local_id.clone(),
|
|
||||||
dest: peer_id.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// query follow state
|
// query follow state
|
||||||
profile.following = match sbot_client.friends_is_following(follow_query).await {
|
profile.following = Some(sbot_client.is_following(&local_id, &peer_id).await?);
|
||||||
Ok(following) if following == "true" => Some(true),
|
|
||||||
Ok(following) if following == "false" => Some(false),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: i don't like that we have to instantiate the same query object
|
// TODO: implement this check in solar_client so that this can be a real value
|
||||||
// twice. see if we can streamline this in golgi
|
profile.blocking = Some(false);
|
||||||
let block_query = RelationshipQuery {
|
|
||||||
source: local_id.clone(),
|
|
||||||
dest: peer_id.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// query block state
|
|
||||||
profile.blocking = match sbot_client.friends_is_blocking(block_query).await {
|
|
||||||
Ok(blocking) if blocking == "true" => Some(true),
|
|
||||||
Ok(blocking) if blocking == "false" => Some(false),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
peer_id
|
peer_id
|
||||||
} else {
|
} else {
|
||||||
@ -245,7 +229,7 @@ pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error
|
|||||||
};
|
};
|
||||||
|
|
||||||
// retrieve the profile info for the given id
|
// retrieve the profile info for the given id
|
||||||
let info = sbot_client.get_profile_info(&id).await?;
|
let info = get_peer_info(&id).await?;
|
||||||
// set each profile field accordingly
|
// set each profile field accordingly
|
||||||
for (key, val) in info {
|
for (key, val) in info {
|
||||||
match key.as_str() {
|
match key.as_str() {
|
||||||
@ -255,26 +239,27 @@ pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error
|
|||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//
|
||||||
// assign the ssb public key
|
// assign the ssb public key
|
||||||
// (could be for the local profile or a peer)
|
// (could be for the local profile or a peer)
|
||||||
profile.id = Some(id);
|
profile.id = Some(id);
|
||||||
|
|
||||||
// determine the path to the blob defined by the value of `profile.image`
|
// TODO: blobs support
|
||||||
if let Some(ref blob_id) = profile.image {
|
// // determine the path to the blob defined by the value of `profile.image`
|
||||||
profile.blob_path = match blobs::get_blob_path(blob_id) {
|
// if let Some(ref blob_id) = profile.image {
|
||||||
Ok(path) => {
|
// profile.blob_path = match blobs::get_blob_path(blob_id) {
|
||||||
// if we get the path, check if the blob is in the blobstore.
|
// Ok(path) => {
|
||||||
// this allows us to default to a placeholder image in the template
|
// // if we get the path, check if the blob is in the blobstore.
|
||||||
if let Ok(exists) = blob_is_stored_locally(&path).await {
|
// // this allows us to default to a placeholder image in the template
|
||||||
profile.blob_exists = exists
|
// if let Ok(exists) = blob_is_stored_locally(&path).await {
|
||||||
};
|
// profile.blob_exists = exists
|
||||||
|
// };
|
||||||
Some(path)
|
//
|
||||||
}
|
// Some(path)
|
||||||
Err(_) => None,
|
// }
|
||||||
}
|
// Err(_) => None,
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
Ok(profile)
|
Ok(profile)
|
||||||
})
|
})
|
||||||
@ -289,123 +274,119 @@ pub fn update_profile_info(
|
|||||||
new_name: Option<String>,
|
new_name: Option<String>,
|
||||||
new_description: Option<String>,
|
new_description: Option<String>,
|
||||||
image: Option<BufferedFile>,
|
image: Option<BufferedFile>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, PeachWebError> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
// retrieve latest solar-sbot configuration parameters
|
||||||
let sbot_config = SbotConfig::read().ok();
|
let sbot_config = SbotConfig::read().ok();
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config)
|
let mut sbot_client = init_sbot_client()
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
// track whether the name, description or image have been updated
|
Err(PeachWebError::NotYetImplemented)
|
||||||
let mut name_updated: bool = false;
|
// // track whether the name, description or image have been updated
|
||||||
let mut description_updated: bool = false;
|
// let mut name_updated: bool = false;
|
||||||
let mut image_updated: bool = false;
|
// let mut description_updated: bool = false;
|
||||||
|
// let mut image_updated: bool = false;
|
||||||
// check if a new_name value has been submitted in the form
|
//
|
||||||
if let Some(name) = new_name {
|
// // check if a new_name value has been submitted in the form
|
||||||
// only update the name if it has changed
|
// if let Some(name) = new_name {
|
||||||
if name != current_name {
|
// // only update the name if it has changed
|
||||||
debug!("Publishing a new Scuttlebutt profile name");
|
// if name != current_name {
|
||||||
if let Err(e) = sbot_client.publish_name(&name).await {
|
// debug!("Publishing a new Scuttlebutt profile name");
|
||||||
return Err(format!("Failed to update name: {}", e));
|
// if let Err(e) = sbot_client.publish_name(&name).await {
|
||||||
} else {
|
// return Err(format!("Failed to update name: {}", e));
|
||||||
name_updated = true
|
// } else {
|
||||||
}
|
// name_updated = true
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
if let Some(description) = new_description {
|
//
|
||||||
// only update the description if it has changed
|
// if let Some(description) = new_description {
|
||||||
if description != current_description {
|
// // only update the description if it has changed
|
||||||
debug!("Publishing a new Scuttlebutt profile description");
|
// if description != current_description {
|
||||||
if let Err(e) = sbot_client.publish_description(&description).await {
|
// debug!("Publishing a new Scuttlebutt profile description");
|
||||||
return Err(format!("Failed to update description: {}", e));
|
// if let Err(e) = sbot_client.publish_description(&description).await {
|
||||||
} else {
|
// return Err(format!("Failed to update description: {}", e));
|
||||||
description_updated = true
|
// } else {
|
||||||
}
|
// description_updated = true
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
// only update the image if a file was uploaded
|
//
|
||||||
if let Some(img) = image {
|
// // only update the image if a file was uploaded
|
||||||
// only write the blob if it has a filename and data > 0 bytes
|
// if let Some(img) = image {
|
||||||
if img.filename.is_some() && !img.data.is_empty() {
|
// // only write the blob if it has a filename and data > 0 bytes
|
||||||
match write_blob_to_store(img).await {
|
// if img.filename.is_some() && !img.data.is_empty() {
|
||||||
Ok(blob_id) => {
|
// match write_blob_to_store(img).await {
|
||||||
// if the file was successfully added to the blobstore,
|
// Ok(blob_id) => {
|
||||||
// publish an about image message with the blob id
|
// // if the file was successfully added to the blobstore,
|
||||||
if let Err(e) = sbot_client.publish_image(&blob_id).await {
|
// // publish an about image message with the blob id
|
||||||
return Err(format!("Failed to update image: {}", e));
|
// if let Err(e) = sbot_client.publish_image(&blob_id).await {
|
||||||
} else {
|
// return Err(format!("Failed to update image: {}", e));
|
||||||
image_updated = true
|
// } else {
|
||||||
}
|
// image_updated = true
|
||||||
}
|
// }
|
||||||
Err(e) => return Err(format!("Failed to add image to blobstore: {}", e)),
|
// }
|
||||||
}
|
// Err(e) => return Err(format!("Failed to add image to blobstore: {}", e)),
|
||||||
} else {
|
// }
|
||||||
image_updated = false
|
// } else {
|
||||||
}
|
// image_updated = false
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
if name_updated || description_updated || image_updated {
|
//
|
||||||
Ok("Profile updated".to_string())
|
// if name_updated || description_updated || image_updated {
|
||||||
} else {
|
// Ok("Profile updated".to_string())
|
||||||
// no updates were made but no errors were encountered either
|
// } else {
|
||||||
Ok("Profile info unchanged".to_string())
|
// // no updates were made but no errors were encountered either
|
||||||
}
|
// Ok("Profile info unchanged".to_string())
|
||||||
|
// }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Follow a peer.
|
/// Follow a peer.
|
||||||
pub fn follow_peer(public_key: &str) -> Result<String, String> {
|
pub fn follow_peer(public_key: &str) -> Result<String, PeachWebError> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
// retrieve latest solar-sbot configuration parameters
|
||||||
let sbot_config = SbotConfig::read().ok();
|
let sbot_config = SbotConfig::read().ok();
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config)
|
let mut sbot_client = init_sbot_client()
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
debug!("Following a Scuttlebutt peer");
|
debug!("Following a Scuttlebutt peer");
|
||||||
match sbot_client.follow(public_key).await {
|
Err(PeachWebError::NotYetImplemented)
|
||||||
Ok(_) => Ok("Followed peer".to_string()),
|
|
||||||
Err(e) => Err(format!("Failed to follow peer: {}", e)),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unfollow a peer.
|
/// Unfollow a peer.
|
||||||
pub fn unfollow_peer(public_key: &str) -> Result<String, String> {
|
pub fn unfollow_peer(public_key: &str) -> Result<String, PeachWebError> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
// retrieve latest solar-sbot configuration parameters
|
||||||
let sbot_config = SbotConfig::read().ok();
|
let sbot_config = SbotConfig::read().ok();
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config)
|
let mut sbot_client = init_sbot_client()
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
debug!("Unfollowing a Scuttlebutt peer");
|
Err(PeachWebError::NotYetImplemented)
|
||||||
match sbot_client.unfollow(public_key).await {
|
// debug!("Unfollowing a Scuttlebutt peer");
|
||||||
Ok(_) => Ok("Unfollowed peer".to_string()),
|
// match sbot_client.unfollow(public_key).await {
|
||||||
Err(e) => Err(format!("Failed to unfollow peer: {}", e)),
|
// Ok(_) => Ok("Unfollowed peer".to_string()),
|
||||||
}
|
// Err(e) => Err(format!("Failed to unfollow peer: {}", e)),
|
||||||
|
// }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block a peer.
|
/// Block a peer.
|
||||||
pub fn block_peer(public_key: &str) -> Result<String, String> {
|
pub fn block_peer(public_key: &str) -> Result<String, String> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
// retrieve latest solar-sbot configuration parameters
|
||||||
let sbot_config = SbotConfig::read().ok();
|
let sbot_config = SbotConfig::read().ok();
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config)
|
let mut sbot_client = init_sbot_client()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
debug!("Blocking a Scuttlebutt peer");
|
debug!("Blocking a Scuttlebutt peer");
|
||||||
match sbot_client.block(public_key).await {
|
match sbot_client.create_block(public_key).await {
|
||||||
Ok(_) => Ok("Blocked peer".to_string()),
|
Ok(_) => Ok("Blocked peer".to_string()),
|
||||||
Err(e) => Err(format!("Failed to block peer: {}", e)),
|
Err(e) => Err(format!("Failed to block peer: {}", e)),
|
||||||
}
|
}
|
||||||
@ -413,176 +394,159 @@ pub fn block_peer(public_key: &str) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Unblock a peer.
|
/// Unblock a peer.
|
||||||
pub fn unblock_peer(public_key: &str) -> Result<String, String> {
|
pub fn unblock_peer(public_key: &str) -> Result<String, PeachWebError> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
// retrieve latest solar-sbot configuration parameters
|
||||||
let sbot_config = SbotConfig::read().ok();
|
let sbot_config = SbotConfig::read().ok();
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config)
|
let mut sbot_client = init_sbot_client()
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
debug!("Unblocking a Scuttlebutt peer");
|
debug!("Unblocking a Scuttlebutt peer");
|
||||||
match sbot_client.unblock(public_key).await {
|
Err(PeachWebError::NotYetImplemented)
|
||||||
Ok(_) => Ok("Unblocked peer".to_string()),
|
// match sbot_client.unblock(public_key).await {
|
||||||
Err(e) => Err(format!("Failed to unblock peer: {}", e)),
|
// Ok(_) => Ok("Unblocked peer".to_string()),
|
||||||
}
|
// Err(e) => Err(format!("Failed to unblock peer: {}", e)),
|
||||||
|
// }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve a list of peers blocked by the local public key.
|
/// Retrieve a list of peers blocked by the local public key.
|
||||||
pub fn get_blocks_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
pub fn get_blocks_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
// populate this vec to return
|
||||||
let sbot_config = SbotConfig::read().ok();
|
let mut to_return: Vec<HashMap<String, String>> = Vec::new();
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
|
let mut sbot_client = init_sbot_client().await?;
|
||||||
|
|
||||||
let blocks = sbot_client.get_blocks().await?;
|
let self_id = sbot_client.whoami().await?;
|
||||||
|
|
||||||
// we'll use this to store the profile info for each peer whom we block
|
let blocks = sbot_client.get_blocks(&self_id).await?;
|
||||||
let mut peer_list = Vec::new();
|
|
||||||
|
|
||||||
if !blocks.is_empty() {
|
if !blocks.is_empty() {
|
||||||
for peer in blocks.iter() {
|
for peer in blocks.iter() {
|
||||||
// trim whitespace (including newline characters) and
|
// trim whitespace (including newline characters) and
|
||||||
// remove the inverted-commas around the id
|
// remove the inverted-commas around the id
|
||||||
|
// TODO: is this necessary?
|
||||||
let key = peer.trim().replace('"', "");
|
let key = peer.trim().replace('"', "");
|
||||||
// retrieve the profile info for the given peer
|
let peer_info = get_peer_info(&key).await?;
|
||||||
let mut peer_info = sbot_client.get_profile_info(&key).await?;
|
|
||||||
// insert the public key of the peer into the info hashmap
|
|
||||||
peer_info.insert("id".to_string(), key.to_string());
|
|
||||||
// we do not even attempt to find the blob for a blocked peer,
|
|
||||||
// since it may be vulgar to cause distress to the local peer.
|
|
||||||
peer_info.insert("blob_exists".to_string(), "false".to_string());
|
|
||||||
// push profile info to peer_list vec
|
// push profile info to peer_list vec
|
||||||
peer_list.push(peer_info)
|
to_return.push(peer_info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the list of blocked peers
|
// return the list of peers
|
||||||
Ok(peer_list)
|
Ok(to_return)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get_peer_info(key: &str) -> Result<HashMap<String, String>, Box<dyn Error>> {
|
||||||
|
let mut sbot_client = init_sbot_client().await?;
|
||||||
|
// key,value dict of info about this peer
|
||||||
|
let mut peer_info = HashMap::new();
|
||||||
|
// retrieve the profile info for the given peer
|
||||||
|
// TODO: get all profile info not just latest_name
|
||||||
|
// TODO: latest_name throws an error
|
||||||
|
// TODO: just show as "error" isntead of aborting, if some field doesn't fetch
|
||||||
|
// let latest_name = sbot_client.latest_name(&key).await?;
|
||||||
|
let latest_name = "latest name".to_string();
|
||||||
|
if let Some(latest_description) = sbot_client.latest_description(&key).await.ok() {
|
||||||
|
peer_info.insert("description".to_string(), latest_description);
|
||||||
|
}
|
||||||
|
// insert the public key of the peer into the info hashmap
|
||||||
|
peer_info.insert("id".to_string(), key.to_string());
|
||||||
|
peer_info.insert("name".to_string(), latest_name);
|
||||||
|
// retrieve the profile image blob id for the given peer
|
||||||
|
// TODO: blob support
|
||||||
|
// if let Some(blob_id) = peer_info.get("image") {
|
||||||
|
// // look-up the path for the image blob
|
||||||
|
// if let Ok(blob_path) = blobs::get_blob_path(blob_id) {
|
||||||
|
// // insert the image blob path of the peer into the info hashmap
|
||||||
|
// peer_info.insert("blob_path".to_string(), blob_path.to_string());
|
||||||
|
// // check if the blob is in the blobstore
|
||||||
|
// // set a flag in the info hashmap
|
||||||
|
// match blob_is_stored_locally(&blob_path).await {
|
||||||
|
// Ok(exists) if exists => {
|
||||||
|
// peer_info.insert("blob_exists".to_string(), "true".to_string())
|
||||||
|
// }
|
||||||
|
// _ => peer_info.insert("blob_exists".to_string(), "false".to_string()),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
Ok(peer_info)
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieve a list of peers followed by the local public key.
|
/// Retrieve a list of peers followed by the local public key.
|
||||||
pub fn get_follows_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
pub fn get_follows_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
|
||||||
let sbot_config = SbotConfig::read().ok();
|
// populate this vec to return
|
||||||
|
let mut to_return: Vec<HashMap<String, String>> = Vec::new();
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
|
let mut sbot_client = init_sbot_client().await?;
|
||||||
|
|
||||||
let follows = sbot_client.get_follows().await?;
|
let self_id = sbot_client.whoami().await?;
|
||||||
|
|
||||||
// we'll use this to store the profile info for each peer who follows us
|
let follows = sbot_client.get_follows(&self_id).await?;
|
||||||
let mut peer_list = Vec::new();
|
|
||||||
|
|
||||||
if !follows.is_empty() {
|
if !follows.is_empty() {
|
||||||
for peer in follows.iter() {
|
for peer in follows.iter() {
|
||||||
// trim whitespace (including newline characters) and
|
// trim whitespace (including newline characters) and
|
||||||
// remove the inverted-commas around the id
|
// remove the inverted-commas around the id
|
||||||
|
// TODO: is this necessary?
|
||||||
let key = peer.trim().replace('"', "");
|
let key = peer.trim().replace('"', "");
|
||||||
// retrieve the profile info for the given peer
|
let peer_info = get_peer_info(&key).await?;
|
||||||
let mut peer_info = sbot_client.get_profile_info(&key).await?;
|
|
||||||
// insert the public key of the peer into the info hashmap
|
|
||||||
peer_info.insert("id".to_string(), key.to_string());
|
|
||||||
// retrieve the profile image blob id for the given peer
|
|
||||||
if let Some(blob_id) = peer_info.get("image") {
|
|
||||||
// look-up the path for the image blob
|
|
||||||
if let Ok(blob_path) = blobs::get_blob_path(blob_id) {
|
|
||||||
// insert the image blob path of the peer into the info hashmap
|
|
||||||
peer_info.insert("blob_path".to_string(), blob_path.to_string());
|
|
||||||
// check if the blob is in the blobstore
|
|
||||||
// set a flag in the info hashmap
|
|
||||||
match blob_is_stored_locally(&blob_path).await {
|
|
||||||
Ok(exists) if exists => {
|
|
||||||
peer_info.insert("blob_exists".to_string(), "true".to_string())
|
|
||||||
}
|
|
||||||
_ => peer_info.insert("blob_exists".to_string(), "false".to_string()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// push profile info to peer_list vec
|
// push profile info to peer_list vec
|
||||||
peer_list.push(peer_info)
|
to_return.push(peer_info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the list of peers
|
// return the list of peers
|
||||||
Ok(peer_list)
|
Ok(to_return)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve a list of peers friended by the local public key.
|
/// Retrieve a list of peers friended by the local public key.
|
||||||
pub fn get_friends_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
pub fn get_friends_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
|
||||||
let sbot_config = SbotConfig::read().ok();
|
// populate this vec to return
|
||||||
|
let mut to_return: Vec<HashMap<String, String>> = Vec::new();
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
|
let mut sbot_client = init_sbot_client().await?;
|
||||||
|
|
||||||
let local_id = sbot_client.whoami().await?;
|
let self_id = sbot_client.whoami().await?;
|
||||||
|
|
||||||
let follows = sbot_client.get_follows().await?;
|
let friends = sbot_client.get_friends(&self_id).await?;
|
||||||
|
|
||||||
// we'll use this to store the profile info for each friend
|
if !friends.is_empty() {
|
||||||
let mut peer_list = Vec::new();
|
for peer in friends.iter() {
|
||||||
|
|
||||||
if !follows.is_empty() {
|
|
||||||
for peer in follows.iter() {
|
|
||||||
// trim whitespace (including newline characters) and
|
// trim whitespace (including newline characters) and
|
||||||
// remove the inverted-commas around the id
|
// remove the inverted-commas around the id
|
||||||
let peer_id = peer.trim().replace('"', "");
|
// TODO: is this necessary?
|
||||||
// retrieve the profile info for the given peer
|
let key = peer.trim().replace('"', "");
|
||||||
let mut peer_info = sbot_client.get_profile_info(&peer_id).await?;
|
let peer_info = get_peer_info(&key).await?;
|
||||||
// insert the public key of the peer into the info hashmap
|
|
||||||
peer_info.insert("id".to_string(), peer_id.to_string());
|
|
||||||
// retrieve the profile image blob id for the given peer
|
|
||||||
if let Some(blob_id) = peer_info.get("image") {
|
|
||||||
// look-up the path for the image blob
|
|
||||||
if let Ok(blob_path) = blobs::get_blob_path(blob_id) {
|
|
||||||
// insert the image blob path of the peer into the info hashmap
|
|
||||||
peer_info.insert("blob_path".to_string(), blob_path.to_string());
|
|
||||||
// check if the blob is in the blobstore
|
|
||||||
// set a flag in the info hashmap
|
|
||||||
match sbot::blob_is_stored_locally(&blob_path).await {
|
|
||||||
Ok(exists) if exists => {
|
|
||||||
peer_info.insert("blob_exists".to_string(), "true".to_string())
|
|
||||||
}
|
|
||||||
_ => peer_info.insert("blob_exists".to_string(), "false".to_string()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the peer follows us (making us friends)
|
// push profile info to peer_list vec
|
||||||
let follow_query = RelationshipQuery {
|
to_return.push(peer_info)
|
||||||
source: peer_id.to_string(),
|
|
||||||
dest: local_id.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// query follow state
|
|
||||||
match sbot_client.friends_is_following(follow_query).await {
|
|
||||||
Ok(following) if following == "true" => {
|
|
||||||
// only push profile info to peer_list vec if they follow us
|
|
||||||
peer_list.push(peer_info)
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the list of peers
|
// return the list of peers
|
||||||
Ok(peer_list)
|
Ok(to_return)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve the local public key (id).
|
/// Retrieve the local public key (id).
|
||||||
pub fn get_local_id() -> Result<String, Box<dyn Error>> {
|
pub fn get_local_id() -> Result<String, Box<dyn Error>> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
// retrieve latest solar-sbot configuration parameters
|
||||||
let sbot_config = SbotConfig::read().ok();
|
let sbot_config = SbotConfig::read().ok();
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
|
let mut sbot_client = init_sbot_client().await?;
|
||||||
|
|
||||||
let local_id = sbot_client.whoami().await?;
|
let local_id = sbot_client.whoami().await?;
|
||||||
|
|
||||||
@ -592,16 +556,20 @@ pub fn get_local_id() -> Result<String, Box<dyn Error>> {
|
|||||||
|
|
||||||
/// Publish a public post.
|
/// Publish a public post.
|
||||||
pub fn publish_public_post(text: String) -> Result<String, String> {
|
pub fn publish_public_post(text: String) -> Result<String, String> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
// retrieve latest solar-sbot configuration parameters
|
||||||
let sbot_config = SbotConfig::read().ok();
|
let sbot_config = SbotConfig::read().ok();
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config)
|
let mut sbot_client = init_sbot_client()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
debug!("Publishing a new Scuttlebutt public post");
|
debug!("Publishing a new Scuttlebutt public post");
|
||||||
match sbot_client.publish_post(&text).await {
|
let post = json!({
|
||||||
|
"type": "post",
|
||||||
|
"text": &text,
|
||||||
|
});
|
||||||
|
match sbot_client.publish(post).await {
|
||||||
Ok(_) => Ok("Published post".to_string()),
|
Ok(_) => Ok("Published post".to_string()),
|
||||||
Err(e) => Err(format!("Failed to publish post: {}", e)),
|
Err(e) => Err(format!("Failed to publish post: {}", e)),
|
||||||
}
|
}
|
||||||
@ -609,23 +577,23 @@ pub fn publish_public_post(text: String) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Publish a private message.
|
/// Publish a private message.
|
||||||
pub fn publish_private_msg(text: String, recipients: Vec<String>) -> Result<String, String> {
|
pub fn publish_private_msg(text: String, recipients: Vec<String>) -> Result<String, PeachWebError> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
// retrieve latest solar-sbot configuration parameters
|
||||||
let sbot_config = SbotConfig::read().ok();
|
let sbot_config = SbotConfig::read().ok();
|
||||||
|
|
||||||
task::block_on(async {
|
task::block_on(async {
|
||||||
let mut sbot_client = init_sbot_with_config(&sbot_config)
|
let mut sbot_client = init_sbot_client()
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
debug!("Publishing a new Scuttlebutt private message");
|
Err(PeachWebError::NotYetImplemented)
|
||||||
match sbot_client
|
// debug!("Publishing a new Scuttlebutt private message");
|
||||||
.publish_private(text.to_string(), recipients)
|
// match sbot_client
|
||||||
.await
|
// .publish_private(text.to_string(), recipients)
|
||||||
{
|
// .await
|
||||||
Ok(_) => Ok("Published private message".to_string()),
|
// {
|
||||||
Err(e) => Err(format!("Failed to publish private message: {}", e)),
|
// Ok(_) => Ok("Published private message".to_string()),
|
||||||
}
|
// Err(e) => Err(format!("Failed to publish private message: {}", e)),
|
||||||
|
// }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -679,20 +647,23 @@ pub async fn write_blob_to_store(image: BufferedFile) -> Result<String, PeachWeb
|
|||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
file.read_to_end(&mut buffer)?;
|
file.read_to_end(&mut buffer)?;
|
||||||
|
|
||||||
// hash the bytes representing the file
|
Err(PeachWebError::NotYetImplemented)
|
||||||
let (hex_hash, blob_id) = blobs::hash_blob(&buffer)?;
|
// TODO: not yet implemented
|
||||||
|
|
||||||
// define the blobstore path and blob filename
|
// // hash the bytes representing the file
|
||||||
let (blob_dir, blob_filename) = hex_hash.split_at(2);
|
// let (hex_hash, blob_id) = blobs::hash_blob(&buffer)?;
|
||||||
let go_ssb_path = get_go_ssb_path()?;
|
//
|
||||||
let blobstore_sub_dir = format!("{}/blobs/sha256/{}", go_ssb_path, blob_dir);
|
// // define the blobstore path and blob filename
|
||||||
|
// let (blob_dir, blob_filename) = hex_hash.split_at(2);
|
||||||
// create the blobstore sub-directory
|
// let go_ssb_path = get_go_ssb_path()?;
|
||||||
fs::create_dir_all(&blobstore_sub_dir)?;
|
// let blobstore_sub_dir = format!("{}/blobs/sha256/{}", go_ssb_path, blob_dir);
|
||||||
|
//
|
||||||
// copy the file to the blobstore
|
// // create the blobstore sub-directory
|
||||||
let blob_path = format!("{}/{}", blobstore_sub_dir, blob_filename);
|
// fs::create_dir_all(&blobstore_sub_dir)?;
|
||||||
fs::copy(temp_path, blob_path)?;
|
//
|
||||||
|
// // copy the file to the blobstore
|
||||||
Ok(blob_id)
|
// let blob_path = format!("{}/{}", blobstore_sub_dir, blob_filename);
|
||||||
|
// fs::copy(temp_path, blob_path)?;
|
||||||
|
//
|
||||||
|
// Ok(blob_id)
|
||||||
}
|
}
|
||||||
|
2
tilde-client/.gitignore
vendored
Normal file
2
tilde-client/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
12
tilde-client/Cargo.toml
Normal file
12
tilde-client/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "tilde-client"
|
||||||
|
version = "0.0.1"
|
||||||
|
authors = ["Max Fowler <max@mfowler.info>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-std = "1.10"
|
||||||
|
anyhow = "1.0.86"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde_yaml = "0.8"
|
37
tilde-client/README.md
Normal file
37
tilde-client/README.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# peach-lib
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
JSON-RPC client library for the PeachCloud ecosystem.
|
||||||
|
|
||||||
|
`peach-lib` offers the ability to programmatically interact with the `peach-network`, `peach-oled` and `peach-stats` microservices.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `peach-lib` crate bundles JSON-RPC client code for making requests to the three PeachCloud microservices which expose JSON-RPC servers (`peach-network`, `peach-oled` and `peach-menu`). The full list of available RPC APIs can be found in the READMEs of the respective microservices ([peach-network](https://github.com/peachcloud/peach-network), [peach-oled](https://github.com/peachcloud/peach-oled), [peach-menu](https://github.com/peachcloud/peach-menu)), or in the [developer documentation for PeachCloud](http://docs.peachcloud.org/software/microservices/index.html).
|
||||||
|
|
||||||
|
The library also includes a custom error type, `PeachError`, which bundles the underlying error types into three variants: `JsonRpcHttp`, `JsonRpcCore` and `Serde`. When used as the returned error type in a `Result` function response, this allows convenient use of the `?` operator (as illustrated in the example usage code below).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Define the dependency in your `Cargo.toml` file:
|
||||||
|
|
||||||
|
`peach-lib = { git = "https://github.com/peachcloud/peach-lib", branch = "main" }`
|
||||||
|
|
||||||
|
Import the required client from the library:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use peach_lib::network_client;
|
||||||
|
```
|
||||||
|
|
||||||
|
Call one of the exposed methods:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
network_client::ip("wlan0")?;
|
||||||
|
```
|
||||||
|
|
||||||
|
Further example usage can be found in the [`peach-menu`](https://github.com/peachcloud/peach-menu) code (see `src/states.rs`).
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
AGPL-3.0
|
18
tilde-client/src/error.rs
Normal file
18
tilde-client/src/error.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// all tilde client errors
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TildeError {
|
||||||
|
pub(crate) message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TildeError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for TildeError {}
|
73
tilde-client/src/lib.rs
Normal file
73
tilde-client/src/lib.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// methods for interacting with tilde sbot
|
||||||
|
|
||||||
|
use crate::error::TildeError;
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::process::{Command, exit};
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
|
||||||
|
pub struct TildeClient {
|
||||||
|
name: String,
|
||||||
|
port: String
|
||||||
|
}
|
||||||
|
pub fn init_sbot() {
|
||||||
|
println!("++ init sbot!");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sbot_client() -> TildeClient {
|
||||||
|
TildeClient {
|
||||||
|
name: "name".to_string(),
|
||||||
|
port: "8009".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl TildeClient {
|
||||||
|
|
||||||
|
pub fn run_tilde_command(&self, command: &str) -> Result<String, TildeError> {
|
||||||
|
let output = Command::new("out/release/tildefriends.standalone")
|
||||||
|
.arg(command)
|
||||||
|
.output().map_err(|e| TildeError {
|
||||||
|
message: format!("Command execution failed: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(TildeError { message: format!("Command failed with status: {}", output.status) })
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = String::from_utf8_lossy(&output.stdout).to_string();
|
||||||
|
println!("Command output: {}", result);
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
pub async fn latest_description(&self, key: &str) -> Result<String, TildeError> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn whoami(&self) -> Result<String, TildeError> {
|
||||||
|
self.run_tilde_command("get_identity")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_following(&self, from_id: &str, to_id: &str) -> Result<bool, TildeError> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_block(&self, key: &str) -> Result<bool, TildeError> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
pub async fn get_blocks(&self, key: &str) -> Result<Vec<String>, TildeError> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_follows(&self, key: &str) -> Result<Vec<String>, TildeError> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_friends(&self, key: &str) -> Result<Vec<String>, TildeError> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish(&self, post: Value) -> Result<Vec<String>, TildeError> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user