Compare commits
50 Commits
refactor_n
...
fix-regres
Author | SHA1 | Date | |
---|---|---|---|
a5415aad99 | |||
037e5c34b6 | |||
699f2b13c9 | |||
c3fbc5cd73 | |||
4a27892ab6 | |||
4adf5547c9 | |||
bdfbd7057f | |||
171d051710 | |||
1ea0ea2ed1 | |||
57ed0ab66a | |||
49ad74595c | |||
17d52c771f | |||
6792e4702d | |||
446927f587 | |||
567b0bbc2a | |||
3ab3e65eb7 | |||
a0e80fcda7 | |||
731bc1958b | |||
58f2ddde05 | |||
4b0b2626a4 | |||
a05e67c22f | |||
c75608fb1a | |||
068d3430d7 | |||
62793f401e | |||
b8f394b901 | |||
9324b3ec0b | |||
f43fbf19f5 | |||
29cc40be48 | |||
570f6a679b | |||
399af51ccc | |||
94bac00664 | |||
c41dae8d04 | |||
e34df3b656 | |||
3399a3c80f | |||
1c26cb70fa | |||
c79bd4b19f | |||
7743511923 | |||
10833078fa | |||
244a2132fa | |||
f737236abc | |||
b5ce677a5b | |||
4d6dbd511e | |||
7fe4715014 | |||
dd33fdd47d | |||
1986d31461 | |||
a824be53b9 | |||
287082381e | |||
9f40378fce | |||
4f5eb3aa04 | |||
f4113f0632 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,5 @@
|
||||
.idea
|
||||
target
|
||||
*peachdeploy.sh
|
||||
*vpsdeploy.sh
|
||||
*bindeploy.sh
|
||||
|
708
Cargo.lock
generated
708
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,5 @@ members = [
|
||||
"peach-monitor",
|
||||
"peach-stats",
|
||||
"peach-jsonrpc-server",
|
||||
"peach-probe",
|
||||
"peach-dyndns-updater"
|
||||
]
|
||||
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "peach-config"
|
||||
version = "0.1.10"
|
||||
version = "0.1.15"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
|
||||
edition = "2018"
|
||||
description = "Command line tool for installing, updating and configuring PeachCloud"
|
||||
@ -35,3 +35,5 @@ structopt = "0.3.13"
|
||||
clap = "2.33.3"
|
||||
log = "0.4"
|
||||
lazy_static = "1.4.0"
|
||||
peach-lib = { path = "../peach-lib" }
|
||||
rpassword = "5.0"
|
||||
|
@ -8,7 +8,7 @@ dtparam=i2c_arm=on
|
||||
# Apply device tree overlay to enable pull-up resistors for buttons
|
||||
device_tree_overlay=overlays/mygpio.dtbo
|
||||
|
||||
kernel=vmlinuz-4.19.0-17-arm64
|
||||
kernel=vmlinuz-4.19.0-18-arm64
|
||||
# For details on the initramfs directive, see
|
||||
# https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=10532
|
||||
initramfs initrd.img-4.19.0-17-arm64
|
||||
initramfs initrd.img-4.19.0-18-arm64
|
||||
|
35
peach-config/src/change_password.rs
Normal file
35
peach-config/src/change_password.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use crate::error::PeachConfigError;
|
||||
use crate::ChangePasswordOpts;
|
||||
use peach_lib::password_utils::set_new_password;
|
||||
|
||||
/// Utility function to set the admin password for peach-web from the command-line.
|
||||
pub fn set_peach_web_password(opts: ChangePasswordOpts) -> Result<(), PeachConfigError> {
|
||||
match opts.password {
|
||||
// read password from CLI arg
|
||||
Some(password) => {
|
||||
set_new_password(&password)
|
||||
.map_err(|err| PeachConfigError::ChangePasswordError { source: err })?;
|
||||
println!(
|
||||
"Your new password has been set for peach-web. You can login through the \
|
||||
web interface with username admin."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
// read password from tty
|
||||
None => {
|
||||
let pass1 = rpassword::read_password_from_tty(Some("New password: "))?;
|
||||
let pass2 = rpassword::read_password_from_tty(Some("Confirm password: "))?;
|
||||
if pass1 != pass2 {
|
||||
Err(PeachConfigError::InvalidPassword)
|
||||
} else {
|
||||
set_new_password(&pass1)
|
||||
.map_err(|err| PeachConfigError::ChangePasswordError { source: err })?;
|
||||
println!(
|
||||
"Your new password has been set for peach-web. You can login through the \
|
||||
web interface with username admin."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
#![allow(clippy::nonstandard_macro_braces)]
|
||||
use peach_lib::error::PeachError;
|
||||
pub use snafu::ResultExt;
|
||||
use snafu::Snafu;
|
||||
|
||||
@ -30,6 +31,10 @@ pub enum PeachConfigError {
|
||||
},
|
||||
#[snafu(display("Error serializing json: {}", source))]
|
||||
SerdeError { source: serde_json::Error },
|
||||
#[snafu(display("Error changing password: {}", source))]
|
||||
ChangePasswordError { source: PeachError },
|
||||
#[snafu(display("Entered passwords did not match. Please try again."))]
|
||||
InvalidPassword,
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for PeachConfigError {
|
||||
|
@ -1,6 +1,8 @@
|
||||
mod change_password;
|
||||
mod constants;
|
||||
mod error;
|
||||
mod generate_manifest;
|
||||
mod set_permissions;
|
||||
mod setup_networking;
|
||||
mod setup_peach;
|
||||
mod setup_peach_deb;
|
||||
@ -12,10 +14,6 @@ use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use structopt::StructOpt;
|
||||
|
||||
use crate::generate_manifest::generate_manifest;
|
||||
use crate::setup_peach::setup_peach;
|
||||
use crate::update::update;
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
#[structopt(
|
||||
name = "peach-config",
|
||||
@ -44,6 +42,14 @@ enum PeachConfig {
|
||||
/// Updates all PeachCloud microservices
|
||||
#[structopt(name = "update")]
|
||||
Update(UpdateOpts),
|
||||
|
||||
/// Changes the password for the peach-web interface
|
||||
#[structopt(name = "changepassword")]
|
||||
ChangePassword(ChangePasswordOpts),
|
||||
|
||||
/// Updates file permissions on PeachCloud device
|
||||
#[structopt(name = "permissions")]
|
||||
SetPermissions,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
@ -76,6 +82,14 @@ pub struct UpdateOpts {
|
||||
list: bool,
|
||||
}
|
||||
|
||||
#[derive(StructOpt, Debug)]
|
||||
pub struct ChangePasswordOpts {
|
||||
/// Optional argument to specify password as CLI argument
|
||||
/// if not specified, this command asks for user input for the passwords
|
||||
#[structopt(short, long)]
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
arg_enum! {
|
||||
/// enum options for real-time clock choices
|
||||
#[derive(Debug)]
|
||||
@ -99,28 +113,48 @@ fn main() {
|
||||
if let Some(subcommand) = opt.commands {
|
||||
match subcommand {
|
||||
PeachConfig::Setup(cfg) => {
|
||||
match setup_peach(cfg.no_input, cfg.default_locale, cfg.i2c, cfg.rtc) {
|
||||
match setup_peach::setup_peach(cfg.no_input, cfg.default_locale, cfg.i2c, cfg.rtc) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!("peach-config encountered an error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
PeachConfig::Manifest => match generate_manifest() {
|
||||
PeachConfig::Manifest => match generate_manifest::generate_manifest() {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"peach-config countered an error generating manifest: {}",
|
||||
"peach-config encountered an error generating manifest: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
},
|
||||
PeachConfig::Update(opts) => match update(opts) {
|
||||
PeachConfig::Update(opts) => match update::update(opts) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!("peach-config encountered an error during update: {}", err)
|
||||
}
|
||||
},
|
||||
PeachConfig::ChangePassword(opts) => {
|
||||
match change_password::set_peach_web_password(opts) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"peach-config encountered an error during password update: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
PeachConfig::SetPermissions => match set_permissions::set_permissions() {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
error!(
|
||||
"peach-config ecountered an error updating file permissions: {}",
|
||||
err
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
peach-config/src/set_permissions.rs
Normal file
21
peach-config/src/set_permissions.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use crate::error::PeachConfigError;
|
||||
use crate::utils::cmd;
|
||||
|
||||
/// All configs are stored in this folder, and should be read/writeable by peach group
|
||||
/// so they can be read and written by all PeachCloud services.
|
||||
pub const CONFIGS_DIR: &str = "/var/lib/peachcloud";
|
||||
pub const PEACH_WEB_DIR: &str = "/usr/share/peach-web";
|
||||
|
||||
/// Utility function to set correct file permissions on the PeachCloud device.
|
||||
/// Accidentally changing file permissions is a fairly common thing to happen,
|
||||
/// so this is a useful CLI function for quickly correcting anything that may be out of order.
|
||||
pub fn set_permissions() -> Result<(), PeachConfigError> {
|
||||
println!("[ UPDATING FILE PERMISSIONS ON PEACHCLOUD DEVICE ]");
|
||||
cmd(&["chmod", "-R", "u+rwX,g+rwX", CONFIGS_DIR])?;
|
||||
cmd(&["chown", "-R", "peach", CONFIGS_DIR])?;
|
||||
cmd(&["chgrp", "-R", "peach", CONFIGS_DIR])?;
|
||||
cmd(&["chmod", "-R", "u+rwX,g+rwX", PEACH_WEB_DIR])?;
|
||||
cmd(&["chown", "-R", "peach-web:peach", PEACH_WEB_DIR])?;
|
||||
println!("[ PERMISSIONS SUCCESSFULLY UPDATED ]");
|
||||
Ok(())
|
||||
}
|
@ -68,6 +68,7 @@ pub fn setup_peach(
|
||||
"libssl-dev",
|
||||
"nginx",
|
||||
"wget",
|
||||
"dnsutils",
|
||||
"-y",
|
||||
])?;
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "peach-dyndns-updater"
|
||||
version = "0.1.6"
|
||||
version = "0.1.8"
|
||||
authors = ["Max Fowler <mfowler@commoninternet.net>"]
|
||||
edition = "2018"
|
||||
description = "Sytemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate."
|
||||
|
29
peach-dyndns-updater/bindeploy.sh
Executable file
29
peach-dyndns-updater/bindeploy.sh
Executable file
@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# exit when any command fails
|
||||
set -e
|
||||
|
||||
KEYFILE=/Users/notplants/.ssh/id_rsa
|
||||
SERVICE=peach-dyndns-updater
|
||||
|
||||
# deploy
|
||||
rsync -avzh --exclude target --exclude .idea --exclude .git -e "ssh -i $KEYFILE" . rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/$SERVICE/
|
||||
rsync -avzh --exclude target --exclude .idea --exclude .git -e "ssh -i $KEYFILE" ~/computer/projects/peachcloud/peach-workspace/peach-lib/ rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/peach-lib/
|
||||
|
||||
echo "++ cross compiling on vps"
|
||||
BIN_PATH=$(ssh -i $KEYFILE rust@167.99.136.83 'cd /srv/peachcloud/automation/peach-workspace/peach-dyndns-updater; /home/rust/.cargo/bin/cargo clean -p peach-lib; /home/rust/.cargo/bin/cargo build --release --target=aarch64-unknown-linux-gnu')
|
||||
|
||||
echo "++ copying ${BIN_PATH} to local"
|
||||
rm -f target/$SERVICE
|
||||
scp -i $KEYFILE rust@167.99.136.83:/srv/peachcloud/automation/peach-workspace/target/aarch64-unknown-linux-gnu/release/peach-dyndns-updater ../target/vps-bin-$SERVICE
|
||||
|
||||
#echo "++ cross compiling"
|
||||
BINFILE="../target/vps-bin-$SERVICE"
|
||||
echo $BINFILE
|
||||
|
||||
|
||||
echo "++ build successful"
|
||||
|
||||
echo "++ copying to pi"
|
||||
ssh -t -i $KEYFILE peach@peach.link 'mkdir -p /srv/dev/bins'
|
||||
scp -i $KEYFILE $BINFILE peach@peach.link:/srv/dev/bins/$SERVICE
|
||||
|
@ -1,6 +1,5 @@
|
||||
use log::info;
|
||||
use peach_lib::dyndns_client::dyndns_update_ip;
|
||||
use log::{info};
|
||||
|
||||
|
||||
fn main() {
|
||||
// initalize the logger
|
||||
@ -9,4 +8,4 @@ fn main() {
|
||||
info!("Running peach-dyndns-updater");
|
||||
let result = dyndns_update_ip();
|
||||
info!("result: {:?}", result);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "peach-lib"
|
||||
version = "1.3.1"
|
||||
version = "1.3.2"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
edition = "2018"
|
||||
|
||||
@ -13,7 +13,7 @@ jsonrpc-core = "8.0.1"
|
||||
log = "0.4"
|
||||
nanorand = "0.6.1"
|
||||
regex = "1"
|
||||
rust-crypto = "0.2.36"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.8"
|
||||
sha3 = "0.10.0"
|
||||
|
@ -17,6 +17,10 @@ pub const YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
|
||||
// lock file (used to avoid race conditions during config reading & writing)
|
||||
pub const LOCK_FILE_PATH: &str = "/var/lib/peachcloud/config.lock";
|
||||
|
||||
// default values
|
||||
pub const DEFAULT_DYN_SERVER_ADDRESS: &str = "http://dynserver.dyn.peachcloud.org";
|
||||
pub const DEFAULT_DYN_NAMESERVER: &str = "ns.peachcloud.org";
|
||||
|
||||
// we make use of Serde default values in order to make PeachCloud
|
||||
// robust and keep running even with a not fully complete config.yml
|
||||
// main type which represents all peachcloud configurations
|
||||
@ -29,6 +33,10 @@ pub struct PeachConfig {
|
||||
#[serde(default)]
|
||||
pub dyn_dns_server_address: String,
|
||||
#[serde(default)]
|
||||
pub dyn_use_custom_server: bool,
|
||||
#[serde(default)]
|
||||
pub dyn_nameserver: String,
|
||||
#[serde(default)]
|
||||
pub dyn_tsig_key_path: String,
|
||||
#[serde(default)] // default is false
|
||||
pub dyn_enabled: bool,
|
||||
@ -63,20 +71,19 @@ fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachErro
|
||||
pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
|
||||
let peach_config_exists = std::path::Path::new(YAML_PATH).exists();
|
||||
|
||||
let peach_config: PeachConfig;
|
||||
|
||||
// if this is the first time loading peach_config, we can create a default here
|
||||
if !peach_config_exists {
|
||||
peach_config = PeachConfig {
|
||||
let peach_config: PeachConfig = if !peach_config_exists {
|
||||
PeachConfig {
|
||||
external_domain: "".to_string(),
|
||||
dyn_domain: "".to_string(),
|
||||
dyn_dns_server_address: "".to_string(),
|
||||
dyn_dns_server_address: DEFAULT_DYN_SERVER_ADDRESS.to_string(),
|
||||
dyn_use_custom_server: false,
|
||||
dyn_nameserver: DEFAULT_DYN_NAMESERVER.to_string(),
|
||||
dyn_tsig_key_path: "".to_string(),
|
||||
dyn_enabled: false,
|
||||
ssb_admin_ids: Vec::new(),
|
||||
admin_password_hash: "".to_string(),
|
||||
temporary_password_hash: "".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
// otherwise we load peach config from disk
|
||||
else {
|
||||
@ -84,8 +91,8 @@ pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
|
||||
source,
|
||||
path: YAML_PATH.to_string(),
|
||||
})?;
|
||||
peach_config = serde_yaml::from_str(&contents)?;
|
||||
}
|
||||
serde_yaml::from_str(&contents)?
|
||||
};
|
||||
|
||||
Ok(peach_config)
|
||||
}
|
||||
@ -122,6 +129,18 @@ pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_dyndns_server_address() -> Result<String, PeachError> {
|
||||
let peach_config = load_peach_config()?;
|
||||
// if the user is using a custom dyn server then load the address from the config
|
||||
if peach_config.dyn_use_custom_server {
|
||||
Ok(peach_config.dyn_dns_server_address)
|
||||
}
|
||||
// otherwise hardcode the address
|
||||
else {
|
||||
Ok(DEFAULT_DYN_SERVER_ADDRESS.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result<PeachConfig, PeachError> {
|
||||
let mut peach_config = load_peach_config()?;
|
||||
peach_config.dyn_enabled = enabled_value;
|
||||
|
@ -9,13 +9,8 @@
|
||||
//!
|
||||
//! The domain for dyndns updates is stored in /var/lib/peachcloud/config.yml
|
||||
//! The tsig key for authenticating the updates is stored in /var/lib/peachcloud/peach-dyndns/tsig.key
|
||||
use std::{
|
||||
fs,
|
||||
fs::OpenOptions,
|
||||
io::Write,
|
||||
process::{Command, Stdio},
|
||||
str::FromStr,
|
||||
};
|
||||
use std::ffi::OsStr;
|
||||
use std::{fs, fs::OpenOptions, io::Write, process::Command, str::FromStr};
|
||||
|
||||
use chrono::prelude::*;
|
||||
use jsonrpc_client_core::{expand_params, jsonrpc_client};
|
||||
@ -23,13 +18,10 @@ use jsonrpc_client_http::HttpTransport;
|
||||
use log::{debug, info};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::{
|
||||
config_manager::{load_peach_config, set_peach_dyndns_config},
|
||||
error::PeachError,
|
||||
};
|
||||
use crate::config_manager::get_dyndns_server_address;
|
||||
use crate::{config_manager, error::PeachError};
|
||||
|
||||
/// constants for dyndns configuration
|
||||
pub const PEACH_DYNDNS_URL: &str = "http://dynserver.dyn.peachcloud.org";
|
||||
pub const TSIG_KEY_PATH: &str = "/var/lib/peachcloud/peach-dyndns/tsig.key";
|
||||
pub const PEACH_DYNDNS_CONFIG_PATH: &str = "/var/lib/peachcloud/peach-dyndns";
|
||||
pub const DYNDNS_LOG_PATH: &str = "/var/lib/peachcloud/peach-dyndns/latest_result.log";
|
||||
@ -62,9 +54,10 @@ pub fn save_dyndns_key(key: &str) -> Result<(), PeachError> {
|
||||
pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError> {
|
||||
debug!("Creating HTTP transport for dyndns client.");
|
||||
let transport = HttpTransport::new().standalone()?;
|
||||
let http_server = PEACH_DYNDNS_URL;
|
||||
debug!("Creating HTTP transport handle on {}.", http_server);
|
||||
let transport_handle = transport.handle(http_server)?;
|
||||
let http_server = get_dyndns_server_address()?;
|
||||
info!("Using dyndns http server address: {:?}", http_server);
|
||||
debug!("Creating HTTP transport handle on {}.", &http_server);
|
||||
let transport_handle = transport.handle(&http_server)?;
|
||||
info!("Creating client for peach-dyndns service.");
|
||||
let mut client = PeachDynDnsClient::new(transport_handle);
|
||||
|
||||
@ -73,7 +66,8 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError>
|
||||
// save new TSIG key
|
||||
save_dyndns_key(&key)?;
|
||||
// save new configuration values
|
||||
let set_config_result = set_peach_dyndns_config(domain, PEACH_DYNDNS_URL, TSIG_KEY_PATH, true);
|
||||
let set_config_result =
|
||||
config_manager::set_peach_dyndns_config(domain, &http_server, TSIG_KEY_PATH, true);
|
||||
match set_config_result {
|
||||
Ok(_) => {
|
||||
let response = "success".to_string();
|
||||
@ -87,9 +81,9 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError>
|
||||
pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError> {
|
||||
debug!("Creating HTTP transport for dyndns client.");
|
||||
let transport = HttpTransport::new().standalone()?;
|
||||
let http_server = PEACH_DYNDNS_URL;
|
||||
debug!("Creating HTTP transport handle on {}.", http_server);
|
||||
let transport_handle = transport.handle(http_server)?;
|
||||
let http_server = get_dyndns_server_address()?;
|
||||
debug!("Creating HTTP transport handle on {}.", &http_server);
|
||||
let transport_handle = transport.handle(&http_server)?;
|
||||
info!("Creating client for peach_network service.");
|
||||
let mut client = PeachDynDnsClient::new(transport_handle);
|
||||
|
||||
@ -113,31 +107,31 @@ fn get_public_ip_address() -> Result<String, PeachError> {
|
||||
/// Reads dyndns configurations from config.yml
|
||||
/// and then uses nsupdate to update the IP address for the configured domain
|
||||
pub fn dyndns_update_ip() -> Result<bool, PeachError> {
|
||||
info!("Running dyndns_update_ip");
|
||||
let peach_config = load_peach_config()?;
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
info!(
|
||||
"Using config:
|
||||
dyn_tsig_key_path: {:?}
|
||||
dyn_domain: {:?}
|
||||
dyn_dns_server_address: {:?}
|
||||
dyn_enabled: {:?}
|
||||
dyn_nameserver: {:?}
|
||||
",
|
||||
peach_config.dyn_tsig_key_path,
|
||||
peach_config.dyn_domain,
|
||||
peach_config.dyn_dns_server_address,
|
||||
peach_config.dyn_enabled,
|
||||
peach_config.dyn_nameserver,
|
||||
);
|
||||
if !peach_config.dyn_enabled {
|
||||
info!("dyndns is not enabled, not updating");
|
||||
Ok(false)
|
||||
} else {
|
||||
// call nsupdate passing appropriate configs
|
||||
let mut nsupdate_command = Command::new("/usr/bin/nsupdate")
|
||||
let mut nsupdate_command = Command::new("/usr/bin/nsupdate");
|
||||
nsupdate_command
|
||||
.arg("-k")
|
||||
.arg(&peach_config.dyn_tsig_key_path)
|
||||
.arg("-v")
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()?;
|
||||
.arg("-v");
|
||||
// pass nsupdate commands via stdin
|
||||
let public_ip_address = get_public_ip_address()?;
|
||||
info!("found public ip address: {}", public_ip_address);
|
||||
@ -148,20 +142,20 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
|
||||
update delete {DOMAIN} A
|
||||
update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS}
|
||||
send",
|
||||
NAMESERVER = "ns.peachcloud.org",
|
||||
NAMESERVER = peach_config.dyn_nameserver,
|
||||
ZONE = peach_config.dyn_domain,
|
||||
DOMAIN = peach_config.dyn_domain,
|
||||
PUBLIC_IP_ADDRESS = public_ip_address,
|
||||
);
|
||||
let mut nsupdate_stdin = nsupdate_command.stdin.take().ok_or(PeachError::NsUpdate {
|
||||
msg: "unable to capture stdin handle for `nsupdate` command".to_string(),
|
||||
})?;
|
||||
write!(nsupdate_stdin, "{}", ns_commands).map_err(|source| PeachError::Write {
|
||||
source,
|
||||
path: peach_config.dyn_tsig_key_path.to_string(),
|
||||
})?;
|
||||
let nsupdate_output = nsupdate_command.wait_with_output()?;
|
||||
info!("nsupdate output: {:?}", nsupdate_output);
|
||||
info!("ns_commands: {:?}", ns_commands);
|
||||
info!("creating nsupdate temp file");
|
||||
let temp_file_path = "/var/lib/peachcloud/nsupdate.sh";
|
||||
// write ns_commands to temp_file
|
||||
fs::write(temp_file_path, ns_commands)?;
|
||||
nsupdate_command.arg(temp_file_path);
|
||||
let nsupdate_output = nsupdate_command.output()?;
|
||||
let args: Vec<&OsStr> = nsupdate_command.get_args().collect();
|
||||
info!("nsupdate command: {:?}", args);
|
||||
// We only return a successful result if nsupdate was successful
|
||||
if nsupdate_output.status.success() {
|
||||
info!("nsupdate succeeded, returning ok");
|
||||
@ -204,7 +198,7 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
|
||||
})?;
|
||||
// replace newline if found
|
||||
// TODO: maybe we can use `.trim()` instead
|
||||
let contents = contents.replace("\n", "");
|
||||
let contents = contents.replace('\n', "");
|
||||
// TODO: consider adding additional context?
|
||||
let time_ran_dt = DateTime::parse_from_rfc3339(&contents).map_err(|source| {
|
||||
PeachError::ParseDateTime {
|
||||
@ -223,20 +217,15 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
|
||||
/// and has successfully run recently (in the last six minutes)
|
||||
pub fn is_dns_updater_online() -> Result<bool, PeachError> {
|
||||
// first check if it is enabled in peach-config
|
||||
let peach_config = load_peach_config()?;
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
let is_enabled = peach_config.dyn_enabled;
|
||||
// then check if it has successfully run within the last 6 minutes (60*6 seconds)
|
||||
let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?;
|
||||
let ran_recently: bool;
|
||||
match num_seconds_since_successful_update {
|
||||
Some(seconds) => {
|
||||
ran_recently = seconds < (60 * 6);
|
||||
}
|
||||
let ran_recently: bool = match num_seconds_since_successful_update {
|
||||
Some(seconds) => seconds < (60 * 6),
|
||||
// if the value is None, then the last time it ran successfully is unknown
|
||||
None => {
|
||||
ran_recently = false;
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
};
|
||||
// debug log
|
||||
info!("is_dyndns_enabled: {:?}", is_enabled);
|
||||
info!("dyndns_ran_recently: {:?}", ran_recently);
|
||||
@ -258,11 +247,10 @@ pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
// helper function which checks if a dyndns domain is new
|
||||
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> bool {
|
||||
// TODO: return `Result<bool, PeachError>` and replace `unwrap` with `?` operator
|
||||
let peach_config = load_peach_config().unwrap();
|
||||
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> Result<bool, PeachError> {
|
||||
let peach_config = config_manager::load_peach_config()?;
|
||||
let previous_dyndns_domain = peach_config.dyn_domain;
|
||||
dyndns_full_domain != previous_dyndns_domain
|
||||
Ok(dyndns_full_domain != previous_dyndns_domain)
|
||||
}
|
||||
|
||||
jsonrpc_client!(pub struct PeachDynDnsClient {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crypto::{digest::Digest, sha3::Sha3};
|
||||
use nanorand::{Rng, WyRand};
|
||||
use sha3::{Digest, Sha3_256};
|
||||
|
||||
use crate::{config_manager, error::PeachError, sbot_client};
|
||||
|
||||
@ -37,9 +37,13 @@ pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
|
||||
|
||||
/// Creates a hash from a password string
|
||||
pub fn hash_password(password: &str) -> String {
|
||||
let mut hasher = Sha3::sha3_256();
|
||||
hasher.input_str(password);
|
||||
hasher.result_str()
|
||||
let mut hasher = Sha3_256::new();
|
||||
// write input message
|
||||
hasher.update(password);
|
||||
// read hash digest
|
||||
let result = hasher.finalize();
|
||||
// convert `u8` to `String`
|
||||
result[0].to_string()
|
||||
}
|
||||
|
||||
/// Sets a new temporary password for the admin user
|
||||
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,43 +1,32 @@
|
||||
[package]
|
||||
name = "peach-network"
|
||||
version = "0.2.13"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
|
||||
edition = "2018"
|
||||
description = "Query and configure network interfaces using JSON-RPC over HTTP."
|
||||
version = "0.4.1"
|
||||
authors = ["Andrew Reid <glyph@mycelial.technology>"]
|
||||
edition = "2021"
|
||||
description = "Query and configure network interfaces."
|
||||
homepage = "https://opencollective.com/peachcloud"
|
||||
repository = "https://github.com/peachcloud/peach-network"
|
||||
repository = "https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-network"
|
||||
readme = "README.md"
|
||||
license = "AGPL-3.0-only"
|
||||
license = "LGPL-3.0-only"
|
||||
publish = false
|
||||
|
||||
[package.metadata.deb]
|
||||
depends = "$auto"
|
||||
extended-description = """\
|
||||
peach-network is a microservice to query and configure network interfaces \
|
||||
using JSON-RPC over HTTP."""
|
||||
maintainer-scripts="debian"
|
||||
systemd-units = { unit-name = "peach-network" }
|
||||
assets = [
|
||||
["target/release/peach-network", "usr/bin/", "755"],
|
||||
["README.md", "usr/share/doc/peach-network/README", "644"],
|
||||
]
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "peachcloud/peach-network", branch = "master" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.6"
|
||||
get_if_addrs = "0.5.3"
|
||||
jsonrpc-core = "11"
|
||||
jsonrpc-http-server = "11"
|
||||
log = "0.4"
|
||||
probes = "0.4"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
miniserde = { version = "0.1.15", optional = true }
|
||||
probes = "0.4.1"
|
||||
serde = { version = "1.0.130", features = ["derive"], optional = true }
|
||||
regex = "1"
|
||||
# replace this with crate import once latest changes have been published
|
||||
wpactrl = { git = "https://github.com/sauyon/wpa-ctrl-rs.git", branch = "master" }
|
||||
|
||||
[dev-dependencies]
|
||||
jsonrpc-test = "11"
|
||||
[features]
|
||||
default = []
|
||||
|
||||
# Provide `Serialize` and `Deserialize` traits for library structs using `miniserde`
|
||||
miniserde_support = ["miniserde"]
|
||||
|
||||
# Provide `Serialize` and `Deserialize` traits for library structs using `serde`
|
||||
serde_support = ["serde"]
|
||||
|
@ -1,178 +1,46 @@
|
||||
# peach-network
|
||||
|
||||
[](https://travis-ci.com/peachcloud/peach-network) 
|
||||

|
||||
|
||||
Networking microservice module for PeachCloud. Query and configure device interfaces using [JSON-RPC](https://www.jsonrpc.org/specification) over http.
|
||||
Network interface state query and modification library.
|
||||
|
||||
Interaction with wireless interfaces occurs primarily through the [wpactrl crate](https://docs.rs/wpactrl/0.3.1/wpactrl/) which provides "a pure-Rust lowlevel library for controlling wpasupplicant remotely". This approach is akin to using `wpa_cli` (a WPA command line client).
|
||||
|
||||
_Note: This module is a work-in-progress._
|
||||
## API Documentation
|
||||
|
||||
### JSON-RPC API
|
||||
API documentation can be built and served with `cargo doc --no-deps --open`. The full set of available data structures and functions is listed in the `peach_network::network` module. A custom error type (`NetworkError`) is also publically exposed for library users; it encapsulates all possible error variants.
|
||||
|
||||
Methods for **retrieving data**:
|
||||
## Example Usage
|
||||
|
||||
| Method | Parameters | Description |
|
||||
| --- | --- | --- |
|
||||
| `available_networks` | `iface` | List SSID, flags (security), frequency and signal level for all networks in range of given interface |
|
||||
| `id` | `iface`, `ssid` | Return ID of given SSID |
|
||||
| `ip` | `iface` | Return IP of given network interface |
|
||||
| `ping` | | Respond with `success` if microservice is running |
|
||||
| `rssi` | `iface` | Return average signal strength (dBm) for given interface |
|
||||
| `rssi_percent` | `iface` | Return average signal strength (%) for given interface |
|
||||
| `saved_networks` | | List all networks saved in wpasupplicant config |
|
||||
| `ssid` | `iface` | Return SSID of currently-connected network for given interface |
|
||||
| `state` | `iface` | Return state of given interface |
|
||||
| `status` | `iface` | Return status parameters for given interface |
|
||||
| `traffic` | `iface` | Return network traffic for given interface |
|
||||
```rust
|
||||
use peach_network::{network, NetworkError};
|
||||
|
||||
Methods for **modifying state**:
|
||||
fn main() -> Result<(), NetworkError> {
|
||||
let wlan_iface = "wlan0";
|
||||
|
||||
| Method | Parameters | Description |
|
||||
| --- | --- | --- |
|
||||
| `activate_ap` | | Activate WiFi access point (start `wpa_supplicant@ap0.service`) |
|
||||
| `activate_client` | | Activate WiFi client connection (start `wpa_supplicant@wlan0.service`) |
|
||||
| `add` | `ssid`, `pass` | Add WiFi credentials to `wpa_supplicant-wlan0.conf` |
|
||||
| `check_iface` | | Activate WiFi access point if client mode is active without a connection |
|
||||
| `connect` | `id`, `iface` | Disable other networks and attempt connection with AP represented by given id |
|
||||
| `delete` | `id`, `iface` | Remove WiFi credentials for given network id and interface |
|
||||
| `disable` | `id`, `iface` | Disable connection with AP represented by given id |
|
||||
| `disconnect` | `iface` | Disconnect given interface |
|
||||
| `modify` | `id`, `iface`, `password` | Set a new password for given network id and interface |
|
||||
| `reassociate` | `iface` | Reassociate with current AP for given interface |
|
||||
| `reconfigure` | | Force wpa_supplicant to re-read its configuration file |
|
||||
| `reconnect` | `iface` | Disconnect and reconnect given interface |
|
||||
| `save` | | Save configuration changes to `wpa_supplicant-wlan0.conf` |
|
||||
let wlan_ip = network::ip(wlan_iface)?;
|
||||
let wlan_ssid = network::ssid(wlan_iface)?;
|
||||
|
||||
### API Documentation
|
||||
let ssid = "Home";
|
||||
let pass = "SuperSecret";
|
||||
|
||||
API documentation can be built and served with `cargo doc --no-deps --open`. This set of documentation is intended for developers who wish to work on the project or better understand the API of the `src/network.rs` module.
|
||||
network::add(&wlan_iface, &ssid, &pass)?;
|
||||
network::save()?;
|
||||
|
||||
### Environment
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
The JSON-RPC HTTP server address and port can be configured with the `PEACH_NETWORK_SERVER` environment variable:
|
||||
## Feature Flags
|
||||
|
||||
`export PEACH_NETWORK_SERVER=127.0.0.1:5000`
|
||||
Feature flags are used to offer `Serialize` and `Deserialize` implementations for all `struct` data types provided by this library. These traits are not provided by default. A choice of `miniserde` and `serde` is provided.
|
||||
|
||||
When not set, the value defaults to `127.0.0.1:5110`.
|
||||
Define the desired feature in the `Cargo.toml` manifest of your project:
|
||||
|
||||
Logging is made available with `env_logger`:
|
||||
```toml
|
||||
peach-network = { version = "0.3.0", features = ["miniserde_support"] }
|
||||
```
|
||||
|
||||
`export RUST_LOG=info`
|
||||
## License
|
||||
|
||||
Other logging levels include `debug`, `warn` and `error`.
|
||||
|
||||
### Setup
|
||||
|
||||
Clone this repo:
|
||||
|
||||
`git clone https://github.com/peachcloud/peach-network.git`
|
||||
|
||||
Move into the repo and compile:
|
||||
|
||||
`cd peach-network`
|
||||
`cargo build --release`
|
||||
|
||||
Run the binary (sudo needed to satisfy permission requirements):
|
||||
|
||||
`sudo ./target/release/peach-network`
|
||||
|
||||
### Debian Packaging
|
||||
|
||||
A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-network` to be easily bundled as a Debian package (`.deb`). The `cargo-deb` [crate](https://crates.io/crates/cargo-deb) can be used to achieve this.
|
||||
|
||||
Install `cargo-deb`:
|
||||
|
||||
`cargo install cargo-deb`
|
||||
|
||||
Move into the repo:
|
||||
|
||||
`cd peach-network`
|
||||
|
||||
Build the package:
|
||||
|
||||
`cargo deb`
|
||||
|
||||
The output will be written to `target/debian/peach-network_0.2.4_arm64.deb` (or similar).
|
||||
|
||||
Build the package (aarch64):
|
||||
|
||||
`cargo deb --target aarch64-unknown-linux-gnu`
|
||||
|
||||
Install the package as follows:
|
||||
|
||||
`sudo dpkg -i target/debian/peach-network_0.2.4_arm64.deb`
|
||||
|
||||
The service will be automatically enabled and started.
|
||||
|
||||
Uninstall the service:
|
||||
|
||||
`sudo apt-get remove peach-network`
|
||||
|
||||
Remove configuration files (not removed with `apt-get remove`):
|
||||
|
||||
`sudo apt-get purge peach-network`
|
||||
|
||||
### Example Usage
|
||||
|
||||
**Retrieve IP address for wlan0**
|
||||
|
||||
With microservice running, open a second terminal window and use `curl` to call server methods:
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "ip", "params" : {"iface": "wlan0" }, "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server responds with:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"192.168.1.21","id":1}`
|
||||
|
||||
**Retrieve SSID of connected access point for wlan1**
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "ssid", "params" : {"iface": "wlan1" }, "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server response when interface is connected:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"Home","id":1}`
|
||||
|
||||
Server response when interface is not connected:
|
||||
|
||||
`{"jsonrpc":"2.0","error":{"code":-32003,"message":"Failed to retrieve SSID for wlan1. Interface may not be connected."},"id":1}`
|
||||
|
||||
**Retrieve list of SSIDs for all networks in range of wlan0**
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "available_networks", "params" : {"iface": "wlan0" }, "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server response when interface is connected:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"[{\"frequency\":\"2412\",\"signal_level\":\"-72\",\"ssid\":\"Home\",\"flags\":\"[WPA2-PSK-CCMP][ESS]\"},{\"frequency\":\"2472\",\"signal_level\":\"-56\",\"ssid\":\"podetium\",\"flags\":\"[WPA2-PSK-CCMP+TKIP][ESS]\"}]","id":1}`
|
||||
|
||||
Server response when interface is not connected:
|
||||
|
||||
`{"jsonrpc":"2.0","error":{"code":-32006,"message":"No networks found in range of wlan0"},"id":1}`
|
||||
|
||||
**Retrieve network traffic statistics for wlan1**
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "traffic", "params" : {"iface": "wlan1" }, "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server response if interface exists:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"{\"received\":26396361,\"transmitted\":22352530}","id":1}`
|
||||
|
||||
Server response when interface is not found:
|
||||
|
||||
`{"jsonrpc":"2.0","error":{"code":-32004,"message":"Failed to retrieve network traffic for wlan3. Interface may not be connected"},"id":1}`
|
||||
|
||||
**Retrieve status information for wlan0**
|
||||
|
||||
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "status", "params" : {"iface": "wlan0" }, "id":1 }' 127.0.0.1:5110`
|
||||
|
||||
Server response if interface exists:
|
||||
|
||||
`{"jsonrpc":"2.0","result":"{\"address\":\"b8:27:eb:9b:5d:5f\",\"bssid\":\"f4:8c:eb:cd:31:81\",\"freq\":\"2412\",\"group_cipher\":\"CCMP\",\"id\":\"0\",\"ip_address\":\"192.168.0.162\",\"key_mgmt\":\"WPA2-PSK\",\"mode\":\"station\",\"pairwise_cipher\":\"CCMP\",\"ssid\":\"Home\",\"wpa_state\":\"COMPLETED\"}","id":1}`
|
||||
|
||||
Server response when interface is not found:
|
||||
|
||||
`{"jsonrpc":"2.0","error":{"code":-32013,"message":"Failed to open control interface for wpasupplicant: No such file or directory (os error 2)"},"id":1}`
|
||||
|
||||
### Licensing
|
||||
|
||||
AGPL-3.0
|
||||
LGPL-3.0.
|
||||
|
@ -1,13 +0,0 @@
|
||||
[Unit]
|
||||
Description=Query and configure network interfaces using JSON-RPC over HTTP.
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=netdev
|
||||
Environment="RUST_LOG=error"
|
||||
ExecStart=/usr/bin/peach-network
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -1,81 +1,169 @@
|
||||
//! Custom error type for `peach-network`.
|
||||
|
||||
use std::io;
|
||||
use std::num::ParseIntError;
|
||||
|
||||
use io::Error as IoError;
|
||||
use jsonrpc_core::{types::error::Error as JsonRpcError, ErrorCode};
|
||||
use probes::ProbeError;
|
||||
use regex::Error as RegexError;
|
||||
use serde_json::Error as SerdeError;
|
||||
use wpactrl::WpaError;
|
||||
|
||||
/// Custom error type encapsulating all possible errors when querying
|
||||
/// network interfaces and modifying their state.
|
||||
#[derive(Debug)]
|
||||
pub enum NetworkError {
|
||||
Add { ssid: String },
|
||||
|
||||
NoState { iface: String, source: IoError },
|
||||
|
||||
Disable { id: String, iface: String },
|
||||
|
||||
Disconnect { iface: String },
|
||||
|
||||
GenWpaPassphrase { ssid: String, source: IoError },
|
||||
|
||||
GenWpaPassphraseWarning { ssid: String, err_msg: String },
|
||||
|
||||
Id { ssid: String, iface: String },
|
||||
|
||||
NoIp { iface: String, source: IoError },
|
||||
|
||||
Rssi { iface: String },
|
||||
|
||||
RssiPercent { iface: String },
|
||||
|
||||
Ssid { iface: String },
|
||||
|
||||
State { iface: String },
|
||||
|
||||
Status { iface: String },
|
||||
|
||||
Traffic { iface: String },
|
||||
|
||||
/// Failed to add network.
|
||||
Add {
|
||||
/// SSID.
|
||||
ssid: String,
|
||||
},
|
||||
/// Failed to retrieve network state.
|
||||
NoState {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
/// Underlying error source.
|
||||
source: IoError,
|
||||
},
|
||||
/// Failed to disable network.
|
||||
Disable {
|
||||
/// ID.
|
||||
id: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to disconnect interface.
|
||||
Disconnect {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to execute wpa_passphrase command.
|
||||
GenWpaPassphrase {
|
||||
/// SSID.
|
||||
ssid: String,
|
||||
/// Underlying error source.
|
||||
source: IoError,
|
||||
},
|
||||
/// Failed to successfully generate wpa passphrase.
|
||||
GenWpaPassphraseWarning {
|
||||
/// SSID.
|
||||
ssid: String,
|
||||
/// Error message describing context.
|
||||
err_msg: String,
|
||||
},
|
||||
/// Failed to retrieve ID for the given SSID and interface.
|
||||
Id {
|
||||
/// SSID.
|
||||
ssid: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve IP address.
|
||||
NoIp {
|
||||
/// Inteface.
|
||||
iface: String,
|
||||
/// Underlying error source.
|
||||
source: IoError,
|
||||
},
|
||||
/// Failed to retrieve RSSI.
|
||||
Rssi {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve signal quality (%).
|
||||
RssiPercent {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve SSID.
|
||||
Ssid {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve state.
|
||||
State {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve status.
|
||||
Status {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retieve network traffic.
|
||||
Traffic {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// No saved network found for the default interface.
|
||||
SavedNetworks,
|
||||
|
||||
AvailableNetworks { iface: String },
|
||||
|
||||
MissingParams(JsonRpcError),
|
||||
|
||||
Modify { id: String, iface: String },
|
||||
|
||||
Ip { iface: String },
|
||||
|
||||
/// No networks found in range.
|
||||
AvailableNetworks {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to set new password.
|
||||
Modify {
|
||||
/// ID.
|
||||
id: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve IP address.
|
||||
Ip {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to parse integer from string.
|
||||
ParseInt(ParseIntError),
|
||||
|
||||
NoTraffic { iface: String, source: ProbeError },
|
||||
|
||||
Reassociate { iface: String },
|
||||
|
||||
/// Failed to retrieve network traffic measurement.
|
||||
NoTraffic {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
/// Underlying error source.
|
||||
source: ProbeError,
|
||||
},
|
||||
/// Failed to reassociate with WiFi network.
|
||||
Reassociate {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to force reread of wpa_supplicant configuration file.
|
||||
Reconfigure,
|
||||
|
||||
Reconnect { iface: String },
|
||||
|
||||
/// Failed to reconnect with WiFi network.
|
||||
Reconnect {
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to execute Regex command.
|
||||
Regex(RegexError),
|
||||
|
||||
Delete { id: String, iface: String },
|
||||
|
||||
/// Failed to delete network.
|
||||
Delete {
|
||||
/// ID.
|
||||
id: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to retrieve state of wlan0 service.
|
||||
WlanState(IoError),
|
||||
|
||||
/// Failed to retrieve connection state of wlan0 interface.
|
||||
WlanOperstate(IoError),
|
||||
|
||||
/// Failed to save wpa_supplicant configuration changes to file.
|
||||
Save,
|
||||
|
||||
Connect { id: String, iface: String },
|
||||
|
||||
StartAp0(IoError),
|
||||
|
||||
StartWlan0(IoError),
|
||||
|
||||
SerdeSerialize(SerdeError),
|
||||
|
||||
/// Failed to connect to network.
|
||||
Connect {
|
||||
/// ID.
|
||||
id: String,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to start systemctl service for a network interface.
|
||||
StartInterface {
|
||||
/// Underlying error source.
|
||||
source: IoError,
|
||||
/// Interface.
|
||||
iface: String,
|
||||
},
|
||||
/// Failed to execute wpa-ctrl command.
|
||||
WpaCtrl(WpaError),
|
||||
}
|
||||
|
||||
@ -98,7 +186,6 @@ impl std::error::Error for NetworkError {
|
||||
NetworkError::Traffic { .. } => None,
|
||||
NetworkError::SavedNetworks => None,
|
||||
NetworkError::AvailableNetworks { .. } => None,
|
||||
NetworkError::MissingParams(ref source) => Some(source),
|
||||
NetworkError::Modify { .. } => None,
|
||||
NetworkError::Ip { .. } => None,
|
||||
NetworkError::ParseInt(ref source) => Some(source),
|
||||
@ -112,9 +199,7 @@ impl std::error::Error for NetworkError {
|
||||
NetworkError::WlanOperstate(ref source) => Some(source),
|
||||
NetworkError::Save => None,
|
||||
NetworkError::Connect { .. } => None,
|
||||
NetworkError::StartWlan0(ref source) => Some(source),
|
||||
NetworkError::StartAp0(ref source) => Some(source),
|
||||
NetworkError::SerdeSerialize(ref source) => Some(source),
|
||||
NetworkError::StartInterface { ref source, .. } => Some(source),
|
||||
NetworkError::WpaCtrl(ref source) => Some(source),
|
||||
}
|
||||
}
|
||||
@ -181,20 +266,13 @@ impl std::fmt::Display for NetworkError {
|
||||
write!(f, "No status found for interface: {}", iface)
|
||||
}
|
||||
NetworkError::Traffic { ref iface } => {
|
||||
write!(
|
||||
f,
|
||||
"Could not find network traffice for interface: {}",
|
||||
iface
|
||||
)
|
||||
write!(f, "Could not find network traffic for interface: {}", iface)
|
||||
}
|
||||
NetworkError::SavedNetworks => {
|
||||
write!(f, "No saved networks found for default interface")
|
||||
}
|
||||
NetworkError::AvailableNetworks { ref iface } => {
|
||||
write!(f, "No networks found in range of interface: {}", iface)
|
||||
}
|
||||
NetworkError::MissingParams(ref source) => {
|
||||
write!(f, "Missing expected parameters: {}", source)
|
||||
write!(f, "No networks found in range of interface: {}", iface)
|
||||
}
|
||||
NetworkError::Modify { ref id, ref iface } => {
|
||||
write!(
|
||||
@ -256,20 +334,16 @@ impl std::fmt::Display for NetworkError {
|
||||
id, iface
|
||||
)
|
||||
}
|
||||
NetworkError::StartWlan0(_) => write!(f, "Failed to start ap0 service"),
|
||||
NetworkError::StartAp0(_) => write!(f, "Failed to start wlan0 service"),
|
||||
NetworkError::SerdeSerialize(_) => write!(f, "JSON serialization failed"),
|
||||
NetworkError::StartInterface { ref iface, .. } => write!(
|
||||
f,
|
||||
"Failed to start systemctl service for {} interface",
|
||||
iface
|
||||
),
|
||||
NetworkError::WpaCtrl(_) => write!(f, "WpaCtrl command failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SerdeError> for NetworkError {
|
||||
fn from(err: SerdeError) -> Self {
|
||||
NetworkError::SerdeSerialize(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WpaError> for NetworkError {
|
||||
fn from(err: WpaError) -> Self {
|
||||
NetworkError::WpaCtrl(err)
|
||||
@ -287,203 +361,3 @@ impl From<RegexError> for NetworkError {
|
||||
NetworkError::Regex(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NetworkError> for JsonRpcError {
|
||||
fn from(err: NetworkError) -> Self {
|
||||
match &err {
|
||||
NetworkError::Add { ssid } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32000),
|
||||
message: format!("Failed to add network for {}", ssid),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::NoState { iface, source } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32022),
|
||||
message: format!(
|
||||
"Failed to retrieve interface state for {}: {}",
|
||||
iface, source
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Disable { id, iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32029),
|
||||
message: format!("Failed to disable network {} for {}", id, iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Disconnect { iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32032),
|
||||
message: format!("Failed to disconnect {}", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::GenWpaPassphrase { ssid, source } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32025),
|
||||
message: format!("Failed to generate wpa passphrase for {}: {}", ssid, source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::GenWpaPassphraseWarning { ssid, err_msg } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32036),
|
||||
message: format!(
|
||||
"Failed to generate wpa passphrase for {}: {}",
|
||||
ssid, err_msg
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Id { iface, ssid } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32026),
|
||||
message: format!("No ID found for {} on interface {}", ssid, iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::NoIp { iface, source } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32001),
|
||||
message: format!("Failed to retrieve IP address for {}: {}", iface, source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Rssi { iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32002),
|
||||
message: format!(
|
||||
"Failed to retrieve RSSI for {}. Interface may not be connected",
|
||||
iface
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::RssiPercent { iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32034),
|
||||
message: format!(
|
||||
"Failed to retrieve signal quality (%) for {}. Interface may not be connected",
|
||||
iface
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Ssid { iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32003),
|
||||
message: format!(
|
||||
"Failed to retrieve SSID for {}. Interface may not be connected",
|
||||
iface
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::State { iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32023),
|
||||
message: format!("No state found for {}. Interface may not exist", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Status { iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32024),
|
||||
message: format!("No status found for {}. Interface may not exist", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Traffic { iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32004),
|
||||
message: format!(
|
||||
"No network traffic statistics found for {}. Interface may not exist",
|
||||
iface
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::SavedNetworks => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32005),
|
||||
message: "No saved networks found".to_string(),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::AvailableNetworks { iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32006),
|
||||
message: format!("No networks found in range of {}", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::MissingParams(e) => e.clone(),
|
||||
NetworkError::Modify { id, iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32033),
|
||||
message: format!("Failed to set new password for network {} on {}", id, iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Ip { iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32007),
|
||||
message: format!("No IP address found for {}", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::ParseInt(source) => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32035),
|
||||
message: format!(
|
||||
"Failed to parse integer from string for RSSI value: {}",
|
||||
source
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::NoTraffic { iface, source } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32015),
|
||||
message: format!(
|
||||
"Failed to retrieve network traffic statistics for {}: {}",
|
||||
iface, source
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Reassociate { iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32008),
|
||||
message: format!("Failed to reassociate with WiFi network for {}", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Reconfigure => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32030),
|
||||
message: "Failed to force reread of wpa_supplicant configuration file".to_string(),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Reconnect { iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32009),
|
||||
message: format!("Failed to reconnect with WiFi network for {}", iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Regex(source) => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32010),
|
||||
message: format!("Regex command error: {}", source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Delete { id, iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32028),
|
||||
message: format!("Failed to delete network {} for {}", id, iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::WlanState(source) => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32011),
|
||||
message: format!("Failed to retrieve state of wlan0 service: {}", source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::WlanOperstate(source) => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32021),
|
||||
message: format!(
|
||||
"Failed to retrieve connection state of wlan0 interface: {}",
|
||||
source
|
||||
),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Save => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32031),
|
||||
message: "Failed to save configuration changes to file".to_string(),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::Connect { id, iface } => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32027),
|
||||
message: format!("Failed to connect to network {} for {}", id, iface),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::StartAp0(source) => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32016),
|
||||
message: format!("Failed to start ap0 service: {}", source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::StartWlan0(source) => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32018),
|
||||
message: format!("Failed to start wlan0 service: {}", source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::SerdeSerialize(source) => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32012),
|
||||
message: format!("JSON serialization failed: {}", source),
|
||||
data: None,
|
||||
},
|
||||
NetworkError::WpaCtrl(source) => JsonRpcError {
|
||||
code: ErrorCode::ServerError(-32013),
|
||||
message: format!("WPA control interface failure: {}", source),
|
||||
data: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,14 +0,0 @@
|
||||
use std::process;
|
||||
|
||||
use log::error;
|
||||
|
||||
fn main() {
|
||||
// initalize the logger
|
||||
env_logger::init();
|
||||
|
||||
// handle errors returned from `run`
|
||||
if let Err(e) = peach_network::run() {
|
||||
error!("Application error: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
//! Retrieve network data and modify interface state.
|
||||
//!
|
||||
//! This module contains the core logic of the `peach-network` microservice and
|
||||
//! This module contains the core logic of the `peach-network` and
|
||||
//! provides convenience wrappers for a range of `wpasupplicant` commands,
|
||||
//! many of which are ordinarily executed using `wpa_cli` (a WPA command line
|
||||
//! client).
|
||||
@ -11,8 +11,8 @@
|
||||
//! Switching between client mode and access point mode is achieved by making
|
||||
//! system calls to systemd (via `systemctl`). Further networking functionality
|
||||
//! is provided by making system calls to retrieve interface state and write
|
||||
//! access point credentials to `wpa_supplicant-wlan0.conf`.
|
||||
//!
|
||||
//! access point credentials to `wpa_supplicant-<wlan_iface>.conf`.
|
||||
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
io::prelude::*,
|
||||
@ -21,68 +21,58 @@ use std::{
|
||||
str,
|
||||
};
|
||||
|
||||
use crate::error::NetworkError;
|
||||
use probes::network;
|
||||
|
||||
#[cfg(feature = "miniserde_support")]
|
||||
use miniserde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "serde_support")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::NetworkError;
|
||||
use crate::utils;
|
||||
|
||||
/// Network interface name.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Iface {
|
||||
pub iface: String,
|
||||
}
|
||||
|
||||
/// Network interface name and network identifier.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IfaceId {
|
||||
pub iface: String,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
/// Network interface name, network identifier and password.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IfaceIdPass {
|
||||
pub iface: String,
|
||||
pub id: String,
|
||||
pub pass: String,
|
||||
}
|
||||
|
||||
/// Network interface name and network SSID.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IfaceSsid {
|
||||
pub iface: String,
|
||||
pub ssid: String,
|
||||
}
|
||||
|
||||
/// Network SSID.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Network {
|
||||
pub ssid: String,
|
||||
}
|
||||
|
||||
/// Access point data retrieved via scan.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct Scan {
|
||||
/// Frequency.
|
||||
pub frequency: String,
|
||||
/// Protocol.
|
||||
pub protocol: String,
|
||||
/// Signal strength.
|
||||
pub signal_level: String,
|
||||
/// SSID.
|
||||
pub ssid: String,
|
||||
}
|
||||
|
||||
/// Status data for a network interface.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct Status {
|
||||
/// MAC address.
|
||||
pub address: Option<String>,
|
||||
/// Basic Service Set Identifier (BSSID).
|
||||
pub bssid: Option<String>,
|
||||
/// Frequency.
|
||||
pub freq: Option<String>,
|
||||
/// Group cipher.
|
||||
pub group_cipher: Option<String>,
|
||||
/// Local ID.
|
||||
pub id: Option<String>,
|
||||
/// IP address.
|
||||
pub ip_address: Option<String>,
|
||||
/// Key management.
|
||||
pub key_mgmt: Option<String>,
|
||||
/// Mode.
|
||||
pub mode: Option<String>,
|
||||
/// Pairwise cipher.
|
||||
pub pairwise_cipher: Option<String>,
|
||||
/// SSID.
|
||||
pub ssid: Option<String>,
|
||||
/// WPA state.
|
||||
pub wpa_state: Option<String>,
|
||||
}
|
||||
|
||||
@ -105,24 +95,16 @@ impl Status {
|
||||
}
|
||||
|
||||
/// Received and transmitted network traffic (bytes).
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||
pub struct Traffic {
|
||||
/// Total bytes received.
|
||||
pub received: u64,
|
||||
/// Total bytes transmitted.
|
||||
pub transmitted: u64,
|
||||
}
|
||||
|
||||
/// SSID and password for a wireless access point.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WiFi {
|
||||
pub ssid: String,
|
||||
pub pass: String,
|
||||
}
|
||||
|
||||
// TODO: wrap this into a helper function:
|
||||
//
|
||||
// let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
// let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
|
||||
/* GET - Methods for retrieving data */
|
||||
|
||||
/// Retrieve list of available wireless access points for a given network
|
||||
@ -133,15 +115,11 @@ pub struct WiFi {
|
||||
/// * `iface` - A string slice holding the name of a wireless network interface
|
||||
///
|
||||
/// If the scan results include one or more access points for the given network
|
||||
/// interface, an `Ok` `Result` type is returned containing `Some(String)` -
|
||||
/// where `String` is a serialized vector of `Scan` structs containing
|
||||
/// data for the in-range access points. If no access points are found,
|
||||
/// a `None` type is returned in the `Result`. In the event of an error, a
|
||||
/// `NetworkError` is returned in the `Result`. The `NetworkError` is then
|
||||
/// enumerated to a specific error type and an appropriate JSON RPC response is
|
||||
/// sent to the caller.
|
||||
///
|
||||
pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// interface, an `Ok` `Result` type is returned containing `Some(Vec<Scan>)`.
|
||||
/// The vector of `Scan` structs contains data for the in-range access points.
|
||||
/// If no access points are found, a `None` type is returned in the `Result`.
|
||||
/// In the event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
wpa.request("SCAN")?;
|
||||
@ -176,8 +154,7 @@ pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
if scan.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let results = serde_json::to_string(&scan)?;
|
||||
Ok(Some(results))
|
||||
Ok(Some(scan))
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,10 +170,7 @@ pub fn available_networks(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// found in the list of saved networks, an `Ok` `Result` type is returned
|
||||
/// containing `Some(String)` - where `String` is the network identifier.
|
||||
/// If no match is found, a `None` type is returned in the `Result`. In the
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`. The
|
||||
/// `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
@ -228,10 +202,7 @@ pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// an `Ok` `Result` type is returned containing `Some(String)` - where `String`
|
||||
/// is the IP address of the interface. If no match is found, a `None` type is
|
||||
/// returned in the `Result`. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`. The `NetworkError` is then enumerated to a
|
||||
/// specific error type and an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
/// returned in the `Result`.
|
||||
pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let net_if: String = iface.to_string();
|
||||
let ifaces = get_if_addrs::get_if_addrs().map_err(|source| NetworkError::NoIp {
|
||||
@ -258,9 +229,7 @@ pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// is the RSSI (Received Signal Strength Indicator) of the connection measured
|
||||
/// in dBm. If signal strength is not found, a `None` type is returned in the
|
||||
/// `Result`. In the event of an error, a `NetworkError` is returned in the
|
||||
/// `Result`. The `NetworkError` is then enumerated to a specific error type and
|
||||
/// an appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// `Result`.
|
||||
pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
@ -287,9 +256,7 @@ pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// is the RSSI (Received Signal Strength Indicator) of the connection measured
|
||||
/// as a percentage. If signal strength is not found, a `None` type is returned
|
||||
/// in the `Result`. In the event of an error, a `NetworkError` is returned in
|
||||
/// the `Result`. The `NetworkError` is then enumerated to a specific error type
|
||||
/// and an appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// the `Result`.
|
||||
pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
@ -319,14 +286,11 @@ pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
///
|
||||
/// If the wpasupplicant configuration file contains credentials for one or
|
||||
/// more access points, an `Ok` `Result` type is returned containing
|
||||
/// `Some(String)` - where `String` is a serialized vector of `Network` structs
|
||||
/// containing the SSIDs of all saved networks. If no network credentials are
|
||||
/// found, a `None` type is returned in the `Result`. In the event of an error,
|
||||
/// a `NetworkError` is returned in the `Result`. The `NetworkError` is then
|
||||
/// enumerated to a specific error type and an appropriate JSON RPC response is
|
||||
/// sent to the caller.
|
||||
///
|
||||
pub fn saved_networks() -> Result<Option<String>, NetworkError> {
|
||||
/// `Some(Vec<Network>)`. The vector of `Network` structs contains the SSIDs
|
||||
/// of all saved networks. If no network credentials are found, a `None` type
|
||||
/// is returned in the `Result`. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`.
|
||||
pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
||||
let networks = wpa.request("LIST_NETWORKS")?;
|
||||
let mut ssids = Vec::new();
|
||||
@ -335,16 +299,14 @@ pub fn saved_networks() -> Result<Option<String>, NetworkError> {
|
||||
let len = v.len();
|
||||
if len > 1 {
|
||||
let ssid = v[1].trim().to_string();
|
||||
let response = Network { ssid };
|
||||
ssids.push(response)
|
||||
ssids.push(ssid)
|
||||
}
|
||||
}
|
||||
|
||||
if ssids.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let results = serde_json::to_string(&ssids)?;
|
||||
Ok(Some(results))
|
||||
Ok(Some(ssids))
|
||||
}
|
||||
}
|
||||
|
||||
@ -358,10 +320,7 @@ pub fn saved_networks() -> Result<Option<String>, NetworkError> {
|
||||
/// an `Ok` `Result` type is returned containing `Some(String)` - where `String`
|
||||
/// is the SSID of the associated network. If SSID is not found, a `None` type
|
||||
/// is returned in the `Result`. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`. The `NetworkError` is then enumerated to a
|
||||
/// specific error type and an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
/// returned in the `Result`.
|
||||
pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
@ -383,9 +342,7 @@ pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// returned containing `Some(String)` - where `String` is the state of the
|
||||
/// network interface. If state is not found, a `None` type is returned in the
|
||||
/// `Result`. In the event of an error, a `NetworkError` is returned in the
|
||||
/// `Result`. The `NetworkError` is then enumerated to a specific error type and
|
||||
/// an appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// `Result`.
|
||||
pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
// construct the interface operstate path
|
||||
let iface_path: String = format!("/sys/class/net/{}/operstate", iface);
|
||||
@ -419,10 +376,7 @@ pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// returned containing `Some(Status)` - where `Status` is a `struct`
|
||||
/// containing the aggregated interface data in named fields. If status is not
|
||||
/// found, a `None` type is returned in the `Result`. In the event of an error,
|
||||
/// a `NetworkError` is returned in the `Result`. The `NetworkError` is then
|
||||
/// enumerated to a specific error type and an appropriate JSON RPC response is
|
||||
/// sent to the caller.
|
||||
///
|
||||
/// a `NetworkError` is returned in the `Result`.
|
||||
pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
@ -475,15 +429,12 @@ pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
|
||||
/// * `iface` - A string slice holding the name of a wireless network interface
|
||||
///
|
||||
/// If the network traffic statistics are found for the given interface, an `Ok`
|
||||
/// `Result` type is returned containing `Some(String)` - where `String` is a
|
||||
/// serialized `Traffic` `struct` with fields for received and transmitted
|
||||
/// network data statistics. If network traffic statistics are not found for the
|
||||
/// given interface, a `None` type is returned in the `Result`. In the event of
|
||||
/// an error, a `NetworkError` is returned in the `Result`. The `NetworkError`
|
||||
/// is then enumerated to a specific error type and an appropriate JSON RPC
|
||||
/// response is sent to the caller.
|
||||
///
|
||||
pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
/// `Result` type is returned containing `Some(Traffic)`. The `Traffic` `struct`
|
||||
/// includes fields for received and transmitted network data statistics. If
|
||||
/// network traffic statistics are not found for the given interface, a `None`
|
||||
/// type is returned in the `Result`. In the event of an error, a `NetworkError`
|
||||
/// is returned in the `Result`.
|
||||
pub fn traffic(iface: &str) -> Result<Option<Traffic>, NetworkError> {
|
||||
let network = network::read().map_err(|source| NetworkError::NoTraffic {
|
||||
iface: iface.to_string(),
|
||||
source,
|
||||
@ -497,9 +448,7 @@ pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
received,
|
||||
transmitted,
|
||||
};
|
||||
// TODO: add test for SerdeSerialize error
|
||||
let t = serde_json::to_string(&traffic)?;
|
||||
return Ok(Some(t));
|
||||
return Ok(Some(traffic));
|
||||
}
|
||||
}
|
||||
|
||||
@ -508,42 +457,25 @@ pub fn traffic(iface: &str) -> Result<Option<String>, NetworkError> {
|
||||
|
||||
/* SET - Methods for modifying state */
|
||||
|
||||
/// Activate wireless access point.
|
||||
/// Start network interface service.
|
||||
///
|
||||
/// A `systemctl `command is invoked which starts the `ap0` interface service.
|
||||
/// If the command executes successfully, an `Ok` `Result` type is returned.
|
||||
/// In the event of an error, a `NetworkError` is returned in the `Result`.
|
||||
/// The `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
pub fn activate_ap() -> Result<(), NetworkError> {
|
||||
// start the ap0 interface service
|
||||
/// A `systemctl `command is invoked which starts the service for the given
|
||||
/// network interface. If the command executes successfully, an `Ok` `Result`
|
||||
/// type is returned. In the event of an error, a `NetworkError` is returned
|
||||
/// in the `Result`.
|
||||
pub fn start_iface_service(iface: &str) -> Result<(), NetworkError> {
|
||||
let iface_service = format!("wpa_supplicant@{}.service", &iface);
|
||||
|
||||
// start the interface service
|
||||
Command::new("sudo")
|
||||
.arg("/usr/bin/systemctl")
|
||||
.arg("start")
|
||||
.arg("wpa_supplicant@ap0.service")
|
||||
.arg(iface_service)
|
||||
.output()
|
||||
.map_err(NetworkError::StartAp0)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Activate wireless client.
|
||||
///
|
||||
/// A `systemctl` command is invoked which starts the `wlan0` interface service.
|
||||
/// If the command executes successfully, an `Ok` `Result` type is returned.
|
||||
/// In the event of an error, a `NetworkError` is returned in the `Result`.
|
||||
/// The `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
pub fn activate_client() -> Result<(), NetworkError> {
|
||||
// start the wlan0 interface service
|
||||
Command::new("sudo")
|
||||
.arg("/usr/bin/systemctl")
|
||||
.arg("start")
|
||||
.arg("wpa_supplicant@wlan0.service")
|
||||
.output()
|
||||
.map_err(NetworkError::StartWlan0)?;
|
||||
.map_err(|source| NetworkError::StartInterface {
|
||||
source,
|
||||
iface: iface.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -552,24 +484,23 @@ pub fn activate_client() -> Result<(), NetworkError> {
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `wlan_iface` - A local wireless interface.
|
||||
/// * `wifi` - An instance of the `WiFi` `struct` with fields `ssid` and `pass`
|
||||
///
|
||||
/// If configuration parameters are successfully generated from the provided
|
||||
/// SSID and password and appended to `wpa_supplicant-wlan0.conf`, an `Ok`
|
||||
/// `Result` type is returned. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`. The `NetworkError` is then enumerated to a
|
||||
/// specific error type and an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
pub fn add(wifi: &WiFi) -> Result<(), NetworkError> {
|
||||
/// SSID and password and appended to `wpa_supplicant-<wlan_iface>.conf` (where
|
||||
/// `<wlan_iface>` is the provided interface parameter), an `Ok` `Result` type
|
||||
/// is returned. In the event of an error, a `NetworkError` is returned in the
|
||||
/// `Result`.
|
||||
pub fn add(wlan_iface: &str, ssid: &str, pass: &str) -> Result<(), NetworkError> {
|
||||
// generate configuration based on provided ssid & password
|
||||
let output = Command::new("wpa_passphrase")
|
||||
.arg(&wifi.ssid)
|
||||
.arg(&wifi.pass)
|
||||
.arg(&ssid)
|
||||
.arg(&pass)
|
||||
.stdout(Stdio::piped())
|
||||
.output()
|
||||
.map_err(|source| NetworkError::GenWpaPassphrase {
|
||||
ssid: wifi.ssid.to_string(),
|
||||
ssid: ssid.to_string(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
@ -577,12 +508,12 @@ pub fn add(wifi: &WiFi) -> Result<(), NetworkError> {
|
||||
let mut wpa_details = "\n".as_bytes().to_vec();
|
||||
wpa_details.extend(&*(output.stdout));
|
||||
|
||||
// append wpa_passphrase output to wpa_supplicant-wlan0.conf if successful
|
||||
let wlan_config = format!("/etc/wpa_supplicant/wpa_supplicant-{}.conf", wlan_iface);
|
||||
|
||||
// append wpa_passphrase output to wpa_supplicant-<wlan_iface>.conf if successful
|
||||
if output.status.success() {
|
||||
// open file in append mode
|
||||
let file = OpenOptions::new()
|
||||
.append(true)
|
||||
.open("/etc/wpa_supplicant/wpa_supplicant-wlan0.conf");
|
||||
let file = OpenOptions::new().append(true).open(wlan_config);
|
||||
|
||||
let _file = match file {
|
||||
// if file exists & open succeeds, write wifi configuration
|
||||
@ -596,40 +527,41 @@ pub fn add(wifi: &WiFi) -> Result<(), NetworkError> {
|
||||
} else {
|
||||
let err_msg = String::from_utf8_lossy(&output.stdout);
|
||||
Err(NetworkError::GenWpaPassphraseWarning {
|
||||
ssid: wifi.ssid.to_string(),
|
||||
ssid: ssid.to_string(),
|
||||
err_msg: err_msg.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Deploy the access point if the `wlan0` interface is `up` without an active
|
||||
/// Deploy an access point if the wireless interface is `up` without an active
|
||||
/// connection.
|
||||
///
|
||||
/// The status of the `wlan0` service and the state of the `wlan0` interface
|
||||
/// The status of the wireless service and the state of the wireless interface
|
||||
/// are checked. If the service is active but the interface is down (ie. not
|
||||
/// currently connected to an access point), then the access point is activated
|
||||
/// by calling the `activate_ap()` function.
|
||||
///
|
||||
pub fn check_iface() -> Result<(), NetworkError> {
|
||||
pub fn check_iface(wlan_iface: &str, ap_iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_service = format!("wpa_supplicant@{}.service", &wlan_iface);
|
||||
|
||||
// returns 0 if the service is currently active
|
||||
let wlan0_status = Command::new("/usr/bin/systemctl")
|
||||
let wlan_status = Command::new("/usr/bin/systemctl")
|
||||
.arg("is-active")
|
||||
.arg("wpa_supplicant@wlan0.service")
|
||||
.arg(wpa_service)
|
||||
.status()
|
||||
.map_err(NetworkError::WlanState)?;
|
||||
|
||||
// returns the current state of the wlan0 interface
|
||||
let iface_state = state("wlan0")?;
|
||||
// returns the current state of the wlan interface
|
||||
let iface_state = state(wlan_iface)?;
|
||||
|
||||
// returns down if the interface is not currently connected to an ap
|
||||
let wlan0_state = match iface_state {
|
||||
let wlan_state = match iface_state {
|
||||
Some(state) => state,
|
||||
None => "error".to_string(),
|
||||
};
|
||||
|
||||
// if wlan0 is active but not connected, start the ap0 service
|
||||
if wlan0_status.success() && wlan0_state == "down" {
|
||||
activate_ap()?
|
||||
// if wlan is active but not connected, start the ap service
|
||||
if wlan_status.success() && wlan_state == "down" {
|
||||
start_iface_service(ap_iface)?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -646,10 +578,7 @@ pub fn check_iface() -> Result<(), NetworkError> {
|
||||
/// If the network connection is successfully activated for the access point
|
||||
/// represented by the given network identifier on the given wireless interface,
|
||||
/// an `Ok` `Result`type is returned. In the event of an error, a `NetworkError`
|
||||
/// is returned in the `Result`. The `NetworkError` is then enumerated to a
|
||||
/// specific error type and an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
/// is returned in the `Result`.
|
||||
pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
@ -668,10 +597,7 @@ pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
/// If the network configuration parameters are successfully deleted for
|
||||
/// the access point represented by the given network identifier, an `Ok`
|
||||
/// `Result`type is returned. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`. The `NetworkError` is then enumerated to a
|
||||
/// specific error type and an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
/// returned in the `Result`.
|
||||
pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
@ -690,9 +616,7 @@ pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
/// If the network connection is successfully disabled for the access point
|
||||
/// represented by the given network identifier, an `Ok` `Result`type is
|
||||
/// returned. In the event of an error, a `NetworkError` is returned in the
|
||||
/// `Result`. The `NetworkError` is then enumerated to a specific error type and
|
||||
/// an appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// `Result`.
|
||||
pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
@ -709,10 +633,7 @@ pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
|
||||
///
|
||||
/// If the network connection is successfully disconnected for the given
|
||||
/// wireless interface, an `Ok` `Result` type is returned. In the event of an
|
||||
/// error, a `NetworkError` is returned in the `Result`. The `NetworkError` is
|
||||
/// then enumerated to a specific error type and an appropriate JSON RPC
|
||||
/// response is sent to the caller.
|
||||
///
|
||||
/// error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
@ -731,10 +652,7 @@ pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
|
||||
///
|
||||
/// If the password is successfully updated for the access point represented by
|
||||
/// the given network identifier, an `Ok` `Result` type is returned. In the
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`. The
|
||||
/// `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
@ -751,10 +669,7 @@ pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
|
||||
///
|
||||
/// If the network connection is successfully reassociated for the given
|
||||
/// wireless interface, an `Ok` `Result` type is returned. In the event of an
|
||||
/// error, a `NetworkError` is returned in the `Result`. The `NetworkError` is
|
||||
/// then enumerated to a specific error type and an appropriate JSON RPC
|
||||
/// response is sent to the caller.
|
||||
///
|
||||
/// error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
@ -767,10 +682,7 @@ pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
|
||||
/// If the reconfigure command is successfully executed, indicating a reread
|
||||
/// of the `wpa_supplicant.conf` file by the `wpa_supplicant` process, an `Ok`
|
||||
/// `Result` type is returned. In the event of an error, a `NetworkError` is
|
||||
/// returned in the `Result`. The `NetworkError` is then enumerated to a
|
||||
/// specific error type and an appropriate JSON RPC response is sent to the
|
||||
/// caller.
|
||||
///
|
||||
/// returned in the `Result`.
|
||||
pub fn reconfigure() -> Result<(), NetworkError> {
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
||||
wpa.request("RECONFIGURE")?;
|
||||
@ -785,10 +697,7 @@ pub fn reconfigure() -> Result<(), NetworkError> {
|
||||
///
|
||||
/// If the network connection is successfully disconnected and reconnected for
|
||||
/// the given wireless interface, an `Ok` `Result` type is returned. In the
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`. The
|
||||
/// `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
|
||||
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
|
||||
@ -801,10 +710,7 @@ pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
|
||||
///
|
||||
/// If wireless network configuration updates are successfully save to the
|
||||
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`. The
|
||||
/// `NetworkError` is then enumerated to a specific error type and an
|
||||
/// appropriate JSON RPC response is sent to the caller.
|
||||
///
|
||||
/// event of an error, a `NetworkError` is returned in the `Result`.
|
||||
pub fn save() -> Result<(), NetworkError> {
|
||||
let mut wpa = wpactrl::WpaCtrl::builder().open()?;
|
||||
wpa.request("SAVE_CONFIG")?;
|
||||
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,4 +0,0 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
objcopy = { path ="aarch64-linux-gnu-objcopy" }
|
||||
strip = { path ="aarch64-linux-gnu-strip" }
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "peach-web"
|
||||
version = "0.4.12"
|
||||
version = "0.4.17"
|
||||
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
|
||||
edition = "2018"
|
||||
description = "peach-web is a web application which provides a web interface for monitoring and interacting with the PeachCloud device. This allows administration of the single-board computer (ie. Raspberry Pi) running PeachCloud, as well as the ssb-server and related plugins."
|
||||
@ -21,6 +21,7 @@ maintainer-scripts="debian"
|
||||
systemd-units = { unit-name = "peach-web" }
|
||||
assets = [
|
||||
["target/release/peach-web", "/usr/bin/", "755"],
|
||||
["Rocket.toml", "/usr/share/peach-web/Rocket.toml", "644"],
|
||||
["templates/**/*", "/usr/share/peach-web/templates/", "644"],
|
||||
["static/*", "/usr/share/peach-web/static/", "644"],
|
||||
["static/css/*", "/usr/share/peach-web/static/css/", "644"],
|
||||
@ -40,6 +41,8 @@ log = "0.4"
|
||||
nest = "1.0.0"
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
peach-lib = { path = "../peach-lib" }
|
||||
peach-network = { path = "../peach-network", features = ["serde_support"] }
|
||||
peach-stats = { path = "../peach-stats", features = ["serde_support"] }
|
||||
percent-encoding = "2.1.0"
|
||||
regex = "1"
|
||||
rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] }
|
||||
|
@ -97,6 +97,20 @@ Remove configuration files (not removed with `apt-get remove`):
|
||||
|
||||
`peach-web` is built on the Rocket webserver and Tera templating engine. It presents a web interface for interacting with the device. HTML is rendered server-side. Request handlers call JSON-RPC microservices and serve HTML and assets. A JSON API is exposed for remote calls and dynamic client-side content updates (via plain JavaScript following unobstructive design principles). Each Tera template is passed a context object. In the case of Rust, this object is a `struct` and must implement `Serialize`. The fields of the context object are available in the context of the template to be rendered.
|
||||
|
||||
### Configuration
|
||||
|
||||
Configuration variables are stored in /var/lib/peachcloud/config.yml.
|
||||
Peach-web also updates this file when changes are made to configurations via
|
||||
the web interface. peach-web has no database, so all configurations are stored in this file.
|
||||
|
||||
#### Dynamic DNS Configuration
|
||||
|
||||
Most users will want to use the default PeachCloud dynamic dns server.
|
||||
If the config dyn_use_custom_server=false, then default values will be used.
|
||||
If the config dyn_use_custom_server=true, then a value must also be set for dyn_dns_server_address (e.g. "http://peachdynserver.commoninternet.net").
|
||||
This value is the URL of the instance of peach-dyndns-server that requests will be sent to for domain registration.
|
||||
Using a custom value can here can be useful for testing.
|
||||
|
||||
### Licensing
|
||||
|
||||
AGPL-3.0
|
||||
|
@ -1,3 +1,6 @@
|
||||
[default]
|
||||
secret_key = "VYVUDivXvu8g6llxeJd9F92pMfocml5xl/Jjv5Sk4yw="
|
||||
|
||||
[development]
|
||||
template_dir = "templates/"
|
||||
disable_auth = true
|
||||
|
@ -5,54 +5,18 @@ set -e
|
||||
adduser --quiet --system peach-web
|
||||
usermod -g peach peach-web
|
||||
|
||||
# create secret passwords folder if it doesn't already exist
|
||||
mkdir -p /var/lib/peachcloud/passwords
|
||||
chown -R peach-web:peach /var/lib/peachcloud/passwords
|
||||
chmod -R u+rwX,go+rX,go-w /var/lib/peachcloud/passwords
|
||||
|
||||
# create nginx config
|
||||
cat <<EOF > /etc/nginx/sites-enabled/default
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name peach.local www.peach.local;
|
||||
|
||||
# nginx authentication
|
||||
auth_basic "If you have forgotten your password visit: http://peach.local/send_password_reset/";
|
||||
auth_basic_user_file /var/lib/peachcloud/passwords/htpasswd;
|
||||
|
||||
# remove trailing slash if found
|
||||
rewrite ^/(.*)/$ /$1 permanent;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
|
||||
# public routes
|
||||
location /send_password_reset {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /reset_password {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /public/ {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /js/ {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /css/ {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
location /icons/ {
|
||||
auth_basic off;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
|
||||
}
|
||||
EOF
|
||||
|
||||
|
@ -40,7 +40,7 @@ pub fn save_dns_configuration(dns_form: DnsForm) -> Result<(), PeachWebError> {
|
||||
if dns_form.enable_dyndns {
|
||||
let full_dynamic_domain = get_full_dynamic_domain(&dns_form.dynamic_domain);
|
||||
// check if this is a new domain or if its already registered
|
||||
let is_new_domain = check_is_new_dyndns_domain(&full_dynamic_domain);
|
||||
let is_new_domain = check_is_new_dyndns_domain(&full_dynamic_domain)?;
|
||||
if is_new_domain {
|
||||
match dyndns_client::register_domain(&full_dynamic_domain) {
|
||||
Ok(_) => {
|
||||
|
@ -3,8 +3,8 @@ use rocket::{
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
serde::json::Value,
|
||||
};
|
||||
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
@ -12,13 +12,16 @@ use std::{
|
||||
process::{Command, Output},
|
||||
};
|
||||
|
||||
use peach_lib::config_manager::load_peach_config;
|
||||
use peach_lib::stats_client::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat};
|
||||
use peach_lib::{dyndns_client, network_client, oled_client, sbot_client, stats_client};
|
||||
use peach_lib::{
|
||||
config_manager::load_peach_config, dyndns_client, network_client, oled_client, sbot_client,
|
||||
};
|
||||
use peach_stats::{
|
||||
stats,
|
||||
stats::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat},
|
||||
};
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
use crate::utils::build_json_response;
|
||||
use rocket::serde::json::Value;
|
||||
|
||||
// HELPERS AND ROUTES FOR /status
|
||||
|
||||
@ -34,7 +37,6 @@ pub struct StatusContext {
|
||||
pub mem_stats: Option<MemStat>,
|
||||
pub network_ping: String,
|
||||
pub oled_ping: String,
|
||||
pub stats_ping: String,
|
||||
pub dyndns_enabled: bool,
|
||||
pub dyndns_is_online: bool,
|
||||
pub config_is_valid: bool,
|
||||
@ -46,9 +48,12 @@ pub struct StatusContext {
|
||||
impl StatusContext {
|
||||
pub fn build() -> StatusContext {
|
||||
// convert result to Option<CpuStatPercentages>, discard any error
|
||||
let cpu_stat_percent = stats_client::cpu_stats_percent().ok();
|
||||
let load_average = stats_client::load_average().ok();
|
||||
let mem_stats = stats_client::mem_stats().ok();
|
||||
let cpu_stat_percent = stats::cpu_stats_percent().ok();
|
||||
let load_average = stats::load_average().ok();
|
||||
let mem_stats = stats::mem_stats().ok();
|
||||
// TODO: add `wpa_supplicant_status` to peach_network to replace this ping call
|
||||
// instead of: "is the network json-rpc server running?", we want to ask:
|
||||
// "is the wpa_supplicant systemd service functioning correctly?"
|
||||
let network_ping = match network_client::ping() {
|
||||
Ok(_) => "ONLINE".to_string(),
|
||||
Err(_) => "OFFLINE".to_string(),
|
||||
@ -57,22 +62,21 @@ impl StatusContext {
|
||||
Ok(_) => "ONLINE".to_string(),
|
||||
Err(_) => "OFFLINE".to_string(),
|
||||
};
|
||||
let stats_ping = match stats_client::ping() {
|
||||
Ok(_) => "ONLINE".to_string(),
|
||||
Err(_) => "OFFLINE".to_string(),
|
||||
};
|
||||
let uptime = match stats_client::uptime() {
|
||||
Ok(mins) => mins,
|
||||
|
||||
let uptime = match stats::uptime() {
|
||||
Ok(secs) => {
|
||||
let uptime_mins = secs / 60;
|
||||
uptime_mins.to_string()
|
||||
}
|
||||
Err(_) => "Unavailable".to_string(),
|
||||
};
|
||||
|
||||
// parse the uptime string to a signed integer (for math)
|
||||
let uptime_parsed = uptime.parse::<i32>().ok();
|
||||
|
||||
// serialize disk usage data into Vec<DiskUsage>
|
||||
let disk_usage_stats = match stats_client::disk_usage() {
|
||||
Ok(disks) => {
|
||||
let partitions: Vec<DiskUsage> = serde_json::from_str(disks.as_str())
|
||||
.expect("Failed to deserialize disk_usage response");
|
||||
partitions
|
||||
}
|
||||
let disk_usage_stats = match stats::disk_usage() {
|
||||
Ok(disks) => disks,
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
|
||||
@ -84,9 +88,6 @@ impl StatusContext {
|
||||
}
|
||||
}
|
||||
|
||||
// parse the uptime string to a signed integer (for math)
|
||||
let uptime_parsed = uptime.parse::<i32>().ok();
|
||||
|
||||
// dyndns_is_online & config_is_valid
|
||||
let dyndns_enabled: bool;
|
||||
let dyndns_is_online: bool;
|
||||
@ -139,7 +140,6 @@ impl StatusContext {
|
||||
mem_stats,
|
||||
network_ping,
|
||||
oled_ping,
|
||||
stats_ping,
|
||||
dyndns_enabled,
|
||||
dyndns_is_online,
|
||||
config_is_valid,
|
||||
|
@ -2,25 +2,77 @@ use rocket::{get, request::FlashMessage};
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::Serialize;
|
||||
|
||||
use peach_lib::network_client;
|
||||
use peach_lib::stats_client::Traffic;
|
||||
use peach_network::{
|
||||
network,
|
||||
network::{Status, Traffic},
|
||||
};
|
||||
|
||||
use crate::routes::authentication::Authenticated;
|
||||
|
||||
// HELPERS AND ROUTES FOR /status/network
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IfaceTraffic {
|
||||
pub rx: u64,
|
||||
pub rx_unit: Option<String>,
|
||||
pub tx: u64,
|
||||
pub tx_unit: Option<String>,
|
||||
}
|
||||
|
||||
impl IfaceTraffic {
|
||||
fn default() -> Self {
|
||||
IfaceTraffic {
|
||||
rx: 0,
|
||||
rx_unit: None,
|
||||
tx: 0,
|
||||
tx_unit: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_traffic(traffic: Traffic) -> Option<IfaceTraffic> {
|
||||
let mut t = IfaceTraffic::default();
|
||||
// modify traffic values & assign measurement units
|
||||
// based on received and transmitted values.
|
||||
// if received > 999 MB, convert it to GB
|
||||
if traffic.received > 1_047_527_424 {
|
||||
t.rx = traffic.received / 1_073_741_824;
|
||||
t.rx_unit = Some("GB".to_string());
|
||||
} else if traffic.received > 0 {
|
||||
// otherwise, convert it to MB
|
||||
t.rx = (traffic.received / 1024) / 1024;
|
||||
t.rx_unit = Some("MB".to_string());
|
||||
} else {
|
||||
t.rx = 0;
|
||||
t.rx_unit = Some("MB".to_string());
|
||||
}
|
||||
|
||||
if traffic.transmitted > 1_047_527_424 {
|
||||
t.tx = traffic.transmitted / 1_073_741_824;
|
||||
t.tx_unit = Some("GB".to_string());
|
||||
} else if traffic.transmitted > 0 {
|
||||
t.tx = (traffic.transmitted / 1024) / 1024;
|
||||
t.tx_unit = Some("MB".to_string());
|
||||
} else {
|
||||
t.tx = 0;
|
||||
t.tx_unit = Some("MB".to_string());
|
||||
}
|
||||
|
||||
Some(t)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NetworkContext {
|
||||
pub ap_ip: String,
|
||||
pub ap_ssid: String,
|
||||
pub ap_state: String,
|
||||
pub ap_traffic: Option<Traffic>,
|
||||
pub ap_traffic: Option<IfaceTraffic>,
|
||||
pub wlan_ip: String,
|
||||
pub wlan_rssi: Option<String>,
|
||||
pub wlan_ssid: String,
|
||||
pub wlan_state: String,
|
||||
pub wlan_status: String,
|
||||
pub wlan_traffic: Option<Traffic>,
|
||||
pub wlan_status: Option<Status>,
|
||||
pub wlan_traffic: Option<IfaceTraffic>,
|
||||
pub flash_name: Option<String>,
|
||||
pub flash_msg: Option<String>,
|
||||
// page title for header in navbar
|
||||
@ -31,101 +83,47 @@ pub struct NetworkContext {
|
||||
|
||||
impl NetworkContext {
|
||||
pub fn build() -> NetworkContext {
|
||||
let ap_ip = match network_client::ip("ap0") {
|
||||
Ok(ip) => ip,
|
||||
Err(_) => "x.x.x.x".to_string(),
|
||||
let ap_ip = match network::ip("ap0") {
|
||||
Ok(Some(ip)) => ip,
|
||||
_ => "x.x.x.x".to_string(),
|
||||
};
|
||||
let ap_ssid = match network_client::ssid("ap0") {
|
||||
Ok(ssid) => ssid,
|
||||
Err(_) => "Not currently activated".to_string(),
|
||||
let ap_ssid = match network::ssid("ap0") {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not currently activated".to_string(),
|
||||
};
|
||||
let ap_state = match network_client::state("ap0") {
|
||||
Ok(state) => state,
|
||||
Err(_) => "Interface unavailable".to_string(),
|
||||
let ap_state = match network::state("ap0") {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
let ap_traffic = match network_client::traffic("ap0") {
|
||||
Ok(traffic) => {
|
||||
let mut t = traffic;
|
||||
// modify traffic values & assign measurement unit
|
||||
// based on received and transmitted values
|
||||
// if received > 999 MB, convert it to GB
|
||||
if t.received > 1_047_527_424 {
|
||||
t.received /= 1_073_741_824;
|
||||
t.rx_unit = Some("GB".to_string());
|
||||
} else if t.received > 0 {
|
||||
// otherwise, convert it to MB
|
||||
t.received = (t.received / 1024) / 1024;
|
||||
t.rx_unit = Some("MB".to_string());
|
||||
} else {
|
||||
t.received = 0;
|
||||
t.rx_unit = Some("MB".to_string());
|
||||
}
|
||||
|
||||
if t.transmitted > 1_047_527_424 {
|
||||
t.transmitted /= 1_073_741_824;
|
||||
t.tx_unit = Some("GB".to_string());
|
||||
} else if t.transmitted > 0 {
|
||||
t.transmitted = (t.transmitted / 1024) / 1024;
|
||||
t.tx_unit = Some("MB".to_string());
|
||||
} else {
|
||||
t.transmitted = 0;
|
||||
t.tx_unit = Some("MB".to_string());
|
||||
}
|
||||
Some(t)
|
||||
}
|
||||
Err(_) => None,
|
||||
let ap_traffic = match network::traffic("ap0") {
|
||||
// convert bytes to mb or gb and add appropriate units
|
||||
Ok(Some(traffic)) => convert_traffic(traffic),
|
||||
_ => None,
|
||||
};
|
||||
let wlan_ip = match network_client::ip("wlan0") {
|
||||
Ok(ip) => ip,
|
||||
Err(_) => "x.x.x.x".to_string(),
|
||||
let wlan_ip = match network::ip("wlan0") {
|
||||
Ok(Some(ip)) => ip,
|
||||
_ => "x.x.x.x".to_string(),
|
||||
};
|
||||
let wlan_rssi = match network_client::rssi_percent("wlan0") {
|
||||
Ok(rssi) => Some(rssi),
|
||||
Err(_) => None,
|
||||
let wlan_rssi = match network::rssi_percent("wlan0") {
|
||||
Ok(rssi) => rssi,
|
||||
_ => None,
|
||||
};
|
||||
let wlan_ssid = match network_client::ssid("wlan0") {
|
||||
Ok(ssid) => ssid,
|
||||
Err(_) => "Not connected".to_string(),
|
||||
let wlan_ssid = match network::ssid("wlan0") {
|
||||
Ok(Some(ssid)) => ssid,
|
||||
_ => "Not connected".to_string(),
|
||||
};
|
||||
let wlan_state = match network_client::state("wlan0") {
|
||||
Ok(state) => state,
|
||||
Err(_) => "Interface unavailable".to_string(),
|
||||
let wlan_state = match network::state("wlan0") {
|
||||
Ok(Some(state)) => state,
|
||||
_ => "Interface unavailable".to_string(),
|
||||
};
|
||||
let wlan_status = match network_client::status("wlan0") {
|
||||
let wlan_status = match network::status("wlan0") {
|
||||
Ok(status) => status,
|
||||
Err(_) => "Interface unavailable".to_string(),
|
||||
_ => None,
|
||||
};
|
||||
let wlan_traffic = match network_client::traffic("wlan0") {
|
||||
Ok(traffic) => {
|
||||
let mut t = traffic;
|
||||
// modify traffic values & assign measurement unit
|
||||
// based on received and transmitted values
|
||||
// if received > 999 MB, convert it to GB
|
||||
if t.received > 1_047_527_424 {
|
||||
t.received /= 1_073_741_824;
|
||||
t.rx_unit = Some("GB".to_string());
|
||||
} else if t.received > 0 {
|
||||
// otherwise, convert it to MB
|
||||
t.received = (t.received / 1024) / 1024;
|
||||
t.rx_unit = Some("MB".to_string());
|
||||
} else {
|
||||
t.received = 0;
|
||||
t.rx_unit = Some("MB".to_string());
|
||||
}
|
||||
|
||||
if t.transmitted > 1_047_527_424 {
|
||||
t.transmitted /= 1_073_741_824;
|
||||
t.tx_unit = Some("GB".to_string());
|
||||
} else if t.transmitted > 0 {
|
||||
t.transmitted = (t.transmitted / 1024) / 1024;
|
||||
t.tx_unit = Some("MB".to_string());
|
||||
} else {
|
||||
t.transmitted = 0;
|
||||
t.tx_unit = Some("MB".to_string());
|
||||
}
|
||||
Some(t)
|
||||
}
|
||||
Err(_) => None,
|
||||
let wlan_traffic = match network::traffic("wlan0") {
|
||||
// convert bytes to mb or gb and add appropriate units
|
||||
Ok(Some(traffic)) => convert_traffic(traffic),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
NetworkContext {
|
||||
|
@ -42,11 +42,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- PEACH-STATS STATUS STACK -->
|
||||
<div class="stack capsule{% if stats_ping == "ONLINE" %} success-border{% else %} warning-border{% endif %}">
|
||||
<img id="statsIcon" class="icon{% if stats_ping == "OFFLINE" %} icon-inactive{% endif %} icon-medium" alt="Stats" title="System statistics microservice status" src="/icons/chart.svg">
|
||||
<div class="stack capsule success-border">
|
||||
<img id="statsIcon" class="icon icon-medium" alt="Stats" title="System statistics microservice status" src="/icons/chart.svg">
|
||||
<div class="stack" style="padding-top: 0.5rem;">
|
||||
<label class="label-small font-near-black">Statistics</label>
|
||||
<label class="label-small font-near-black">{{ stats_ping }}</label>
|
||||
<label class="label-small font-near-black">AVAILABLE</label>
|
||||
</div>
|
||||
</div>
|
||||
{# Display status for dynsdns, config & sbot #}
|
||||
|
Reference in New Issue
Block a user