5 Commits

271 changed files with 12977 additions and 11561 deletions

View File

@ -1,33 +0,0 @@
kind: pipeline
type: docker
name: test-on-amd64
platform:
arch: amd64
steps:
- name: rustfmt
image: rust:buster
commands:
- rustup component add rustfmt
- cargo fmt --check
- name: clippy
image: rust:buster
commands:
- rustup component add clippy
- cargo clippy -- -D warnings
- name: test
image: rust:buster
commands:
- cargo test
- name: build
image: rust:buster
commands:
- cargo build
trigger:
event:
- pull_request

1
.gitignore vendored
View File

@ -2,4 +2,3 @@
target target
*peachdeploy.sh *peachdeploy.sh
*vpsdeploy.sh *vpsdeploy.sh
*bindeploy.sh

3161
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
[workspace] [workspace]
members = [ members = [
"peach-buttons",
"peach-oled", "peach-oled",
"peach-lib", "peach-lib",
"peach-config", "peach-config",
@ -9,7 +11,5 @@ members = [
"peach-monitor", "peach-monitor",
"peach-stats", "peach-stats",
"peach-jsonrpc-server", "peach-jsonrpc-server",
"peach-dyndns-updater", "peach-dyndns-updater"
"tilde-client"
] ]

View File

@ -1,74 +0,0 @@
# [PeachCloud](http://peachcloud.org) :peach: :cloud:
_Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware product._
[**_Support us on OpenCollective!_**](https://opencollective.com/peachcloud)
[![Build Status](https://build.coopcloud.tech/api/badges/PeachCloud/peach-workspace/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/PeachCloud/peach-workspace)
## Background
- April 2018 project proposal: [`%HqwAsltORROCh4uyOq6iV+SsqU3OuNUevnq+5dwCqVI=.sha256`](https://viewer.scuttlebot.io/%25HqwAsltORROCh4uyOq6iV%2BSsqU3OuNUevnq%2B5dwCqVI%3D.sha256)
- November 2018 project pivot: [`%9NCyTf+oBxG0APlXRCKtrGZj3t+i+Kp3pKPN1gtFX2c=.sha256`](https://viewer.scuttlebot.io/%259NCyTf%2BoBxG0APlXRCKtrGZj3t%2Bi%2BKp3pKPN1gtFX2c%3D.sha256)
## Active Repositories
**Documentation**
- [peach-devdocs](https://github.com/peachcloud/peach-devdocs) - Developer documentation for PeachCloud in the form of a Markdown book
**Devops**
- [peach-vps](https://github.com/peachcloud/peach-vps) - Setup scripts and configuration files for deploying a PeachCloud development server
**Image building & device configuration**
- [peach-config](https://github.com/peachcloud/peach-config) - Configuration instructions, files and scripts
- [peach-img-builder](https://github.com/peachcloud/peach-img-builder) - Vmdb2 script for building a Debian disc image for Raspberry Pi with PeachCloud pre-installed
**Microservices**
- [peach-buttons](https://github.com/peachcloud/peach-buttons) - Emit GPIO events using JSON-RPC pubsub over WS
- [peach-oled](https://github.com/peachcloud/peach-oled) - Write and draw to OLED display using JSON-RPC over HTTP
- [peach-menu](https://github.com/peachcloud/peach-menu) - A menu for monitoring and interacting with the PeachCloud device
- [peach-network](https://github.com/peachcloud/peach-network) - Query and configure network interfaces using JSON-RPC over HTTP
- [peach-stats](https://github.com/peachcloud/peach-stats) - Query system statistics using JSON-RPC over HTTP
- [peach-lib](https://github.com/peachcloud/peach-lib) - JSON-RPC client library for the PeachCloud ecosystem
- [peach-monitor](https://github.com/peachcloud/peach-monitor) - Monitor network data usage and set alert flags based on user-defined thresholds
**Diagnostics**
- [peach-probe](https://github.com/peachcloud/peach-probe) - Probe PeachCloud microservices to evaluate their state and ensure correct API responses
**Web interface**
- [peach-patterns](https://github.com/peachcloud/peach-patterns) - Pattern library for the PeachCloud UI design system
- [peach-web](https://github.com/peachcloud/peach-web) - A web interface for monitoring and interacting with the PeachCloud device
## Continuous Integration
[Drone CI](https://docs.drone.io/) is used to provide continuous integration for this workspace. The configuration file can be found in `.drone.yml` in the root of this repository. It is currently configured to run `cargo fmt`, `cargo clippy`, `cargo test` and `cargo build` on every `pull request` event. The pipeline runs on the AMD64 Debian Buster image from the official Rust Docker image repository.
The status of the current and previous CI builds can be viewed via the [Drone CI Build UI](https://build.coopcloud.tech/PeachCloud/peach-workspace) (kindly hosted by Co-op Cloud).
Adding `[CI SKIP]` to the end of a commit message results in the CI checks being skipped for the next event. For example:
```
git commit -m "update readme [CI SKIP]"
git push origin main
```
## Developer Diaries
- [@ahdinosaur](https://github.com/ahdinosaur): `@6ilZq3kN0F+dXFHAPjAwMm87JEb/VdB+LC9eIMW3sa0=.ed25519`
- 1: [`%bSkZCJBmNYUmECNKYOiWkgEeRxrlo2UghNBzE6Cph94=.sha256`](https://viewer.scuttlebot.io/%25bSkZCJBmNYUmECNKYOiWkgEeRxrlo2UghNBzE6Cph94%3D.sha256)
- 2: [`%2L7gYAh2ih+7eFCrtObPWIUYHuGnJjwj4KCXrCIsWhM=.sha256`](https://viewer.scuttlebot.io/%252L7gYAh2ih%2B7eFCrtObPWIUYHuGnJjwj4KCXrCIsWhM%3D.sha256)
- [@mycognosist](https://github.com/mycognosist): `@HEqy940T6uB+T+d9Jaa58aNfRzLx9eRWqkZljBmnkmk=.ed25519`
- [`%mKUByRp4Gib6fqP1q2/dHg+ueSoR+Sj2Y0D7T0Np0D4=.sha256`](https://viewer.scuttlebot.io/%25mKUByRp4Gib6fqP1q2%2FdHg%2BueSoR%2BSj2Y0D7T0Np0D4%3D.sha256)
## Accounts
- [GitHub](https://github.com/peachcloud)
- [Twitter](https://twitter.com/peachcloudorg)
- [Email](mailto:peachcloudorg@gmail.com)
- [OpenCollective](https://opencollective.com/peachcloud)

View File

@ -2,35 +2,58 @@
_Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware product._ _Better [Scuttlebutt](https://scuttlebutt.nz) cloud infrastructure as a hardware product._
![image of peachcloud device](https://peach.commoninternet.net/assets/peachcloud.jpg) [**_Support us on OpenCollective!_**](https://opencollective.com/peachcloud)
## About
This project is a hack that combines PeachCloud with TildFriends.
The original PeachCloud project was paused when most development in the Scuttlebutt ecosystem stopped (reference),
but even after most funding and development and left the ecosystem, a version of the Sbot in C called TildeFriends was finished,
and continues to be maintained.
This fork of the PeachCloud project makes use of the TildeFriends sbot to make a minimal and functional PeachCloud Scuttlebutt pub,
that can be easily deployed and operated by a non-technical user.
Due to the timing and conditions of the Scuttlebutt ecosystem, and the rise of new protocols,
for this particular hack, the focus was on getting the project working in a minimal form (aka more hacky),
with less of a focus on long-term sustainability of the project as an ecosystem.
This project serves to provide a working simple-to-use pub for a P2P protocol which is in some ways frozen in amber,
as well as to share the PeachCloud interface and project, as a design-inspiration for other projects.
## Setup
TODO: how to install with yunohost
TODO: how to install without yunohost
## Background ## Background
- April 2018 project proposal: [`%HqwAsltORROCh4uyOq6iV+SsqU3OuNUevnq+5dwCqVI=.sha256`](https://viewer.scuttlebot.io/%25HqwAsltORROCh4uyOq6iV%2BSsqU3OuNUevnq%2B5dwCqVI%3D.sha256) - April 2018 project proposal: [`%HqwAsltORROCh4uyOq6iV+SsqU3OuNUevnq+5dwCqVI=.sha256`](https://viewer.scuttlebot.io/%25HqwAsltORROCh4uyOq6iV%2BSsqU3OuNUevnq%2B5dwCqVI%3D.sha256)
- November 2018 project pivot: [`%9NCyTf+oBxG0APlXRCKtrGZj3t+i+Kp3pKPN1gtFX2c=.sha256`](https://viewer.scuttlebot.io/%259NCyTf%2BoBxG0APlXRCKtrGZj3t%2Bi%2BKp3pKPN1gtFX2c%3D.sha256) - November 2018 project pivot: [`%9NCyTf+oBxG0APlXRCKtrGZj3t+i+Kp3pKPN1gtFX2c=.sha256`](https://viewer.scuttlebot.io/%259NCyTf%2BoBxG0APlXRCKtrGZj3t%2Bi%2BKp3pKPN1gtFX2c%3D.sha256)
- [Original PeachCloud ReadMe](/ORIGINAL-PEACHCLOUD-README.md)
- [Original PeachCloud Documentation](https://peach.commoninternet.net) ## Active Repositories
**Documentation**
- [peach-devdocs](https://github.com/peachcloud/peach-devdocs) - Developer documentation for PeachCloud in the form of a Markdown book
**Devops**
- [peach-vps](https://github.com/peachcloud/peach-vps) - Setup scripts and configuration files for deploying a PeachCloud development server
**Image building & device configuration**
- [peach-config](https://github.com/peachcloud/peach-config) - Configuration instructions, files and scripts
- [peach-img-builder](https://github.com/peachcloud/peach-img-builder) - Vmdb2 script for building a Debian disc image for Raspberry Pi with PeachCloud pre-installed
**Microservices**
- [peach-buttons](https://github.com/peachcloud/peach-buttons) - Emit GPIO events using JSON-RPC pubsub over WS
- [peach-oled](https://github.com/peachcloud/peach-oled) - Write and draw to OLED display using JSON-RPC over HTTP
- [peach-menu](https://github.com/peachcloud/peach-menu) - A menu for monitoring and interacting with the PeachCloud device
- [peach-network](https://github.com/peachcloud/peach-network) - Query and configure network interfaces using JSON-RPC over HTTP
- [peach-stats](https://github.com/peachcloud/peach-stats) - Query system statistics using JSON-RPC over HTTP
- [peach-lib](https://github.com/peachcloud/peach-lib) - JSON-RPC client library for the PeachCloud ecosystem
- [peach-monitor](https://github.com/peachcloud/peach-monitor) - Monitor network data usage and set alert flags based on user-defined thresholds
**Diagnostics**
- [peach-probe](https://github.com/peachcloud/peach-probe) - Probe PeachCloud microservices to evaluate their state and ensure correct API responses
**Web interface**
- [peach-patterns](https://github.com/peachcloud/peach-patterns) - Pattern library for the PeachCloud UI design system
- [peach-web](https://github.com/peachcloud/peach-web) - A web interface for monitoring and interacting with the PeachCloud device
## Developer Diaries
- [@ahdinosaur](https://github.com/ahdinosaur): `@6ilZq3kN0F+dXFHAPjAwMm87JEb/VdB+LC9eIMW3sa0=.ed25519`
- 1: [`%bSkZCJBmNYUmECNKYOiWkgEeRxrlo2UghNBzE6Cph94=.sha256`](https://viewer.scuttlebot.io/%25bSkZCJBmNYUmECNKYOiWkgEeRxrlo2UghNBzE6Cph94%3D.sha256)
- 2: [`%2L7gYAh2ih+7eFCrtObPWIUYHuGnJjwj4KCXrCIsWhM=.sha256`](https://viewer.scuttlebot.io/%252L7gYAh2ih%2B7eFCrtObPWIUYHuGnJjwj4KCXrCIsWhM%3D.sha256)
- [@mycognosist](https://github.com/mycognosist): `@HEqy940T6uB+T+d9Jaa58aNfRzLx9eRWqkZljBmnkmk=.ed25519`
- [`%mKUByRp4Gib6fqP1q2/dHg+ueSoR+Sj2Y0D7T0Np0D4=.sha256`](https://viewer.scuttlebot.io/%25mKUByRp4Gib6fqP1q2%2FdHg%2BueSoR%2BSj2Y0D7T0Np0D4%3D.sha256)
## Accounts
- [GitHub](https://github.com/peachcloud)
- [Twitter](https://twitter.com/peachcloudorg)
- [Email](mailto:peachcloudorg@gmail.com)
- [OpenCollective](https://opencollective.com/peachcloud)

View File

@ -1,34 +0,0 @@
---
name: "Bug Report Template"
about: "This template is for submitting bugs."
title: "[BUG] "
ref: "main"
labels:
- bug
- "help needed"
---
> Please fill out the sections below.
> Be kind and objective when writing in text.
> Thanks for the report! :)
**Brief description of the bug:**
**Steps to reproduce the bug:**
**Expected behaviour:**
**Technical details:**
_Is peach-web running on an x86-64 or arm64 machine?_
_What operating system distribution is it running on?_

View File

@ -1,15 +0,0 @@
---
name: "Feature Suggestion Template"
about: "This template is for submitting feature suggestions."
title: "[FEATURE] "
ref: "main"
labels:
- enhancement
---
**Brief description of the feature you'd like to suggest:**

View File

@ -1,4 +0,0 @@
#! /bin/bash
cargo build --package peach-web --release
rsync -azvh /home/notplants/computer/projects/peachpub/peach-workspace/target/release/peach-web root@159.89.42.156:/var/www/peachpub_ynh/peach-web
ssh root@159.89.42.156 'systemctl restart peachpub_ynh-peach-web'

View File

@ -1,6 +1,6 @@
[package] [package]
name = "peach-config" name = "peach-config"
version = "0.1.27" version = "0.1.15"
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"] authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
edition = "2018" edition = "2018"
description = "Command line tool for installing, updating and configuring PeachCloud" description = "Command line tool for installing, updating and configuring PeachCloud"
@ -37,5 +37,3 @@ log = "0.4"
lazy_static = "1.4.0" lazy_static = "1.4.0"
peach-lib = { path = "../peach-lib" } peach-lib = { path = "../peach-lib" }
rpassword = "5.0" rpassword = "5.0"
golgi = { git = "https://git.coopcloud.tech/golgi-ssb/golgi.git" }
async-std = "1.10.0"

View File

@ -3,14 +3,17 @@
pub const CONF: &str = "/var/lib/peachcloud/conf"; pub const CONF: &str = "/var/lib/peachcloud/conf";
// List of package names which are installed via apt-get // List of package names which are installed via apt-get
pub const SERVICES: [&str; 8] = [ pub const SERVICES: [&str; 11] = [
"peach-oled",
"peach-network",
"peach-stats",
"peach-web", "peach-web",
"peach-probe",
"peach-menu", "peach-menu",
"peach-buttons", "peach-buttons",
"peach-oled", "peach-monitor",
"peach-probe",
"peach-dyndns-updater", "peach-dyndns-updater",
"go-sbot", "peach-go-sbot",
"peach-config", "peach-config",
]; ];

View File

@ -1,5 +1,4 @@
#![allow(clippy::nonstandard_macro_braces)] #![allow(clippy::nonstandard_macro_braces)]
use golgi::error::GolgiError;
use peach_lib::error::PeachError; use peach_lib::error::PeachError;
pub use snafu::ResultExt; pub use snafu::ResultExt;
use snafu::Snafu; use snafu::Snafu;
@ -36,14 +35,6 @@ pub enum PeachConfigError {
ChangePasswordError { source: PeachError }, ChangePasswordError { source: PeachError },
#[snafu(display("Entered passwords did not match. Please try again."))] #[snafu(display("Entered passwords did not match. Please try again."))]
InvalidPassword, InvalidPassword,
#[snafu(display("Error in peach lib: {}", source))]
PeachLibError { source: PeachError },
#[snafu(display("Error in golgi: {}", source))]
Golgi { source: GolgiError },
#[snafu(display("{}", message))]
CmdInputError { message: String },
#[snafu(display("{}", message))]
WaitForSbotError { message: String },
} }
impl From<std::io::Error> for PeachConfigError { impl From<std::io::Error> for PeachConfigError {
@ -60,15 +51,3 @@ impl From<serde_json::Error> for PeachConfigError {
PeachConfigError::SerdeError { source: err } PeachConfigError::SerdeError { source: err }
} }
} }
impl From<PeachError> for PeachConfigError {
fn from(err: PeachError) -> PeachConfigError {
PeachConfigError::PeachLibError { source: err }
}
}
impl From<GolgiError> for PeachConfigError {
fn from(err: GolgiError) -> PeachConfigError {
PeachConfigError::Golgi { source: err }
}
}

View File

@ -1,32 +1,40 @@
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use snafu::ResultExt; use snafu::ResultExt;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
use crate::constants::{HARDWARE_CONFIG_FILE, SERVICES}; use crate::constants::HARDWARE_CONFIG_FILE;
use crate::error::{FileReadError, FileWriteError, PeachConfigError}; use crate::error::{FileReadError, FileWriteError, PeachConfigError};
use crate::utils::get_output; use crate::utils::get_output;
use crate::RtcOption; use crate::RtcOption;
/// Helper function which returns the version of a package currently installed,
/// as an Ok(String) if found, and as an Err if not found
pub fn get_package_version_number(package: &str) -> Result<String, PeachConfigError> {
let version = get_output(&["dpkg-query", "--showformat=${Version}", "--show", package])?;
Ok(version)
}
/// Returns a HashMap<String, String> of all the peach-packages which are currently installed /// Returns a HashMap<String, String> of all the peach-packages which are currently installed
/// mapped to their version number e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" } /// mapped to their version number e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" }
pub fn get_currently_installed_microservices() -> Result<HashMap<String, String>, PeachConfigError> pub fn get_currently_installed_microservices() -> Result<HashMap<String, String>, PeachConfigError>
{ {
// gets a list of all packages currently installed with dpkg-query // gets a list of all packages currently installed with dpkg
let peach_packages: HashMap<String, String> = SERVICES let packages = get_output(&["dpkg", "-l"])?;
.iter()
.filter_map(|service| { // this regex matches packages which contain the word peach in them
let version = get_package_version_number(service); // and has two match groups
match version { // 1. the first match group gets the package name
Ok(v) => Some((service.to_string(), v)), // 2. the second match group gets the version number of the package
Err(_) => None, let re: Regex = Regex::new(r"\S+\s+(\S*peach\S+)\s+(\S+).*\n").unwrap();
// the following iterator, iterates through the captures matched via the regex
// and for each capture, creates a value in the hash map,
// which maps the name of the package, to its version number
// e.g. { "peach-probe": "1.2.0", "peach-network": "1.4.0" }
let peach_packages: HashMap<String, String> = re
.captures_iter(&packages)
.filter_map(|cap| {
let groups = (cap.get(1), cap.get(2));
match groups {
(Some(package), Some(version)) => {
Some((package.as_str().to_string(), version.as_str().to_string()))
}
_ => None,
} }
}) })
.collect(); .collect();

View File

@ -2,15 +2,12 @@ mod change_password;
mod constants; mod constants;
mod error; mod error;
mod generate_manifest; mod generate_manifest;
mod publish_address;
mod set_permissions; mod set_permissions;
mod setup_networking; mod setup_networking;
mod setup_peach; mod setup_peach;
mod setup_peach_deb; mod setup_peach_deb;
mod status;
mod update; mod update;
mod utils; mod utils;
mod wait_for_sbot;
use clap::arg_enum; use clap::arg_enum;
use log::error; use log::error;
@ -47,25 +44,12 @@ enum PeachConfig {
Update(UpdateOpts), Update(UpdateOpts),
/// Changes the password for the peach-web interface /// Changes the password for the peach-web interface
#[structopt(name = "change-password")] #[structopt(name = "changepassword")]
ChangePassword(ChangePasswordOpts), ChangePassword(ChangePasswordOpts),
/// Updates file permissions on PeachCloud device /// Updates file permissions on PeachCloud device
#[structopt(name = "permissions")] #[structopt(name = "permissions")]
SetPermissions, SetPermissions,
/// Returns sbot id if sbot is running
#[structopt(name = "whoami")]
WhoAmI,
/// Publish domain and port.
/// It takes an address argument of the form host:port
#[structopt(name = "publish-address")]
PublishAddress(PublishAddressOpts),
/// Wait for a successful connection to sbot
#[structopt(name = "wait-for-sbot")]
WaitForSbot,
} }
#[derive(StructOpt, Debug)] #[derive(StructOpt, Debug)]
@ -106,13 +90,6 @@ pub struct ChangePasswordOpts {
password: Option<String>, password: Option<String>,
} }
#[derive(StructOpt, Debug)]
pub struct PublishAddressOpts {
/// Specify address in the form domain:port
#[structopt(short, long)]
address: String,
}
arg_enum! { arg_enum! {
/// enum options for real-time clock choices /// enum options for real-time clock choices
#[derive(Debug)] #[derive(Debug)]
@ -125,7 +102,7 @@ arg_enum! {
} }
} }
async fn run() { fn main() {
// initialize the logger // initialize the logger
env_logger::init(); env_logger::init();
@ -178,42 +155,6 @@ async fn run() {
) )
} }
}, },
PeachConfig::WhoAmI => match status::whoami().await {
Ok(sbot_id) => {
println!("{:?}", sbot_id);
{}
}
Err(err) => {
error!("sbot whoami encountered an error: {}", err)
}
},
PeachConfig::PublishAddress(opts) => {
match publish_address::publish_address(opts.address).await {
Ok(_) => {}
Err(err) => {
error!(
"peach-config encountered an error during publish address: {}",
err
)
}
}
}
PeachConfig::WaitForSbot => match wait_for_sbot::wait_for_sbot().await {
Ok(sbot_id) => {
println!("connected with sbot and found sbot_id: {:?}", sbot_id)
}
Err(err) => {
error!("peach-config did not successfully connect to sbot: {}", err)
}
},
} }
} }
} }
// Enable an async main function and execute the `run()` function,
// catching any errors and printing them to `stderr` before exiting the
// process.
#[async_std::main]
async fn main() {
run().await;
}

View File

@ -1,37 +0,0 @@
use crate::error::PeachConfigError;
use golgi::kuska_ssb::api::dto::content::PubAddress;
use golgi::messages::SsbMessageContent;
use peach_lib::sbot::init_sbot;
/// Utility function to publish the address (domain:port) of the pub
/// publishing the address causes the domain and port to be used for invite generation,
/// and also gossips this pub address to their peers
pub async fn publish_address(address: String) -> Result<(), PeachConfigError> {
// split address into domain:port
let split: Vec<&str> = address.split(':').collect();
let (domain, port): (&str, &str) = (split[0], split[1]);
// convert port to u16
let port_as_u16: u16 = port
.parse()
.map_err(|_err| PeachConfigError::CmdInputError {
message: "Failure to parse domain and port. Address must be of the format host:port."
.to_string(),
})?;
// publish address
let mut sbot = init_sbot().await?;
let pub_id = sbot.whoami().await?;
// Compose a `pub` address type message.
let pub_address_msg = SsbMessageContent::Pub {
address: Some(PubAddress {
// Host name (can be an IP address if onboarding over WiFi).
host: Some(domain.to_string()),
// Port.
port: port_as_u16,
// Public key.
key: pub_id,
}),
};
// Publish the `pub` address message.
let _pub_msg_ref = sbot.publish(pub_address_msg).await?;
Ok(())
}

View File

@ -1,30 +1,21 @@
use lazy_static::lazy_static;
use peach_lib::config_manager;
use crate::error::PeachConfigError; use crate::error::PeachConfigError;
use crate::utils::cmd; use crate::utils::cmd;
lazy_static! { /// All configs are stored in this folder, and should be read/writeable by peach group
pub static ref PEACH_CONFIGDIR: String = config_manager::get_config_value("PEACH_CONFIGDIR") /// so they can be read and written by all PeachCloud services.
.expect("Failed to load config value for PEACH_CONFIGDIR"); pub const CONFIGS_DIR: &str = "/var/lib/peachcloud";
pub static ref PEACH_WEBDIR: String = config_manager::get_config_value("PEACH_WEBDIR") pub const PEACH_WEB_DIR: &str = "/usr/share/peach-web";
.expect("Failed to load config value for PEACH_WEBDIR");
pub static ref PEACH_HOMEDIR: String = config_manager::get_config_value("PEACH_HOMEDIR")
.expect("Failed to load config value for PEACH_HOMEDIR");
}
/// Utility function to set correct file permissions on the PeachCloud device. /// Utility function to set correct file permissions on the PeachCloud device.
/// Accidentally changing file permissions is a fairly common thing to happen, /// Accidentally changing file permissions is a fairly common thing to happen,
/// so this is a useful CLI function for quickly correcting anything that may be out of order. /// so this is a useful CLI function for quickly correcting anything that may be out of order.
pub fn set_permissions() -> Result<(), PeachConfigError> { pub fn set_permissions() -> Result<(), PeachConfigError> {
println!("[ UPDATING FILE PERMISSIONS ON PEACHCLOUD DEVICE ]"); println!("[ UPDATING FILE PERMISSIONS ON PEACHCLOUD DEVICE ]");
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_CONFIGDIR])?; cmd(&["chmod", "-R", "u+rwX,g+rwX", CONFIGS_DIR])?;
cmd(&["chown", "-R", "peach:peach", &PEACH_CONFIGDIR])?; cmd(&["chown", "-R", "peach", CONFIGS_DIR])?;
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_WEBDIR])?; cmd(&["chgrp", "-R", "peach", CONFIGS_DIR])?;
cmd(&["chown", "-R", "peach:peach", &PEACH_WEBDIR])?; cmd(&["chmod", "-R", "u+rwX,g+rwX", PEACH_WEB_DIR])?;
cmd(&["chmod", "-R", "u+rwX,g+rwX", &PEACH_HOMEDIR])?; cmd(&["chown", "-R", "peach-web:peach", PEACH_WEB_DIR])?;
cmd(&["chown", "-R", "peach:peach", &PEACH_HOMEDIR])?;
println!("[ PERMISSIONS SUCCESSFULLY UPDATED ]"); println!("[ PERMISSIONS SUCCESSFULLY UPDATED ]");
Ok(()) Ok(())
} }

View File

@ -4,7 +4,6 @@ use std::fs;
use crate::error::{FileWriteError, PeachConfigError}; use crate::error::{FileWriteError, PeachConfigError};
use crate::generate_manifest::save_hardware_config; use crate::generate_manifest::save_hardware_config;
use crate::set_permissions::set_permissions;
use crate::setup_networking::configure_networking; use crate::setup_networking::configure_networking;
use crate::setup_peach_deb::setup_peach_deb; use crate::setup_peach_deb::setup_peach_deb;
use crate::update::update_microservices; use crate::update::update_microservices;
@ -69,7 +68,6 @@ pub fn setup_peach(
"libssl-dev", "libssl-dev",
"nginx", "nginx",
"wget", "wget",
"dnsutils",
"-y", "-y",
])?; ])?;
@ -240,9 +238,6 @@ pub fn setup_peach(
info!("[ SAVING LOG OF HARDWARE CONFIGURATIONS ]"); info!("[ SAVING LOG OF HARDWARE CONFIGURATIONS ]");
save_hardware_config(i2c, rtc)?; save_hardware_config(i2c, rtc)?;
info!("[ SETTING FILE PERMISSIONS ]");
set_permissions()?;
info!("[ PEACHCLOUD SETUP COMPLETE ]"); info!("[ PEACHCLOUD SETUP COMPLETE ]");
info!("[ ------------------------- ]"); info!("[ ------------------------- ]");
info!("[ please reboot your device ]"); info!("[ please reboot your device ]");

View File

@ -1,9 +0,0 @@
use crate::error::PeachConfigError;
use peach_lib::sbot::init_sbot;
/// Utility function to check if sbot is running via the whoami method
pub async fn whoami() -> Result<String, PeachConfigError> {
let mut sbot = init_sbot().await?;
let sbot_id = sbot.whoami().await?;
Ok(sbot_id)
}

View File

@ -47,8 +47,8 @@ pub fn update_microservices() -> Result<(), PeachConfigError> {
cmd(&["apt-get", "update"])?; cmd(&["apt-get", "update"])?;
// filter out peach-config from list of services // filter out peach-config from list of services
let services_to_update: Vec<&str> = SERVICES let services_to_update: Vec<&str> = SERVICES
.iter() .to_vec()
.copied() .into_iter()
.filter(|&x| x != "peach-config") .filter(|&x| x != "peach-config")
.collect(); .collect();

View File

@ -1,52 +0,0 @@
use std::{thread, time};
use crate::error::PeachConfigError;
use peach_lib::sbot::init_sbot;
static MAX_NUM_ATTEMPTS: u8 = 10;
/// Utility function to wait for a successful whoami call with sbot
/// After each attempt to call whoami it waits 2 seconds,
/// and if after MAX_NUM_ATTEMPTS (10) there is no successful whoami call
/// it returns an Error. Otherwise it returns Ok(sbot_id).
pub async fn wait_for_sbot() -> Result<String, PeachConfigError> {
let mut num_attempts = 0;
let mut whoami = None;
while num_attempts < MAX_NUM_ATTEMPTS {
let mut sbot = None;
let sbot_res = init_sbot().await;
match sbot_res {
Ok(sbot_instance) => {
sbot = Some(sbot_instance);
}
Err(err) => {
eprintln!("failed to connect to sbot: {:?}", err);
}
}
if sbot.is_some() {
let sbot_id_res = sbot.unwrap().whoami().await;
match sbot_id_res {
Ok(sbot_id) => {
whoami = Some(sbot_id);
break;
}
Err(err) => {
eprintln!("whoami failed: {:?}", err);
}
}
}
println!("trying to connect to sbot again {:?}", num_attempts);
num_attempts += 1;
let sleep_duration = time::Duration::from_secs(2);
thread::sleep(sleep_duration);
}
whoami.ok_or(PeachConfigError::WaitForSbotError {
message: "Failed to find sbot_id after 10 attempts".to_string(),
})
}

View File

@ -1,6 +1,6 @@
[package] [package]
name = "peach-dyndns-updater" name = "peach-dyndns-updater"
version = "0.1.8" version = "0.1.6"
authors = ["Max Fowler <mfowler@commoninternet.net>"] authors = ["Max Fowler <mfowler@commoninternet.net>"]
edition = "2018" edition = "2018"
description = "Sytemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate." description = "Sytemd timer which keeps a dynamic dns subdomain up to date with the latest device IP using nsupdate."

View File

@ -1,5 +1,6 @@
use log::info;
use peach_lib::dyndns_client::dyndns_update_ip; use peach_lib::dyndns_client::dyndns_update_ip;
use log::{info};
fn main() { fn main() {
// initalize the logger // initalize the logger

View File

@ -18,8 +18,8 @@ env_logger = "0.9"
jsonrpc-core = "18" jsonrpc-core = "18"
jsonrpc-http-server = "18" jsonrpc-http-server = "18"
log = "0.4" log = "0.4"
peach-stats = { path = "../peach-stats", features = ["serde_support"] } miniserde = "0.1.15"
serde_json = "1.0.74" peach-stats = { path = "../peach-stats", features = ["miniserde_support"] }
[dev-dependencies] [dev-dependencies]
jsonrpc-test = "18" jsonrpc-test = "18"

View File

@ -1,16 +1,12 @@
use std::fmt; use std::fmt;
use jsonrpc_core::{Error as JsonRpcError, ErrorCode}; use jsonrpc_core::{Error as JsonRpcError, ErrorCode};
use serde_json::error::Error as SerdeJsonError;
use peach_stats::StatsError; use peach_stats::StatsError;
/// Custom error type encapsulating all possible errors for a JSON-RPC server /// Custom error type encapsulating all possible errors for a JSON-RPC server
/// and associated methods. /// and associated methods.
#[derive(Debug)] #[derive(Debug)]
pub enum JsonRpcServerError { pub enum JsonRpcServerError {
/// Failed to serialize a string from a data structure.
Serde(SerdeJsonError),
/// An error returned from the `peach-stats` library. /// An error returned from the `peach-stats` library.
Stats(StatsError), Stats(StatsError),
/// An expected JSON-RPC method parameter was not provided. /// An expected JSON-RPC method parameter was not provided.
@ -28,9 +24,6 @@ impl fmt::Display for JsonRpcServerError {
JsonRpcServerError::MissingParameter(ref source) => { JsonRpcServerError::MissingParameter(ref source) => {
write!(f, "Missing expected parameter: {}", source) write!(f, "Missing expected parameter: {}", source)
} }
JsonRpcServerError::Serde(ref source) => {
write!(f, "{}", source)
}
JsonRpcServerError::Stats(ref source) => { JsonRpcServerError::Stats(ref source) => {
write!(f, "{}", source) write!(f, "{}", source)
} }
@ -41,11 +34,6 @@ impl fmt::Display for JsonRpcServerError {
impl From<JsonRpcServerError> for JsonRpcError { impl From<JsonRpcServerError> for JsonRpcError {
fn from(err: JsonRpcServerError) -> Self { fn from(err: JsonRpcServerError) -> Self {
match &err { match &err {
JsonRpcServerError::Serde(source) => JsonRpcError {
code: ErrorCode::ServerError(-32002),
message: format!("{}", source),
data: None,
},
JsonRpcServerError::Stats(source) => JsonRpcError { JsonRpcServerError::Stats(source) => JsonRpcError {
code: ErrorCode::ServerError(-32001), code: ErrorCode::ServerError(-32001),
message: format!("{}", source), message: format!("{}", source),

View File

@ -8,6 +8,7 @@ use std::result::Result;
use jsonrpc_core::{IoHandler, Value}; use jsonrpc_core::{IoHandler, Value};
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder}; use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
use log::info; use log::info;
use miniserde::json;
use peach_stats::stats; use peach_stats::stats;
mod error; mod error;
@ -29,7 +30,7 @@ pub fn run() -> Result<(), JsonRpcServerError> {
io.add_sync_method("cpu_stats", move |_| { io.add_sync_method("cpu_stats", move |_| {
info!("Fetching CPU statistics."); info!("Fetching CPU statistics.");
let cpu = stats::cpu_stats().map_err(JsonRpcServerError::Stats)?; let cpu = stats::cpu_stats().map_err(JsonRpcServerError::Stats)?;
let json_cpu = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?; let json_cpu = json::to_string(&cpu);
Ok(Value::String(json_cpu)) Ok(Value::String(json_cpu))
}); });
@ -37,7 +38,7 @@ pub fn run() -> Result<(), JsonRpcServerError> {
io.add_sync_method("cpu_stats_percent", move |_| { io.add_sync_method("cpu_stats_percent", move |_| {
info!("Fetching CPU statistics as percentages."); info!("Fetching CPU statistics as percentages.");
let cpu = stats::cpu_stats_percent().map_err(JsonRpcServerError::Stats)?; let cpu = stats::cpu_stats_percent().map_err(JsonRpcServerError::Stats)?;
let json_cpu = serde_json::to_string(&cpu).map_err(JsonRpcServerError::Serde)?; let json_cpu = json::to_string(&cpu);
Ok(Value::String(json_cpu)) Ok(Value::String(json_cpu))
}); });
@ -45,7 +46,7 @@ pub fn run() -> Result<(), JsonRpcServerError> {
io.add_sync_method("disk_usage", move |_| { io.add_sync_method("disk_usage", move |_| {
info!("Fetching disk usage statistics."); info!("Fetching disk usage statistics.");
let disks = stats::disk_usage().map_err(JsonRpcServerError::Stats)?; let disks = stats::disk_usage().map_err(JsonRpcServerError::Stats)?;
let json_disks = serde_json::to_string(&disks).map_err(JsonRpcServerError::Serde)?; let json_disks = json::to_string(&disks);
Ok(Value::String(json_disks)) Ok(Value::String(json_disks))
}); });
@ -53,7 +54,7 @@ pub fn run() -> Result<(), JsonRpcServerError> {
io.add_sync_method("load_average", move |_| { io.add_sync_method("load_average", move |_| {
info!("Fetching system load average statistics."); info!("Fetching system load average statistics.");
let avg = stats::load_average().map_err(JsonRpcServerError::Stats)?; let avg = stats::load_average().map_err(JsonRpcServerError::Stats)?;
let json_avg = serde_json::to_string(&avg).map_err(JsonRpcServerError::Serde)?; let json_avg = json::to_string(&avg);
Ok(Value::String(json_avg)) Ok(Value::String(json_avg))
}); });
@ -61,7 +62,7 @@ pub fn run() -> Result<(), JsonRpcServerError> {
io.add_sync_method("mem_stats", move |_| { io.add_sync_method("mem_stats", move |_| {
info!("Fetching current memory statistics."); info!("Fetching current memory statistics.");
let mem = stats::mem_stats().map_err(JsonRpcServerError::Stats)?; let mem = stats::mem_stats().map_err(JsonRpcServerError::Stats)?;
let json_mem = serde_json::to_string(&mem).map_err(JsonRpcServerError::Serde)?; let json_mem = json::to_string(&mem);
Ok(Value::String(json_mem)) Ok(Value::String(json_mem))
}); });
@ -69,7 +70,7 @@ pub fn run() -> Result<(), JsonRpcServerError> {
io.add_sync_method("uptime", move |_| { io.add_sync_method("uptime", move |_| {
info!("Fetching system uptime."); info!("Fetching system uptime.");
let uptime = stats::uptime().map_err(JsonRpcServerError::Stats)?; let uptime = stats::uptime().map_err(JsonRpcServerError::Stats)?;
let json_uptime = serde_json::to_string(&uptime).map_err(JsonRpcServerError::Serde)?; let json_uptime = json::to_string(&uptime);
Ok(Value::String(json_uptime)) Ok(Value::String(json_uptime))
}); });

View File

@ -1,27 +1,19 @@
[package] [package]
name = "peach-lib" name = "peach-lib"
version = "1.3.4" version = "1.3.1"
authors = ["Andrew Reid <glyph@mycelial.technology>"] authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
async-std = "1.10" chrono = "0.4.19"
chrono = "0.4" fslock="0.1.6"
dirs = "4.0"
fslock="0.1"
kuska-ssb = { git = "https://github.com/Kuska-ssb/ssb" }
tilde-client = { path = "../tilde-client" }
jsonrpc-client-core = "0.5" jsonrpc-client-core = "0.5"
jsonrpc-client-http = "0.5" jsonrpc-client-http = "0.5"
jsonrpc-core = "8.0" jsonrpc-core = "8.0.1"
jsonrpc_client = "0.7"
log = "0.4" log = "0.4"
nanorand = { version = "0.6", features = ["getrandom"] } nanorand = "0.6.1"
regex = "1" regex = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde_yaml = "0.8" serde_yaml = "0.8"
toml = "0.5" sha3 = "0.10.0"
sha3 = "0.10"
lazy_static = "1.4"
anyhow = "1.0.86"

View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

View File

@ -0,0 +1,12 @@
[package]
name = "debug"
version = "0.1.0"
authors = ["notplants <mfowler.email@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
peach-lib = { path = "../" }
env_logger = "0.6"
chrono = "0.4.19"

View File

@ -0,0 +1,65 @@
use peach_lib::dyndns_client::{dyndns_update_ip, register_domain, is_dns_updater_online, log_successful_nsupdate, get_num_seconds_since_successful_dns_update };
use peach_lib::password_utils::{verify_password, set_new_password, verify_temporary_password, set_new_temporary_password, send_password_reset};
use peach_lib::config_manager::{add_ssb_admin_id, delete_ssb_admin_id};
use peach_lib::sbot_client;
use std::process;
use chrono::prelude::*;
fn main() {
// initalize the logger
env_logger::init();
//
// println!("Hello, world its debug!");
// let result = set_new_password("password3");
// println!("result: {:?}", result);
//
// let result = verify_password("password1");
// println!("result should be error: {:?}", result);
//
// let result = verify_password("password3");
// println!("result should be ok: {:?}", result);
//
//
// println!("Testing temporary passwords");
// let result = set_new_temporary_password("abcd");
// println!("result: {:?}", result);
//
// let result = verify_temporary_password("password1");
// println!("result should be error: {:?}", result);
//
// let result = verify_temporary_password("abcd");
// println!("result should be ok: {:?}", result);
//
let result = send_password_reset();
println!("send password reset result should be ok: {:?}", result);
// sbot_client::post("hi cat");
// let result = sbot_client::whoami();
// let result = sbot_client::create_invite(50);
// let result = sbot_client::post("is this working");
// println!("result: {:?}", result);
// let result = sbot_client::post("nice we have contact");
// let result = sbot_client::update_pub_name("vermont-pub");
// let result = sbot_client::private_message("this is a private message", "@LZx+HP6/fcjUm7vef2eaBKAQ9gAKfzmrMVGzzdJiQtA=.ed25519");
// println!("result: {:?}", result);
// let result = send_password_reset();
// let result = add_ssb_admin_id("xyzdab");
// println!("result: {:?}", result);
// let result = delete_ssb_admin_id("xyzdab");
// println!("result: {:?}", result);
// let result = delete_ssb_admin_id("ab");
// println!("result: {:?}", result);
//// let result = log_successful_nsupdate();
//// let result = get_num_seconds_since_successful_dns_update();
// let is_online = is_dns_updater_online();
// println!("is online: {:?}", is_online);
//
//// let result = get_last_successful_dns_update();
//// println!("result: {:?}", result);
//// register_domain("newquarter299.dyn.peachcloud.org");
// let result = dyndns_update_ip();
// println!("result: {:?}", result);
}

View File

@ -1,11 +0,0 @@
use peach_lib::config_manager::{get_config_value, save_config_value};
fn main() {
println!("Running example of PeachCloud configuration management");
let v = get_config_value("ADDR").unwrap();
println!("ADDR: {}", v);
save_config_value("ADDR", "1.1.1.1");
let v = get_config_value("ADDR").unwrap();
println!("ADDR: {}", v);
}

View File

@ -1,283 +1,148 @@
//! Interfaces for writing and reading PeachCloud configurations, stored in yaml. //! Interfaces for writing and reading PeachCloud configurations, stored in yaml.
//! //!
//! Different PeachCloud microservices import peach-lib, so that they can share //! Different PeachCloud microservices import peach-lib, so that they can share this interface.
//! this interface.
//!
//! Config values are looked up from three locations in this order by key name:
//! 1. from environmental variables
//! 2. from a configuration file
//! 3. from default values
//! //!
//! The configuration file is located at: "/var/lib/peachcloud/config.yml" //! The configuration file is located at: "/var/lib/peachcloud/config.yml"
//! unless its path is configured by setting PEACH_CONFIG_PATH env variable.
use std::collections::{BTreeMap, HashMap}; use std::fs;
use std::{env, fs};
use fslock::LockFile; use fslock::LockFile;
use lazy_static::lazy_static; use serde::{Deserialize, Serialize};
use log::debug;
use crate::error::PeachError; use crate::error::PeachError;
// load path to main configuration file // main configuration file
// from PEACH_CONFIG_PATH if that environment variable is set pub const YAML_PATH: &str = "/var/lib/peachcloud/config.yml";
// or using the default value if not set
pub const DEFAULT_YAML_PATH: &str = "/var/lib/peachcloud/config.yml"; // lock file (used to avoid race conditions during config reading & writing)
lazy_static! { pub const LOCK_FILE_PATH: &str = "/var/lib/peachcloud/config.lock";
static ref CONFIG_PATH: String = {
if let Ok(val) = env::var("PEACH_CONFIG_PATH") { // we make use of Serde default values in order to make PeachCloud
val // robust and keep running even with a not fully complete config.yml
} // main type which represents all peachcloud configurations
else { #[derive(Debug, PartialEq, Serialize, Deserialize)]
DEFAULT_YAML_PATH.to_string() pub struct PeachConfig {
} #[serde(default)]
}; pub external_domain: String,
// lock file (used to avoid race conditions during config reading & writing) #[serde(default)]
// the lock file path is the config file path + ".lock" pub dyn_domain: String,
static ref LOCK_FILE_PATH: String = format!("{}.lock", *CONFIG_PATH); #[serde(default)]
pub dyn_dns_server_address: String,
#[serde(default)]
pub dyn_tsig_key_path: String,
#[serde(default)] // default is false
pub dyn_enabled: bool,
#[serde(default)] // default is empty vector
pub ssb_admin_ids: Vec<String>,
#[serde(default)]
pub admin_password_hash: String,
#[serde(default)]
pub temporary_password_hash: String,
} }
// Default values for PeachCloud configs which are used for any key which is not set // helper functions for serializing and deserializing PeachConfig from disc
// via an environment variable or in a saved configuration file. fn save_peach_config(peach_config: PeachConfig) -> Result<PeachConfig, PeachError> {
pub fn get_peach_config_defaults() -> HashMap<String, String> {
let peach_config_defaults: HashMap<&str, &str> = HashMap::from([
("STANDALONE_MODE", "true"),
("DISABLE_AUTH", "false"),
("ADDR", "127.0.0.1"),
("PORT", "8000"),
("EXTERNAL_DOMAIN", ""),
("DYN_DOMAIN", ""),
(
"DYN_DNS_SERVER_ADDRESS",
"http://dynserver.dyn.peachcloud.org",
),
("DYN_USE_CUSTOM_SERVER", "true"),
("DYN_TSIG_KEY_PATH", ""),
("DYN_NAMESERVER", "ns.peachcloud.org"),
("DYN_ENABLED", "false"),
("SSB_ADMIN_IDS", ""),
("ADMIN_PASSWORD_HASH", "47"),
("TEMPORARY_PASSWORD_HASH", ""),
("TILDE_SBOT_DATADIR", "/home/peach/.local/share/tildefriends/"),
("TILDE_SBOT_SERVICE", "tilde-sbot.service"),
("TILDE_BINARY_PATH", "/home/peach/tildefriends.standalone"),
("TILDE_WRAPPER_PATH", "/home/peach/run-tilde-sbot.sh"),
("TILDE_CONFIG_PATH", "/home/peach/.local/share/tildefriends/tilde-sbot.toml"),
("PEACH_CONFIGDIR", "/var/lib/peachcloud"),
("PEACH_HOMEDIR", "/home/peach"),
("PEACH_WEBDIR", "/usr/share/peach-web"),
]);
// convert HashMap<&str, &str> to HashMap<String, String> and return
let pc_defaults: HashMap<String, String> = peach_config_defaults
.iter()
.map(|(key, val)| (key.to_string(), val.to_string()))
.collect();
pc_defaults
}
// primary interface for getting config values
// Config values are looked up from three locations in this order by key name:
// 1. from environmental variables
// 2. from a configuration file
// 3. from default values
pub fn get_config_value(key: &str) -> Result<String, PeachError> {
// first check if there is an environmental variable set
if let Ok(val) = env::var(key) {
Ok(val)
} else {
// then check if a value is set in the config file
let peach_config_on_disc = load_peach_config_from_disc()?;
let val = peach_config_on_disc.get(key);
// if no value is found in the config file, then get the default value
match val {
// return config value
Some(v) => Ok(v.to_string()),
// get default value
None => {
match get_peach_config_defaults().get(key) {
Some(v) => Ok(v.to_string()),
// if this key was not found in the defaults, then it was an invalid key
None => Err(PeachError::InvalidKey {
key: key.to_string(),
}),
}
}
}
}
}
// helper function to load PeachCloud configuration file saved to disc
pub fn load_peach_config_from_disc() -> Result<HashMap<String, String>, PeachError> {
let peach_config_exists = std::path::Path::new(CONFIG_PATH.as_str()).exists();
// if config file does not exist, return an emtpy HashMap
if !peach_config_exists {
let peach_config: HashMap<String, String> = HashMap::new();
Ok(peach_config)
}
// otherwise we load peach config from disk
else {
debug!("Loading peach config: {} exists", CONFIG_PATH.as_str());
let contents =
fs::read_to_string(CONFIG_PATH.as_str()).map_err(|source| PeachError::Read {
source,
path: CONFIG_PATH.to_string(),
})?;
let peach_config: HashMap<String, String> = serde_yaml::from_str(&contents)?;
Ok(peach_config)
}
}
// helper function to save PeachCloud configuration file to disc
// takes in a Hashmap<String, String> and saves the whole HashMap as a yaml file
// with the keys in alphabetical order
pub fn save_peach_config_to_disc(
peach_config: HashMap<String, String>,
) -> Result<HashMap<String, String>, PeachError> {
// use a file lock to avoid race conditions while saving config // use a file lock to avoid race conditions while saving config
let mut lock = LockFile::open(&*LOCK_FILE_PATH).map_err(|source| PeachError::Read { let mut lock = LockFile::open(LOCK_FILE_PATH)?;
source,
path: LOCK_FILE_PATH.to_string(),
})?;
lock.lock()?; lock.lock()?;
// first convert Hashmap to BTreeMap (so that keys are saved in deterministic alphabetical order) let yaml_str = serde_yaml::to_string(&peach_config)?;
let ordered: BTreeMap<_, _> = peach_config.iter().collect();
// then serialize BTreeMap as yaml
let yaml_str = serde_yaml::to_string(&ordered)?;
// write yaml to file fs::write(YAML_PATH, yaml_str).map_err(|source| PeachError::Write {
fs::write(CONFIG_PATH.as_str(), yaml_str).map_err(|source| PeachError::Write {
source, source,
path: CONFIG_PATH.to_string(), path: YAML_PATH.to_string(),
})?; })?;
// unlock file lock // unlock file lock
lock.unlock()?; lock.unlock()?;
// return modified HashMap // return peach_config
Ok(peach_config) Ok(peach_config)
} }
// helper functions for serializing and deserializing PeachConfig values from disc pub fn load_peach_config() -> Result<PeachConfig, PeachError> {
pub fn save_config_value(key: &str, value: &str) -> Result<HashMap<String, String>, PeachError> { let peach_config_exists = std::path::Path::new(YAML_PATH).exists();
// get current config from disc
let mut peach_config = load_peach_config_from_disc()?;
// insert new key/value let peach_config: PeachConfig;
peach_config.insert(key.to_string(), value.to_string());
// save the modified hashmap to disc // if this is the first time loading peach_config, we can create a default here
save_peach_config_to_disc(peach_config) if !peach_config_exists {
peach_config = PeachConfig {
external_domain: "".to_string(),
dyn_domain: "".to_string(),
dyn_dns_server_address: "".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 {
let contents = fs::read_to_string(YAML_PATH).map_err(|source| PeachError::Read {
source,
path: YAML_PATH.to_string(),
})?;
peach_config = serde_yaml::from_str(&contents)?;
}
Ok(peach_config)
} }
// set all dyn configuration values at once // interfaces for setting specific config values
pub fn set_peach_dyndns_config( pub fn set_peach_dyndns_config(
dyn_domain: &str, dyn_domain: &str,
dyn_dns_server_address: &str, dyn_dns_server_address: &str,
dyn_tsig_key_path: &str, dyn_tsig_key_path: &str,
dyn_enabled: bool, dyn_enabled: bool,
) -> Result<HashMap<String, String>, PeachError> { ) -> Result<PeachConfig, PeachError> {
let mut peach_config = load_peach_config_from_disc()?; let mut peach_config = load_peach_config()?;
let dyn_enabled_str = match dyn_enabled { peach_config.dyn_domain = dyn_domain.to_string();
true => "true", peach_config.dyn_dns_server_address = dyn_dns_server_address.to_string();
false => "false", peach_config.dyn_tsig_key_path = dyn_tsig_key_path.to_string();
}; peach_config.dyn_enabled = dyn_enabled;
peach_config.insert("DYN_DOMAIN".to_string(), dyn_domain.to_string()); save_peach_config(peach_config)
peach_config.insert(
"DYN_DNS_SERVER_ADDRESS".to_string(),
dyn_dns_server_address.to_string(),
);
peach_config.insert(
"DYN_TSIG_KEY_PATH".to_string(),
dyn_tsig_key_path.to_string(),
);
peach_config.insert("DYN_ENABLED".to_string(), dyn_enabled_str.to_string());
save_peach_config_to_disc(peach_config)
} }
pub fn set_external_domain( pub fn set_external_domain(new_external_domain: &str) -> Result<PeachConfig, PeachError> {
new_external_domain: &str, let mut peach_config = load_peach_config()?;
) -> Result<HashMap<String, String>, PeachError> { peach_config.external_domain = new_external_domain.to_string();
save_config_value("EXTERNAL_DOMAIN", new_external_domain) save_peach_config(peach_config)
} }
pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> { pub fn get_peachcloud_domain() -> Result<Option<String>, PeachError> {
let external_domain = get_config_value("EXTERNAL_DOMAIN")?; let peach_config = load_peach_config()?;
let dyn_domain = get_config_value("DYN_DOMAIN")?; if !peach_config.external_domain.is_empty() {
if !external_domain.is_empty() { Ok(Some(peach_config.external_domain))
Ok(Some(external_domain)) } else if !peach_config.dyn_domain.is_empty() {
} else if !dyn_domain.is_empty() { Ok(Some(peach_config.dyn_domain))
Ok(Some(dyn_domain))
} else { } else {
Ok(None) Ok(None)
} }
} }
pub fn get_dyndns_server_address() -> Result<String, PeachError> { pub fn set_dyndns_enabled_value(enabled_value: bool) -> Result<PeachConfig, PeachError> {
get_config_value("DYN_DNS_SERVER_ADDRESS") let mut peach_config = load_peach_config()?;
peach_config.dyn_enabled = enabled_value;
save_peach_config(peach_config)
} }
pub fn set_dyndns_enabled_value( pub fn add_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
enabled_value: bool, let mut peach_config = load_peach_config()?;
) -> Result<HashMap<String, String>, PeachError> { peach_config.ssb_admin_ids.push(ssb_id.to_string());
match enabled_value { save_peach_config(peach_config)
true => save_config_value("DYN_ENABLED", "true"),
false => save_config_value("DYN_ENABLED", "false"),
}
} }
pub fn get_dyndns_enabled_value() -> Result<bool, PeachError> { pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<PeachConfig, PeachError> {
let val = get_config_value("DYN_ENABLED")?; let mut peach_config = load_peach_config()?;
Ok(val == "true") let mut ssb_admin_ids = peach_config.ssb_admin_ids;
}
pub fn set_admin_password_hash(
password_hash: String,
) -> Result<HashMap<String, String>, PeachError> {
save_config_value("ADMIN_PASSWORD_HASH", &password_hash)
}
pub fn get_admin_password_hash() -> Result<String, PeachError> {
let admin_password_hash = get_config_value("ADMIN_PASSWORD_HASH")?;
if !admin_password_hash.is_empty() {
Ok(admin_password_hash)
} else {
Err(PeachError::PasswordNotSet)
}
}
pub fn set_temporary_password_hash(
password_hash: String,
) -> Result<HashMap<String, String>, PeachError> {
save_config_value("TEMPORARY_PASSWORD_HASH", &password_hash)
}
pub fn get_temporary_password_hash() -> Result<String, PeachError> {
let admin_password_hash = get_config_value("TEMPORARY_PASSWORD_HASH")?;
if !admin_password_hash.is_empty() {
Ok(admin_password_hash)
} else {
Err(PeachError::PasswordNotSet)
}
}
// add ssb_id to vector of admin ids and save new value for SSB_ADMIN_IDS
pub fn add_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
let mut ssb_admin_ids = get_ssb_admin_ids()?;
ssb_admin_ids.push(ssb_id.to_string());
save_ssb_admin_ids(ssb_admin_ids)
}
// remove ssb_id from vector of admin ids if found and save new value for SSB_ADMIN_IDS
// if value is not found then return an error
pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
let mut ssb_admin_ids = get_ssb_admin_ids()?;
let index_result = ssb_admin_ids.iter().position(|x| *x == ssb_id); let index_result = ssb_admin_ids.iter().position(|x| *x == ssb_id);
match index_result { match index_result {
Some(index) => { Some(index) => {
ssb_admin_ids.remove(index); ssb_admin_ids.remove(index);
save_ssb_admin_ids(ssb_admin_ids) peach_config.ssb_admin_ids = ssb_admin_ids;
save_peach_config(peach_config)
} }
None => Err(PeachError::SsbAdminIdNotFound { None => Err(PeachError::SsbAdminIdNotFound {
id: ssb_id.to_string(), id: ssb_id.to_string(),
@ -285,20 +150,32 @@ pub fn delete_ssb_admin_id(ssb_id: &str) -> Result<Vec<String>, PeachError> {
} }
} }
// looks up the String value for SSB_ADMIN_IDS and converts it into a Vec<String> pub fn set_admin_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> {
pub fn get_ssb_admin_ids() -> Result<Vec<String>, PeachError> { let mut peach_config = load_peach_config()?;
let ssb_admin_ids_str = get_config_value("SSB_ADMIN_IDS")?; peach_config.admin_password_hash = password_hash.to_string();
if ssb_admin_ids_str.trim().is_empty() { save_peach_config(peach_config)
Ok(vec![]) }
pub fn get_admin_password_hash() -> Result<String, PeachError> {
let peach_config = load_peach_config()?;
if !peach_config.admin_password_hash.is_empty() {
Ok(peach_config.admin_password_hash)
} else { } else {
let ssb_admin_ids: Vec<String> = serde_json::from_str(&ssb_admin_ids_str)?; Err(PeachError::PasswordNotSet)
Ok(ssb_admin_ids)
} }
} }
// takes in a Vec<String> and saves SSB_ADMIN_IDS as a json string representation of this vec pub fn set_temporary_password_hash(password_hash: &str) -> Result<PeachConfig, PeachError> {
pub fn save_ssb_admin_ids(ssb_admin_ids: Vec<String>) -> Result<Vec<String>, PeachError> { let mut peach_config = load_peach_config()?;
let ssb_admin_ids_as_json_str = serde_json::to_string(&ssb_admin_ids)?; peach_config.temporary_password_hash = password_hash.to_string();
save_config_value("SSB_ADMIN_IDS", &ssb_admin_ids_as_json_str)?; save_peach_config(peach_config)
Ok(ssb_admin_ids) }
pub fn get_temporary_password_hash() -> Result<String, PeachError> {
let peach_config = load_peach_config()?;
if !peach_config.temporary_password_hash.is_empty() {
Ok(peach_config.temporary_password_hash)
} else {
Err(PeachError::PasswordNotSet)
}
} }

View File

@ -9,8 +9,13 @@
//! //!
//! The domain for dyndns updates is stored in /var/lib/peachcloud/config.yml //! 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 //! The tsig key for authenticating the updates is stored in /var/lib/peachcloud/peach-dyndns/tsig.key
use std::ffi::OsStr; use std::{
use std::{fs, fs::OpenOptions, io::Write, process::Command, str::FromStr}; fs,
fs::OpenOptions,
io::Write,
process::{Command, Stdio},
str::FromStr,
};
use chrono::prelude::*; use chrono::prelude::*;
use jsonrpc_client_core::{expand_params, jsonrpc_client}; use jsonrpc_client_core::{expand_params, jsonrpc_client};
@ -18,12 +23,13 @@ use jsonrpc_client_http::HttpTransport;
use log::{debug, info}; use log::{debug, info};
use regex::Regex; use regex::Regex;
use crate::config_manager::{ use crate::{
get_config_value, get_dyndns_enabled_value, get_dyndns_server_address, config_manager::{load_peach_config, set_peach_dyndns_config},
error::PeachError,
}; };
use crate::{config_manager, error::PeachError};
/// constants for dyndns configuration /// 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 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 PEACH_DYNDNS_CONFIG_PATH: &str = "/var/lib/peachcloud/peach-dyndns";
pub const DYNDNS_LOG_PATH: &str = "/var/lib/peachcloud/peach-dyndns/latest_result.log"; pub const DYNDNS_LOG_PATH: &str = "/var/lib/peachcloud/peach-dyndns/latest_result.log";
@ -56,10 +62,9 @@ pub fn save_dyndns_key(key: &str) -> Result<(), PeachError> {
pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError> { pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError> {
debug!("Creating HTTP transport for dyndns client."); debug!("Creating HTTP transport for dyndns client.");
let transport = HttpTransport::new().standalone()?; let transport = HttpTransport::new().standalone()?;
let http_server = get_dyndns_server_address()?; let http_server = PEACH_DYNDNS_URL;
info!("Using dyndns http server address: {:?}", http_server); debug!("Creating HTTP transport handle on {}.", http_server);
debug!("Creating HTTP transport handle on {}.", &http_server); let transport_handle = transport.handle(http_server)?;
let transport_handle = transport.handle(&http_server)?;
info!("Creating client for peach-dyndns service."); info!("Creating client for peach-dyndns service.");
let mut client = PeachDynDnsClient::new(transport_handle); let mut client = PeachDynDnsClient::new(transport_handle);
@ -68,8 +73,7 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError>
// save new TSIG key // save new TSIG key
save_dyndns_key(&key)?; save_dyndns_key(&key)?;
// save new configuration values // save new configuration values
let set_config_result = let set_config_result = set_peach_dyndns_config(domain, PEACH_DYNDNS_URL, TSIG_KEY_PATH, true);
config_manager::set_peach_dyndns_config(domain, &http_server, TSIG_KEY_PATH, true);
match set_config_result { match set_config_result {
Ok(_) => { Ok(_) => {
let response = "success".to_string(); let response = "success".to_string();
@ -83,9 +87,9 @@ pub fn register_domain(domain: &str) -> std::result::Result<String, PeachError>
pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError> { pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError> {
debug!("Creating HTTP transport for dyndns client."); debug!("Creating HTTP transport for dyndns client.");
let transport = HttpTransport::new().standalone()?; let transport = HttpTransport::new().standalone()?;
let http_server = get_dyndns_server_address()?; let http_server = PEACH_DYNDNS_URL;
debug!("Creating HTTP transport handle on {}.", &http_server); debug!("Creating HTTP transport handle on {}.", http_server);
let transport_handle = transport.handle(&http_server)?; let transport_handle = transport.handle(http_server)?;
info!("Creating client for peach_network service."); info!("Creating client for peach_network service.");
let mut client = PeachDynDnsClient::new(transport_handle); let mut client = PeachDynDnsClient::new(transport_handle);
@ -101,7 +105,7 @@ pub fn is_domain_available(domain: &str) -> std::result::Result<bool, PeachError
/// Helper function to get public ip address of PeachCloud device. /// Helper function to get public ip address of PeachCloud device.
fn get_public_ip_address() -> Result<String, PeachError> { fn get_public_ip_address() -> Result<String, PeachError> {
// TODO: consider other ways to get public IP address // TODO: consider other ways to get public IP address
let output = Command::new("curl").arg("ifconfig.me").output()?; let output = Command::new("/usr/bin/curl").arg("ifconfig.me").output()?;
let command_output = String::from_utf8(output.stdout)?; let command_output = String::from_utf8(output.stdout)?;
Ok(command_output) Ok(command_output)
} }
@ -109,28 +113,31 @@ fn get_public_ip_address() -> Result<String, PeachError> {
/// Reads dyndns configurations from config.yml /// Reads dyndns configurations from config.yml
/// and then uses nsupdate to update the IP address for the configured domain /// and then uses nsupdate to update the IP address for the configured domain
pub fn dyndns_update_ip() -> Result<bool, PeachError> { pub fn dyndns_update_ip() -> Result<bool, PeachError> {
let dyn_tsig_key_path = get_config_value("DYN_TSIG_KEY_PATH")?; info!("Running dyndns_update_ip");
let dyn_enabled = get_dyndns_enabled_value()?; let peach_config = load_peach_config()?;
let dyn_domain = get_config_value("DYN_DOMAIN")?;
let dyn_dns_server_address = get_config_value("DYN_DNS_SERVER_ADDRESS")?;
let dyn_nameserver = get_config_value("DYN_NAMESERVER")?;
info!( info!(
"Using config: "Using config:
dyn_tsig_key_path: {:?} dyn_tsig_key_path: {:?}
dyn_domain: {:?} dyn_domain: {:?}
dyn_dns_server_address: {:?} dyn_dns_server_address: {:?}
dyn_enabled: {:?} dyn_enabled: {:?}
dyn_nameserver: {:?}
", ",
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,
); );
if !dyn_enabled { if !peach_config.dyn_enabled {
info!("dyndns is not enabled, not updating"); info!("dyndns is not enabled, not updating");
Ok(false) Ok(false)
} else { } else {
// call nsupdate passing appropriate configs // call nsupdate passing appropriate configs
let mut nsupdate_command = Command::new("nsupdate"); let mut nsupdate_command = Command::new("/usr/bin/nsupdate")
nsupdate_command.arg("-k").arg(&dyn_tsig_key_path).arg("-v"); .arg("-k")
.arg(&peach_config.dyn_tsig_key_path)
.arg("-v")
.stdin(Stdio::piped())
.spawn()?;
// pass nsupdate commands via stdin // pass nsupdate commands via stdin
let public_ip_address = get_public_ip_address()?; let public_ip_address = get_public_ip_address()?;
info!("found public ip address: {}", public_ip_address); info!("found public ip address: {}", public_ip_address);
@ -141,20 +148,20 @@ pub fn dyndns_update_ip() -> Result<bool, PeachError> {
update delete {DOMAIN} A update delete {DOMAIN} A
update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS} update add {DOMAIN} 30 A {PUBLIC_IP_ADDRESS}
send", send",
NAMESERVER = dyn_nameserver, NAMESERVER = "ns.peachcloud.org",
ZONE = dyn_domain, ZONE = peach_config.dyn_domain,
DOMAIN = dyn_domain, DOMAIN = peach_config.dyn_domain,
PUBLIC_IP_ADDRESS = public_ip_address, PUBLIC_IP_ADDRESS = public_ip_address,
); );
info!("ns_commands: {:?}", ns_commands); let mut nsupdate_stdin = nsupdate_command.stdin.take().ok_or(PeachError::NsUpdate {
info!("creating nsupdate temp file"); msg: "unable to capture stdin handle for `nsupdate` command".to_string(),
let temp_file_path = "/var/lib/peachcloud/nsupdate.sh"; })?;
// write ns_commands to temp_file write!(nsupdate_stdin, "{}", ns_commands).map_err(|source| PeachError::Write {
fs::write(temp_file_path, ns_commands)?; source,
nsupdate_command.arg(temp_file_path); path: peach_config.dyn_tsig_key_path.to_string(),
let nsupdate_output = nsupdate_command.output()?; })?;
let args: Vec<&OsStr> = nsupdate_command.get_args().collect(); let nsupdate_output = nsupdate_command.wait_with_output()?;
info!("nsupdate command: {:?}", args); info!("nsupdate output: {:?}", nsupdate_output);
// We only return a successful result if nsupdate was successful // We only return a successful result if nsupdate was successful
if nsupdate_output.status.success() { if nsupdate_output.status.success() {
info!("nsupdate succeeded, returning ok"); info!("nsupdate succeeded, returning ok");
@ -197,7 +204,7 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
})?; })?;
// replace newline if found // replace newline if found
// TODO: maybe we can use `.trim()` instead // TODO: maybe we can use `.trim()` instead
let contents = contents.replace('\n', ""); let contents = contents.replace("\n", "");
// TODO: consider adding additional context? // TODO: consider adding additional context?
let time_ran_dt = DateTime::parse_from_rfc3339(&contents).map_err(|source| { let time_ran_dt = DateTime::parse_from_rfc3339(&contents).map_err(|source| {
PeachError::ParseDateTime { PeachError::ParseDateTime {
@ -216,14 +223,20 @@ pub fn get_num_seconds_since_successful_dns_update() -> Result<Option<i64>, Peac
/// and has successfully run recently (in the last six minutes) /// and has successfully run recently (in the last six minutes)
pub fn is_dns_updater_online() -> Result<bool, PeachError> { pub fn is_dns_updater_online() -> Result<bool, PeachError> {
// first check if it is enabled in peach-config // first check if it is enabled in peach-config
let is_enabled = get_dyndns_enabled_value()?; let peach_config = 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) // then check if it has successfully run within the last 6 minutes (60*6 seconds)
let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?; let num_seconds_since_successful_update = get_num_seconds_since_successful_dns_update()?;
let ran_recently: bool = match num_seconds_since_successful_update { let ran_recently: bool;
Some(seconds) => seconds < (60 * 6), match num_seconds_since_successful_update {
Some(seconds) => {
ran_recently = seconds < (60 * 6);
}
// if the value is None, then the last time it ran successfully is unknown // if the value is None, then the last time it ran successfully is unknown
None => false, None => {
}; ran_recently = false;
}
}
// debug log // debug log
info!("is_dyndns_enabled: {:?}", is_enabled); info!("is_dyndns_enabled: {:?}", is_enabled);
info!("dyndns_ran_recently: {:?}", ran_recently); info!("dyndns_ran_recently: {:?}", ran_recently);
@ -245,9 +258,11 @@ pub fn get_dyndns_subdomain(dyndns_full_domain: &str) -> Option<String> {
} }
// helper function which checks if a dyndns domain is new // helper function which checks if a dyndns domain is new
pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> Result<bool, PeachError> { pub fn check_is_new_dyndns_domain(dyndns_full_domain: &str) -> bool {
let previous_dyndns_domain = get_config_value("DYN_DOMAIN")?; // TODO: return `Result<bool, PeachError>` and replace `unwrap` with `?` operator
Ok(dyndns_full_domain != previous_dyndns_domain) let peach_config = load_peach_config().unwrap();
let previous_dyndns_domain = peach_config.dyn_domain;
dyndns_full_domain != previous_dyndns_domain
} }
jsonrpc_client!(pub struct PeachDynDnsClient { jsonrpc_client!(pub struct PeachDynDnsClient {

View File

@ -1,22 +1,12 @@
#![warn(missing_docs)] #![warn(missing_docs)]
//! Error handling for various aspects of the PeachCloud system, including the network, OLED, stats and dyndns JSON-RPC clients, as well as the configuration manager, sbot client and password utilities. //! Error handling for various aspects of the PeachCloud system, including the network, OLED, stats and dyndns JSON-RPC clients, as well as the configuration manager, sbot client and password utilities.
use std::{io, str, string}; use std::{io, str, string};
use jsonrpc_client::JsonRpcError;
use anyhow::Error; // Add the anyhow crate for errors
/// This type represents all possible errors that can occur when interacting with the PeachCloud library. /// This type represents all possible errors that can occur when interacting with the PeachCloud library.
#[derive(Debug)] #[derive(Debug)]
pub enum PeachError { pub enum PeachError {
/// Represents looking up a Config value with a non-existent key
InvalidKey {
/// the key value which was invalid
key: String,
},
/// Represents a failure to determine the path of the user's home directory.
HomeDir,
/// Represents all other cases of `std::io::Error`. /// Represents all other cases of `std::io::Error`.
Io(io::Error), Io(io::Error),
@ -68,18 +58,15 @@ pub enum PeachError {
/// Represents a failure to parse or compile a regular expression. /// Represents a failure to parse or compile a regular expression.
Regex(regex::Error), Regex(regex::Error),
/// Represents a failure to successfully execute an sbot command (via golgi). /// Represents a failure to successfully execute an sbot command.
Sbot(String), SbotCli {
/// The `stderr` output from the sbot command.
msg: String,
},
/// Represents a failure to serialize or deserialize JSON. /// Represents a failure to serialize or deserialize JSON.
SerdeJson(serde_json::error::Error), SerdeJson(serde_json::error::Error),
/// Represents a failure to deserialize TOML.
TomlDeser(toml::de::Error),
/// Represents a failure to serialize TOML.
TomlSer(toml::ser::Error),
/// Represents a failure to serialize or deserialize YAML. /// Represents a failure to serialize or deserialize YAML.
SerdeYaml(serde_yaml::Error), SerdeYaml(serde_yaml::Error),
@ -100,29 +87,14 @@ pub enum PeachError {
Write { Write {
/// The underlying source of the error. /// The underlying source of the error.
source: io::Error, source: io::Error,
/// The file path for the write attempt. /// The file path for the write attemp.
path: String, path: String,
}, },
/// Represents a JsonRpcError with Solar
JsonRpcError(JsonRpcError),
/// Represents an Anyhow error with Solar
SolarClientError(String),
/// Represents an Anyhow error with Solar
TildeClientError(String),
/// Represents an error with encoding or decoding an SsbMessage
SsbMessageError(String),
} }
impl std::error::Error for PeachError { impl std::error::Error for PeachError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self { match *self {
PeachError::HomeDir => None,
PeachError::InvalidKey { .. } => None,
PeachError::Io(_) => None, PeachError::Io(_) => None,
PeachError::JsonRpcClientCore(_) => None, PeachError::JsonRpcClientCore(_) => None,
PeachError::JsonRpcCore(_) => None, PeachError::JsonRpcCore(_) => None,
@ -135,19 +107,13 @@ impl std::error::Error for PeachError {
PeachError::PasswordNotSet => None, PeachError::PasswordNotSet => None,
PeachError::Read { ref source, .. } => Some(source), PeachError::Read { ref source, .. } => Some(source),
PeachError::Regex(_) => None, PeachError::Regex(_) => None,
PeachError::Sbot(_) => None, PeachError::SbotCli { .. } => None,
PeachError::SerdeJson(_) => None, PeachError::SerdeJson(_) => None,
PeachError::SerdeYaml(_) => None, PeachError::SerdeYaml(_) => None,
PeachError::SsbAdminIdNotFound { .. } => None, PeachError::SsbAdminIdNotFound { .. } => None,
PeachError::TomlDeser(_) => None,
PeachError::TomlSer(_) => None,
PeachError::Utf8ToStr(_) => None, PeachError::Utf8ToStr(_) => None,
PeachError::Utf8ToString(_) => None, PeachError::Utf8ToString(_) => None,
PeachError::Write { ref source, .. } => Some(source), PeachError::Write { ref source, .. } => Some(source),
PeachError::JsonRpcError(_) => None,
PeachError::SolarClientError(_) => None,
PeachError::TildeClientError(_) => None,
PeachError::SsbMessageError(_) => None,
} }
} }
} }
@ -155,15 +121,6 @@ impl std::error::Error for PeachError {
impl std::fmt::Display for PeachError { impl std::fmt::Display for PeachError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self { match *self {
PeachError::InvalidKey { ref key } => {
write!(f, "Invalid key in config lookup for key: {}", key)
}
PeachError::HomeDir => {
write!(
f,
"Unable to determine the path of the user's home directory"
)
}
PeachError::Io(ref err) => err.fmt(f), PeachError::Io(ref err) => err.fmt(f),
PeachError::JsonRpcClientCore(ref err) => err.fmt(f), PeachError::JsonRpcClientCore(ref err) => err.fmt(f),
PeachError::JsonRpcCore(ref err) => { PeachError::JsonRpcCore(ref err) => {
@ -178,19 +135,22 @@ impl std::fmt::Display for PeachError {
write!(f, "Date/time parse error: {}", path) write!(f, "Date/time parse error: {}", path)
} }
PeachError::PasswordIncorrect => { PeachError::PasswordIncorrect => {
write!(f, "password is incorrect") write!(f, "Password error: user-supplied password is incorrect")
} }
PeachError::PasswordMismatch => { PeachError::PasswordMismatch => {
write!(f, "passwords do not match") write!(f, "Password error: user-supplied passwords do not match")
} }
PeachError::PasswordNotSet => { PeachError::PasswordNotSet => {
write!(f, "hash value in YAML configuration file is empty") write!(
f,
"Password error: hash value in YAML configuration file is empty"
)
} }
PeachError::Read { ref path, .. } => { PeachError::Read { ref path, .. } => {
write!(f, "Read error: {}", path) write!(f, "Read error: {}", path)
} }
PeachError::Regex(ref err) => err.fmt(f), PeachError::Regex(ref err) => err.fmt(f),
PeachError::Sbot(ref msg) => { PeachError::SbotCli { ref msg } => {
write!(f, "Sbot error: {}", msg) write!(f, "Sbot error: {}", msg)
} }
PeachError::SerdeJson(ref err) => err.fmt(f), PeachError::SerdeJson(ref err) => err.fmt(f),
@ -198,17 +158,11 @@ impl std::fmt::Display for PeachError {
PeachError::SsbAdminIdNotFound { ref id } => { PeachError::SsbAdminIdNotFound { ref id } => {
write!(f, "Config error: SSB admin ID `{}` not found", id) write!(f, "Config error: SSB admin ID `{}` not found", id)
} }
PeachError::TomlDeser(ref err) => err.fmt(f),
PeachError::TomlSer(ref err) => err.fmt(f),
PeachError::Utf8ToStr(ref err) => err.fmt(f), PeachError::Utf8ToStr(ref err) => err.fmt(f),
PeachError::Utf8ToString(ref err) => err.fmt(f), PeachError::Utf8ToString(ref err) => err.fmt(f),
PeachError::Write { ref path, .. } => { PeachError::Write { ref path, .. } => {
write!(f, "Write error: {}", path) write!(f, "Write error: {}", path)
} }
PeachError::JsonRpcError(ref err) => err.fmt(f),
PeachError::SolarClientError(ref err) => err.fmt(f),
PeachError::TildeClientError(ref err) => err.fmt(f),
PeachError::SsbMessageError(ref err) => err.fmt(f),
} }
} }
} }
@ -255,18 +209,6 @@ impl From<serde_yaml::Error> for PeachError {
} }
} }
impl From<toml::de::Error> for PeachError {
fn from(err: toml::de::Error) -> PeachError {
PeachError::TomlDeser(err)
}
}
impl From<toml::ser::Error> for PeachError {
fn from(err: toml::ser::Error) -> PeachError {
PeachError::TomlSer(err)
}
}
impl From<str::Utf8Error> for PeachError { impl From<str::Utf8Error> for PeachError {
fn from(err: str::Utf8Error) -> PeachError { fn from(err: str::Utf8Error) -> PeachError {
PeachError::Utf8ToStr(err) PeachError::Utf8ToStr(err)
@ -278,16 +220,3 @@ impl From<string::FromUtf8Error> for PeachError {
PeachError::Utf8ToString(err) PeachError::Utf8ToString(err)
} }
} }
impl From<JsonRpcError> for PeachError {
fn from(err: JsonRpcError) -> PeachError {
PeachError::JsonRpcError(err)
}
}
impl From<anyhow::Error> for PeachError {
fn from(error: anyhow::Error) -> Self {
// TODO: include whole error somehow?
PeachError::SolarClientError(error.to_string())
}
}

View File

@ -4,13 +4,11 @@ pub mod error;
pub mod network_client; pub mod network_client;
pub mod oled_client; pub mod oled_client;
pub mod password_utils; pub mod password_utils;
pub mod sbot; pub mod sbot_client;
pub mod stats_client; pub mod stats_client;
pub mod ssb_messages;
// re-export error types // re-export error types
pub use jsonrpc_client_core; pub use jsonrpc_client_core;
pub use jsonrpc_core; pub use jsonrpc_core;
pub use serde_json; pub use serde_json;
pub use serde_yaml; pub use serde_yaml;
pub use tilde_client;

View File

@ -1,16 +1,13 @@
use async_std::task;
use log::debug;
use nanorand::{Rng, WyRand}; use nanorand::{Rng, WyRand};
use sha3::{Digest, Sha3_256}; use sha3::{Digest, Sha3_256};
use crate::sbot::init_sbot; use crate::{config_manager, error::PeachError, sbot_client};
use crate::{config_manager, error::PeachError, sbot::SbotConfig};
/// Returns Ok(()) if the supplied password is correct, /// Returns Ok(()) if the supplied password is correct,
/// and returns Err if the supplied password is incorrect. /// and returns Err if the supplied password is incorrect.
pub fn verify_password(password: &str) -> Result<(), PeachError> { pub fn verify_password(password: &str) -> Result<(), PeachError> {
let real_admin_password_hash = config_manager::get_admin_password_hash()?; let real_admin_password_hash = config_manager::get_admin_password_hash()?;
let password_hash = hash_password(password); let password_hash = hash_password(&password.to_string());
if real_admin_password_hash == password_hash { if real_admin_password_hash == password_hash {
Ok(()) Ok(())
} else { } else {
@ -32,8 +29,8 @@ pub fn validate_new_passwords(new_password1: &str, new_password2: &str) -> Resul
/// Sets a new password for the admin user /// Sets a new password for the admin user
pub fn set_new_password(new_password: &str) -> Result<(), PeachError> { pub fn set_new_password(new_password: &str) -> Result<(), PeachError> {
let new_password_hash = hash_password(new_password); let new_password_hash = hash_password(&new_password.to_string());
config_manager::set_admin_password_hash(new_password_hash)?; config_manager::set_admin_password_hash(&new_password_hash)?;
Ok(()) Ok(())
} }
@ -52,8 +49,8 @@ pub fn hash_password(password: &str) -> String {
/// Sets a new temporary password for the admin user /// Sets a new temporary password for the admin user
/// which can be used to reset the permanent password /// which can be used to reset the permanent password
pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> { pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError> {
let new_password_hash = hash_password(new_password); let new_password_hash = hash_password(&new_password.to_string());
config_manager::set_temporary_password_hash(new_password_hash)?; config_manager::set_temporary_password_hash(&new_password_hash)?;
Ok(()) Ok(())
} }
@ -62,7 +59,7 @@ pub fn set_new_temporary_password(new_password: &str) -> Result<(), PeachError>
/// and returns Err if the supplied temp_password is incorrect /// and returns Err if the supplied temp_password is incorrect
pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> { pub fn verify_temporary_password(password: &str) -> Result<(), PeachError> {
let temporary_admin_password_hash = config_manager::get_temporary_password_hash()?; let temporary_admin_password_hash = config_manager::get_temporary_password_hash()?;
let password_hash = hash_password(password); let password_hash = hash_password(&password.to_string());
if temporary_admin_password_hash == password_hash { if temporary_admin_password_hash == password_hash {
Ok(()) Ok(())
} else { } else {
@ -86,7 +83,7 @@ pub fn send_password_reset() -> Result<(), PeachError> {
"Your new temporary password is: {} "Your new temporary password is: {}
If you are on the same WiFi network as your PeachCloud device you can reset your password \ If you are on the same WiFi network as your PeachCloud device you can reset your password \
using this link: http://peach.local/auth/reset", using this link: http://peach.local/reset_password",
temporary_password temporary_password
); );
// if there is an external domain, then include remote link in message // if there is an external domain, then include remote link in message
@ -95,7 +92,7 @@ using this link: http://peach.local/auth/reset",
Some(domain) => { Some(domain) => {
format!( format!(
"\n\nOr if you are on a different WiFi network, you can reset your password \ "\n\nOr if you are on a different WiFi network, you can reset your password \
using the the following link: {}/auth/reset", using the the following link: {}/reset_password",
domain domain
) )
} }
@ -103,25 +100,9 @@ using this link: http://peach.local/auth/reset",
}; };
msg += &remote_link; msg += &remote_link;
// finally send the message to the admins // finally send the message to the admins
let ssb_admin_ids = config_manager::get_ssb_admin_ids()?; let peach_config = config_manager::load_peach_config()?;
for ssb_admin_id in ssb_admin_ids { for ssb_admin_id in peach_config.ssb_admin_ids {
// use golgi to send a private message on scuttlebutt sbot_client::private_message(&msg, &ssb_admin_id)?;
match task::block_on(publish_private_msg(&msg, &ssb_admin_id)) {
Ok(_) => (),
Err(e) => return Err(e),
}
} }
Ok(()) Ok(())
} }
async fn publish_private_msg(msg: &str, recipient: &str) -> Result<(), PeachError> {
// initialise sbot connection with ip:port and shscap from config file
let mut sbot_client = init_sbot().await?;
debug!("Publishing a Scuttlebutt private message with temporary password");
match sbot_client.private_message(recipient, msg).await {
Ok(_) => Ok(()),
Err(e) => Err(PeachError::TildeClientError(format!("Failed to publish private message: {}", e))),
}
}

View File

@ -1,281 +0,0 @@
//! Data types and associated methods for monitoring and configuring solar-sbot.
use std::{fs, fs::File, io, io::Write, path::PathBuf, process::Command, str};
use std::os::linux::raw::ino_t;
use tilde_client::{TildeClient};
use log::debug;
use crate::config_manager;
use serde::{Deserialize, Serialize};
use crate::error::PeachError;
/* HELPER FUNCTIONS */
// iterate over the given directory path to determine the size of the directory
fn dir_size(path: impl Into<PathBuf>) -> io::Result<u64> {
fn dir_size(mut dir: fs::ReadDir) -> io::Result<u64> {
dir.try_fold(0, |acc, file| {
let file = file?;
let size = match file.metadata()? {
data if data.is_dir() => dir_size(fs::read_dir(file.path())?)?,
data => data.len(),
};
Ok(acc + size)
})
}
dir_size(fs::read_dir(path.into())?)
}
/* SBOT-RELATED TYPES AND METHODS */
/// solar-sbot process status.
#[derive(Debug, Serialize, Deserialize)]
pub struct SbotStatus {
/// Current process state.
pub state: Option<String>,
/// Current process boot state.
pub boot_state: Option<String>,
/// Current process memory usage in bytes.
pub memory: Option<u32>,
/// Uptime for the process (if state is `active`).
pub uptime: Option<String>,
/// Downtime for the process (if state is `inactive`).
pub downtime: Option<String>,
/// Size of the blobs directory in bytes.
pub blobstore: Option<u64>,
}
/// Default builder for `SbotStatus`.
impl Default for SbotStatus {
fn default() -> Self {
Self {
state: None,
boot_state: None,
memory: None,
uptime: None,
downtime: None,
blobstore: None,
}
}
}
impl SbotStatus {
/// Retrieve statistics for the solar-sbot systemd process by querying `systemctl`.
pub fn read() -> Result<Self, PeachError> {
let mut status = SbotStatus::default();
// note this command does not need to be run as sudo
// because non-privileged users are able to run systemctl show
let service_name = config_manager::get_config_value("TILDE_SBOT_SERVICE")?;
let info_output = Command::new("systemctl")
.arg("show")
.arg(service_name)
.arg("--no-page")
.output()?;
let service_info = std::str::from_utf8(&info_output.stdout)?;
for line in service_info.lines() {
if line.starts_with("ActiveState=") {
if let Some(state) = line.strip_prefix("ActiveState=") {
status.state = Some(state.to_string())
}
} else if line.starts_with("MemoryCurrent=") {
if let Some(memory) = line.strip_prefix("MemoryCurrent=") {
status.memory = memory.parse().ok()
}
}
}
// note this command does not need to be run as sudo
// because non-privileged users are able to run systemctl status
let status_output = Command::new("systemctl")
.arg("status")
.arg(config_manager::get_config_value("TILDE_SBOT_SERVICE")?)
.output()?;
let service_status = str::from_utf8(&status_output.stdout)?;
//.map_err(PeachError::Utf8ToStr)?;
for line in service_status.lines() {
// example of the output line we're looking for:
// `Loaded: loaded (/home/glyph/.config/systemd/user/solar-sbot.service; enabled; vendor
// preset: enabled)`
if line.contains("Loaded:") {
let before_boot_state = line.find(';');
let after_boot_state = line.rfind(';');
if let (Some(start), Some(end)) = (before_boot_state, after_boot_state) {
// extract the enabled / disabled from the `Loaded: ...` line
// using the index of the first ';' + 2 and the last ';'
status.boot_state = Some(line[start + 2..end].to_string());
}
// example of the output line we're looking for here:
// `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago`
} else if line.contains("Active:") {
let before_time = line.find(';');
let after_time = line.find(" ago");
if let (Some(start), Some(end)) = (before_time, after_time) {
// extract the uptime / downtime from the `Active: ...` line
// using the index of ';' + 2 and the index of " ago"
let time = Some(&line[start + 2..end]);
// if service is active then the `time` reading is uptime
if status.state == Some("active".to_string()) {
status.uptime = time.map(|t| t.to_string())
// if service is inactive then the `time` reading is downtime
} else if status.state == Some("inactive".to_string()) {
status.downtime = time.map(|t| t.to_string())
}
}
}
}
// TOOD restore this
// get path to blobstore
// let blobstore_path = format!(
// "{}/blobs/sha256",
// config_manager::get_config_value("TILDE_SBOT_DATADIR")?
// );
let blobstore_path = format!(
"{}",
config_manager::get_config_value("TILDE_SBOT_DATADIR")?
);
// determine the size of the blobstore directory in bytes
status.blobstore = dir_size(blobstore_path).ok();
Ok(status)
}
}
/// solar-sbot configuration parameters.
#[derive(Debug, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct Config {
// TODO: maybe define as a Path type?
/// Directory path for the log and indexes.
pub database_directory: String,
/// Secret-handshake app-key (aka. network key).
pub shscap: String,
/// HMAC hash used to sign messages.
pub hmac: String,
/// Replication hops (1: friends, 2: friends of friends).
pub replication_hops: u8,
/// Ip address of pub
pub ip: String,
/// Address to listen on.
pub ssb_port: String,
/// Enable sending local UDP broadcasts.
pub localadv: bool,
/// Enable listening for UDP broadcasts and connecting.
pub localdiscov: bool,
/// Enable syncing by using epidemic-broadcast-trees (EBT).
#[serde(rename(serialize = "enable_ebt", deserialize = "enable-ebt"))]
pub enable_ebt: bool,
/// Bypass graph auth and fetch remote's feed (useful for pubs that are restoring their data
/// from peer; user beware - caveats about).
pub promisc: bool,
/// Disable the UNIX socket RPC interface.
pub nounixsock: bool,
}
// TODO: make this real
#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct SbotConfig {
pub database_directory: String,
pub shscap: String,
pub hmac: String,
pub replication_hops: i8,
pub ip: String,
pub ssb_port: String,
// TODO: below settings have not been configured with tilde
pub localadv: bool,
pub localdiscov: bool,
pub enable_ebt: bool,
pub promisc: bool,
pub nounixsock: bool,
}
/// Default configuration values for solar-sbot.
impl Default for SbotConfig {
fn default() -> Self {
Self {
database_directory: "~/.local/share/tildefriends".to_string(),
shscap: "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s=".to_string(),
hmac: "".to_string(),
replication_hops: 1,
ip: "".to_string(),
ssb_port: "8008".to_string(),
localadv: false,
localdiscov: false,
enable_ebt: false,
promisc: false,
nounixsock: false,
}
}
}
impl SbotConfig {
/// Read the solar-sbot `config.toml` file from file and deserialize into `SbotConfig`.
pub fn read() -> Result<Self, PeachError> {
// determine path of user's solar-sbot config.toml
let config_path = format!(
"{}/tilde-sbot.toml",
config_manager::get_config_value("TILDE_SBOT_DATADIR")?
);
println!("TILDE_SBOT_CONFIG_PATH: {}", config_path);
let config_contents = fs::read_to_string(config_path)?;
let config: SbotConfig = toml::from_str(&config_contents)?;
Ok(config)
}
/// Write the given `SbotConfig` to the tilde-sbot `tilde-sbot.toml` file.
pub fn write(config: SbotConfig) -> Result<(), PeachError> {
let repo_comment = "# For details about tilde-sbot configuration, please visit tilde friends documentation\n".to_string();
// convert the provided `SbotConfig` instance to a string
let config_string = toml::to_string(&config)?;
// determine path of user's solar-sbot config.toml
let config_path = format!(
"{}/tilde-sbot.toml",
config_manager::get_config_value("TILDE_SBOT_DATADIR")?
);
// open config file for writing
let mut file = File::create(config_path)?;
// write the repo comment to file
write!(file, "{}", repo_comment)?;
// write the config string to file
write!(file, "{}", config_string)?;
Ok(())
}
}
/// Initialise an sbot client
pub async fn init_sbot() -> Result<TildeClient, PeachError> {
// read sbot config
let sbot_config = match SbotConfig::read() {
Ok(config) => config,
// build default config if an error is returned from the read attempt
Err(_) => SbotConfig::default(),
};
debug!("Initialising an sbot client with configuration parameters");
let database_path = format!("{}/db.sqlite", config_manager::get_config_value("TILDE_SBOT_DATADIR")?);
let sbot_client = TildeClient {
ssb_port: sbot_config.ssb_port,
tilde_binary_path: config_manager::get_config_value("TILDE_BINARY_PATH")?,
tilde_database_path: database_path,
};
Ok(sbot_client)
}

View File

@ -0,0 +1,111 @@
//! Interfaces for monitoring and configuring go-sbot using sbotcli.
use std::process::Command;
use serde::{Deserialize, Serialize};
use crate::error::PeachError;
pub fn is_sbot_online() -> Result<bool, PeachError> {
let output = Command::new("/usr/bin/systemctl")
.arg("status")
.arg("peach-go-sbot")
.output()?;
let status = output.status;
// returns true if the service had an exist status of 0 (is running)
let is_running = status.success();
Ok(is_running)
}
/// currently go-sbotcli determines where the working directory is
/// using the home directory of th user that invokes it
/// this could be changed to be supplied as CLI arg
/// but for now all sbotcli commands must first become peach-go-sbot before running
/// the sudoers file is configured to allow this to happen without a password
pub fn sbotcli_command() -> Command {
let mut command = Command::new("sudo");
command
.arg("-u")
.arg("peach-go-sbot")
.arg("/usr/bin/sbotcli");
command
}
pub fn post(msg: &str) -> Result<(), PeachError> {
let mut command = sbotcli_command();
let output = command.arg("publish").arg("post").arg(msg).output()?;
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCli {
msg: format!("Error making ssb post: {}", stderr),
})
}
}
#[derive(Serialize, Deserialize)]
struct WhoAmIValue {
id: String,
}
pub fn whoami() -> Result<String, PeachError> {
let mut command = sbotcli_command();
let output = command.arg("call").arg("whoami").output()?;
let text_output = std::str::from_utf8(&output.stdout)?;
let value: WhoAmIValue = serde_json::from_str(text_output)?;
let id = value.id;
Ok(id)
}
pub fn create_invite(uses: i32) -> Result<String, PeachError> {
let mut command = sbotcli_command();
let output = command
.arg("invite")
.arg("create")
.arg("--uses")
.arg(uses.to_string())
.output()?;
let text_output = std::str::from_utf8(&output.stdout)?;
let output = text_output.replace("\n", "");
Ok(output)
}
pub fn update_pub_name(new_name: &str) -> Result<(), PeachError> {
let pub_ssb_id = whoami()?;
let mut command = sbotcli_command();
let output = command
.arg("publish")
.arg("about")
.arg("--name")
.arg(new_name)
.arg(pub_ssb_id)
.output()?;
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCli {
msg: format!("Error updating pub name: {}", stderr),
})
}
}
pub fn private_message(msg: &str, recipient: &str) -> Result<(), PeachError> {
let mut command = sbotcli_command();
let output = command
.arg("publish")
.arg("post")
.arg("--recps")
.arg(recipient)
.arg(msg)
.output()?;
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr)?;
Err(PeachError::SbotCli {
msg: format!("Error sending ssb private message: {}", stderr),
})
}
}

View File

@ -1,104 +0,0 @@
//! Message types and conversion methods.
use kuska_ssb::api::dto::content::TypedMessage;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt::Debug;
use crate::error::PeachError;
use crate::error::PeachError::SsbMessageError;
/// `SsbMessageContent` is a type alias for `TypedMessage` from the `kuska_ssb` library.
/// It is aliased in golgi to fit the naming convention of the other message
/// types: `SsbMessageKVT` and `SsbMessageValue`.
///
/// See the [kuska source code](https://github.com/Kuska-ssb/ssb/blob/master/src/api/dto/content.rs#L103) for the type definition of `TypedMessage`.
pub type SsbMessageContent = TypedMessage;
/// The `value` of an SSB message (the `V` in `KVT`).
///
/// More information concerning the data model can be found in the
/// [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata).
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[allow(missing_docs)]
pub struct SsbMessageValue {
pub previous: Option<String>,
pub author: String,
pub sequence: u64,
pub timestamp: f64,
pub hash: String,
pub content: Value,
pub signature: String,
}
/// Message content types.
#[derive(Debug, Eq, PartialEq)]
#[allow(missing_docs)]
pub enum SsbMessageContentType {
About,
Vote,
Post,
Contact,
Unrecognized,
}
impl SsbMessageValue {
/// Get the type field of the message content as an enum, if found.
///
/// If no `type` field is found or the `type` field is not a string,
/// it returns an `Err(GolgiError::ContentType)`.
///
/// If a `type` field is found but with an unknown string,
/// it returns an `Ok(SsbMessageContentType::Unrecognized)`.
pub fn get_message_type(&self) -> Result<SsbMessageContentType, PeachError> {
let msg_type = self
.content
.get("type")
.ok_or_else(|| SsbMessageError("type field not found".to_string()))?;
let mtype_str: &str = msg_type.as_str().ok_or_else(|| {
SsbMessageError("type field value is not a string as expected".to_string())
})?;
let enum_type = match mtype_str {
"about" => SsbMessageContentType::About,
"post" => SsbMessageContentType::Post,
"vote" => SsbMessageContentType::Vote,
"contact" => SsbMessageContentType::Contact,
_ => SsbMessageContentType::Unrecognized,
};
Ok(enum_type)
}
/// Helper function which returns `true` if this message is of the given type,
/// and `false` if the type does not match or is not found.
pub fn is_message_type(&self, message_type: SsbMessageContentType) -> bool {
let self_message_type = self.get_message_type();
match self_message_type {
Ok(mtype) => mtype == message_type,
Err(_err) => false,
}
}
/// Convert the content JSON value into an `SsbMessageContent` `enum`,
/// using the `type` field as a tag to select which variant of the `enum`
/// to deserialize into.
///
/// See the [Serde docs on internally-tagged enum representations](https://serde.rs/enum-representations.html#internally-tagged) for further details.
pub fn into_ssb_message_content(self) -> Result<SsbMessageContent, PeachError> {
let m: SsbMessageContent = serde_json::from_value(self.content)?;
Ok(m)
}
}
/// An SSB message represented as a key-value-timestamp (`KVT`).
///
/// More information concerning the data model can be found in the
/// [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata).
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
#[allow(missing_docs)]
pub struct SsbMessageKVT {
pub key: String,
pub value: SsbMessageValue,
pub timestamp: Option<f64>,
pub rts: Option<f64>,
}

View File

@ -1,6 +1,6 @@
[package] [package]
name = "peach-network" name = "peach-network"
version = "0.5.0" version = "0.4.1"
authors = ["Andrew Reid <glyph@mycelial.technology>"] authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2021" edition = "2021"
description = "Query and configure network interfaces." description = "Query and configure network interfaces."

View File

@ -1,6 +1,6 @@
# peach-network # peach-network
![Generic badge](https://img.shields.io/badge/version-0.4.2-<COLOR>.svg) ![Generic badge](https://img.shields.io/badge/version-0.4.0-<COLOR>.svg)
Network interface state query and modification library. Network interface state query and modification library.

View File

@ -6,7 +6,7 @@ use std::num::ParseIntError;
use io::Error as IoError; use io::Error as IoError;
use probes::ProbeError; use probes::ProbeError;
use regex::Error as RegexError; use regex::Error as RegexError;
use wpactrl::Error as WpaError; use wpactrl::WpaError;
/// Custom error type encapsulating all possible errors when querying /// Custom error type encapsulating all possible errors when querying
/// network interfaces and modifying their state. /// network interfaces and modifying their state.

View File

@ -14,7 +14,6 @@
//! access point credentials to `wpa_supplicant-<wlan_iface>.conf`. //! access point credentials to `wpa_supplicant-<wlan_iface>.conf`.
use std::{ use std::{
collections::HashMap,
fs::OpenOptions, fs::OpenOptions,
io::prelude::*, io::prelude::*,
process::{Command, Stdio}, process::{Command, Stdio},
@ -23,7 +22,6 @@ use std::{
}; };
use probes::network; use probes::network;
use wpactrl::Client as WpaClient;
#[cfg(feature = "miniserde_support")] #[cfg(feature = "miniserde_support")]
use miniserde::{Deserialize, Serialize}; use miniserde::{Deserialize, Serialize};
@ -107,86 +105,8 @@ pub struct Traffic {
pub transmitted: u64, pub transmitted: u64,
} }
/// Access point data including state and signal strength.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct AccessPoint {
/// Access point data retrieved via scan.
pub detail: Option<Scan>,
/// Current state of the access point (e.g. "Available" or "Out of range").
pub state: String,
/// Signal strength of the access point as a percentage.
pub signal: Option<i32>,
}
impl AccessPoint {
fn available(detail: Option<Scan>, signal: Option<i32>) -> AccessPoint {
AccessPoint {
detail,
state: String::from("Available"),
signal,
}
}
fn saved() -> AccessPoint {
AccessPoint {
detail: None,
state: String::from("Out of range"),
signal: None,
}
}
}
/* GET - Methods for retrieving data */ /* GET - Methods for retrieving data */
/// Retrieve combined list of available (in-range) and saved wireless access
/// points for a given network interface.
///
/// # Arguments
///
/// * `iface` - A string slice holding the name of a wireless network interface
///
/// If the list results include one or more access points for the given network
/// interface, an `Ok` `Result` type is returned containing `HashMap<String,
/// AccessPoint>`.
///
/// Each entry in the returned `HashMap` contains an SSID (`String`) and
/// `AccessPoint` `struct`. If no access points are found, an empty `HashMap`
/// is returned in the `Result`. In the event of an error, a `NetworkError`
/// is returned in the `Result`.
pub fn all_networks(iface: &str) -> Result<HashMap<String, AccessPoint>, NetworkError> {
let mut wlan_networks = HashMap::new();
if let Ok(Some(networks)) = available_networks(iface) {
for ap in networks {
let ssid = ap.ssid.clone();
let rssi = ap.signal_level.clone();
// parse the string to a signed integer (for math)
let rssi_parsed = rssi.parse::<i32>().unwrap();
// perform rssi (dBm) to quality (%) conversion
let quality_percent = 2 * (rssi_parsed + 100);
let ap_detail = AccessPoint::available(Some(ap), Some(quality_percent));
wlan_networks.insert(ssid, ap_detail);
}
}
if let Ok(Some(networks)) = saved_networks() {
for saved_ssid in networks {
if !wlan_networks.contains_key(&saved_ssid) {
let ssid = saved_ssid.clone();
let ap_detail = AccessPoint::saved();
wlan_networks.insert(ssid, ap_detail);
}
}
}
Ok(wlan_networks)
}
/// Retrieve list of available wireless access points for a given network /// Retrieve list of available wireless access points for a given network
/// interface. /// interface.
/// ///
@ -201,7 +121,7 @@ pub fn all_networks(iface: &str) -> Result<HashMap<String, AccessPoint>, Network
/// In the event of an error, a `NetworkError` is returned in the `Result`. /// In the event of an error, a `NetworkError` is returned in the `Result`.
pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError> { pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
wpa.request("SCAN")?; wpa.request("SCAN")?;
let networks = wpa.request("SCAN_RESULTS")?; let networks = wpa.request("SCAN_RESULTS")?;
let mut scan = Vec::new(); let mut scan = Vec::new();
@ -218,7 +138,7 @@ pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError
// we only want to return the auth / crypto flags // we only want to return the auth / crypto flags
if flags_vec[0] != "[ESS]" { if flags_vec[0] != "[ESS]" {
// parse auth / crypto flag and assign it to protocol // parse auth / crypto flag and assign it to protocol
protocol.push_str(flags_vec[0].replace('[', "").replace(']', "").as_str()); protocol.push_str(flags_vec[0].replace("[", "").replace("]", "").as_str());
} }
let ssid = v[4].to_string(); let ssid = v[4].to_string();
let response = Scan { let response = Scan {
@ -253,7 +173,7 @@ pub fn available_networks(iface: &str) -> Result<Option<Vec<Scan>>, NetworkError
/// event of an error, a `NetworkError` is returned in the `Result`. /// event of an error, a `NetworkError` is returned in the `Result`.
pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> { pub fn id(iface: &str, ssid: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let networks = wpa.request("LIST_NETWORKS")?; let networks = wpa.request("LIST_NETWORKS")?;
let mut id = Vec::new(); let mut id = Vec::new();
for network in networks.lines() { for network in networks.lines() {
@ -312,7 +232,7 @@ pub fn ip(iface: &str) -> Result<Option<String>, NetworkError> {
/// `Result`. /// `Result`.
pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> { pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let status = wpa.request("SIGNAL_POLL")?; let status = wpa.request("SIGNAL_POLL")?;
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?; let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
@ -339,7 +259,7 @@ pub fn rssi(iface: &str) -> Result<Option<String>, NetworkError> {
/// the `Result`. /// the `Result`.
pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> { pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let status = wpa.request("SIGNAL_POLL")?; let status = wpa.request("SIGNAL_POLL")?;
let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?; let rssi = utils::regex_finder(r"RSSI=(.*)\n", &status)?;
@ -371,7 +291,7 @@ pub fn rssi_percent(iface: &str) -> Result<Option<String>, NetworkError> {
/// is returned in the `Result`. In the event of an error, a `NetworkError` is /// is returned in the `Result`. In the event of an error, a `NetworkError` is
/// returned in the `Result`. /// returned in the `Result`.
pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> { pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
let mut wpa = WpaClient::builder().open()?; let mut wpa = wpactrl::WpaCtrl::builder().open()?;
let networks = wpa.request("LIST_NETWORKS")?; let networks = wpa.request("LIST_NETWORKS")?;
let mut ssids = Vec::new(); let mut ssids = Vec::new();
for network in networks.lines() { for network in networks.lines() {
@ -403,7 +323,7 @@ pub fn saved_networks() -> Result<Option<Vec<String>>, NetworkError> {
/// returned in the `Result`. /// returned in the `Result`.
pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> { pub fn ssid(iface: &str) -> Result<Option<String>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let status = wpa.request("STATUS")?; let status = wpa.request("STATUS")?;
// pass the regex pattern and status output to the regex finder // pass the regex pattern and status output to the regex finder
@ -459,7 +379,7 @@ pub fn state(iface: &str) -> Result<Option<String>, NetworkError> {
/// a `NetworkError` is returned in the `Result`. /// a `NetworkError` is returned in the `Result`.
pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> { pub fn status(iface: &str) -> Result<Option<Status>, NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let wpa_status = wpa.request("STATUS")?; let wpa_status = wpa.request("STATUS")?;
// pass the regex pattern and status output to the regex finder // pass the regex pattern and status output to the regex finder
@ -659,7 +579,7 @@ pub fn check_iface(wlan_iface: &str, ap_iface: &str) -> Result<(), NetworkError>
/// is returned in the `Result`. /// is returned in the `Result`.
pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> { pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let select = format!("SELECT {}", id); let select = format!("SELECT {}", id);
wpa.request(&select)?; wpa.request(&select)?;
Ok(()) Ok(())
@ -678,7 +598,7 @@ pub fn connect(id: &str, iface: &str) -> Result<(), NetworkError> {
/// returned in the `Result`. /// returned in the `Result`.
pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> { pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let remove = format!("REMOVE_NETWORK {}", id); let remove = format!("REMOVE_NETWORK {}", id);
wpa.request(&remove)?; wpa.request(&remove)?;
Ok(()) Ok(())
@ -697,7 +617,7 @@ pub fn delete(id: &str, iface: &str) -> Result<(), NetworkError> {
/// `Result`. /// `Result`.
pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> { pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let disable = format!("DISABLE_NETWORK {}", id); let disable = format!("DISABLE_NETWORK {}", id);
wpa.request(&disable)?; wpa.request(&disable)?;
Ok(()) Ok(())
@ -714,7 +634,7 @@ pub fn disable(id: &str, iface: &str) -> Result<(), NetworkError> {
/// error, a `NetworkError` is returned in the `Result`. /// error, a `NetworkError` is returned in the `Result`.
pub fn disconnect(iface: &str) -> Result<(), NetworkError> { pub fn disconnect(iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let disconnect = "DISCONNECT".to_string(); let disconnect = "DISCONNECT".to_string();
wpa.request(&disconnect)?; wpa.request(&disconnect)?;
Ok(()) Ok(())
@ -765,7 +685,7 @@ pub fn forget(iface: &str, ssid: &str) -> Result<(), NetworkError> {
/// event of an error, a `NetworkError` is returned in the `Result`. /// event of an error, a `NetworkError` is returned in the `Result`.
pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> { pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
let new_pass = format!("NEW_PASSWORD {} {}", id, pass); let new_pass = format!("NEW_PASSWORD {} {}", id, pass);
wpa.request(&new_pass)?; wpa.request(&new_pass)?;
Ok(()) Ok(())
@ -782,7 +702,7 @@ pub fn modify(id: &str, iface: &str, pass: &str) -> Result<(), NetworkError> {
/// error, a `NetworkError` is returned in the `Result`. /// error, a `NetworkError` is returned in the `Result`.
pub fn reassociate(iface: &str) -> Result<(), NetworkError> { pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
wpa.request("REASSOCIATE")?; wpa.request("REASSOCIATE")?;
Ok(()) Ok(())
} }
@ -794,7 +714,7 @@ pub fn reassociate(iface: &str) -> Result<(), NetworkError> {
/// `Result` type is returned. In the event of an error, a `NetworkError` is /// `Result` type is returned. In the event of an error, a `NetworkError` is
/// returned in the `Result`. /// returned in the `Result`.
pub fn reconfigure() -> Result<(), NetworkError> { pub fn reconfigure() -> Result<(), NetworkError> {
let mut wpa = WpaClient::builder().open()?; let mut wpa = wpactrl::WpaCtrl::builder().open()?;
wpa.request("RECONFIGURE")?; wpa.request("RECONFIGURE")?;
Ok(()) Ok(())
} }
@ -810,7 +730,7 @@ pub fn reconfigure() -> Result<(), NetworkError> {
/// event of an error, a `NetworkError` is returned in the `Result`. /// event of an error, a `NetworkError` is returned in the `Result`.
pub fn reconnect(iface: &str) -> Result<(), NetworkError> { pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface); let wpa_path: String = format!("/var/run/wpa_supplicant/{}", iface);
let mut wpa = WpaClient::builder().ctrl_path(wpa_path).open()?; let mut wpa = wpactrl::WpaCtrl::builder().ctrl_path(wpa_path).open()?;
wpa.request("DISCONNECT")?; wpa.request("DISCONNECT")?;
wpa.request("RECONNECT")?; wpa.request("RECONNECT")?;
Ok(()) Ok(())
@ -822,7 +742,7 @@ pub fn reconnect(iface: &str) -> Result<(), NetworkError> {
/// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the /// `wpa_supplicant.conf` file, an `Ok` `Result` type is returned. In the
/// event of an error, a `NetworkError` is returned in the `Result`. /// event of an error, a `NetworkError` is returned in the `Result`.
pub fn save() -> Result<(), NetworkError> { pub fn save() -> Result<(), NetworkError> {
let mut wpa = WpaClient::builder().open()?; let mut wpa = wpactrl::WpaCtrl::builder().open()?;
wpa.request("SAVE_CONFIG")?; wpa.request("SAVE_CONFIG")?;
Ok(()) Ok(())
} }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "peach-stats" name = "peach-stats"
version = "0.3.1" version = "0.2.0"
authors = ["Andrew Reid <glyph@mycelial.technology>"] authors = ["Andrew Reid <glyph@mycelial.technology>"]
edition = "2018" edition = "2018"
description = "Query system statistics. Provides a wrapper around the probes and systemstat crates." description = "Query system statistics. Provides a wrapper around the probes and systemstat crates."

View File

@ -1,10 +1,10 @@
# peach-stats # peach-stats
![Generic badge](https://img.shields.io/badge/version-0.3.0-<COLOR>.svg) ![Generic badge](https://img.shields.io/badge/version-0.2.0-<COLOR>.svg)
System statistics library for PeachCloud. Provides a wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates. System statistics library for PeachCloud. Provides a wrapper around the [probes](https://crates.io/crates/probes) and [systemstat](https://crates.io/crates/systemstat) crates.
Currently offers the following system statistics and associated data structures: Currently offers the following statistics and associated data structures:
- CPU: `user`, `system`, `nice`, `idle` (as values or percentages) - CPU: `user`, `system`, `nice`, `idle` (as values or percentages)
- Disk usage: `filesystem`, `one_k_blocks`, `one_k_blocks_used`, - Disk usage: `filesystem`, `one_k_blocks`, `one_k_blocks_used`,
@ -13,14 +13,10 @@ Currently offers the following system statistics and associated data structures:
- Memory: `total`, `free`, `used` - Memory: `total`, `free`, `used`
- Uptime: `seconds` - Uptime: `seconds`
As well as the following go-sbot process statistics:
- Sbot: `state`, `memory`, `uptime`, `downtime`
## Example Usage ## Example Usage
```rust ```rust
use peach_stats::{sbot, stats, StatsError}; use peach_stats::{stats, StatsError};
fn main() -> Result<(), StatsError> { fn main() -> Result<(), StatsError> {
let cpu = stats::cpu_stats()?; let cpu = stats::cpu_stats()?;
@ -29,7 +25,6 @@ fn main() -> Result<(), StatsError> {
let load = stats::load_average()?; let load = stats::load_average()?;
let mem = stats::mem_stats()?; let mem = stats::mem_stats()?;
let uptime = stats::uptime()?; let uptime = stats::uptime()?;
let sbot_process = sbot::sbot_stats()?;
// do things with the retrieved values... // do things with the retrieved values...

View File

@ -1,7 +1,7 @@
//! Custom error type for `peach-stats`. //! Custom error type for `peach-stats`.
use probes::ProbeError; use probes::ProbeError;
use std::{error, fmt, io::Error as IoError, str::Utf8Error}; use std::{error, fmt, io::Error as IoError};
/// Custom error type encapsulating all possible errors when retrieving system /// Custom error type encapsulating all possible errors when retrieving system
/// statistics. /// statistics.
@ -17,10 +17,6 @@ pub enum StatsError {
MemStat(ProbeError), MemStat(ProbeError),
/// Failed to retrieve system uptime. /// Failed to retrieve system uptime.
Uptime(IoError), Uptime(IoError),
/// Systemctl command returned an error.
Systemctl(IoError),
/// Failed to interpret sequence of `u8` as a string.
Utf8String(Utf8Error),
} }
impl error::Error for StatsError {} impl error::Error for StatsError {}
@ -43,12 +39,6 @@ impl fmt::Display for StatsError {
StatsError::Uptime(ref source) => { StatsError::Uptime(ref source) => {
write!(f, "Failed to retrieve system uptime: {}", source) write!(f, "Failed to retrieve system uptime: {}", source)
} }
StatsError::Systemctl(ref source) => {
write!(f, "Systemctl command returned an error: {}", source)
}
StatsError::Utf8String(ref source) => {
write!(f, "Failed to convert stdout to string: {}", source)
}
} }
} }
} }

View File

@ -43,7 +43,6 @@
//! ``` //! ```
pub mod error; pub mod error;
pub mod sbot;
pub mod stats; pub mod stats;
pub use crate::error::StatsError; pub use crate::error::StatsError;

View File

@ -1,111 +0,0 @@
//! Systemd go-sbot process statistics retrieval functions and associated data types.
use std::{process::Command, str};
#[cfg(feature = "miniserde_support")]
use miniserde::{Deserialize, Serialize};
#[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize};
use crate::StatsError;
/// go-sbot process statistics.
#[derive(Debug)]
#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct SbotStat {
/// Current process state.
pub state: Option<String>,
/// Current process boot state.
pub boot_state: Option<String>,
/// Current process memory usage in bytes.
pub memory: Option<u32>,
/// Uptime for the process (if state is `active`).
pub uptime: Option<String>,
/// Downtime for the process (if state is `inactive`).
pub downtime: Option<String>,
}
impl SbotStat {
/// Default builder for `SbotStat`.
fn default() -> Self {
Self {
state: None,
boot_state: None,
memory: None,
uptime: None,
downtime: None,
}
}
}
/// Retrieve statistics for the go-sbot systemd process by querying `systemctl`.
pub fn sbot_stats() -> Result<SbotStat, StatsError> {
let mut status = SbotStat::default();
let info_output = Command::new("sudo")
.arg("systemctl")
.arg("show")
.arg("go-sbot.service")
.arg("--no-page")
.output()
.map_err(StatsError::Systemctl)?;
let service_info = std::str::from_utf8(&info_output.stdout).map_err(StatsError::Utf8String)?;
for line in service_info.lines() {
if line.starts_with("ActiveState=") {
if let Some(state) = line.strip_prefix("ActiveState=") {
status.state = Some(state.to_string())
}
} else if line.starts_with("MemoryCurrent=") {
if let Some(memory) = line.strip_prefix("MemoryCurrent=") {
status.memory = memory.parse().ok()
}
}
}
let status_output = Command::new("sudo")
.arg("systemctl")
.arg("status")
.arg("go-sbot.service")
.output()
.map_err(StatsError::Systemctl)?;
let service_status = str::from_utf8(&status_output.stdout).map_err(StatsError::Utf8String)?;
for line in service_status.lines() {
// example of the output line we're looking for:
// `Loaded: loaded (/home/glyph/.config/systemd/user/go-sbot.service; enabled; vendor
// preset: enabled)`
if line.contains("Loaded:") {
let before_boot_state = line.find(';');
let after_boot_state = line.rfind(';');
if let (Some(start), Some(end)) = (before_boot_state, after_boot_state) {
// extract the enabled / disabled from the `Loaded: ...` line
// using the index of the first ';' + 2 and the last ';'
status.boot_state = Some(line[start + 2..end].to_string());
}
// example of the output line we're looking for here:
// `Active: active (running) since Mon 2022-01-24 16:22:51 SAST; 4min 14s ago`
} else if line.contains("Active:") {
let before_time = line.find(';');
let after_time = line.find(" ago");
if let (Some(start), Some(end)) = (before_time, after_time) {
// extract the uptime / downtime from the `Active: ...` line
// using the index of ';' + 2 and the index of " ago"
let time = Some(&line[start + 2..end]);
// if service is active then the `time` reading is uptime
if status.state == Some("active".to_string()) {
status.uptime = time.map(|t| t.to_string())
// if service is inactive then the `time` reading is downtime
} else if status.state == Some("inactive".to_string()) {
status.downtime = time.map(|t| t.to_string())
}
}
}
}
Ok(status)
}

15
peach-web-lite/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "peach-web-lite"
version = "0.1.0"
edition = "2021"
[dependencies]
env_logger = "0.9.0"
lazy_static = "1.4.0"
log = "0.4.14"
maud = "0.23.0"
peach-lib = { path = "../peach-lib" }
peach-network = { path = "../peach-network" }
peach-stats = { path = "../peach-stats" }
rouille = "3.5.0"
golgi = { path = "/home/glyph/Projects/playground/rust/golgi" }

24
peach-web-lite/README.md Normal file
View File

@ -0,0 +1,24 @@
# peach-web (lite)
A web interface for managing a Scuttlebutt pub.
## Application Structure
The application is divided between the route handlers (`src/routes`), context-builders (`src/context`), templates (`src/templates`) and authentication helper functions. The web server itself is initialised in `src/main.rs`. The context-builders are responsible for retrieving data which is required by the templates. This allows separation of data retrieval and data representation (the job of the templates).
## Built With
[Rouille](https://crates.io/crates/rouille): "a Rust web micro-framework".
[maud](https://crates.io/crates/maud): "Compile-time HTML templates".
[golgi](https://git.coopcloud.tech/golgi-ssb/golgi): "an experimental Scuttlebutt client library".
[peach-lib](https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-lib).
[peach-network](https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-network).
[peach-stats](https://git.coopcloud.tech/PeachCloud/peach-workspace/src/branch/main/peach-stats)
## Design Goals
Be lean and fast.
## Licensing
AGPL-3.0

68
peach-web-lite/ap_card Normal file
View File

@ -0,0 +1,68 @@
{%- if ap_state == "up" %}
<!-- NETWORK CARD -->
<div class="card center">
<!-- NETWORK INFO BOX -->
<div class="capsule capsule-container success-border">
<!-- NETWORK STATUS GRID -->
<div class="two-grid" title="PeachCloud network mode and status">
<!-- top-right config icon -->
<a class="link two-grid-top-right" href="/settings/network" title="Configure network settings">
<img id="configureNetworking" class="icon-small" src="/icons/cog.svg" alt="Configure">
</a>
<!-- left column -->
<!-- network mode icon with label -->
<div class="grid-column-1">
<img id="netModeIcon" class="center icon icon-active" src="/icons/router.svg" alt="WiFi router">
<label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="Access Point Online">ONLINE</label>
</div>
<!-- right column -->
<!-- network mode, ssid & ip with labels -->
<div class="grid-column-2">
<label class="label-small font-gray" for="netMode" title="Network Mode">MODE</label>
<p id="netMode" class="card-text" title="Network Mode">Access Point</p>
<label class="label-small font-gray" for="netSsid" title="Access Point SSID">SSID</label>
<p id="netSsid" class="card-text" title="SSID">peach</p>
<label class="label-small font-gray" for="netIp" title="Access Point IP Address">IP</label>
<p id="netIp" class="card-text" title="IP">{{ ap_ip }}</p>
</div>
</div>
<!-- horizontal dividing line -->
<hr>
<!-- DEVICES AND TRAFFIC GRID -->
<div class="three-grid card-container">
<div class="stack">
<img id="devices" class="icon icon-medium" title="Connected devices" src="/icons/devices.svg" alt="Digital devices">
<div class="flex-grid" style="padding-top: 0.5rem;">
<label class="label-medium" for="devices" style="padding-right: 3px;" title="Number of connected devices"></label>
</div>
<label class="label-small font-gray">DEVICES</label>
</div>
<div class="stack">
<img id="dataDownload" class="icon icon-medium" title="Download" src="/icons/down-arrow.svg" alt="Download">
<div class="flex-grid" style="padding-top: 0.5rem;">
{%- if ap_traffic -%}
<label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in {{ ap_traffic.rx_unit }}">{{ ap_traffic.received }}</label>
<label class="label-small font-near-black">{{ ap_traffic.rx_unit }}</label>
{%- else -%}
<label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total"></label>
<label class="label-small font-near-black"></label>
{%- endif -%}
</div>
<label class="label-small font-gray">DOWNLOAD</label>
</div>
<div class="stack">
<img id="dataUpload" class="icon icon-medium" title="Upload" src="/icons/up-arrow.svg" alt="Upload">
<div class="flex-grid" style="padding-top: 0.5rem;">
{%- if ap_traffic -%}
<label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in {{ ap_traffic.tx_unit }}">{{ ap_traffic.transmitted }}</label>
<label class="label-small font-near-black">{{ ap_traffic.tx_unit }}</label>
{%- else -%}
<label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total"></label>
<label class="label-small font-near-black"></label>
{%- endif -%}
</div>
<label class="label-small font-gray">UPLOAD</label>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,12 @@
[ peach-web-lite ]
230 total dependencies
Finished release [optimized] target(s) in 35.12s
[ peach-web ]
522 total dependencies
Finished release [optimized] target(s) in 1m 33s

View File

@ -0,0 +1,23 @@
use log::debug;
use peach_lib::password_utils;
use crate::error::PeachWebError;
/// Password save form request handler. This function is for use by a user who is already logged in to change their password.
pub fn save_password_form(
current_password: String,
new_password1: String,
new_password2: String,
) -> Result<(), PeachWebError> {
debug!("attempting to change password");
password_utils::verify_password(&current_password)?;
// if the previous line did not throw an error, then the old password is correct
password_utils::validate_new_passwords(&new_password1, &new_password2)?;
// if the previous line did not throw an error, then the new password is valid
password_utils::set_new_password(&new_password1)?;
Ok(())
}

View File

@ -0,0 +1,8 @@
use peach_lib::config_manager;
use crate::error::PeachWebError;
pub fn ssb_admin_ids() -> Result<Vec<String>, PeachWebError> {
let peach_config = config_manager::load_peach_config()?;
Ok(peach_config.ssb_admin_ids)
}

View File

@ -0,0 +1,4 @@
pub mod admin;
pub mod network;
pub mod status;
pub mod test;

View File

@ -0,0 +1,402 @@
//! Data retrieval for the purpose of hydrating HTML templates.
use std::collections::HashMap;
use log::info;
use peach_lib::{
config_manager, dyndns_client,
error::PeachError,
jsonrpc_client_core::{Error, ErrorKind},
jsonrpc_core::types::error::ErrorCode,
};
use peach_network::{
network,
network::{Scan, Status, Traffic},
};
use crate::error::PeachWebError;
#[derive(Debug)]
pub struct AccessPoint {
pub detail: Option<Scan>,
pub signal: Option<i32>,
pub state: String,
}
pub fn ap_state() -> String {
match network::state("ap0") {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
}
}
fn convert_traffic(traffic: Traffic) -> Option<IfaceTraffic> {
// modify traffic values & assign measurement unit
// based on received and transmitted values
let (rx, rx_unit) = if traffic.received > 1_047_527_424 {
// convert to GB
(traffic.received / 1_073_741_824, "GB".to_string())
} else if traffic.received > 0 {
// otherwise, convert it to MB
((traffic.received / 1024) / 1024, "MB".to_string())
} else {
(0, "MB".to_string())
};
let (tx, tx_unit) = if traffic.transmitted > 1_047_527_424 {
// convert to GB
(traffic.transmitted / 1_073_741_824, "GB".to_string())
} else if traffic.transmitted > 0 {
((traffic.transmitted / 1024) / 1024, "MB".to_string())
} else {
(0, "MB".to_string())
};
Some(IfaceTraffic {
rx,
rx_unit,
tx,
tx_unit,
})
}
#[derive(Debug)]
pub struct ConfigureDNSContext {
pub external_domain: String,
pub dyndns_subdomain: String,
pub enable_dyndns: bool,
pub is_dyndns_online: bool,
}
impl ConfigureDNSContext {
pub fn build() -> ConfigureDNSContext {
let peach_config = config_manager::load_peach_config().unwrap();
let dyndns_fulldomain = peach_config.dyn_domain;
let is_dyndns_online = dyndns_client::is_dns_updater_online().unwrap();
let dyndns_subdomain =
dyndns_client::get_dyndns_subdomain(&dyndns_fulldomain).unwrap_or(dyndns_fulldomain);
ConfigureDNSContext {
external_domain: peach_config.external_domain,
dyndns_subdomain,
enable_dyndns: peach_config.dyn_enabled,
is_dyndns_online,
}
}
}
// TODO: this should probably rather go into the appropriate `routes` file
pub fn save_dns_configuration(
external_domain: String,
enable_dyndns: bool,
dynamic_domain: String,
) -> Result<(), PeachWebError> {
// first save local configurations
config_manager::set_external_domain(&external_domain)?;
config_manager::set_dyndns_enabled_value(enable_dyndns)?;
// if dynamic dns is enabled and this is a new domain name, then register it
if enable_dyndns {
let full_dynamic_domain = dyndns_client::get_full_dynamic_domain(&dynamic_domain);
// check if this is a new domain or if its already registered
let is_new_domain = dyndns_client::check_is_new_dyndns_domain(&full_dynamic_domain);
if is_new_domain {
match dyndns_client::register_domain(&full_dynamic_domain) {
Ok(_) => {
info!("Registered new dyndns domain");
// successful update
Ok(())
}
Err(err) => {
info!("Failed to register dyndns domain: {:?}", err);
// json response for failed update
let msg: String = match err {
// TODO: make this a nest high-level match
// PeachError::JsonRpcClientCore(Error(ErrorKind::JsonRpcError(err), _))
PeachError::JsonRpcClientCore(source) => {
match source {
Error(ErrorKind::JsonRpcError(err), _state) => match err.code {
ErrorCode::ServerError(-32030) => {
format!("Error registering domain: {} was previously registered", full_dynamic_domain)
}
_ => {
format!("Failed to register dyndns domain {:?}", err)
}
},
_ => {
format!("Failed to register dyndns domain: {:?}", source)
}
}
}
_ => "Failed to register dyndns domain".to_string(),
};
Err(PeachWebError::FailedToRegisterDynDomain(msg))
}
}
}
// if the domain is already registered, then dont re-register, and just return success
else {
Ok(())
}
} else {
Ok(())
}
}
#[derive(Debug)]
pub struct IfaceTraffic {
pub rx: u64,
pub rx_unit: String,
pub tx: u64,
pub tx_unit: String,
}
#[derive(Debug)]
pub struct NetworkDetailContext {
pub saved_aps: Vec<String>,
pub wlan_ip: String,
pub wlan_networks: HashMap<String, AccessPoint>,
pub wlan_rssi: Option<String>,
pub wlan_ssid: String,
pub wlan_state: String,
pub wlan_status: Option<Status>,
pub wlan_traffic: Option<IfaceTraffic>,
}
impl NetworkDetailContext {
pub fn build() -> NetworkDetailContext {
// TODO: read this value from the config file
let wlan_iface = "wlan0".to_string();
let wlan_ip = match network::ip(&wlan_iface) {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
// list of networks saved in wpa_supplicant.conf
let wlan_list = match network::saved_networks() {
Ok(Some(ssids)) => ssids,
_ => Vec::new(),
};
// list of networks saved in wpa_supplicant.conf
let saved_aps = wlan_list.clone();
let wlan_rssi = match network::rssi_percent(&wlan_iface) {
Ok(rssi) => rssi,
Err(_) => None,
};
// list of networks currently in range (online & accessible)
let wlan_scan = match network::available_networks(&wlan_iface) {
Ok(Some(networks)) => networks,
_ => Vec::new(),
};
let wlan_ssid = match network::ssid(&wlan_iface) {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
let wlan_state = match network::state(&wlan_iface) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let wlan_status = match network::status(&wlan_iface) {
Ok(status) => status,
// interface unavailable
_ => None,
};
let wlan_traffic = match network::traffic(&wlan_iface) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => None,
};
// create a hashmap to combine wlan_list & wlan_scan without repetition
let mut wlan_networks = HashMap::new();
for ap in wlan_scan {
let ssid = ap.ssid.clone();
let rssi = ap.signal_level.clone();
// parse the string to a signed integer (for math)
let rssi_parsed = rssi.parse::<i32>().unwrap();
// perform rssi (dBm) to quality (%) conversion
let quality_percent = 2 * (rssi_parsed + 100);
let ap_detail = AccessPoint {
detail: Some(ap),
state: "Available".to_string(),
signal: Some(quality_percent),
};
wlan_networks.insert(ssid, ap_detail);
}
for network in wlan_list {
// avoid repetition by checking that ssid is not already in list
if !wlan_networks.contains_key(&network) {
let ssid = network.clone();
let net_detail = AccessPoint {
detail: None,
state: "Not in range".to_string(),
signal: None,
};
wlan_networks.insert(ssid, net_detail);
}
}
NetworkDetailContext {
saved_aps,
wlan_ip,
wlan_networks,
wlan_rssi,
wlan_ssid,
wlan_state,
wlan_status,
wlan_traffic,
}
}
}
#[derive(Debug)]
pub struct NetworkListContext {
pub ap_state: String,
pub wlan_networks: HashMap<String, String>,
pub wlan_ssid: String,
}
impl NetworkListContext {
pub fn build() -> NetworkListContext {
// TODO: read these values from the config file
let ap_iface = "ap0".to_string();
let wlan_iface = "wlan0".to_string();
//let wlan_iface = "wlp0s20f0u2".to_string();
// list of networks saved in wpa_supplicant.conf
let wlan_list = match network::saved_networks() {
Ok(Some(ssids)) => ssids,
_ => Vec::new(),
};
// list of networks currently in range (online & accessible)
let wlan_scan = match network::available_networks(&wlan_iface) {
Ok(Some(networks)) => networks,
_ => Vec::new(),
};
let wlan_ssid = match network::ssid(&wlan_iface) {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
// create a hashmap to combine wlan_list & wlan_scan without repetition
let mut wlan_networks = HashMap::new();
for ap in wlan_scan {
wlan_networks.insert(ap.ssid, "Available".to_string());
}
for network in wlan_list {
// insert ssid (with state) only if it doesn't already exist
wlan_networks
.entry(network)
.or_insert_with(|| "Not in range".to_string());
}
let ap_state = match network::state(&ap_iface) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
NetworkListContext {
ap_state,
wlan_networks,
wlan_ssid,
}
}
}
#[derive(Debug)]
pub struct NetworkStatusContext {
pub ap_ip: String,
pub ap_ssid: String,
pub ap_state: String,
pub ap_traffic: Option<IfaceTraffic>,
pub wlan_ip: String,
pub wlan_rssi: Option<String>,
pub wlan_ssid: String,
pub wlan_state: String,
pub wlan_status: Option<Status>,
pub wlan_traffic: Option<IfaceTraffic>,
}
impl NetworkStatusContext {
pub fn build() -> Self {
// TODO: read these values from config file
let ap_iface = "ap0".to_string();
let wlan_iface = "wlan0".to_string();
let ap_ip = match network::ip(&ap_iface) {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
let ap_ssid = match network::ssid(&ap_iface) {
Ok(Some(ssid)) => ssid,
_ => "Not currently activated".to_string(),
};
let ap_state = match network::state(&ap_iface) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let ap_traffic = match network::traffic(&ap_iface) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => None,
};
let wlan_ip = match network::ip(&wlan_iface) {
Ok(Some(ip)) => ip,
_ => "x.x.x.x".to_string(),
};
let wlan_rssi = match network::rssi_percent(&wlan_iface) {
Ok(rssi) => rssi,
_ => None,
};
let wlan_ssid = match network::ssid(&wlan_iface) {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
let wlan_state = match network::state(&wlan_iface) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let wlan_status = match network::status(&wlan_iface) {
Ok(status) => status,
_ => None,
};
let wlan_traffic = match network::traffic(&wlan_iface) {
// convert bytes to mb or gb and add appropriate units
Ok(Some(traffic)) => convert_traffic(traffic),
_ => None,
};
NetworkStatusContext {
ap_ip,
ap_ssid,
ap_state,
ap_traffic,
wlan_ip,
wlan_rssi,
wlan_ssid,
wlan_state,
wlan_status,
wlan_traffic,
}
}
}

View File

@ -0,0 +1,76 @@
use peach_stats::{stats, stats::LoadAverage};
/// System statistics data.
pub struct StatusContext {
pub cpu_usage_percent: Option<f32>,
pub disk_usage_percent: Option<u32>,
pub disk_free: Option<u64>,
pub load_average: Option<LoadAverage>,
pub mem_usage_percent: Option<u64>,
pub mem_used: Option<u64>,
pub mem_free: Option<u64>,
pub mem_total: Option<u64>,
pub uptime: Option<u32>,
}
impl StatusContext {
pub fn build() -> StatusContext {
// convert result to Option<CpuStatPercentages>, discard any error
let cpu_usage_percent = stats::cpu_stats_percent()
.ok()
.map(|cpu| (cpu.nice + cpu.system + cpu.user).round());
let load_average = stats::load_average().ok();
let mem_stats = stats::mem_stats().ok();
let (mem_usage_percent, mem_used, mem_free, mem_total) = match mem_stats {
Some(mem) => (
Some((mem.used / mem.total) * 100),
Some(mem.used / 1024),
Some(mem.free / 1024),
Some(mem.total / 1024),
),
None => (None, None, None, None),
};
let uptime = match stats::uptime() {
Ok(secs) => {
let uptime_mins = secs / 60;
uptime_mins.to_string()
}
Err(_) => "Unavailable".to_string(),
};
// parse the uptime string to a signed integer (for math)
let uptime_parsed = uptime.parse::<u32>().ok();
let disk_usage_stats = match stats::disk_usage() {
Ok(disks) => disks,
Err(_) => Vec::new(),
};
// select only the partition we're interested in: "/"
let disk_stats = disk_usage_stats.iter().find(|disk| disk.mountpoint == "/");
let (disk_usage_percent, disk_free) = match disk_stats {
Some(disk) => (
Some(disk.used_percentage),
// calculate free disk space in megabytes
Some(disk.one_k_blocks_free / 1024),
),
None => (None, None),
};
StatusContext {
cpu_usage_percent,
disk_usage_percent,
disk_free,
load_average,
mem_usage_percent,
mem_used,
mem_free,
mem_total,
uptime: uptime_parsed,
}
}
}

View File

@ -0,0 +1,12 @@
use golgi;
use golgi::sbot::Sbot;
pub async fn test_async() -> Result<(), Box<dyn std::error::Error>> {
let mut sbot_client = Sbot::init(Some("127.0.0.1:8009".to_string()), None).await?;
let id = sbot_client.whoami().await?;
println!("whoami: {}", id);
Ok(())
}

View File

@ -0,0 +1,60 @@
//! Custom error type representing all possible error variants for peach-web.
use peach_lib::error::PeachError;
use peach_lib::{serde_json, serde_yaml};
use serde_json::error::Error as JsonError;
use serde_yaml::Error as YamlError;
/// Custom error type encapsulating all possible errors for the web application.
#[derive(Debug)]
pub enum PeachWebError {
Json(JsonError),
Yaml(YamlError),
FailedToRegisterDynDomain(String),
PeachLib { source: PeachError, msg: String },
}
impl std::error::Error for PeachWebError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
PeachWebError::Json(ref source) => Some(source),
PeachWebError::Yaml(ref source) => Some(source),
PeachWebError::FailedToRegisterDynDomain(_) => None,
PeachWebError::PeachLib { ref source, .. } => Some(source),
}
}
}
impl std::fmt::Display for PeachWebError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
PeachWebError::Json(ref source) => write!(f, "Serde JSON error: {}", source),
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
write!(f, "DYN DNS error: {}", msg)
}
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
}
}
}
impl From<JsonError> for PeachWebError {
fn from(err: JsonError) -> PeachWebError {
PeachWebError::Json(err)
}
}
impl From<YamlError> for PeachWebError {
fn from(err: YamlError) -> PeachWebError {
PeachWebError::Yaml(err)
}
}
impl From<PeachError> for PeachWebError {
fn from(err: PeachError) -> PeachWebError {
PeachWebError::PeachLib {
source: err,
msg: "".to_string(),
}
}
}

View File

@ -0,0 +1,42 @@
use std::env;
use lazy_static::lazy_static;
use log::info;
mod auth;
mod context;
mod error;
mod router;
mod routes;
mod templates;
lazy_static! {
// determine run-mode from env var; default to standalone mode (aka peachpub)
static ref STANDALONE_MODE: bool = match env::var("PEACH_STANDALONE_MODE") {
// parse the value to a boolean; default to true for any error
Ok(val) => val.parse().unwrap_or(true),
Err(_) => true
};
}
fn main() {
env_logger::init();
rouille::start_server("localhost:8000", move |request| {
info!("Now listening on localhost:8000");
// static file server: matches on assets in the `static` directory
if let Some(request) = request.remove_prefix("/static") {
return rouille::match_assets(&request, "static");
}
// configure the router based on run-mode
if *STANDALONE_MODE {
info!("Running in standalone mode");
router::minimal_router(request)
} else {
info!("Running in fully-featured mode");
router::complete_router(request)
}
});
}

View File

@ -0,0 +1,194 @@
use rouille::{router, Request, Response};
use crate::routes;
/// Define router for standalone mode (PeachPub).
pub fn minimal_router(request: &Request) -> Response {
router!(request,
(GET) (/) => {
routes::home::menu()
},
(GET) (/help) => {
routes::help::menu()
},
(GET) (/login) => {
routes::login::login()
},
(POST) (/login) => {
routes::login::login_post(request)
},
(GET) (/scuttlebutt/blocks) => {
routes::scuttlebutt::blocks()
},
(GET) (/scuttlebutt/follows) => {
routes::scuttlebutt::follows()
},
(GET) (/scuttlebutt/followers) => {
routes::scuttlebutt::followers()
},
(GET) (/scuttlebutt/friends) => {
routes::scuttlebutt::friends()
},
(GET) (/scuttlebutt/peers) => {
routes::scuttlebutt::peers()
},
(GET) (/scuttlebutt/private) => {
routes::scuttlebutt::private()
},
(GET) (/scuttlebutt/profile) => {
routes::scuttlebutt::profile()
},
(GET) (/settings) => {
routes::settings::menu()
},
(GET) (/settings/admin) => {
routes::settings::admin()
},
(GET) (/settings/admin/configure) => {
routes::settings::admin_configure()
},
(GET) (/settings/admin/add) => {
routes::settings::admin_add()
},
(POST) (/settings/admin/add) => {
routes::settings::admin_add_post(request)
},
(GET) (/settings/admin/change_password) => {
routes::settings::admin_change_password()
},
(POST) (/settings/admin/change_password) => {
routes::settings::admin_change_password_post(request)
},
(POST) (/settings/admin/delete) => {
routes::settings::admin_delete_post(request)
},
(GET) (/settings/scuttlebutt) => {
routes::settings::scuttlebutt()
},
(GET) (/status) => {
routes::status::status()
},
// return 404 if not match is found
_ => routes::catchers::not_found()
)
}
/// Define router for fully-featured mode (PeachCloud).
pub fn complete_router(request: &Request) -> Response {
router!(request,
(GET) (/async) => {
routes::home::async_test()
},
(GET) (/) => {
routes::home::menu()
},
(GET) (/help) => {
routes::help::menu()
},
(GET) (/login) => {
routes::login::login()
},
(POST) (/login) => {
routes::login::login_post(request)
},
(GET) (/scuttlebutt/blocks) => {
routes::scuttlebutt::blocks()
},
(GET) (/scuttlebutt/follows) => {
routes::scuttlebutt::follows()
},
(GET) (/scuttlebutt/followers) => {
routes::scuttlebutt::followers()
},
(GET) (/scuttlebutt/friends) => {
routes::scuttlebutt::friends()
},
(GET) (/scuttlebutt/peers) => {
routes::scuttlebutt::peers()
},
(GET) (/scuttlebutt/private) => {
routes::scuttlebutt::private()
},
(GET) (/scuttlebutt/profile) => {
routes::scuttlebutt::profile()
},
(GET) (/settings) => {
routes::settings::menu()
},
(GET) (/settings/admin) => {
routes::settings::admin()
},
(GET) (/settings/admin/configure) => {
routes::settings::admin_configure()
},
(GET) (/settings/admin/add) => {
routes::settings::admin_add()
},
(POST) (/settings/admin/add) => {
routes::settings::admin_add_post(request)
},
(GET) (/settings/admin/change_password) => {
routes::settings::admin_change_password()
},
(POST) (/settings/admin/change_password) => {
routes::settings::admin_change_password_post(request)
},
(POST) (/settings/admin/delete) => {
routes::settings::admin_delete_post(request)
},
(GET) (/settings/network) => {
routes::settings::network()
},
(GET) (/settings/network/wifi) => {
routes::settings::network_list_aps()
},
(POST) (/settings/network/wifi/connect) => {
routes::settings::network_connect_wifi(request)
},
(POST) (/settings/network/wifi/disconnect) => {
routes::settings::network_disconnect_wifi(request)
},
(POST) (/settings/network/wifi/forget) => {
routes::settings::network_forget_wifi(request)
},
(GET) (/settings/network/wifi/ssid/{ssid: String}) => {
routes::settings::network_detail(ssid)
},
(GET) (/settings/network/wifi/add) => {
routes::settings::network_add_ap(None)
},
(GET) (/settings/network/wifi/add/{ssid: String}) => {
routes::settings::network_add_ap(Some(ssid))
},
(POST) (/settings/network/wifi/add) => {
routes::settings::network_add_ap_post(request)
},
(GET) (/settings/network/wifi/modify) => {
routes::settings::network_modify_ap(None)
},
// TODO: see if we can use the ?= syntax for ssid param
(GET) (/settings/network/wifi/modify/{ssid: String}) => {
routes::settings::network_modify_ap(Some(ssid))
},
(POST) (/settings/network/wifi/modify) => {
routes::settings::network_modify_ap_post(request)
},
(GET) (/settings/network/dns) => {
routes::settings::network_configure_dns()
},
(POST) (/settings/network/dns) => {
routes::settings::network_configure_dns_post(request)
},
(GET) (/settings/scuttlebutt) => {
routes::settings::scuttlebutt()
},
(GET) (/status) => {
routes::status::status()
},
(GET) (/status/network) => {
routes::status::network()
},
// return 404 if not match is found
_ => routes::catchers::not_found()
)
}

View File

@ -0,0 +1,10 @@
use log::debug;
use rouille::Response;
use crate::templates;
pub fn not_found() -> Response {
debug!("received GET request for a route which is not defined");
Response::html(templates::catchers::not_found()).with_status_code(404)
}

View File

@ -0,0 +1,10 @@
use log::debug;
use rouille::Response;
use crate::templates;
pub fn menu() -> Response {
debug!("received GET request for: /help");
Response::html(templates::help::menu())
}

View File

@ -0,0 +1,14 @@
use log::debug;
use rouille::Response;
use crate::templates;
pub fn menu() -> Response {
debug!("received GET request for: /");
Response::html(templates::home::menu())
}
pub fn async_test() -> Response {
Response::html(templates::home::async_test())
}

View File

@ -0,0 +1,26 @@
use log::debug;
use rouille::{post_input, try_or_400, Request, Response};
use crate::templates;
pub fn login() -> Response {
debug!("received GET request for: /login");
Response::html(templates::login::login())
}
pub fn login_post(request: &Request) -> Response {
debug!("received POST request for: /login");
let data = try_or_400!(post_input!(request, {
username: String,
password: String,
}));
// TODO: handle authentication...
debug!("{:?}", data);
// TODO: add flash message
Response::redirect_302("/")
}

View File

@ -0,0 +1,7 @@
pub mod catchers;
pub mod help;
pub mod home;
pub mod login;
pub mod scuttlebutt;
pub mod settings;
pub mod status;

View File

@ -0,0 +1,46 @@
use log::debug;
use rouille::Response;
use crate::templates;
pub fn blocks() -> Response {
debug!("received GET request for: /scuttlebutt/blocks");
Response::html(templates::scuttlebutt::peers_list("Blocks".to_string()))
}
pub fn follows() -> Response {
debug!("received GET request for: /scuttlebutt/follows");
Response::html(templates::scuttlebutt::peers_list("Follows".to_string()))
}
pub fn followers() -> Response {
debug!("received GET request for: /scuttlebutt/followers");
Response::html(templates::scuttlebutt::peers_list("Followers".to_string()))
}
pub fn friends() -> Response {
debug!("received GET request for: /scuttlebutt/friends");
Response::html(templates::scuttlebutt::peers_list("Friends".to_string()))
}
pub fn peers() -> Response {
debug!("received GET request for: /scuttlebutt/peers");
Response::html(templates::scuttlebutt::peers())
}
pub fn private() -> Response {
debug!("received GET request for: /scuttlebutt/private");
Response::html(templates::scuttlebutt::private())
}
pub fn profile() -> Response {
debug!("received GET request for: /scuttlebutt/profile");
Response::html(templates::scuttlebutt::profile(None, None))
}

View File

@ -0,0 +1,340 @@
use log::{debug, warn};
use peach_lib::config_manager;
use peach_network::network;
use rouille::{post_input, try_or_400, Request, Response};
use crate::auth;
use crate::context;
use crate::templates;
pub fn menu() -> Response {
debug!("received GET request for: /settings");
Response::html(templates::settings::menu::menu())
}
pub fn admin() -> Response {
debug!("received GET request for: /settings/admin");
Response::html(templates::settings::admin::menu())
}
pub fn admin_configure() -> Response {
debug!("received GET request for: /settings/admin/configure");
Response::html(templates::settings::admin::configure(None, None))
}
pub fn admin_add() -> Response {
debug!("received GET request for: /settings/admin/add");
Response::html(templates::settings::admin::add(None, None))
}
pub fn admin_add_post(request: &Request) -> Response {
debug!("received POST request for: /settings/admin/add");
let data = try_or_400!(post_input!(request, { ssb_id: String }));
debug!("{:?}", data);
let (flash_name, flash_msg) = match config_manager::add_ssb_admin_id(&data.ssb_id) {
Ok(_) => (
"success".to_string(),
"Added new SSB administrator ID".to_string(),
),
Err(e) => (
"error".to_string(),
format!("Failed to add new SSB administrator ID: {}", e),
),
};
Response::html(templates::settings::admin::add(
Some(flash_msg),
Some(flash_name),
))
}
pub fn admin_change_password() -> Response {
debug!("received GET request for: /settings/admin/change_password");
Response::html(templates::settings::admin::change_password(None, None))
}
pub fn admin_change_password_post(request: &Request) -> Response {
debug!("received POST request for: /settings/admin/change_password");
let data = try_or_400!(post_input!(request, {
current_password: String,
new_password1: String,
new_password2: String
}));
// attempt to update the password
let (flash_name, flash_msg) = match auth::save_password_form(
data.current_password,
data.new_password1,
data.new_password2,
) {
Ok(_) => ("success".to_string(), "Saved new password".to_string()),
Err(e) => (
"error".to_string(),
format!("Failed to save new password: {}", e),
),
};
Response::html(templates::settings::admin::change_password(
Some(flash_msg),
Some(flash_name),
))
}
pub fn admin_delete_post(request: &Request) -> Response {
debug!("received POST request for: /settings/admin/delete");
let data = try_or_400!(post_input!(request, { ssb_id: String }));
let (flash_name, flash_msg) = match config_manager::delete_ssb_admin_id(&data.ssb_id) {
Ok(_) => (
"success".to_string(),
"Removed SSB administrator ID".to_string(),
),
Err(e) => (
"error".to_string(),
format!("Failed to remove SSB administrator ID: {}", e),
),
};
Response::html(templates::settings::admin::configure(
Some(flash_msg),
Some(flash_name),
))
}
pub fn network() -> Response {
debug!("received GET request for: /settings/network");
Response::html(templates::settings::network::menu())
}
pub fn network_add_ap(ssid: Option<String>) -> Response {
debug!("received GET request for: /settings/network/wifi/add");
Response::html(templates::settings::network::add_ap(ssid, None, None))
}
pub fn network_add_ap_post(request: &Request) -> Response {
debug!("received POST request for: /settings/network/wifi/add");
// TODO: read this value from the config file instead
let wlan_iface = "wlan0".to_string();
let data = try_or_400!(post_input!(request, { ssid: String, pass: String }));
/* ADD WIFI CREDENTIALS FOR AP */
// check if the credentials already exist for this access point
let creds_exist = match network::saved_networks() {
Ok(Some(ssids)) => ssids.contains(&data.ssid),
_ => false,
};
let (flash_name, flash_msg) = if creds_exist {
(
"error".to_string(),
"Network credentials already exist for this access point".to_string(),
)
} else {
// if credentials not found, generate and write wifi config to wpa_supplicant
match network::add(&wlan_iface, &data.ssid, &data.pass) {
Ok(_) => {
debug!("Added WiFi credentials.");
// force reread of wpa_supplicant.conf file with new credentials
match network::reconfigure() {
Ok(_) => debug!("Successfully reconfigured wpa_supplicant"),
Err(_) => warn!("Failed to reconfigure wpa_supplicant"),
}
("success".to_string(), "Added WiFi credentials".to_string())
}
Err(e) => {
debug!("Failed to add WiFi credentials.");
("error".to_string(), format!("{}", e))
}
}
};
Response::html(templates::settings::network::add_ap(
None,
Some(flash_msg),
Some(flash_name),
))
}
pub fn network_configure_dns() -> Response {
debug!("received GET request for: /settings/network/dns");
Response::html(templates::settings::network::configure_dns(None, None))
}
pub fn network_configure_dns_post(request: &Request) -> Response {
debug!("received POST request for: /settings/network/dns");
let data = try_or_400!(
post_input!(request, { external_domain: String, enable_dyndns: bool, dynamic_domain: String })
);
let (flash_name, flash_msg) = match context::network::save_dns_configuration(
data.external_domain,
data.enable_dyndns,
data.dynamic_domain,
) {
Ok(_) => (
"success".to_string(),
"New dynamic DNS configuration is now enabled".to_string(),
),
Err(e) => (
"error".to_string(),
format!("Failed to save DNS configuration: {}", e),
),
};
Response::html(templates::settings::network::configure_dns(
Some(flash_msg),
Some(flash_name),
))
}
pub fn network_modify_ap(ssid: Option<String>) -> Response {
debug!("received GET request for: /settings/network/wifi/modify");
Response::html(templates::settings::network::modify_ap(ssid, None, None))
}
pub fn network_modify_ap_post(request: &Request) -> Response {
debug!("received POST request for: /settings/network/wifi/modify");
// TODO: read this value from the config file instead
let wlan_iface = "wlan0".to_string();
let data = try_or_400!(post_input!(request, { ssid: String, pass: String }));
/* MODIFY WIFI CREDENTIALS FOR AP */
let (flash_name, flash_msg) = match network::update(&wlan_iface, &data.ssid, &data.pass) {
Ok(_) => ("success".to_string(), "WiFi password updated".to_string()),
Err(e) => (
"error".to_string(),
format!("Failed to update WiFi password: {}", e),
),
};
Response::html(templates::settings::network::modify_ap(
None,
Some(flash_msg),
Some(flash_name),
))
}
pub fn network_connect_wifi(request: &Request) -> Response {
debug!("received POST request for: /settings/network/wifi/connect");
// TODO: read this value from the config file instead
let wlan_iface = "wlan0".to_string();
let data = try_or_400!(post_input!(request, { ssid: String }));
let (flash_name, flash_msg) = match network::id(&wlan_iface, &data.ssid) {
Ok(Some(id)) => match network::connect(&id, &wlan_iface) {
Ok(_) => (
"success".to_string(),
"Connected to chosen network".to_string(),
),
Err(e) => (
"error".to_string(),
format!("Failed to connect to chosen network: {}", e),
),
},
_ => (
"error".to_string(),
"Failed to retrieve the network ID".to_string(),
),
};
Response::html(templates::settings::network::network_detail(
data.ssid,
Some(flash_msg),
Some(flash_name),
))
}
pub fn network_disconnect_wifi(request: &Request) -> Response {
debug!("received POST request for: /settings/network/wifi/disconnect");
// TODO: read this value from the config file instead
let wlan_iface = "wlan0".to_string();
let data = try_or_400!(post_input!(request, { ssid: String }));
let (flash_name, flash_msg) = match network::disable(&wlan_iface, &data.ssid) {
Ok(_) => (
"success".to_string(),
"Disconnected from WiFi network".to_string(),
),
Err(e) => (
"error".to_string(),
format!("Failed to disconnect from WiFi network: {}", e),
),
};
Response::html(templates::settings::network::network_detail(
data.ssid,
Some(flash_msg),
Some(flash_name),
))
}
pub fn network_forget_wifi(request: &Request) -> Response {
debug!("received POST request for: /settings/network/wifi/forget");
// TODO: read this value from the config file instead
let wlan_iface = "wlan0".to_string();
let data = try_or_400!(post_input!(request, { ssid: String }));
let (flash_name, flash_msg) = match network::forget(&wlan_iface, &data.ssid) {
Ok(_) => (
"success".to_string(),
"WiFi credentials removed".to_string(),
),
Err(e) => (
"error".to_string(),
format!("Failed to remove WiFi credentials: {}", e),
),
};
Response::html(templates::settings::network::network_detail(
data.ssid,
Some(flash_msg),
Some(flash_name),
))
}
pub fn network_detail(ssid: String) -> Response {
debug!("received GET request for: /settings/network/wifi/<selected>");
Response::html(templates::settings::network::network_detail(
ssid, None, None,
))
}
pub fn network_list_aps() -> Response {
debug!("received GET request for: /settings/network/wifi");
Response::html(templates::settings::network::list_aps())
}
pub fn scuttlebutt() -> Response {
debug!("received GET request for: /settings/scuttlebutt");
Response::html(templates::settings::scuttlebutt::scuttlebutt())
}

View File

@ -0,0 +1,16 @@
use log::debug;
use rouille::Response;
use crate::templates;
pub fn network() -> Response {
debug!("received GET request for: /status/network");
Response::html(templates::status::network())
}
pub fn status() -> Response {
debug!("received GET request for: /status");
Response::html(templates::status::status())
}

View File

@ -0,0 +1,51 @@
use maud::{html, PreEscaped, DOCTYPE};
pub fn base(back: String, title: String, content: PreEscaped<String>) -> PreEscaped<String> {
html! {
(DOCTYPE)
html lang="en" {
head {
meta charset="utf-8";
title { "PeachCloud" }
meta name="description" content="PeachCloud Network";
meta name="author" content="glyph";
meta name="viewport" content="width=device-width, initial-scale=1.0";
link rel="icon" type="image/x-icon" href="/static/icons/peach-icon.png";
link rel="stylesheet" href="/static/css/peachcloud.css";
}
body {
// render the navigation template
(nav(back, title, content))
}
}
}
}
pub fn nav(back: String, title: String, content: PreEscaped<String>) -> PreEscaped<String> {
html! {
(PreEscaped("<!-- TOP NAV BAR -->"))
nav class="nav-bar" {
a class="nav-item" href=(back) title="Back" {
img class="icon-medium nav-icon-left icon-active" src="/static/icons/back.svg" alt="Back";
}
h1 class="nav-title" { (title) }
a class="nav-item" id="logoutButton" href="/logout" title="Logout" {
img class="icon-medium nav-icon-right icon-active" src="/static/icons/enter.svg" alt="Enter";
}
}
(PreEscaped("<!-- MAIN CONTENT CONTAINER -->"))
main { (content) }
(PreEscaped("<!-- BOTTOM NAV BAR -->"))
nav class="nav-bar" {
a class="nav-item" href="https://scuttlebutt.nz/" {
img class="icon-medium nav-icon-left" title="Scuttlebutt Website" src="/static/icons/hermies.png" alt="Secure Scuttlebutt";
}
a class="nav-item" href="/" {
img class="icon nav-icon-left" src="/static/icons/peach-icon.png" alt="PeachCloud" title="Home";
}
a class="nav-item" href="/power" {
img class="icon-medium nav-icon-right icon-active" title="Shutdown" src="/static/icons/power.svg" alt="Power switch";
}
}
}
}

View File

@ -0,0 +1,21 @@
use maud::{html, PreEscaped};
use crate::templates;
pub fn not_found() -> PreEscaped<String> {
let back = "/".to_string();
let title = "404 Not Found".to_string();
let content = html! {
div class="card center" {
div class="capsule-container" {
div class="capsule info-border" {
p { "No PeachCloud resource exists for this URL. Please ensure that the URL in the address bar is correct." }
p { "Click the back arrow in the top-left or the PeachCloud logo at the bottom of your screen to return Home." }
}
}
}
};
templates::base::base(back, title, content)
}

View File

@ -0,0 +1,19 @@
use maud::{html, PreEscaped};
use crate::templates;
// /help
pub fn menu() -> PreEscaped<String> {
let back = "/".to_string();
let title = "Help".to_string();
let content = html! {
div class="card center" {
div class="card-container" {
p { "help content goes here" }
}
}
};
templates::base::base(back, title, content)
}

View File

@ -0,0 +1,77 @@
use maud::{html, PreEscaped};
use crate::context;
use crate::templates;
pub async fn async_test() -> PreEscaped<String> {
let back = "".to_string();
let title = "".to_string();
let whoami = context::test::test_async().await.unwrap();
let content = html! {
p { (whoami) }
};
templates::base::base(back, title, content)
}
pub fn menu() -> PreEscaped<String> {
let back = "".to_string();
let title = "".to_string();
let content = html! {
(PreEscaped("<!-- RADIAL MENU -->"))
div class="grid" {
(PreEscaped("<!-- top-left -->"))
(PreEscaped("<!-- PEERS LINK AND ICON -->"))
a class="top-left" href="/scuttlebutt/peers" title="Scuttlebutt Peers" {
div class="circle circle-small" {
img class="icon-medium" src="/static/icons/users.svg";
}
}
(PreEscaped("<!-- top-middle -->"))
(PreEscaped("<!-- CURRENT USER LINK AND ICON -->"))
a class="top-middle" href="/scuttlebutt/profile" title="Profile" {
div class="circle circle-small" {
img class="icon-medium" src="/static/icons/user.svg";
}
}
(PreEscaped("<!-- top-right -->"))
(PreEscaped("<!-- MESSAGES LINK AND ICON -->"))
a class="top-right" href="/scuttlebutt/private" title="Private Messages" {
div class="circle circle-small" {
img class="icon-medium" src="/static/icons/envelope.svg";
}
}
(PreEscaped("<!-- middle -->"))
a class="middle" href="/hello" {
div class="circle circle-large" { }
}
(PreEscaped("<!-- bottom-left -->"))
(PreEscaped("<!-- SYSTEM STATUS LINK AND ICON -->"))
a class="bottom-left" href="/status" title="Status" {
div class="circle circle-small" {
img class="icon-medium" src="/static/icons/heart-pulse.svg";
}
}
(PreEscaped("<!-- bottom-middle -->"))
(PreEscaped("<!-- PEACHCLOUD GUIDEBOOK LINK AND ICON -->"))
a class="bottom-middle" href="/help" title="Help Menu" {
div class="circle circle-small" {
img class="icon-medium" src="/static/icons/book.svg";
}
}
(PreEscaped("<!-- bottom-right -->"))
(PreEscaped("<!-- SYSTEM SETTINGS LINK AND ICON -->"))
a class="bottom-right" href="/settings" title="Settings Menu" {
div class="circle circle-small" {
img class="icon-medium" src="/static/icons/cog.svg";
}
}
}
};
// we pass the content of this template into the base template
templates::base::base(back, title, content)
}

View File

@ -0,0 +1,30 @@
use maud::{html, PreEscaped};
use crate::templates;
// https://github.com/tomaka/rouille/blob/master/examples/login-session.rs
// /login
pub fn login() -> PreEscaped<String> {
let back = "/".to_string();
let title = "Login".to_string();
let content = html! {
div class="card center" {
div class="card-container" {
form id="login_form" action="/login" method="post" {
// input field for username
input id="username" name="username" class="center input" type="text" placeholder="Username" title="Username for authentication" autofocus { }
// input field for password
input id="password" name="password" class="center input" type="password" placeholder="Password" title="Password for given username" { }
div id="buttonDiv" {
// login button
input id="loginUser" class="button button-primary center" title="Login" type="submit" value="Login" { }
}
}
}
}
};
templates::base::base(back, title, content)
}

View File

@ -0,0 +1,9 @@
pub mod base;
pub mod catchers;
pub mod help;
pub mod home;
pub mod login;
pub mod scuttlebutt;
pub mod settings;
pub mod snippets;
pub mod status;

View File

@ -0,0 +1,109 @@
use maud::{html, PreEscaped};
use crate::templates;
// /scuttlebutt/peers
pub fn peers() -> PreEscaped<String> {
let back = "/".to_string();
let title = "Scuttlebutt Peers".to_string();
let content = html! {
(PreEscaped("<!-- SCUTTLEBUTT PEERS -->"))
div class="card center" {
div class="card-container" {
(PreEscaped("<!-- BUTTONS -->"))
div id="buttons" {
a id="friends" class="button button-primary center" href="/scuttlebutt/friends" title="List Friends" { "Friends" }
a id="follows" class="button button-primary center" href="/scuttlebutt/follows" title="List Follows" { "Follows" }
a id="followers" class="button button-primary center" href="/scuttlebutt/followers" title="List Followers" { "Followers" }
a id="blocks" class="button button-primary center" href="/scuttlebutt/blocks" title="List Blocks" { "Blocks" }
}
}
}
};
templates::base::base(back, title, content)
}
// /scuttlebutt/friends
// /scuttlebutt/follows
// /scuttlebutt/followers
// /scuttlebutt/blocks
pub fn peers_list(title: String) -> PreEscaped<String> {
let back = "/scuttlebutt/peers".to_string();
let content = html! {
(PreEscaped("<!-- SCUTTLEBUTT PEERS LIST -->"))
div class="card center" {
ul class="list" {
// for peer in peers
li {
a class="list-item link light-bg" href="/scuttlebutt/profile/(pub_key)" {
img id="peerImage" class="icon list-icon" src="{ image_path }" alt="{ peer_name }'s profile image";
p id="peerName" class="list-text" { "(name)" }
label class="label-small label-ellipsis list-label font-gray" for="peerName" title="{ peer_name }'s Public Key" { "(public_key)" }
}
}
}
}
};
templates::base::base(back, title, content)
}
// /scuttlebutt/private
pub fn private() -> PreEscaped<String> {
let back = "/".to_string();
let title = "Private Messages".to_string();
let content = html! {
div class="card center" {
div class="card-container" {
p { "private message content goes here" }
}
}
};
templates::base::base(back, title, content)
}
// /scuttlebutt/profile
pub fn profile(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
let back = "/".to_string();
let title = "Scuttlebutt Profile".to_string();
let content = html! {
(PreEscaped("<!-- USER PROFILE -->"))
div class="card center" {
(PreEscaped("<!-- PROFILE INFO BOX -->"))
div class="capsule capsule-profile" title="Scuttlebutt account profile information" {
(PreEscaped("<!-- edit profile button -->"))
img id="editProfile" class="icon-small nav-icon-right" src="/icons/pencil.svg" alt="Profile picture";
(PreEscaped("<!-- PROFILE BIO -->"))
(PreEscaped("<!-- profile picture -->"))
img id="profilePicture" class="icon-large" src="{ image_path }" alt="Profile picture";
(PreEscaped("<!-- name, public key & description -->"))
p id="profileName" class="card-text" title="Name" { "(name)" }
label class="label-small label-ellipsis font-gray" style="user-select: all;" for="profileName" title="Public Key" { "(public_key)" }
p id="profileDescription" style="margin-top: 1rem" class="card-text" title="Description" { "(description)" }
}
(PreEscaped("<!-- PUBLIC POST FORM -->"))
form id="postForm" action="/scuttlebutt/post" method="post" {
(PreEscaped("<!-- input for message contents -->"))
textarea id="publicPost" class="center input message-input" title="Compose Public Post" { }
input id="publishPost" class="button button-primary center" title="Publish" type="submit" value="Publish";
}
(PreEscaped("<!-- BUTTONS -->"))
(PreEscaped("<!-- TODO: each of these buttons needs to be a form with a public key -->"))
div id="buttons" {
a id="followPeer" class="button button-primary center" href="/scuttlebutt/follow" title="Follow Peer" { "Follow" }
a id="blockPeer" class="button button-warning center" href="/scuttlebutt/block" title="Block Peer" { "Block" }
a id="privateMessage" class="button button-primary center" href="/scuttlebutt/private_message" title="Private Message" { "Private Message" }
}
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::snippets::flash_message(flash_msg, flash_name))
}
};
templates::base::base(back, title, content)
}

View File

@ -0,0 +1,122 @@
use maud::{html, PreEscaped};
use crate::context::admin;
use crate::templates;
// /settings/admin
pub fn menu() -> PreEscaped<String> {
let back = "/settings".to_string();
let title = "Administrator Settings".to_string();
let content = html! {
(PreEscaped("<!-- ADMIN SETTINGS MENU -->"))
div class="card center" {
(PreEscaped("<!-- BUTTONS -->"))
div id="settingsButtons" {
a id="change" class="button button-primary center" href="/settings/admin/change_password" title="Change Password" { "Change Password" }
a id="configure" class="button button-primary center" href="/settings/admin/configure" title="Configure Admin" { "Configure Admin" }
}
}
};
templates::base::base(back, title, content)
}
// /settings/admin/add
pub fn add(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
let back = "/settings/admin/configure".to_string();
let title = "Add Administrator".to_string();
let content = html! {
(PreEscaped("<!-- ADD ADMIN FORM -->"))
div class="card center" {
div class="card-container" {
form id="addAdminForm" action="/settings/admin/add" method="post" {
input id="ssb_id" name="ssb_id" class="center input" type="text" placeholder="SSB ID" title="SSB ID of Admin" value="";
div id="buttonDiv" {
input id="addAdmin" class="button button-primary center" title="Add" type="submit" value="Add";
a class="button button-secondary center" href="/settings/admin/configure" title="Cancel" { "Cancel" }
}
}
// render flash message, if any
(templates::snippets::flash_message(flash_msg, flash_name))
}
}
};
templates::base::base(back, title, content)
}
// /settings/admin/configure
pub fn configure(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
let ssb_admin_ids = admin::ssb_admin_ids();
let back = "/settings/admin".to_string();
let title = "Administrator Configuration".to_string();
let content = html! {
(PreEscaped("<!-- CONFIGURE ADMIN PAGE -->"))
div class="card center" {
div class="text-container" {
h4 { "Current Admins" }
@match ssb_admin_ids {
Ok(admins) => {
@if admins.is_empty() {
div { "No administators are currently configured" }
} else {
@for admin in admins {
div {
form action="/settings/admin/delete" method="post" {
input type="hidden" name="ssb_id" value="{{admin}}";
input type="submit" value="X" title="Delete" {
span { (admin) }
}
}
}
}
}
},
Err(e) => div { "Encountered an error while trying to retrieve list of administrators: " (e) }
}
a class="button button-primary center full-width" style="margin-top: 25px;" href="/settings/admin/add" title="Add Admin" { "Add Admin" }
}
// render flash message, if any
(templates::snippets::flash_message(flash_msg, flash_name))
}
};
templates::base::base(back, title, content)
}
// /settings/admin/change_password
pub fn change_password(
flash_msg: Option<String>,
flash_name: Option<String>,
) -> PreEscaped<String> {
let back = "/settings/admin".to_string();
let title = "Change Password".to_string();
let content = html! {
(PreEscaped("<!-- CHANGE PASSWORD FORM -->"))
div class="card center" {
div class="form-container" {
form id="changePassword" action="/settings/admin/change_password" method="post" {
(PreEscaped("<!-- input for current password -->"))
input id="currentPassword" class="center input" name="current_password" type="password" placeholder="Current password" title="Current password" autofocus;
(PreEscaped("<!-- input for new password -->"))
input id="newPassword" class="center input" name="new_password1" type="password" placeholder="New password" title="New password";
(PreEscaped("<!-- input for duplicate new password -->"))
input id="newPasswordDuplicate" class="center input" name="new_password2" type="password" placeholder="Re-enter new password" title="New password duplicate";
div id="buttonDiv" {
input id="savePassword" class="button button-primary center" title="Add" type="submit" value="Save";
a class="button button-secondary center" href="/settings/admin" title="Cancel" { "Cancel" }
}
}
// render flash message, if any
(templates::snippets::flash_message(flash_msg, flash_name))
}
}
};
templates::base::base(back, title, content)
}

View File

@ -0,0 +1,37 @@
use maud::{html, PreEscaped};
use crate::{templates, STANDALONE_MODE};
// /settings
pub fn menu() -> PreEscaped<String> {
let back = "/".to_string();
let title = "Settings".to_string();
// render a minimal menu (no network settings) if running in standalone mode
let content = if *STANDALONE_MODE {
html! {
(PreEscaped("<!-- SETTINGS MENU -->"))
div class="card center" {
(PreEscaped("<!-- BUTTONS -->"))
div id="settingsButtons" {
a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" }
a id="admin" class="button button-primary center" href="/settings/admin" title="Administrator Settings" { "Administration" }
}
}
}
} else {
html! {
(PreEscaped("<!-- SETTINGS MENU -->"))
div class="card center" {
(PreEscaped("<!-- BUTTONS -->"))
div id="settingsButtons" {
a id="network" class="button button-primary center" href="/settings/network" title="Network Settings" { "Network" }
a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" }
a id="admin" class="button button-primary center" href="/settings/admin" title="Administrator Settings" { "Administration" }
}
}
}
};
templates::base::base(back, title, content)
}

View File

@ -0,0 +1,4 @@
pub mod admin;
pub mod menu;
pub mod network;
pub mod scuttlebutt;

View File

@ -0,0 +1,319 @@
use maud::{html, PreEscaped};
use crate::context::{
network,
network::{ConfigureDNSContext, NetworkDetailContext, NetworkListContext},
};
use crate::templates;
// /settings/network/wifi/add
pub fn add_ap(
ssid: Option<String>,
flash_msg: Option<String>,
flash_name: Option<String>,
) -> PreEscaped<String> {
let back = "/settings/network".to_string();
let title = "Add WiFi Network".to_string();
let content = html! {
(PreEscaped("<!-- NETWORK ADD CREDENTIALS FORM -->"))
div class="card center" {
div class="card-container" {
form id="wifiCreds" action="/settings/network/wifi/add" method="post" {
(PreEscaped("<!-- input for network ssid -->"))
input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value=[ssid] autofocus;
(PreEscaped("<!-- input for network password -->"))
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
div id="buttonDiv" {
input id="addWifi" class="button button-primary center" title="Add" type="submit" value="Add";
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
}
}
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::snippets::flash_message(flash_msg, flash_name))
}
}
};
templates::base::base(back, title, content)
}
/* TODO: I JUST OVERWROTE THE network_detail FUNCTION :'( :'( :'( */
// /settings/network/wifi
pub fn network_detail(
// the ssid of the network we wish to examine in detail
selected: String,
flash_msg: Option<String>,
flash_name: Option<String>,
) -> PreEscaped<String> {
// retrieve network detail data
let context = NetworkDetailContext::build();
// have credentials for the access point we're viewing previously been saved?
// ie. is this a known access point?
let selected_is_saved = context.saved_aps.contains(&selected);
let back = "/settings/network".to_string();
let title = "WiFi Network Detail".to_string();
let content = html! {
(PreEscaped("<!-- NETWORK DETAIL -->"))
// select only the access point we are interested in
@match context.wlan_networks.get_key_value(&selected) {
Some((ssid, ap)) => {
@let capsule_class = if ssid == &context.wlan_ssid {
"two-grid capsule success-border"
} else {
"two-grid capsule"
};
@let ap_status = if ssid == &context.wlan_ssid {
"CONNECTED"
} else if ap.state == "Available" {
"AVAILABLE"
} else {
"NOT IN RANGE"
};
@let ap_protocol = match &ap.detail {
Some(scan) => scan.protocol.clone(),
None => "Unknown".to_string()
};
@let ap_signal = match ap.signal {
Some(signal) => signal.to_string(),
None => "Unknown".to_string()
};
(PreEscaped("<!-- NETWORK CARD -->"))
div class="card center" {
(PreEscaped("<!-- NETWORK INFO BOX -->"))
div class=(capsule_class) title="PeachCloud network mode and status" {
(PreEscaped("<!-- left column -->"))
(PreEscaped("<!-- NETWORK STATUS ICON -->"))
div class="grid-column-1" {
img id="wifiIcon" class="center icon" src="/icons/wifi.svg" alt="WiFi icon";
label class="center label-small font-gray" for="wifiIcon" title="Access Point Status" { (ap_status) }
}
(PreEscaped("<!-- right column -->"))
(PreEscaped("<!-- NETWORK DETAILED INFO -->"))
div class="grid-column-2" {
label class="label-small font-gray" for="netSsid" title="WiFi network SSID" { "SSID" }
p id="netSsid" class="card-text" title="SSID" { (ssid) }
label class="label-small font-gray" for="netSec" title="Security protocol" { "SECURITY" }
p id="netSec" class="card-text" title="Security protocol in use by "{(ssid)}"" { (ap_protocol) }
label class="label-small font-gray" for="netSig" title="Signal Strength" { "SIGNAL" }
p id="netSig" class="card-text" title="Signal strength of WiFi access point" { (ap_signal) }
}
}
(PreEscaped("<!-- BUTTONS -->"))
div class="card-container" style="padding-top: 0;" {
div id="buttonDiv" {
@if context.wlan_ssid == selected {
form id="wifiDisconnect" action="/settings/network/wifi/disconnect" method="post" {
// hidden element: allows ssid to be sent in request
input id="disconnectSsid" name="ssid" type="text" value=(ssid) style="display: none;";
input id="disconnectWifi" class="button button-warning center" title="Disconnect from Network" type="submit" value="Disconnect";
}
}
// If the selected access point appears in the list,
// display the Modify and Forget buttons.
@if context.saved_aps.contains(&selected) {
@if context.wlan_ssid != selected && ap.state == "Available" {
form id="wifiConnect" action="/settings/network/wifi/connect" method="post" {
// hidden element: allows ssid to be sent in request
input id="connectSsid" name="ssid" type="text" value=(ssid) style="display: none;";
input id="connectWifi" class="button button-primary center" title="Connect to Network" type="submit" value="Connect";
}
}
a class="button button-primary center" href="/settings/network/wifi/modify/"{(ssid)}"" { "Modify" }
form id="wifiForget" action="/settings/network/wifi/forget" method="post" {
// hidden element: allows ssid to be sent in request
input id="forgetSsid" name="ssid" type="text" value=(ssid) style="display: none;";
input id="forgetWifi" class="button button-warning center" title="Forget Network" type="submit" value="Forget";
}
}
@if !selected_is_saved {
// Display the Add button if AP creds not already in saved networks list
a class="button button-primary center" href="/settings/network/wifi/add/"{(ssid)}"" { "Add" };
}
a class="button button-secondary center" href="/settings/network/wifi" title="Cancel" { "Cancel" }
}
(templates::snippets::flash_message(flash_msg, flash_name))
}
}
}
// TODO: improve the styling of this
None => {
div class="card center" {
p { "Selected access point was not found in-range or in the list of saved access points" }
}
}
}
};
templates::base::base(back, title, content)
}
// /settings/network/wifi
pub fn list_aps() -> PreEscaped<String> {
// retrieve network list data
let context = NetworkListContext::build();
let back = "/settings/network".to_string();
let title = "WiFi Networks".to_string();
let content = html! {
(PreEscaped("<!-- NETWORK ACCESS POINT LIST -->"))
div class="card center" {
div class="center list-container" {
ul class="list" {
@if context.ap_state == *"up" {
li class="list-item light-bg warning-border" { "Enable WiFi client mode to view saved and available networks." }
} @else if !context.wlan_networks.is_empty() {
@for (ssid, state) in context.wlan_networks {
li {
@if ssid == context.wlan_ssid {
a class="list-item link primary-bg" href="/settings/network/wifi/ssid/"{(ssid)}"" {
img id="netStatus" class="icon icon-active icon-medium list-icon" src="/static/icons/wifi.svg" alt="WiFi online";
p class="list-text" { (context.wlan_ssid) }
label class="label-small list-label font-gray" for="netStatus" title="Status" { "Connected" }
}
} @else if state == "Available" {
a class="list-item link light-bg" href="/settings/network/wifi/ssid/"{(ssid)}"" {
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/static/icons/wifi.svg" alt="WiFi offline";
p class="list-text" { (ssid) }
label class="label-small list-label font-gray" for="netStatus" title="Status" { (state) }
}
} @else {
a class="list-item link" href="/settings/network/wifi/ssid/"{(ssid)}"" {
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/static/icons/wifi.svg" alt="WiFi offline";
p class="list-text" { (ssid) }
label class="label-small list-label font-gray" for="netStatus" title="Status" { (state) }
}
}
}
}
}
li class="list-item light-bg" { "No saved or available networks found." }
}
}
}
};
templates::base::base(back, title, content)
}
// /settings/network/wifi
pub fn modify_ap(
ssid: Option<String>,
flash_msg: Option<String>,
flash_name: Option<String>,
) -> PreEscaped<String> {
let back = "/settings/network".to_string();
let title = "Modify WiFi Network".to_string();
let content = html! {
(PreEscaped("<!-- NETWORK MODIFY AP PASSWORD FORM -->"))
div class="card center" {
div class="card-container" {
form id="wifiModify" action="/settings/network/wifi/modify" method="post" {
(PreEscaped("<!-- input for network ssid -->"))
input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value=[ssid] autofocus;
(PreEscaped("<!-- input for network password -->"))
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
div id="buttonDiv" {
input id="savePassword" class="button button-primary center" title="Save" type="submit" value="Save";
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
}
}
(templates::snippets::flash_message(flash_msg, flash_name))
}
}
};
templates::base::base(back, title, content)
}
// /settings/network/dns
pub fn configure_dns(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
// retrieve dyndns-related data
let context = ConfigureDNSContext::build();
let back = "/settings/network".to_string();
let title = "Configure DNS".to_string();
let content = html! {
(PreEscaped("<!-- CONFIGURE DNS FORM -->"))
div class="card center" {
div class="form-container" {
@if context.enable_dyndns {
(PreEscaped("<!-- DYNDNS STATUS INDICATOR -->"))
div id="dyndns-status-indicator" class="stack capsule{% if is_dyndns_online %} success-border{% else %} warning-border{% endif %}" {
div class="stack" {
@if context.is_dyndns_online {
label class="label-small font-near-black" { "Dynamic DNS is currently online." }
} else {
label class="label-small font-near-black" { "Dynamic DNS is enabled but may be offline." }
}
}
}
}
form id="configureDNS" action="/settings/network/dns" method="post" {
div class="input-wrapper" {
(PreEscaped("<!-- input for externaldomain -->"))
label id="external_domain" class="label-small input-label font-near-black" {
label class="label-small input-label font-gray" for="external_domain" style="padding-top: 0.25rem;" { "External Domain (optional)" }
input id="external_domain" class="form-input" style="margin-bottom: 0;" name="external_domain" type="text" title="external domain" value=(context.external_domain);
}
}
div class="input-wrapper" {
div {
(PreEscaped("<!-- checkbox for dynds flag -->"))
label class="label-small input-label font-gray" { "Enable Dynamic DNS" }
input style="margin-left: 0px;" id="enable_dyndns" name="enable_dyndns" title="Activate dyndns" type="checkbox" { @if context.enable_dyndns { "checked" } };
}
}
div class="input-wrapper" {
(PreEscaped("<!-- input for dyndns -->"))
label id="cut" class="label-small input-label font-near-black" {
label class="label-small input-label font-gray" for="cut" style="padding-top: 0.25rem;" { "Dynamic DNS Domain" }
input id="dyndns_domain" class="alert-input" name="dynamic_domain" placeholder="" type="text" title="dyndns_domain" value=(context.dyndns_subdomain) { ".dyn.peachcloud.org" }
}
}
div id="buttonDiv" {
input id="configureDNSButton" class="button button-primary center" title="Add" type="submit" value="Save";
}
}
}
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::snippets::flash_message(flash_msg, flash_name))
}
};
templates::base::base(back, title, content)
}
// /settings/network
pub fn menu() -> PreEscaped<String> {
let ap_state = network::ap_state();
let back = "/settings".to_string();
let title = "Network Settings".to_string();
let content = html! {
(PreEscaped("<!-- NETWORK SETTINGS CARD -->"))
div class="card center" {
(PreEscaped("<!-- BUTTONS -->"))
div id="buttons" {
a class="button button-primary center" href="/settings/network/wifi/add" title="Add WiFi Network" { "Add WiFi Network" }
a id="configureDNS" class="button button-primary center" href="/settings/network/dns" title="Configure DNS" { "Configure DNS" }
(PreEscaped("<!-- if ap is up, show \"Enable WiFi\" button, else show \"Deploy Access Point\" -->"))
@if ap_state == *"up" {
a id="connectWifi" class="button button-primary center" href="/settings/network/wifi/activate" title="Enable WiFi" { "Enable WiFi" }
} @else {
a id="deployAccessPoint" class="button button-primary center" href="/settings/network/ap/activate" title="Deploy Access Point" { "Deploy Access Point" }
}
a id="listWifi" class="button button-primary center" href="/settings/network/wifi" title="List WiFi Networks" { "List WiFi Networks" }
a id="viewStatus" class="button button-primary center" href="/status/network" title="View Network Status" { "View Network Status" }
}
}
};
templates::base::base(back, title, content)
}

View File

@ -0,0 +1,29 @@
use maud::{html, PreEscaped};
use crate::templates;
// /settings/scuttlebutt
pub fn scuttlebutt() -> PreEscaped<String> {
let back = "/settings".to_string();
let title = "Scuttlebutt Settings".to_string();
let content = html! {
(PreEscaped("<!-- SCUTTLEBUTT SETTINGS MENU -->"))
div class="card center" {
(PreEscaped("<!-- BUTTONS -->"))
div id="settingsButtons" {
a id="networkKey" class="button button-primary center" href="/settings/scuttlebutt/network_key" title="Set Network Key" { "Set Network Key" }
a id="replicationHops" class="button button-primary center" href="/settings/scuttlebutt/hops" title="Set Replication Hops" { "Set Replication Hops" }
a id="removeFeeds" class="button button-primary center" href="/settings/scuttlebutt/remove_feeds" title="Remove Blocked Feeds" { "Remove Blocked Feeds" }
a id="setDirectory" class="button button-primary center" href="/settings/scuttlebutt/set_directory" title="Set Database Directory" { "Set Database Directory" }
a id="checkFilesystem" class="button button-primary center" href="/settings/scuttlebutt/check_fs" title="Check Filesystem" { "Check Filesystem" }
a id="repairFilesystem" class="button button-primary center" href="/settings/scuttlebutt/repair" title="Repair Filesystem" { "Repair Filesystem" }
a id="disable" class="button button-primary center" href="/settings/scuttlebutt/disable" title="Disable Sbot" { "Disable Sbot" }
a id="enable" class="button button-primary center" href="/settings/scuttlebutt/enable" title="Enable Sbot" { "Enable Sbot" }
a id="restart" class="button button-primary center" href="/settings/scuttlebutt/restart" title="Restart Sbot" { "Restart Sbot" }
}
}
};
templates::base::base(back, title, content)
}

View File

@ -0,0 +1,22 @@
use maud::{html, PreEscaped};
pub fn flash_message(flash_msg: Option<String>, flash_name: Option<String>) -> PreEscaped<String> {
html! {
(PreEscaped("<!-- FLASH MESSAGE -->"))
@if flash_msg.is_some() {
@if flash_name == Some("success".to_string()) {
div class="capsule center-text flash-message font-success" {
(flash_msg.unwrap())
}
} @else if flash_name == Some("info".to_string()) {
div class="capsule center-text flash-message font-info" {
(flash_msg.unwrap())
}
} @else if flash_name == Some("error".to_string()) {
div class="capsule center-text flash-message font-failure" {
(flash_msg.unwrap())
}
}
}
}
}

View File

@ -0,0 +1,496 @@
use maud::{html, PreEscaped};
use crate::context::{network::NetworkStatusContext, status::StatusContext};
use crate::{templates, STANDALONE_MODE};
fn ap_network_card(status: NetworkStatusContext) -> PreEscaped<String> {
html! {
(PreEscaped("<!-- NETWORK CARD -->"))
div class="card center" {
(PreEscaped("<!-- NETWORK INFO BOX -->"))
div class="capsule capsule-container success-border" {
(PreEscaped("<!-- NETWORK STATUS GRID -->"))
div class="two-grid" title="PeachCloud network mode and status" {
(PreEscaped("<!-- top-right config icon -->"))
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
}
(PreEscaped("<!-- left column -->"))
(PreEscaped("<!-- network mode icon with label -->"))
div class="grid-column-1" {
img id="netModeIcon" class="center icon icon-active" src="/static/icons/router.svg" alt="WiFi router";
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="Access Point Online" { "ONLINE" }
}
(PreEscaped("<!-- right column -->"))
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
div class="grid-column-2" {
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
p id="netMode" class="card-text" title="Network Mode" { "Access Point" }
label class="label-small font-gray" for="netSsid" title="Access Point SSID" { "SSID" }
p id="netSsid" class="card-text" title="SSID" { (status.ap_ssid) }
label class="label-small font-gray" for="netIp" title="Access Point IP Address" { "IP" }
p id="netIp" class="card-text" title="IP" { (status.ap_ip) }
}
}
(PreEscaped("<!-- horizontal dividing line -->"))
hr;
(PreEscaped("<!-- DEVICES AND TRAFFIC GRID -->"))
div class="three-grid card-container" {
// devices stack
div class="stack" {
img id="devices" class="icon icon-medium" title="Connected devices" src="/static/icons/devices.svg" alt="Digital devices";
div class="flex-grid" style="padding-top: 0.5rem;" {
label class="label-medium" for="devices" style="padding-right: 3px;" title="Number of connected devices";
}
label class="label-small font-gray" { "DEVICES" }
}
// download stack
div class="stack" {
img id="dataDownload" class="icon icon-medium" title="Download" src="/static/icons/down-arrow.svg" alt="Download";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = &status.ap_traffic {
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in "{ (traffic.rx_unit) }"" { (traffic.rx) }
label class="label-small font-near-black" { (traffic.rx_unit) }
} @else {
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total";
label class="label-small font-near-black" { "0" }
}
}
label class="label-small font-gray" { "DOWNLOAD" }
}
// upload stack
div class="stack" {
img id="dataUpload" class="icon icon-medium" title="Upload" src="/static/icons/up-arrow.svg" alt="Upload";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = status.ap_traffic {
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in "{ (traffic.tx_unit) }"" { (traffic.tx) }
label class="label-small font-near-black" { (traffic.tx_unit) }
} @else {
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total";
label class="label-small font-near-black" { "0" }
}
}
label class="label-small font-gray" { "UPLOAD" }
}
}
}
}
}
}
fn wlan_network_card(status: NetworkStatusContext) -> PreEscaped<String> {
let capsule = if status.wlan_state == *"up" {
"capsule capsule-container success-border"
} else {
"capsule capsule-container warning-border"
};
html! {
(PreEscaped("<!-- NETWORK CARD -->"))
div class="card center" {
(PreEscaped("<!-- NETWORK INFO BOX -->"))
div class=(capsule) {
@if status.wlan_state == *"up" {
(PreEscaped("<!-- NETWORK STATUS GRID -->"))
div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" {
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
}
(PreEscaped("<!-- NETWORK STATUS -->"))
(PreEscaped("<!-- left column -->"))
(PreEscaped("<!-- network mode icon with label -->"))
div class="grid-column-1" {
img id="netModeIcon" class="center icon icon-active" src="/static/icons/wifi.svg" alt="WiFi online";
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "ONLINE" }
}
div class="grid-column-2" {
(PreEscaped("<!-- right column -->"))
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" }
label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" }
p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) }
label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" }
p id="netIp" class="card-text" title="IP" { (status.wlan_ip) }
}
}
} @else {
div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" {
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
}
div class="grid-column-1" {
img id="netModeIcon" class="center icon icon-inactive" src="/static/icons/wifi.svg" alt="WiFi offline";
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "OFFLINE" }
}
div class="grid-column-2" {
(PreEscaped("<!-- right column -->"))
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" }
label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" }
p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) }
label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" }
p id="netIp" class="card-text" title="IP" { (status.wlan_ip) }
}
}
}
(PreEscaped("<!-- horizontal dividing line -->"))
hr;
(PreEscaped("<!-- SIGNAL AND TRAFFIC GRID -->"))
(PreEscaped("<!-- row of icons representing network statistics -->"))
div class="three-grid card-container" {
div class="stack" {
img id="netSignal" class="icon icon-medium" alt="Signal" title="WiFi Signal (%)" src="/static/icons/low-signal.svg";
div class="flex-grid" style="padding-top: 0.5rem;" {
label class="label-medium" for="netSignal" style="padding-right: 3px;" title="Signal strength of WiFi connection (%)" {
@if let Some(wlan_rssi) = status.wlan_rssi { (wlan_rssi) } @else { "0" }
}
}
label class="label-small font-gray" { "SIGNAL" }
}
div class="stack" {
img id="dataDownload" class="icon icon-medium" alt="Download" title="WiFi download total" src="/static/icons/down-arrow.svg";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = &status.wlan_traffic {
(PreEscaped("<!-- display wlan traffic data -->"))
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in "{ (traffic.rx_unit) }"" { (traffic.rx) }
label class="label-small font-near-black" { (traffic.rx_unit) }
} @else {
(PreEscaped("<!-- no wlan traffic data to display -->"))
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total" { "0" }
label class="label-small font-near-black" { "MB" }
}
}
label class="label-small font-gray" { "DOWNLOAD" }
}
div class="stack" {
img id="dataUpload" class="icon icon-medium" alt="Upload" title="WiFi upload total" src="/static/icons/up-arrow.svg";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = status.wlan_traffic {
(PreEscaped("<!-- display wlan traffic data -->"))
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in "{ (traffic.tx_unit) }"" { (traffic.tx) }
label class="label-small font-near-black" { (traffic.tx_unit) }
} @else {
(PreEscaped("<!-- no wlan traffic data to display -->"))
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total" { "0" }
label class="label-small font-near-black" { "MB" }
}
}
label class="label-small font-gray" { "UPLOAD" }
}
}
}
}
}
}
/*
* WORKS ... kinda
fn wlan_network_card(status: NetworkStatusContext) -> PreEscaped<String> {
html! {
(PreEscaped("<!-- NETWORK CARD -->"))
div class="card center" {
(PreEscaped("<!-- NETWORK INFO BOX -->"))
@if status.wlan_state == *"up" {
div class="capsule capsule-container success-border" {
(PreEscaped("<!-- NETWORK STATUS GRID -->"))
div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" {
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
}
(PreEscaped("<!-- NETWORK STATUS -->"))
(PreEscaped("<!-- left column -->"))
(PreEscaped("<!-- network mode icon with label -->"))
div class="grid-column-1" {
img id="netModeIcon" class="center icon icon-active" src="/static/icons/wifi.svg" alt="WiFi online";
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "ONLINE" }
}
div class="grid-column-2" {
(PreEscaped("<!-- right column -->"))
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" }
label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" }
p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) }
label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" }
p id="netIp" class="card-text" title="IP" { (status.wlan_ip) }
}
}
}
} @else {
div class="capsule capsule-container warning-border" {
div id="netInfoBox" class="two-grid" title="PeachCloud network mode and status" {
a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" {
img id="configureNetworking" class="icon-small" src="/static/icons/cog.svg" alt="Configure";
}
div class="grid-column-1" {
img id="netModeIcon" class="center icon icon-inactive" src="/static/icons/wifi.svg" alt="WiFi offline";
label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title="WiFi Client Status" { "OFFLINE" }
}
div class="grid-column-2" {
(PreEscaped("<!-- right column -->"))
(PreEscaped("<!-- network mode, ssid & ip with labels -->"))
label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" }
p id="netMode" class="card-text" title="Network Mode" { "WiFi Client" }
label class="label-small font-gray" for="netSsid" title="WiFi SSID" { "SSID" }
p id="netSsid" class="card-text" title="SSID" { (status.wlan_ssid) }
label class="label-small font-gray" for="netIp" title="WiFi Client IP Address" { "IP" }
p id="netIp" class="card-text" title="IP" { (status.wlan_ip) }
}
}
}
}
(PreEscaped("<!-- horizontal dividing line -->"))
hr;
(PreEscaped("<!-- SIGNAL AND TRAFFIC GRID -->"))
(PreEscaped("<!-- row of icons representing network statistics -->"))
div class="three-grid card-container" {
div class="stack" {
img id="netSignal" class="icon icon-medium" alt="Signal" title="WiFi Signal (%)" src="/static/icons/low-signal.svg";
div class="flex-grid" style="padding-top: 0.5rem;" {
label class="label-medium" for="netSignal" style="padding-right: 3px;" title="Signal strength of WiFi connection (%)" {
@if let Some(wlan_rssi) = status.wlan_rssi { (wlan_rssi) } @else { "0" }
}
}
label class="label-small font-gray" { "SIGNAL" }
}
div class="stack" {
img id="dataDownload" class="icon icon-medium" alt="Download" title="WiFi download total" src="/static/icons/down-arrow.svg";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = &status.wlan_traffic {
(PreEscaped("<!-- display wlan traffic data -->"))
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total in "{ (traffic.rx_unit) }"" { (traffic.rx) }
label class="label-small font-near-black" { (traffic.rx_unit) }
} @else {
(PreEscaped("<!-- no wlan traffic data to display -->"))
label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total" { "0" }
label class="label-small font-near-black" { "MB" }
}
}
label class="label-small font-gray" { "DOWNLOAD" }
}
div class="stack" {
img id="dataUpload" class="icon icon-medium" alt="Upload" title="WiFi upload total" src="/static/icons/up-arrow.svg";
div class="flex-grid" style="padding-top: 0.5rem;" {
@if let Some(traffic) = status.wlan_traffic {
(PreEscaped("<!-- display wlan traffic data -->"))
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total in "{ (traffic.tx_unit) }"" { (traffic.tx) }
label class="label-small font-near-black" { (traffic.tx_unit) }
} @else {
(PreEscaped("<!-- no wlan traffic data to display -->"))
label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total" { "0" }
label class="label-small font-near-black" { "MB" }
}
}
label class="label-small font-gray" { "UPLOAD" }
}
}
}
}
}
*/
pub fn network() -> PreEscaped<String> {
let back = "/status".to_string();
let title = "Network Status".to_string();
// retrieve network status data
let status = NetworkStatusContext::build();
let content = html! {
(PreEscaped("<!-- NETWORK STATUS -->"))
// if ap is up, show ap card, else show wlan card
@if status.ap_state == *"up" {
(ap_network_card(status))
} @else {
(wlan_network_card(status))
}
};
templates::base::base(back, title, content)
}
fn scuttlebutt_status() -> PreEscaped<String> {
html! {
(PreEscaped("<!-- SCUTTLEBUTT STATUS -->"))
div class="card center" {
div class="card-container" {
p { "Network key: " }
p { "Replication hops: " }
p { "Sbot version: " }
p { "Process status: " }
p { "Process uptime: " }
p { "Blobstore size: " }
p { "Latest sequence number: " }
p { "Last time you visited this page, latest sequence was x ... now it's y" }
p { "Number of follows / followers / friends / blocks" }
}
}
}
}
pub fn status() -> PreEscaped<String> {
let back = "/".to_string();
let title = if *STANDALONE_MODE {
"Scuttlebutt Status".to_string()
} else {
"System Status".to_string()
};
// retrieve system status data
let status = StatusContext::build();
// render the scuttlebutt status template
let content = if *STANDALONE_MODE {
scuttlebutt_status()
// or render the complete system status template
} else {
html! {
(PreEscaped("<!-- SYSTEM STATUS -->"))
div class="card center" {
div class="card-container" {
div class="three-grid" {
(PreEscaped("<!-- PEACH-NETWORK STATUS STACK -->"))
(PreEscaped("<!-- Display microservice status for network, oled & stats -->"))
a class="link" href="/status/network" {
div class="stack capsule success-border" {
img id="networkIcon" class="icon icon-medium" alt="Network" title="Network microservice status" src="/static/icons/wifi.svg";
div class="stack" style="padding-top: 0.5rem;" {
label class="label-small font-near-black" { "Networking" };
label class="label-small font-near-black" { "(network_ping)" };
}
}
}
(PreEscaped("<!-- PEACH-OLED STATUS STACK -->"))
div class="stack capsule success-border" {
img id="oledIcon" class="icon icon-medium" alt="Display" title="OLED display microservice status" src="/static/icons/lcd.svg";
div class="stack" style="padding-top: 0.5rem;" {
label class="label-small font-near-black" { "Display" };
label class="label-small font-near-black" { "(oled_ping)" };
}
}
(PreEscaped("<!-- PEACH-STATS STATUS STACK -->"))
div class="stack capsule success-border" {
img id="statsIcon" class="icon icon-medium" alt="Stats" title="System statistics microservice status" src="/static/icons/chart.svg";
div class="stack" style="padding-top: 0.5rem;" {
label class="label-small font-near-black" { "Statistics" };
label class="label-small font-near-black" { "AVAILABLE" };
}
}
(PreEscaped("<!-- DYNDNS STATUS STACK -->"))
(PreEscaped("<!-- Display status for dynsdns, config & sbot -->"))
div class="stack capsule success-border" {
img id="networkIcon" class="icon icon-medium" alt="Dyndns" title="Dyndns status" src="/static/icons/dns.png";
div class="stack" style="padding-top: 0.5rem;" {
label class="label-small font-near-black" { "Dyn DNS" };
label class="label-small font-near-black" { "(dns_ping)" };
}
}
(PreEscaped("<!-- CONFIG STATUS STACK -->"))
// TODO: render capsule border according to status
div class="stack capsule success-border" {
img id="networkIcon" class="icon icon-medium" alt="Config" title="Config status" src="/static/icons/clipboard.png";
div class="stack" style="padding-top: 0.5rem;" {
label class="label-small font-near-black" { "Config" };
label class="label-small font-near-black" { "(status)" };
}
}
(PreEscaped("<!-- SBOT STATUS STACK -->"))
div class="stack capsule success-border" {
img id="networkIcon" class="icon icon-medium" alt="Sbot" title="Sbot status" src="/static/icons/hermies.svg";
div class="stack" style="padding-top: 0.5rem;" {
label class="label-small font-near-black" { "Sbot" };
label class="label-small font-near-black" { "(sbot_status)" }
}
}
}
}
div class="card-container" {
(PreEscaped("<!-- Display CPU usage meter -->"))
@match status.cpu_usage_percent {
Some(x) => {
div class="flex-grid" {
span class="card-text" { "CPU" }
span class="label-small push-right" { (x) "%" }
}
meter value=(x) min="0" max="100" title="CPU usage" {
div class="meter-gauge" {
span style="width: "{(x)}"%;" { "CPU Usage" }
}
}
},
_ => p class="card-text" { "CPU usage data unavailable" }
}
(PreEscaped("<!-- Display memory usage meter -->"))
@match status.mem_usage_percent {
Some(x) => {
@let (mem_free, mem_unit) = match status.mem_free {
Some(x) => {
if x > 1024 {
((x / 1024), "GB".to_string())
} else {
(x, "MB".to_string())
}
},
_ => (0, "MB".to_string())
};
@let mem_total = status.mem_total.unwrap_or(0);
@let mem_used = status.mem_used.unwrap_or(0);
div class="flex-grid" {
span class="card-text" { "Memory" }
span class="label-small push-right" { (x) " % ("(mem_free)" "(mem_unit)" free)" }
}
meter value=(mem_used) min="0" max=(mem_total) title="Memory usage" {
div class="meter-gauge" {
span style="width: "{(x)}"%;" { "Memory Usage" }
}
}
},
_ => p class="card-text" { "Memory usage data unavailable" }
}
(PreEscaped("<!-- Display disk usage meter -->"))
@match status.disk_usage_percent {
Some(x) => {
// define free disk space with appropriate unit (GB if > 1024)
@let (disk_free, disk_unit) = match status.disk_free {
Some(x) => {
if x > 1024 {
((x / 1024), "GB".to_string())
} else {
(x, "MB".to_string())
}
},
_ => (0, "MB".to_string())
};
div class="flex-grid" {
span class="card-text" { "Disk" };
span class="label-small push-right" { (x) " % ("(disk_free)" "(disk_unit)")" };
}
meter value=(x) min="0" max="100" title="Disk usage" {
div class="meter-gauge" {
span style="width: "{(x)}"%;" { "Disk Usage" };
}
}
},
_ => p class="card-text" { "Disk usage data unavailable" }
}
(PreEscaped("<!-- Display system uptime in minutes -->"))
@match status.uptime {
Some(x) => {
@if x < 60 {
p class="capsule center-text" { "Uptime: "(x)" minutes" }
} @else {
@let hours = x / 60;
@let mins = x % 60;
p class="capsule center-text" { "Uptime: "(hours)" hours, "(mins)" minutes" }
}
},
_ => p class="card-text" { "Uptime data unavailable" }
}
}
}
}
};
templates::base::base(back, title, content)
}

View File

@ -0,0 +1,177 @@
/*
VARIABLES
*/
@custom-media --breakpoint-not-small screen and (min-width: 30em);
@custom-media --breakpoint-medium screen and (min-width: 30em) and (max-width: 60em);
@custom-media --breakpoint-large screen and (min-width: 60em);
:root {
--sans-serif: -apple-system, BlinkMacSystemFont, 'avenir next', avenir, helvetica, 'helvetica neue', ubuntu, roboto, noto, 'segoe ui', arial, sans-serif;
--serif: georgia, serif;
--code: consolas, monaco, monospace;
--font-size-headline: 6rem;
--font-size-subheadline: 5rem;
--font-size-1: 3rem;
--font-size-2: 2.25rem;
--font-size-3: 1.5rem;
--font-size-4: 1.25rem;
--font-size-5: 1rem;
--font-size-6: .875rem;
--font-size-7: .75rem;
--letter-spacing-tight:-.05em;
--letter-spacing-1:.1em;
--letter-spacing-2:.25em;
--line-height-solid: 1;
--line-height-title: 1.25;
--line-height-copy: 1.5;
--measure: 30em;
--measure-narrow: 20em;
--measure-wide: 34em;
--spacing-none: 0;
--spacing-extra-small: .25rem;
--spacing-small: .5rem;
--spacing-medium: 1rem;
--spacing-large: 2rem;
--spacing-extra-large: 4rem;
--spacing-extra-extra-large: 8rem;
--spacing-extra-extra-extra-large: 16rem;
--spacing-copy-separator: 1.5em;
--height-1: 1rem;
--height-2: 2rem;
--height-3: 4rem;
--height-4: 8rem;
--height-5: 16rem;
--width-1: 1rem;
--width-2: 2rem;
--width-3: 4rem;
--width-4: 8rem;
--width-5: 16rem;
--max-width-1: 1rem;
--max-width-2: 2rem;
--max-width-3: 4rem;
--max-width-4: 8rem;
--max-width-5: 16rem;
--max-width-6: 32rem;
--max-width-7: 48rem;
--max-width-8: 64rem;
--max-width-9: 96rem;
--border-radius-none: 0;
--border-radius-1: .125rem;
--border-radius-2: .25rem;
--border-radius-3: .5rem;
--border-radius-4: 1rem;
--border-radius-circle: 100%;
--border-radius-pill: 9999px;
--border-width-none: 0;
--border-width-1: .125rem;
--border-width-2: .25rem;
--border-width-3: .5rem;
--border-width-4: 1rem;
--border-width-5: 2rem;
--box-shadow-1: 0px 0px 4px 2px rgba( 0, 0, 0, 0.2 );
--box-shadow-2: 0px 0px 8px 2px rgba( 0, 0, 0, 0.2 );
--box-shadow-3: 2px 2px 4px 2px rgba( 0, 0, 0, 0.2 );
--box-shadow-4: 2px 2px 8px 0px rgba( 0, 0, 0, 0.2 );
--box-shadow-5: 4px 4px 8px 0px rgba( 0, 0, 0, 0.2 );
--black: #000;
--near-black: #111;
--dark-gray:#333;
--mid-gray:#555;
--gray: #777;
--silver: #999;
--light-silver: #aaa;
--moon-gray: #ccc;
--light-gray: #eee;
--near-white: #f4f4f4;
--white: #fff;
--transparent: transparent;
--black-90: rgba(0,0,0,.9);
--black-80: rgba(0,0,0,.8);
--black-70: rgba(0,0,0,.7);
--black-60: rgba(0,0,0,.6);
--black-50: rgba(0,0,0,.5);
--black-40: rgba(0,0,0,.4);
--black-30: rgba(0,0,0,.3);
--black-20: rgba(0,0,0,.2);
--black-10: rgba(0,0,0,.1);
--black-05: rgba(0,0,0,.05);
--black-025: rgba(0,0,0,.025);
--black-0125: rgba(0,0,0,.0125);
--white-90: rgba(255,255,255,.9);
--white-80: rgba(255,255,255,.8);
--white-70: rgba(255,255,255,.7);
--white-60: rgba(255,255,255,.6);
--white-50: rgba(255,255,255,.5);
--white-40: rgba(255,255,255,.4);
--white-30: rgba(255,255,255,.3);
--white-20: rgba(255,255,255,.2);
--white-10: rgba(255,255,255,.1);
--white-05: rgba(255,255,255,.05);
--white-025: rgba(255,255,255,.025);
--white-0125: rgba(255,255,255,.0125);
--dark-red: #e7040f;
--red: #ff4136;
--light-red: #ff725c;
--orange: #ff6300;
--gold: #ffb700;
--yellow: #ffd700;
--light-yellow: #fbf1a9;
--purple: #5e2ca5;
--light-purple: #a463f2;
--dark-pink: #d5008f;
--hot-pink: #ff41b4;
--pink: #ff80cc;
--light-pink: #ffa3d7;
--dark-green: #137752;
--green: #19a974;
--light-green: #9eebcf;
--navy: #001b44;
--dark-blue: #00449e;
--blue: #357edd;
--light-blue: #96ccff;
--lightest-blue: #cdecff;
--washed-blue: #f6fffe;
--washed-green: #e8fdf5;
--washed-yellow: #fffceb;
--washed-red: #ffdfdf;
/* PEACHCLOUD-SPECIFIC VARIABLES */
--primary: var(--light-green);
--secondary: var(--near-white);
--success: var(--green);
--info: var(--blue);
--warning: var(--orange);
--danger: var(--red);
--light: var(--light-gray);
--dark: var(--near-black);
/* we need to add shades for each accent colour
*
* --info-100
* --info-200
* --info-300 -> var(--blue)
* --info-400
* --info-500
*/
}

View File

@ -0,0 +1,971 @@
@import url('/static/css/_variables.css');
/* ------------------------------ *\
* peachcloud.css
*
* Index
* - ALIGNMENT
* - BODY
* - BUTTONS
* - CARDS
* - CAPSULES
* - CIRCLES
* - COLORS
* - GRIDS
* - HTML
* - FLASH MESSAGE
* - FONTS
* - ICONS
* - INPUTS
* - LABELS
* - LINKS
* - LISTS
* - MAIN
* - METERS
* - NAVIGATION
* - PARAGRAPHS
* - SWITCHES / SLIDERS
*
\* ------------------------------ */
/*
* ALIGNMENT
*/
.center {
display: block;
margin-left: auto;
margin-right: auto;
}
.center-text {
text-align: center;
}
.center-vert {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.push-right {
margin-left: auto;
padding-right: 0;
}
.top-left {
/* place-self combines align-self and justify-self */
place-self: end center;
}
@media only screen and (min-width: 600px) {
.top-left {
place-self: end;
}
}
.top-right {
place-self: end center;
}
@media only screen and (min-width: 600px) {
.top-right {
place-self: end start;
}
}
.top-middle {
place-self: center;
}
@media only screen and (min-width: 600px) {
.top-middle {
padding-bottom: 2rem;
place-self: center;
}
}
.middle {
place-self: center;
grid-column-start: 1;
grid-column-end: 4;
}
.bottom-middle {
place-self: center;
}
@media only screen and (min-width: 600px) {
.bottom-middle {
padding-top: 2rem;
place-self: center;
}
}
.bottom-left {
place-self: start center;
}
@media only screen and (min-width: 600px) {
.bottom-left {
place-self: start end;
}
}
.bottom-right {
place-self: start center;
}
@media only screen and (min-width: 600px) {
.bottom-right {
place-self: start;
}
}
/*
* BODY
*/
body {
background-color: var(--moon-gray);
height: 100%;
display: flex;
flex-direction: column;
margin: 0;
}
/*
* BUTTONS
*/
.button {
border: 1px solid var(--near-black);
border-radius: var(--border-radius-2);
/* Needed to render inputs & buttons of equal width */
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: var(--near-black);
cursor: pointer;
padding: 10px;
text-align: center;
text-decoration: none;
font-size: var(--font-size-5);
font-family: var(--sans-serif);
width: 80%;
margin-top: 5px;
margin-bottom: 5px;
}
.button.full-width {
width: 100%;
}
.button-div {
grid-column-start: 1;
grid-column-end: 4;
margin-bottom: 1rem;
}
.button-primary {
background-color: var(--light-gray);
}
.button-primary:hover {
background-color: var(--primary);
}
.button-primary:focus {
background-color: var(--primary);
outline: none;
}
.button-secondary {
background-color: var(--light-gray);
}
.button-secondary:hover {
background-color: var(--light-silver);
}
.button-secondary:focus {
background-color: var(--light-silver);
outline: none;
}
.button-warning {
background-color: var(--light-gray);
}
.button-warning:hover {
background-color: var(--light-red);
}
.button-warning:focus {
background-color: var(--light-red);
outline: none;
}
/*
* CAPSULES
*/
.capsule {
padding: 1rem;
border: var(--border-width-1) solid;
border-radius: var(--border-radius-3);
background-color: var(--light-gray);
/* margin-top: 1rem; */
/* margin-bottom: 1rem; */
}
.capsule-container {
margin-left: 1rem;
margin-right: 1rem;
padding-bottom: 1rem;
}
@media only screen and (min-width: 600px) {
.capsule-container {
margin-left: 0;
margin-right: 0;
}
}
/*
* CARDS
*/
.card {
min-height: 50vh;
max-height: 90vh;
position: relative;
width: 100%;
margin-top: 1rem;
}
@media only screen and (min-width: 600px) {
.card {
min-height: 50vh;
max-height: 90vh;
width: 20rem;
}
}
.card-container {
justify-content: center;
padding: 0.5rem;
}
.form-container {
justify-content: center;
padding-top: 1rem;
padding-bottom: 1rem;
width: 80%;
margin: auto;
}
.text-container {
width: 80%;
margin: auto;
}
.card-text {
margin: 0;
font-size: var(--font-size-5);
padding-bottom: 0.3rem;
}
.container {
display: grid;
grid-template-columns: 2fr 5fr 2fr;
grid-template-rows: auto;
grid-row-gap: 1rem;
align-items: center;
justify-items: center;
margin-bottom: 1rem;
margin-top: 1rem;
}
/*
* CIRCLES
*/
.circle {
align-items: center;
background: var(--light-gray);
border-radius: 50%;
box-shadow: var(--box-shadow-3);
display: flex;
justify-content: center;
position: relative;
}
.circle-small {
height: 5rem;
width: 5rem;
}
.circle-medium {
height: 8rem;
width: 8rem;
}
.circle-large {
height: 13rem;
width: 13rem;
}
.circle-success {
background-color: var(--success);
color: var(--white);
font-size: var(--font-size-4);
}
.circle-warning {
background-color: var(--warning);
color: var(--white);
font-size: var(--font-size-4);
}
.circle-error {
background-color: var(--danger);
color: var(--white);
font-size: var(--font-size-4);
}
/* quartered-circle: circle for the center of radial-menu */
.quartered-circle {
width: 100px;
height: 100px;
}
.quarter {
width: 50%;
height: 50%;
}
.quarter-link {
left: 50%;
margin: -2em;
top: 50%;
}
.quarter-icon {
position: absolute;
bottom: 1em;
left: 1.5em;
}
/*
* COLORS
*/
.primary-bg {
background-color: var(--primary);
}
.secondary-bg {
background-color: var(--secondary);
}
.success-bg {
background-color: var(--success);
}
.info-bg {
background-color: var(--info);
}
.warning-bg {
background-color: var(--warning);
}
.danger-bg {
background-color: var(--danger);
}
.light-bg {
background-color: var(--light);
}
.primary-border {
border-color: var(--primary);
}
.success-border {
border-color: var(--success);
}
.info-border {
border-color: var(--info);
}
.warning-border {
border-color: var(--warning);
}
.danger-border {
border-color: var(--danger);
}
.dark-gray-border {
border-color: var(--dark-gray);
}
/*
* GRIDS
*/
.grid {
display: grid;
grid-template-columns: 2fr 1fr 2fr;
grid-template-rows: 2fr 1fr 2fr;
height: 80vh;
}
.flex-grid {
display: flex;
align-content: space-between;
align-items: baseline;
justify-content: center;
}
.two-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: auto;
align-items: center;
justify-content: center;
justify-items: center;
padding-bottom: 1rem;
/* margin-right: 2rem; */
/* margin-left: 2rem; */
/* padding-top: 1.5rem; */
}
.two-grid-top-right {
grid-column: 2;
justify-self: right;
padding: 0;
}
.three-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto;
grid-gap: 10px;
align-items: center;
justify-content: center;
}
.profile-grid {
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-rows: auto;
grid-gap: 10px;
align-items: center;
justify-content: center;
justify-items: center;
margin-right: 2rem;
margin-left: 2rem;
padding-top: 1.5rem;
padding-bottom: 1rem;
}
.stack {
display: grid;
align-items: flex-end;
justify-items: center;
justify-content: center;
}
.three-grid-icon-1 {
align-self: center;
grid-column: 1;
grid-row: 1;
justify-self: center;
margin-bottom: 10px;
max-width: 55%;
text-align: center;
}
.three-grid-icon-2 {
align-self: center;
grid-column: 2;
grid-row: 1;
justify-self: center;
margin-bottom: 10px;
max-width: 55%;
text-align: center;
}
.three-grid-icon-3 {
align-self: center;
grid-column: 3;
grid-row: 1;
justify-self: center;
margin-bottom: 10px;
max-width: 55%;
text-align: center;
}
.three-grid-label-1 {
align-self: center;
grid-column: 1;
grid-row: 1;
justify-self: center;
text-align: center;
}
.three-grid-label-2 {
align-self: center;
grid-column: 2;
grid-row: 1;
justify-self: center;
text-align: center;
}
.three-grid-label-3 {
align-self: center;
grid-column: 3;
grid-row: 1;
justify-self: center;
text-align: center;
}
.grid-column-1 {
grid-column: 1;
}
.grid-column-2 {
grid-column: 2;
justify-self: left;
}
.grid-column-3 {
grid-column: 3;
}
/*
* HTML
*/
html {
height: 100%;
}
/*
* FLASH MESSAGE
*/
.flash-message {
font-family: var(--sans-serif);
font-size: var(--font-size-6);
margin-left: 2rem;
margin-right: 2rem;
margin-top: 1rem;
}
/*
* FONTS
*/
.font-near-black {
color: var(--near-black);
}
.font-gray {
color: var(--mid-gray);
}
.font-light-gray {
color: var(--silver);
}
.font-success {
color: var(--success);
}
.font-warning {
color: var(--warning);
}
.font-failure {
color: var(--danger);
}
/*
* ICONS
*/
.icon {
width: 3rem;
}
.icon-small {
width: 1rem;
}
.icon-medium {
width: 2rem;
}
.icon-large {
width: 5rem;
}
.icon-100 {
width: 100%;
}
/* icon-active: sets color of icon svg to near-black */
.icon-active {
filter: invert(0%) sepia(1%) saturate(4171%) hue-rotate(79deg) brightness(86%) contrast(87%);
}
/* icon-inactive: sets color of icon svg to gray */
.icon-inactive {
filter: invert(72%) sepia(8%) saturate(14%) hue-rotate(316deg) brightness(93%) contrast(92%);
}
/*
* INPUTS
*/
.input {
/* Needed to render inputs & buttons of equal width */
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin-top: 0.5rem;
margin-bottom: 1rem;
padding-left: 5px;
line-height: 1.5rem;
width: 80%;
}
.form-input {
margin-bottom: 0;
margin-left: 0px;
border: 0px;
padding-left: 5px;
line-height: 1.5rem;
width: 100%;
}
.message-input {
height: 7rem;
overflow: auto;
resize: vertical;
}
.alert-input {
/* Needed to render inputs & buttons of equal width */
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin-right: 0.25rem;
padding-right: 0.25rem;
text-align: right;
width: 7rem;
}
.input-wrapper {
margin-bottom: 15px;
}
/*
* LABELS
*/
.label-small {
font-family: var(--sans-serif);
font-size: var(--font-size-7);
display: block;
}
.label-medium {
font-size: var(--font-size-3);
display: block;
}
.label-large {
font-size: var(--font-size-2);
display: block;
}
.label-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
width: 10rem;
}
.input-label {
margin-bottom: 0.4rem;
}
/*
* LINKS
*/
.link {
text-decoration: none;
color: var(--font-near-black);
}
/*
* LISTS
*/
.list {
padding-left: 0;
margin-left: 0;
max-width: var(--max-width-6);
border: 1px solid var(--light-silver);
border-radius: var(--border-radius-2);
list-style-type: none;
font-family: var(--sans-serif);
}
.list-container {
width: var(--max-width-5);
}
.list-icon {
align-self: center;
justify-self: right;
grid-column: 2;
grid-row: 1/3;
}
.list-item {
display: grid;
padding: 1rem;
border-bottom-color: var(--light-silver);
border-bottom-style: solid;
border-bottom-width: 1px;
}
.list-text {
justify-self: left;
grid-column: 1;
grid-row: 1;
margin: 0;
font-size: var(--font-size-5);
}
.list-label {
justify-self: left;
grid-column: 1;
grid-row: 2;
}
/*
* MAIN
*/
main {
flex: 1 0 auto;
}
/*
* METERS
*/
meter {
border: 1px solid #ccc;
border-radius: 3px;
display: block;
/* height: 1rem; */
margin: 0 auto;
margin-bottom: 1rem;
width: 100%;
/* remove default styling */
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
/* Firefox */
background: none; /* remove default background */
background-color: var(--near-white);
box-shadow: 0 5px 5px -5px #333 inset;
}
meter::-webkit-meter-bar {
background: none; /* remove default background */
background-color: var(--near-white);
box-shadow: 0 5px 5px -5px #333 inset;
}
meter::-webkit-meter-optimum-value {
background-size: 100% 100%;
box-shadow: 0 5px 5px -5px #999 inset;
transition: width .5s;
}
/* Firefox styling */
meter::-moz-meter-bar {
background: var(--mid-gray);
background-size: 100% 100%;
box-shadow: 0 5px 5px -5px #999 inset;
}
.meter-gauge {
background-color: var(--near-white);
border: 1px solid #ccc;
border-radius: 3px;
box-shadow: 0 5px 5px -5px #333 inset;
display: block;
}
/* Chrome styling */
.meter-gauge > span {
background: var(--mid-gray);
background-size: 100% 100%;
box-shadow: 0 5px 5px -5px #999 inset;
display: block;
height: inherit;
text-indent: -9999px;
}
/*
* NAVIGATION
*/
.nav-bar {
display: flex;
align-items: center;
width: 100%;
height: 2em;
padding-top: 1rem;
padding-bottom: 1rem;
justify-content: space-between;
}
.nav-title {
font-family: var(--sans-serif);
font-size: var(--font-size-4);
font-weight: normal;
margin: 0;
}
.nav-icon {
width: auto;
height: 90%;
cursor: pointer;
}
.nav-icon-left {
float: left;
padding-left: 10px;
}
.nav-icon-right {
float: right;
padding-right: 10px;
}
.nav-item {
display: inline-block;
list-style-type: none;
}
/*
* PARAGRAPHS
*/
p {
font-family: var(--sans-serif);
overflow-wrap: anywhere;
}
/*
* SWITCHES / SLIDERS
*/
/* switch: the box around the slider */
.switch {
display: inline-block;
height: 34px;
position: relative;
width: 60px;
}
/* hide default HTML checkbox */
.switch input {
height: 0;
opacity: 0;
width: 0;
}
.switch-icon-left {
align-self: center;
grid-column: 1;
grid-row: 1;
justify-self: center;
}
.switch-icon-right {
align-self: center;
grid-column: 3;
grid-row: 1;
justify-self: center;
}
.slider {
background-color: var(--moon-gray);
bottom: 0;
cursor: pointer;
left: 0;
position: absolute;
right: 0;
top: 0;
transition: .4s;
-webkit-transition: .4s;
}
.slider:before {
background-color: var(--white);
bottom: 4px;
content: "";
height: 26px;
left: 4px;
position: absolute;
transition: .4s;
-webkit-transition: .4s;
width: 26px;
}
input:checked + .slider {
background-color: var(--near-black);
}
input:focus + .slider {
box-shadow: 0 0 1px var(--near-black);
}
input:checked + .slider:before {
-ms-transform: translateX(26px);
transform: translateX(26px);
-webkit-transform: translateX(26px);
}
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
/*
* TITLES
*/
.title-medium {
font-size: var(--font-size-4);
font-family: var(--sans-serif);
max-width: var(--max-width-6);
}

View File

@ -0,0 +1 @@
yo :)

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512.001 512.001" style="enable-background:new 0 0 512.001 512.001;" xml:space="preserve">
<g>
<g>
<path d="M503.839,395.379l-195.7-338.962C297.257,37.569,277.766,26.315,256,26.315c-21.765,0-41.257,11.254-52.139,30.102
L8.162,395.378c-10.883,18.85-10.883,41.356,0,60.205c10.883,18.849,30.373,30.102,52.139,30.102h391.398
c21.765,0,41.256-11.254,52.14-30.101C514.722,436.734,514.722,414.228,503.839,395.379z M477.861,440.586
c-5.461,9.458-15.241,15.104-26.162,15.104H60.301c-10.922,0-20.702-5.646-26.162-15.104c-5.46-9.458-5.46-20.75,0-30.208
L229.84,71.416c5.46-9.458,15.24-15.104,26.161-15.104c10.92,0,20.701,5.646,26.161,15.104l195.7,338.962
C483.321,419.836,483.321,431.128,477.861,440.586z"/>
</g>
</g>
<g>
<g>
<rect x="241.001" y="176.01" width="29.996" height="149.982"/>
</g>
</g>
<g>
<g>
<path d="M256,355.99c-11.027,0-19.998,8.971-19.998,19.998s8.971,19.998,19.998,19.998c11.026,0,19.998-8.971,19.998-19.998
S267.027,355.99,256,355.99z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 477.175 477.175" style="enable-background:new 0 0 477.175 477.175;" xml:space="preserve">
<g>
<path d="M145.188,238.575l215.5-215.5c5.3-5.3,5.3-13.8,0-19.1s-13.8-5.3-19.1,0l-225.1,225.1c-5.3,5.3-5.3,13.8,0,19.1l225.1,225
c2.6,2.6,6.1,4,9.5,4s6.9-1.3,9.5-4c5.3-5.3,5.3-13.8,0-19.1L145.188,238.575z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 768 B

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
<path fill="#000000" d="M14.5 18h-10c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h10c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z"></path>
<path fill="#000000" d="M16.5 3c-0.276 0-0.5 0.224-0.5 0.5v15c0 0.276-0.224 0.5-0.5 0.5h-11c-0.827 0-1.5-0.673-1.5-1.5s0.673-1.5 1.5-1.5h9c0.827 0 1.5-0.673 1.5-1.5v-12c0-0.827-0.673-1.5-1.5-1.5h-10c-0.827 0-1.5 0.673-1.5 1.5v15c0 1.378 1.122 2.5 2.5 2.5h11c0.827 0 1.5-0.673 1.5-1.5v-15c0-0.276-0.224-0.5-0.5-0.5zM3.5 2h10c0.276 0 0.5 0.224 0.5 0.5v12c0 0.276-0.224 0.5-0.5 0.5h-9c-0.562 0-1.082 0.187-1.5 0.501v-13.001c0-0.276 0.224-0.5 0.5-0.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 916 B

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
<path fill="#000000" d="M17.5 20h-16c-0.827 0-1.5-0.673-1.5-1.5v-16c0-0.827 0.673-1.5 1.5-1.5h16c0.827 0 1.5 0.673 1.5 1.5v16c0 0.827-0.673 1.5-1.5 1.5zM1.5 2c-0.276 0-0.5 0.224-0.5 0.5v16c0 0.276 0.224 0.5 0.5 0.5h16c0.276 0 0.5-0.224 0.5-0.5v-16c0-0.276-0.224-0.5-0.5-0.5h-16z"></path>
<path fill="#000000" d="M6.5 17h-2c-0.276 0-0.5-0.224-0.5-0.5v-9c0-0.276 0.224-0.5 0.5-0.5h2c0.276 0 0.5 0.224 0.5 0.5v9c0 0.276-0.224 0.5-0.5 0.5zM5 16h1v-8h-1v8z"></path>
<path fill="#000000" d="M10.5 17h-2c-0.276 0-0.5-0.224-0.5-0.5v-12c0-0.276 0.224-0.5 0.5-0.5h2c0.276 0 0.5 0.224 0.5 0.5v12c0 0.276-0.224 0.5-0.5 0.5zM9 16h1v-11h-1v11z"></path>
<path fill="#000000" d="M14.5 17h-2c-0.276 0-0.5-0.224-0.5-0.5v-5c0-0.276 0.224-0.5 0.5-0.5h2c0.276 0 0.5 0.224 0.5 0.5v5c0 0.276-0.224 0.5-0.5 0.5zM13 16h1v-4h-1v4z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="612px" height="612px" viewBox="0 0 612 612" style="enable-background:new 0 0 612 612;" xml:space="preserve">
<g>
<g id="cloud-off">
<path d="M494.7,229.5c-17.851-86.7-94.351-153-188.7-153c-38.25,0-73.95,10.2-102,30.6l38.25,38.25
c17.85-12.75,40.8-17.85,63.75-17.85c76.5,0,140.25,63.75,140.25,140.25v12.75h38.25c43.35,0,76.5,33.15,76.5,76.5
c0,28.05-15.3,53.55-40.8,66.3l38.25,38.25C591.6,438.6,612,400.35,612,357C612,290.7,558.45,234.6,494.7,229.5z M76.5,109.65
l71.4,68.85C66.3,183.6,0,249.9,0,331.5c0,84.15,68.85,153,153,153h298.35l51,51l33.15-33.15L109.65,76.5L76.5,109.65z
M196.35,229.5l204,204H153c-56.1,0-102-45.9-102-102c0-56.1,45.9-102,102-102H196.35z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg height="638pt" viewBox="-20 -129 638.67144 638" width="638pt" xmlns="http://www.w3.org/2000/svg"><path d="m478.90625 132.8125c-4.785156.003906-9.5625.292969-14.3125.863281-12.894531-41.988281-51.628906-70.683593-95.550781-70.773437-10.933594-.011719-21.789063 1.804687-32.121094 5.363281-25.578125-55.308594-86.195313-85.367187-145.699219-72.25-59.511718 13.121094-101.867187 65.875-101.824218 126.808594.003906 10.53125 1.316406 21.019531 3.890624 31.222656-56.695312 8.65625-97.203124 59.46875-92.988281 116.667969 4.207031 57.203125 51.71875 101.542968 109.070313 101.796875h369.535156c66.191406 0 119.847656-53.660157 119.847656-119.851563s-53.65625-119.847656-119.847656-119.847656zm0 219.722656h-369.535156c-49.238282.214844-89.472656-39.253906-90.207032-88.488281-.730468-49.234375 38.304688-89.878906 87.53125-91.132813 3.195313-.089843 6.152344-1.703124 7.957032-4.339843 1.8125-2.640625 2.246094-5.980469 1.171875-8.992188-19.824219-56.730469 9.664062-118.855469 66.152343-139.367187 56.484376-20.511719 118.964844 8.226562 140.15625 64.460937.96875 2.609375 2.976563 4.691407 5.546876 5.753907 2.574218 1.0625 5.46875 1.003906 7.992187-.160157 10.457031-4.863281 21.84375-7.382812 33.371094-7.394531 38 .070312 70.722656 26.835938 78.3125 64.070312 1.085937 5.414063 6.359375 8.914063 11.765625 7.820313 6.511718-1.304687 13.136718-1.96875 19.785156-1.976563 55.160156 0 99.875 44.71875 99.875 99.871094 0 55.160156-44.714844 99.875-99.875 99.875zm0 0"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
<path fill="#000000" d="M7.631 19.702c-0.041 0-0.083-0.005-0.125-0.016-0.898-0.231-1.761-0.587-2.564-1.059-0.233-0.137-0.315-0.434-0.186-0.671 0.159-0.292 0.243-0.622 0.243-0.957 0-1.103-0.897-2-2-2-0.334 0-0.665 0.084-0.957 0.243-0.237 0.129-0.534 0.047-0.671-0.186-0.472-0.804-0.828-1.666-1.059-2.564-0.065-0.254 0.077-0.515 0.325-0.598 0.814-0.274 1.362-1.036 1.362-1.895s-0.547-1.621-1.362-1.895c-0.248-0.084-0.39-0.344-0.325-0.598 0.231-0.898 0.587-1.761 1.059-2.564 0.137-0.233 0.434-0.315 0.671-0.186 0.291 0.159 0.622 0.243 0.957 0.243 1.103 0 2-0.897 2-2 0-0.334-0.084-0.665-0.243-0.957-0.129-0.237-0.047-0.534 0.186-0.671 0.804-0.472 1.666-0.828 2.564-1.059 0.254-0.065 0.515 0.077 0.598 0.325 0.274 0.814 1.036 1.362 1.895 1.362s1.621-0.547 1.895-1.362c0.084-0.248 0.345-0.39 0.598-0.325 0.898 0.231 1.761 0.587 2.564 1.059 0.233 0.137 0.315 0.434 0.186 0.671-0.159 0.292-0.243 0.622-0.243 0.957 0 1.103 0.897 2 2 2 0.334 0 0.665-0.084 0.957-0.243 0.237-0.129 0.534-0.047 0.671 0.186 0.472 0.804 0.828 1.666 1.059 2.564 0.065 0.254-0.077 0.515-0.325 0.598-0.814 0.274-1.362 1.036-1.362 1.895s0.547 1.621 1.362 1.895c0.248 0.084 0.39 0.344 0.325 0.598-0.231 0.898-0.587 1.761-1.059 2.564-0.137 0.233-0.434 0.315-0.671 0.186-0.292-0.159-0.622-0.243-0.957-0.243-1.103 0-2 0.897-2 2 0 0.334 0.084 0.665 0.243 0.957 0.129 0.237 0.047 0.534-0.186 0.671-0.804 0.472-1.666 0.828-2.564 1.059-0.254 0.065-0.515-0.077-0.598-0.325-0.274-0.814-1.036-1.362-1.895-1.362s-1.621 0.547-1.895 1.362c-0.070 0.207-0.264 0.341-0.474 0.341zM10 17c1.127 0 2.142 0.628 2.655 1.602 0.52-0.161 1.026-0.369 1.51-0.622-0.108-0.314-0.164-0.646-0.164-0.98 0-1.654 1.346-3 3-3 0.334 0 0.666 0.056 0.98 0.164 0.253-0.484 0.462-0.989 0.622-1.51-0.974-0.512-1.602-1.527-1.602-2.655s0.628-2.142 1.602-2.655c-0.161-0.52-0.369-1.026-0.622-1.51-0.314 0.108-0.646 0.164-0.98 0.164-1.654 0-3-1.346-3-3 0-0.334 0.056-0.666 0.164-0.98-0.484-0.253-0.989-0.462-1.51-0.622-0.512 0.974-1.527 1.602-2.655 1.602s-2.142-0.628-2.655-1.602c-0.52 0.16-1.026 0.369-1.51 0.622 0.108 0.314 0.164 0.646 0.164 0.98 0 1.654-1.346 3-3 3-0.334 0-0.666-0.056-0.98-0.164-0.253 0.484-0.462 0.989-0.622 1.51 0.974 0.512 1.602 1.527 1.602 2.655s-0.628 2.142-1.602 2.655c0.16 0.52 0.369 1.026 0.622 1.51 0.314-0.108 0.646-0.164 0.98-0.164 1.654 0 3 1.346 3 3 0 0.334-0.056 0.666-0.164 0.98 0.484 0.253 0.989 0.462 1.51 0.622 0.512-0.974 1.527-1.602 2.655-1.602z"></path>
<path fill="#000000" d="M10 13c-1.654 0-3-1.346-3-3s1.346-3 3-3 3 1.346 3 3-1.346 3-3 3zM10 8c-1.103 0-2 0.897-2 2s0.897 2 2 2c1.103 0 2-0.897 2-2s-0.897-2-2-2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,5 @@
<?xml version='1.0' encoding='iso-8859-1'?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 457.68 457.68" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 457.68 457.68">
<path d="m439.48,167.086v-111.249c0-17.81-14.49-32.3-32.3-32.3h-374.88c-17.811,0-32.3,14.49-32.3,32.3v226.63c0,17.81 14.49,32.3 32.3,32.3h106.243l-12.162,13.09h-18.221c-4.142,0-7.5,3.358-7.5,7.5s3.358,7.5 7.5,7.5h104.361v72.334c0,10.449 8.501,18.951 18.951,18.951h80.627c10.449,0 18.951-8.501 18.951-18.951v-15.234h100.94c14.166,0 25.69-11.529 25.69-25.7v-182.59c0-11.563-7.674-21.364-18.2-24.581zm3.2,24.581v2.049h-172.49v-2.049c0-5.9 4.8-10.7 10.7-10.7h151.1c5.895,0.001 10.69,4.801 10.69,10.7zm-130.581,63.364h-41.909v-46.315h172.49v148.491h-111.63v-83.226c0-10.449-8.502-18.95-18.951-18.95zm3.951,28.809h-88.528v-9.858c0-2.178 1.772-3.951 3.951-3.951h80.627c2.178,0 3.951,1.772 3.951,3.951v9.858zm108.429-220.503v102.63h-143.59c-14.171,0-25.7,11.529-25.7,25.7v63.364h-23.718c-10.441,0-18.936,8.488-18.949,18.926h-197.523v-210.62h409.48zm-196.959,235.503h88.528v91.495h-88.528v-91.495zm-195.221-260.303h374.88c6.85,2.13163e-14 12.765,4.012 15.565,9.8h-406.011c2.801-5.788 8.716-9.8 15.566-9.8zm-16.025,250.421h196.247v10.81h-180.222c-7.243-0.001-13.452-4.48-16.025-10.81zm130.582,38.899l12.162-13.09h53.503v13.09h-65.665zm165.242,91.286h-80.627c-2.178,0-3.951-1.772-3.951-3.951v-9.857h88.528v9.857c0.001,2.178-1.772,3.951-3.95,3.951zm119.891-34.185h-100.94v-12.75h111.63v2.05c0,5.899-4.795,10.7-10.69,10.7z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M441.156,322.876l-48.666-47.386c-3.319-3.243-8.619-3.234-11.93,0.017l-81.894,80.299V8.533
c0-4.71-3.823-8.533-8.533-8.533h-68.267c-4.71,0-8.533,3.823-8.533,8.533v347.273l-81.894-80.299
c-3.311-3.243-8.602-3.251-11.921-0.017l-48.666,47.386c-1.655,1.604-2.586,3.806-2.586,6.11c0,2.304,0.939,4.506,2.586,6.11
l179.2,174.481c1.655,1.613,3.806,2.423,5.948,2.423c2.15,0,4.292-0.811,5.956-2.423l179.2-174.481
c1.647-1.604,2.577-3.806,2.577-6.11C443.733,326.682,442.803,324.48,441.156,322.876z M255.991,491.563L89.028,328.986
l36.412-35.456l90.445,88.695c2.449,2.406,6.11,3.115,9.276,1.775c3.174-1.331,5.231-4.429,5.231-7.868V17.067h51.2v359.066
c0,3.439,2.065,6.537,5.231,7.868c3.166,1.34,6.818,0.631,9.276-1.775l90.445-88.695l36.42,35.456L255.991,491.563z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg height="512pt" viewBox="0 0 512 512" width="512pt" xmlns="http://www.w3.org/2000/svg"><path d="m218.667969 240h-202.667969c-8.832031 0-16-7.167969-16-16s7.167969-16 16-16h202.667969c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16zm0 0"/><path d="m138.667969 320c-4.097657 0-8.191407-1.558594-11.308594-4.691406-6.25-6.253906-6.25-16.386719 0-22.636719l68.695313-68.691406-68.695313-68.671875c-6.25-6.253906-6.25-16.386719 0-22.636719s16.382813-6.25 22.636719 0l80 80c6.25 6.25 6.25 16.382813 0 22.636719l-80 80c-3.136719 3.132812-7.234375 4.691406-11.328125 4.691406zm0 0"/><path d="m341.332031 512c-23.53125 0-42.664062-19.136719-42.664062-42.667969v-384c0-18.238281 11.605469-34.515625 28.882812-40.511719l128.171875-42.730468c28.671875-8.789063 56.277344 12.480468 56.277344 40.578125v384c0 18.21875-11.605469 34.472656-28.863281 40.488281l-128.214844 42.753906c-4.671875 1.449219-9 2.089844-13.589844 2.089844zm128-480c-1.386719 0-2.558593.171875-3.816406.554688l-127.636719 42.558593c-4.183594 1.453125-7.210937 5.675781-7.210937 10.21875v384c0 7.277344 7.890625 12.183594 14.484375 10.113281l127.636718-42.558593c4.160157-1.453125 7.210938-5.675781 7.210938-10.21875v-384c0-5.867188-4.777344-10.667969-10.667969-10.667969zm0 0"/><path d="m186.667969 106.667969c-8.832031 0-16-7.167969-16-16v-32c0-32.363281 26.300781-58.667969 58.664062-58.667969h240c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16h-240c-14.699219 0-26.664062 11.96875-26.664062 26.667969v32c0 8.832031-7.167969 16-16 16zm0 0"/><path d="m314.667969 448h-85.335938c-32.363281 0-58.664062-26.304688-58.664062-58.667969v-32c0-8.832031 7.167969-16 16-16s16 7.167969 16 16v32c0 14.699219 11.964843 26.667969 26.664062 26.667969h85.335938c8.832031 0 16 7.167969 16 16s-7.167969 16-16 16zm0 0"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
<path fill="#000000" d="M17.5 6h-16c-0.827 0-1.5 0.673-1.5 1.5v9c0 0.827 0.673 1.5 1.5 1.5h16c0.827 0 1.5-0.673 1.5-1.5v-9c0-0.827-0.673-1.5-1.5-1.5zM17.5 7c0.030 0 0.058 0.003 0.087 0.008l-7.532 5.021c-0.29 0.193-0.819 0.193-1.109 0l-7.532-5.021c0.028-0.005 0.057-0.008 0.087-0.008h16zM17.5 17h-16c-0.276 0-0.5-0.224-0.5-0.5v-8.566l7.391 4.927c0.311 0.207 0.71 0.311 1.109 0.311s0.798-0.104 1.109-0.311l7.391-4.927v8.566c0 0.276-0.224 0.5-0.5 0.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 777 B

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
<path fill="#000000" d="M11.5 8c0.276 0 0.5-0.224 0.5-0.5v-4c0-0.827-0.673-1.5-1.5-1.5h-9c-0.827 0-1.5 0.673-1.5 1.5v12c0 0.746 0.537 1.56 1.222 1.853l5.162 2.212c0.178 0.076 0.359 0.114 0.532 0.114 0.213-0 0.416-0.058 0.589-0.172 0.314-0.207 0.495-0.575 0.495-1.008v-1.5h2.5c0.827 0 1.5-0.673 1.5-1.5v-4c0-0.276-0.224-0.5-0.5-0.5s-0.5 0.224-0.5 0.5v4c0 0.276-0.224 0.5-0.5 0.5h-2.5v-9.5c0-0.746-0.537-1.56-1.222-1.853l-3.842-1.647h7.564c0.276 0 0.5 0.224 0.5 0.5v4c0 0.276 0.224 0.5 0.5 0.5zM6.384 5.566c0.322 0.138 0.616 0.584 0.616 0.934v12c0 0.104-0.028 0.162-0.045 0.173s-0.081 0.014-0.177-0.027l-5.162-2.212c-0.322-0.138-0.616-0.583-0.616-0.934v-12c0-0.079 0.018-0.153 0.051-0.22l5.333 2.286z"></path>
<path fill="#000000" d="M18.354 9.146l-3-3c-0.195-0.195-0.512-0.195-0.707 0s-0.195 0.512 0 0.707l2.146 2.146h-6.293c-0.276 0-0.5 0.224-0.5 0.5s0.224 0.5 0.5 0.5h6.293l-2.146 2.146c-0.195 0.195-0.195 0.512 0 0.707 0.098 0.098 0.226 0.146 0.354 0.146s0.256-0.049 0.354-0.146l3-3c0.195-0.195 0.195-0.512 0-0.707z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by IcoMoon.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20">
<path fill="#000000" d="M9.5 19c-0.084 0-0.167-0.021-0.243-0.063-0.116-0.065-2.877-1.611-5.369-4.082-0.196-0.194-0.197-0.511-0.003-0.707s0.511-0.197 0.707-0.003c1.979 1.962 4.186 3.346 4.908 3.776 0.723-0.431 2.932-1.817 4.908-3.776 0.196-0.194 0.513-0.193 0.707 0.003s0.193 0.513-0.003 0.707c-2.493 2.471-5.253 4.017-5.369 4.082-0.076 0.042-0.159 0.063-0.243 0.063z"></path>
<path fill="#000000" d="M1.279 11c-0.188 0-0.368-0.106-0.453-0.287-0.548-1.165-0.826-2.33-0.826-3.463 0-2.895 2.355-5.25 5.25-5.25 0.98 0 2.021 0.367 2.931 1.034 0.532 0.39 0.985 0.86 1.319 1.359 0.334-0.499 0.787-0.969 1.319-1.359 0.91-0.667 1.951-1.034 2.931-1.034 2.895 0 5.25 2.355 5.25 5.25 0 1.133-0.278 2.298-0.826 3.463-0.118 0.25-0.415 0.357-0.665 0.24s-0.357-0.415-0.24-0.665c0.485-1.031 0.731-2.053 0.731-3.037 0-2.343-1.907-4.25-4.25-4.25-1.703 0-3.357 1.401-3.776 2.658-0.068 0.204-0.259 0.342-0.474 0.342s-0.406-0.138-0.474-0.342c-0.419-1.257-2.073-2.658-3.776-2.658-2.343 0-4.25 1.907-4.25 4.25 0 0.984 0.246 2.006 0.731 3.037 0.118 0.25 0.010 0.548-0.24 0.665-0.069 0.032-0.141 0.048-0.212 0.048z"></path>
<path fill="#000000" d="M10.515 15c-0.005 0-0.009-0-0.013-0-0.202-0.004-0.569-0.109-0.753-0.766l-1.217-4.334-0.807 3.279c-0.158 0.643-0.525 0.778-0.73 0.8s-0.592-0.027-0.889-0.62l-0.606-1.211c-0.029-0.058-0.056-0.094-0.076-0.117-0.003 0.004-0.007 0.009-0.011 0.015-0.37 0.543-1.192 0.953-1.913 0.953h-1c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h1c0.421 0 0.921-0.272 1.087-0.516 0.223-0.327 0.547-0.501 0.891-0.478 0.374 0.025 0.708 0.279 0.917 0.696l0.445 0.89 0.936-3.803c0.158-0.64 0.482-0.779 0.726-0.783s0.572 0.125 0.751 0.76l1.284 4.576 1.178-3.608c0.205-0.628 0.582-0.736 0.788-0.745s0.59 0.068 0.847 0.677l0.724 1.719c0.136 0.322 0.578 0.616 0.927 0.616h1.5c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5h-1.5c-0.747 0-1.559-0.539-1.849-1.228l-0.592-1.406-1.274 3.9c-0.207 0.634-0.566 0.733-0.771 0.733z"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Some files were not shown because too many files have changed in this diff Show More