Merge pull request 'Reintroduce networking-related templates and routes' (#138) from system_mode into main

Reviewed-on: #138
This commit is contained in:
glyph 2022-10-18 11:14:20 +00:00
commit d3ab490c05
13 changed files with 989 additions and 326 deletions

5
Cargo.lock generated
View File

@ -2293,7 +2293,7 @@ dependencies = [
[[package]]
name = "peach-network"
version = "0.4.2"
version = "0.5.0"
dependencies = [
"get_if_addrs",
"miniserde",
@ -2346,6 +2346,7 @@ dependencies = [
"log 0.4.17",
"maud",
"peach-lib",
"peach-network",
"rouille",
"temporary",
"xdg",
@ -2407,7 +2408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f77e66f6d6d898cbbd4a09c48fd3507cfc210b7c83055de02a38b5f7a1e6d216"
dependencies = [
"libc",
"time 0.1.44",
"time 0.3.11",
]
[[package]]

View File

@ -45,8 +45,10 @@ log = "0.4"
maud = "0.23"
peach-lib = { path = "../peach-lib" }
# these will be reintroduced when the full peachcloud mode is added
#peach-network = { path = "../peach-network" }
peach-network = { path = "../peach-network" }
#peach-stats = { path = "../peach-stats" }
rouille = { version = "3.5", default-features = false }
temporary = "0.6"
# TODO: uncomment this when data usage feature is in place
#vnstat_parse = "0.1.0"
xdg = "2.2"

View File

@ -200,6 +200,50 @@ pub fn mount_peachpub_routes(
routes::settings::scuttlebutt::default::write_config()
},
(GET) (/settings/network) => {
Response::html(routes::settings::network::menu::build_template(request)).reset_flash()
},
(GET) (/settings/network/dns) => {
Response::html(routes::settings::network::configure_dns::build_template(request)).reset_flash()
},
(POST) (/settings/network/dns) => {
routes::settings::network::configure_dns::handle_form(request)
},
(GET) (/settings/network/wifi) => {
Response::html(routes::settings::network::list_aps::build_template())
},
(GET) (/settings/network/wifi/add) => {
Response::html(routes::settings::network::add_ap::build_template(request, None)).reset_flash()
},
(POST) (/settings/network/wifi/add) => {
routes::settings::network::add_ap::handle_form(request)
},
(GET) (/settings/network/wifi/add/{ssid: String}) => {
Response::html(routes::settings::network::add_ap::build_template(request, Some(ssid))).reset_flash()
},
(GET) (/settings/network/wifi/modify) => {
Response::html(routes::settings::network::modify_ap::build_template(request, None)).reset_flash()
},
(POST) (/settings/network/wifi/modify) => {
routes::settings::network::modify_ap::handle_form(request)
},
(GET) (/settings/network/wifi/modify/{ssid: String}) => {
Response::html(routes::settings::network::modify_ap::build_template(request, Some(ssid))).reset_flash()
},
(GET) (/settings/network/wifi/{ssid: String}) => {
Response::html(routes::settings::network::ap_details::build_template(request, ssid))
},
(GET) (/settings/theme/{theme: String}) => {
routes::settings::theme::set_theme(theme)
},

View File

@ -1,6 +1,6 @@
pub mod admin;
//pub mod dns;
pub mod menu;
//pub mod network;
pub mod network;
pub mod scuttlebutt;
pub mod theme;

View File

@ -1,322 +0,0 @@
use log::{debug, warn};
use rocket::{
form::{Form, FromForm},
get, post,
request::FlashMessage,
response::{Flash, Redirect},
uri, UriDisplayQuery,
};
use rocket_dyn_templates::{tera::Context, Template};
use peach_network::network;
use crate::{
context,
context::network::{NetworkAlertContext, NetworkDetailContext, NetworkListContext},
routes::authentication::Authenticated,
utils::{monitor, monitor::Threshold},
AP_IFACE, WLAN_IFACE,
};
// STRUCTS USED BY NETWORK ROUTES
#[derive(Debug, FromForm, UriDisplayQuery)]
pub struct Ssid {
pub ssid: String,
}
#[derive(Debug, FromForm)]
pub struct WiFi {
pub ssid: String,
pub pass: String,
}
// HELPERS AND ROUTES FOR /settings/network/wifi/usage/reset
#[get("/wifi/usage/reset")]
pub fn wifi_usage_reset(_auth: Authenticated) -> Flash<Redirect> {
let url = uri!(wifi_usage);
match monitor::reset_data() {
Ok(_) => Flash::success(Redirect::to(url), "Reset stored network traffic total"),
Err(_) => Flash::error(
Redirect::to(url),
"Failed to reset stored network traffic total",
),
}
}
#[post("/wifi/connect", data = "<network>")]
pub fn connect_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
let ssid = &network.ssid;
let url = uri!(network_detail(ssid = ssid));
match network::id(&*WLAN_IFACE, ssid) {
Ok(Some(id)) => match network::connect(&id, &*WLAN_IFACE) {
Ok(_) => Flash::success(Redirect::to(url), "Connected to chosen network"),
Err(_) => Flash::error(Redirect::to(url), "Failed to connect to chosen network"),
},
_ => Flash::error(Redirect::to(url), "Failed to retrieve the network ID"),
}
}
#[post("/wifi/disconnect", data = "<network>")]
pub fn disconnect_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
let ssid = &network.ssid;
let url = uri!(network_home);
match network::disable(&*WLAN_IFACE, ssid) {
Ok(_) => Flash::success(Redirect::to(url), "Disconnected from WiFi network"),
Err(_) => Flash::error(Redirect::to(url), "Failed to disconnect from WiFi network"),
}
}
#[post("/wifi/forget", data = "<network>")]
pub fn forget_wifi(network: Form<Ssid>, _auth: Authenticated) -> Flash<Redirect> {
let ssid = &network.ssid;
let url = uri!(network_home);
match network::forget(&*WLAN_IFACE, ssid) {
Ok(_) => Flash::success(Redirect::to(url), "WiFi credentials removed"),
Err(_) => Flash::error(
Redirect::to(url),
"Failed to remove WiFi credentials".to_string(),
),
}
}
#[get("/wifi/modify?<ssid>")]
pub fn wifi_password(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/network/wifi".to_string()));
context.insert("title", &Some("Update WiFi Password".to_string()));
context.insert("selected", &Some(ssid.to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("settings/network/modify_ap", &context.into_json())
}
#[post("/wifi/modify", data = "<wifi>")]
pub fn wifi_set_password(wifi: Form<WiFi>, _auth: Authenticated) -> Flash<Redirect> {
let ssid = &wifi.ssid;
let pass = &wifi.pass;
let url = uri!(network_detail(ssid = ssid));
match network::update(&*WLAN_IFACE, ssid, pass) {
Ok(_) => Flash::success(Redirect::to(url), "WiFi password updated".to_string()),
Err(_) => Flash::error(
Redirect::to(url),
"Failed to update WiFi password".to_string(),
),
}
}
// HELPERS AND ROUTES FOR /settings/network
#[get("/")]
pub fn network_home(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
// assign context
let mut context = Context::new();
context.insert("back", &Some("/settings"));
context.insert("title", &Some("Network Configuration"));
context.insert("ap_state", &context::network::ap_state());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
// template_dir is set in Rocket.toml
Template::render("settings/network/menu", &context.into_json())
}
// HELPERS AND ROUTES FOR /settings/network/ap/activate
#[get("/ap/activate")]
pub fn deploy_ap(_auth: Authenticated) -> Flash<Redirect> {
// activate the wireless access point
debug!("Activating WiFi access point.");
match network::start_iface_service(&*AP_IFACE) {
Ok(_) => Flash::success(
Redirect::to("/settings/network"),
"Activated WiFi access point",
),
Err(_) => Flash::error(
Redirect::to("/settings/network"),
"Failed to activate WiFi access point",
),
}
}
// HELPERS AND ROUTES FOR /settings/network/wifi
#[get("/wifi")]
pub fn wifi_list(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
// assign context through context_builder call
let mut context = NetworkListContext::build();
context.back = Some("/settings/network".to_string());
context.title = Some("WiFi Networks".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/network/list_aps", &context)
}
// HELPERS AND ROUTES FOR /settings/network/wifi<ssid>
#[get("/wifi?<ssid>")]
pub fn network_detail(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = NetworkDetailContext::build();
context.back = Some("/settings/network/wifi".to_string());
context.title = Some("WiFi Network".to_string());
context.selected = Some(ssid.to_string());
if let Some(flash) = flash {
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
Template::render("settings/network/ap_details", &context)
}
// HELPERS AND ROUTES FOR /settings/network/wifi/activate
#[get("/wifi/activate")]
pub fn deploy_client(_auth: Authenticated) -> Flash<Redirect> {
// activate the wireless client
debug!("Activating WiFi client mode.");
match network::start_iface_service(&*WLAN_IFACE) {
Ok(_) => Flash::success(Redirect::to("/settings/network"), "Activated WiFi client"),
Err(_) => Flash::error(
Redirect::to("/settings/network"),
"Failed to activate WiFi client",
),
}
}
// HELPERS AND ROUTES FOR /settings/network/wifi/add
#[get("/wifi/add")]
pub fn add_wifi(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/network".to_string()));
context.insert("title", &Some("Add WiFi Network".to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("settings/network/add_ap", &context.into_json())
}
#[get("/wifi/add?<ssid>")]
pub fn add_ssid(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/network".to_string()));
context.insert("title", &Some("Add WiFi Network".to_string()));
context.insert("selected", &Some(ssid.to_string()));
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.insert("flash_name", &Some(flash.kind().to_string()));
context.insert("flash_msg", &Some(flash.message().to_string()));
};
Template::render("settings/network/add_ap", &context.into_json())
}
#[post("/wifi/add", data = "<wifi>")]
pub fn add_credentials(wifi: Form<WiFi>, _auth: Authenticated) -> Template {
let mut context = Context::new();
context.insert("back", &Some("/settings/network".to_string()));
context.insert("title", &Some("Add WiFi Network".to_string()));
// check if the credentials already exist for this access point
// note: this is nicer but it's an unstable feature:
// if check_saved_aps(&wifi.ssid).contains(true)
// use unwrap_or instead, set value to false if err is returned
//let creds_exist = network::saved_networks(&wifi.ssid).unwrap_or(false);
let creds_exist = match network::saved_networks() {
Ok(Some(networks)) => networks.contains(&wifi.ssid),
_ => false,
};
// if credentials not found, generate and write wifi config to wpa_supplicant
let (flash_name, flash_msg) = if creds_exist {
(
"error".to_string(),
"Network credentials already exist for this access point".to_string(),
)
} else {
match network::add(&*WLAN_IFACE, &wifi.ssid, &wifi.pass) {
Ok(_) => {
debug!("Added WiFi credentials.");
// force reread of wpa_supplicant.conf file with new credentials
match network::reconfigure() {
Ok(_) => debug!("Successfully reconfigured wpa_supplicant"),
Err(_) => warn!("Failed to reconfigure wpa_supplicant"),
}
("success".to_string(), "Added WiFi credentials".to_string())
}
Err(e) => {
debug!("Failed to add WiFi credentials.");
("error".to_string(), format!("{}", e))
}
}
};
context.insert("flash_name", &Some(flash_name));
context.insert("flash_msg", &Some(flash_msg));
Template::render("settings/network/add_ap", &context.into_json())
}
// HELPERS AND ROUTES FOR WIFI USAGE
#[get("/wifi/usage")]
pub fn wifi_usage(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = NetworkAlertContext::build();
// set back icon link to network route
context.back = Some("/settings/network".to_string());
context.title = Some("Network Data Usage".to_string());
// check to see if there is a flash message to display
if let Some(flash) = flash {
// add flash message contents to the context object
context.flash_name = Some(flash.kind().to_string());
context.flash_msg = Some(flash.message().to_string());
};
// template_dir is set in Rocket.toml
Template::render("settings/network/data_usage_limits", &context)
}
#[post("/wifi/usage", data = "<thresholds>")]
pub fn wifi_usage_alerts(thresholds: Form<Threshold>, _auth: Authenticated) -> Flash<Redirect> {
match monitor::update_store(thresholds.into_inner()) {
Ok(_) => {
debug!("WiFi data usage thresholds updated.");
Flash::success(
Redirect::to("/settings/network/wifi/usage"),
"Updated alert thresholds and flags",
)
}
Err(_) => {
warn!("Failed to update WiFi data usage thresholds.");
Flash::error(
Redirect::to("/settings/network/wifi/usage"),
"Failed to update alert thresholds and flags",
)
}
}
}

View File

@ -0,0 +1,96 @@
use maud::{html, Markup, PreEscaped};
use peach_network::network;
use rouille::{post_input, try_or_400, Request, Response};
use crate::{
templates,
utils::{
flash::{FlashRequest, FlashResponse},
theme,
},
};
// ROUTE: /settings/network/wifi/add
fn render_ssid_input(selected_ap: Option<String>) -> Markup {
html! {
(PreEscaped("<!-- input for network ssid -->"))
input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value=[selected_ap] autofocus;
}
}
fn render_password_input() -> Markup {
html! {
(PreEscaped("<!-- input for network password -->"))
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
}
}
fn render_buttons() -> Markup {
html! {
(PreEscaped("<!-- BUTTONS -->"))
div id="buttons" {
input id="addWifi" class="button button-primary center" title="Add" type="submit" value="Add";
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
}
}
}
/// WiFi access point credentials form template builder.
pub fn build_template(request: &Request, selected_ap: Option<String>) -> PreEscaped<String> {
let (flash_name, flash_msg) = request.retrieve_flash();
let form_template = html! {
(PreEscaped("<!-- WIFI ADD CREDENTIALS FORM -->"))
div class="card center" {
form id="wifiCreds" action="/settings/network/wifi/add" method="post" {
(render_ssid_input(selected_ap))
(render_password_input())
(render_buttons())
}
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
};
let body = templates::nav::build_template(
form_template,
"Add WiFi Network",
Some("/settings/network"),
);
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}
/// Parse the SSID and password for an access point and save the new credentials.
pub fn handle_form(request: &Request) -> Response {
let data = try_or_400!(post_input!(request, {
ssid: String,
pass: String,
}));
let (name, msg) = match network::add("wlan0", &data.ssid, &data.pass) {
Ok(_) => match network::reconfigure() {
Ok(_) => ("success".to_string(), "Added WiFi credentials".to_string()),
Err(err) => (
"error".to_string(),
format!(
"Added WiFi credentials but failed to reconfigure interface: {}",
err
),
),
},
Err(err) => (
"error".to_string(),
format!("Failed to add WiFi credentials for {}: {}", &data.ssid, err),
),
};
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
Response::redirect_303("/settings/network/wifi/add").add_flash(flash_name, flash_msg)
}

View File

@ -0,0 +1,196 @@
use std::collections::HashMap;
use maud::{html, Markup, PreEscaped};
use peach_network::{network, network::AccessPoint, NetworkError};
use rouille::Request;
use crate::{
templates,
utils::{flash::FlashRequest, theme},
};
// ROUTE: /settings/network/wifi?<ssid>
fn render_network_status_icon(ssid: &str, wlan_ssid: &str, ap_state: &str) -> Markup {
let status_label_value = if ssid == wlan_ssid {
"CONNECTED"
} else if ap_state == "Available" {
"AVAILABLE"
} else {
"NOT IN RANGE"
};
html! {
(PreEscaped("<!-- NETWORK STATUS ICON -->"))
div class="grid-column-1" {
img id="wifiIcon" class="center icon" src="/icons/wifi.svg" alt="WiFi icon";
label class="center label-small font-gray" for="wifiIcon" title="Access Point Status" { (status_label_value) }
}
}
}
fn render_network_detailed_info(ssid: &str, ap_protocol: &str, ap_signal: Option<i32>) -> Markup {
let ap_signal_value = match ap_signal {
Some(signal) => signal.to_string(),
None => "Unknown".to_string(),
};
html! {
(PreEscaped("<!-- NETWORK DETAILED INFO -->"))
div class="grid-column-2" {
label class="label-small font-gray" for="netSsid" title="WiFi network SSID" { "SSID" };
p id="netSsid" class="card-text" title="SSID" { (ssid) }
label class="label-small font-gray" for="netSec" title="Security protocol" { "SECURITY" };
p id="netSec" class="card-text" title={ "Security protocol in use by " (ssid) } { (ap_protocol) }
label class="label-small font-gray" for="netSig" title="Signal Strength" { "SIGNAL" };
p id="netSig" class="card-text" title="Signal strength of WiFi access point" { (ap_signal_value) }
}
}
}
fn render_disconnect_form(ssid: &str) -> Markup {
html! {
form id="wifiDisconnect" action="/settings/network/wifi/disconnect" method="post" {
(PreEscaped("<!-- hidden element: allows ssid to be sent in request -->"))
input id="disconnectSsid" name="ssid" type="text" value=(ssid) style="display: none;";
input id="disconnectWifi" class="button button-warning center" title="Disconnect from Network" type="submit" value="Disconnect";
}
}
}
fn render_connect_form(ssid: &str) -> Markup {
html! {
form id="wifiConnect" action="/settings/network/wifi/connect" method="post" {
(PreEscaped("<!-- hidden element: allows ssid to be sent in request -->"))
input id="connectSsid" name="ssid" type="text" value=(ssid) style="display: none;";
input id="connectWifi" class="button button-primary center" title="Connect to Network" type="submit" value="Connect";
}
}
}
fn render_forget_form(ssid: &str) -> Markup {
html! {
form id="wifiForget" action="/settings/network/wifi/forget" method="post" {
(PreEscaped("<!-- hidden element: allows ssid to be sent in request -->"))
input id="forgetSsid" name="ssid" type="text" value=(ssid) style="display: none;";
input id="forgetWifi" class="button button-warning center" title="Forget Network" type="submit" value="Forget";
}
}
}
fn render_buttons(
selected_ap: &str,
wlan_ssid: &str,
ap: &AccessPoint,
saved_wifi_networks: Vec<String>,
) -> Markup {
html! {
(PreEscaped("<!-- BUTTONS -->"))
div id="buttons" {
@if wlan_ssid == selected_ap {
(render_disconnect_form(selected_ap))
}
@if saved_wifi_networks.contains(&selected_ap.to_string()) {
@if wlan_ssid != selected_ap && ap.state == "Available" {
(render_connect_form(selected_ap))
}
a class="button button-primary center" href={ "/settings/network/wifi/modify?ssid=" (selected_ap) } { "Modify" }
(render_forget_form(selected_ap))
} @else {
// display the Add button if AP creds not already in saved
// networks list
a class="button button-primary center" href={ "/settings/network/wifi/add?ssid=" (selected_ap) } { "Add" }
}
a class="button button-secondary center" href="/settings/network/wifi" title="Cancel" { "Cancel" }
}
}
}
/// Retrieve the list of all saved and in-range networks (including SSID and
/// AP details for each network), the list of all saved networks (SSIDs only)
/// and the SSID for the WiFi interface.
fn retrieve_network_data() -> (
Result<HashMap<String, AccessPoint>, NetworkError>,
Vec<String>,
String,
) {
let all_wifi_networks = network::all_networks("wlan0");
let saved_wifi_networks = match network::saved_networks() {
Ok(Some(ssids)) => ssids,
_ => Vec::new(),
};
let wlan_ssid = match network::ssid("wlan0") {
Ok(Some(ssid)) => ssid,
_ => String::from("Not connected"),
};
(all_wifi_networks, saved_wifi_networks, wlan_ssid)
}
/// WiFi access point (AP) template builder.
///
/// Render a UI card with details about the selected access point, including
/// the connection state, security protocol being used, the SSID and the
/// signal strength. Buttons are also rendering based on the state of the
/// access point and whether or not credentials for the AP have previously
/// been saved.
///
/// If the AP is available (ie. in-range) then a Connect button is rendered.
/// A Disconnect button is rendered if the WiFi client is currently
/// connected to the AP.
///
/// If credentials have not previously been saved for the AP, an Add button is
/// rendered. Forget and Modify buttons are rendered if credentials for the AP
/// have previously been saved.
pub fn build_template(request: &Request, selected_ap: String) -> PreEscaped<String> {
let (flash_name, flash_msg) = request.retrieve_flash();
let (all_wifi_networks, saved_wifi_networks, wlan_ssid) = retrieve_network_data();
let network_info_box_class = if selected_ap == wlan_ssid {
"two-grid capsule success-border"
} else {
"two-grid capsule"
};
let network_list_template = html! {
(PreEscaped("<!-- NETWORK CARD -->"))
div class="card center" {
@if let Ok(wlan_networks) = all_wifi_networks {
// select only the access point we are interested in displaying
@if let Some((ssid, ap)) = wlan_networks.get_key_value(&selected_ap) {
@let ap_protocol = match &ap.detail {
Some(detail) => detail.protocol.clone(),
None => "None".to_string()
};
(PreEscaped("<!-- NETWORK INFO BOX -->"))
div class=(network_info_box_class) title="PeachCloud network mode and status" {
(PreEscaped("<!-- left column -->"))
(render_network_status_icon(ssid, &wlan_ssid, &ap.state))
(PreEscaped("<!-- right column -->"))
(render_network_detailed_info(ssid, &ap_protocol, ap.signal))
}
(render_buttons(ssid, &wlan_ssid, ap, saved_wifi_networks))
} @else {
p class="card-text list-item" { (selected_ap) " not found in saved or in-range networks" }
}
} @else {
p class="card-text list-item" { "No saved or in-range networks found" }
}
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
};
let body = templates::nav::build_template(
network_list_template,
"WiFi Networks",
Some("/settings/network"),
);
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}

View File

@ -0,0 +1,201 @@
use log::info;
use maud::{html, Markup, PreEscaped};
use peach_lib::{
config_manager, dyndns_client,
error::PeachError,
jsonrpc_client_core::{Error, ErrorKind},
jsonrpc_core::types::error::ErrorCode,
};
use rouille::{post_input, try_or_400, Request, Response};
use crate::{
error::PeachWebError,
templates,
utils::{
flash::{FlashRequest, FlashResponse},
theme,
},
};
// ROUTE: /settings/network/dns
fn render_dyndns_status_indicator() -> Markup {
let (indicator_class, indicator_label) = match dyndns_client::is_dns_updater_online() {
Ok(true) => ("success-border", "Dynamic DNS is currently online."),
_ => (
"warning-border",
"Dynamic DNS is enabled but may be offline.",
),
};
html! {
(PreEscaped("<!-- DYNDNS STATUS INDICATOR -->"))
div id="dyndns-status-indicator" class={ "stack capsule " (indicator_class) } {
div class="stack" {
label class="label-small font-near-black" { (indicator_label) }
}
}
}
}
fn render_external_domain_input() -> Markup {
let external_domain = config_manager::get_config_value("EXTERNAL_DOMAIN").ok();
html! {
div class="input-wrapper" {
(PreEscaped("<!-- input for externaldomain -->"))
label id="external_domain" class="label-small input-label font-near-black" {
label class="label-small input-label font-gray" for="external_domain" style="padding-top: 0.25rem;" { "External Domain (optional)" }
input id="external_domain" class="form-input" style="margin-bottom: 0;" name="external_domain" type="text" title="external domain" value=[external_domain];
}
}
}
}
fn render_dyndns_enabled_checkbox() -> Markup {
let dyndns_enabled = config_manager::get_dyndns_enabled_value().unwrap_or(false);
html! {
div class="input-wrapper" {
div {
(PreEscaped("<!-- checkbox for dyndns flag -->"))
label class="label-small input-label font-gray" { "Enable Dynamic DNS" }
input style="margin-left: 0px;" id="enable_dyndns" name="enable_dyndns" title="Activate dynamic DNS" type="checkbox" checked[dyndns_enabled];
}
}
}
}
fn render_dynamic_domain_input() -> Markup {
let dyndns_domain =
config_manager::get_config_value("DYN_DOMAIN").unwrap_or_else(|_| String::from(""));
let dyndns_subdomain =
dyndns_client::get_dyndns_subdomain(&dyndns_domain).unwrap_or(dyndns_domain);
html! {
div class="input-wrapper" {
(PreEscaped("<!-- input for dyndns domain -->"))
label id="cut" class="label-small input-label font-near-black" {
label class="label-small input-label font-gray" for="cut" style="padding-top: 0.25rem;" { "Dynamic DNS Domain" }
input id="dyndns_domain" class="alert-input" name="dynamic_domain" placeholder="" type="text" title="dyndns_domain" value=(dyndns_subdomain);
{ ".dyn.peachcloud.org" }
}
}
}
}
fn render_save_button() -> Markup {
html! {
div id="buttonDiv" style="margin-top: 2rem;" {
input id="configureDNSButton" class="button button-primary center" title="Add" type="submit" value="Save";
}
}
}
/// DNS configuration form template builder.
pub fn build_template(request: &Request) -> PreEscaped<String> {
let (flash_name, flash_msg) = request.retrieve_flash();
let dyndns_enabled = config_manager::get_dyndns_enabled_value().unwrap_or(false);
let form_template = html! {
(PreEscaped("<!-- CONFIGURE DNS FORM -->"))
div class="card center" {
@if dyndns_enabled {
(render_dyndns_status_indicator())
}
form id="configureDNS" class="center" action="/settings/network/dns" method="post" {
(render_external_domain_input())
(render_dyndns_enabled_checkbox())
(render_dynamic_domain_input())
(render_save_button())
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
}
};
let body = templates::nav::build_template(
form_template,
"Configure Dynamic DNS",
Some("/settings/network"),
);
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}
pub fn save_dns_configuration(
external_domain: String,
enable_dyndns: bool,
dynamic_domain: String,
) -> Result<(), PeachWebError> {
// first save local configurations
config_manager::set_external_domain(&external_domain)?;
config_manager::set_dyndns_enabled_value(enable_dyndns)?;
let full_dynamic_domain = dyndns_client::get_full_dynamic_domain(&dynamic_domain);
// if dynamic dns is enabled and this is a new domain name, then register it
if enable_dyndns && dyndns_client::check_is_new_dyndns_domain(&full_dynamic_domain)? {
if let Err(registration_err) = dyndns_client::register_domain(&full_dynamic_domain) {
info!("Failed to register dyndns domain: {:?}", registration_err);
// error message describing the failed update
let err_msg = match registration_err {
PeachError::JsonRpcClientCore(Error(ErrorKind::JsonRpcError(rpc_err), _)) => {
if let ErrorCode::ServerError(-32030) = rpc_err.code {
format!(
"Error registering domain: {} was previously registered",
full_dynamic_domain
)
} else {
format!("Failed to register dyndns domain: {:?}", rpc_err)
}
}
_ => "Failed to register dyndns domain".to_string(),
};
Err(PeachWebError::FailedToRegisterDynDomain(err_msg))
} else {
info!("Registered new dyndns domain");
Ok(())
}
} else {
info!("Domain {} already registered", dynamic_domain);
Ok(())
}
}
/// Parse the DNS configuration parameters and apply them.
pub fn handle_form(request: &Request) -> Response {
let data = try_or_400!(post_input!(request, {
external_domain: String,
enable_dyndns: bool,
dynamic_domain: String,
}));
let (name, msg) = match save_dns_configuration(
data.external_domain,
data.enable_dyndns,
data.dynamic_domain,
) {
Ok(_) => (
"success".to_string(),
"New dynamic DNS configuration is now enabled".to_string(),
),
Err(err) => (
"error".to_string(),
format!("Failed to save DNS configuration: {}", err),
),
};
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
Response::redirect_303("/settings/network/dns").add_flash(flash_name, flash_msg)
}

View File

@ -0,0 +1,163 @@
// TODO:
//
// This template and associated feature set requires vnstat_parse.
// - https://crates.io/crates/vnstat_parse
//
// Use the PeachCloud config system to store warning and cutoff flags,
// as well as the associated totals (thresholds):
//
// - DATA_WARNING_ENABLED
// - DATA_WARNING_LIMIT
// - DATA_CUTOFF_ENABLED
// - DATA_CUTOFF_LIMIT
use maud::{html, Markup, PreEscaped};
use peach_network::network;
use rouille::Request;
use vnstat_parse::Vnstat;
use crate::{
templates,
utils::{flash::FlashRequest, theme},
};
// ROUTE: /settings/network/wifi/usage
fn render_data_usage_total_capsule() -> Markup {
html! {
div class="stack capsule" style="margin-left: 2rem; margin-right: 2rem;" {
div class="flex-grid" {
label id="dataTotal" class="label-large" title="Data download total in MB" {
data_total.total / 1024 / 1024 | round
}
label class="label-small font-near-black" { "MB" }
}
label class="center-text label-small font-gray" { "USAGE TOTAL" }
}
}
}
fn render_warning_threshold_icon() -> Markup {
// threshold.warn_flag
let warning_enabled = true;
let icon_class = match warning_enabled {
true => "icon",
false => "icon icon-inactive",
};
html! {
div class="card-container container" {
div {
img id="warnIcon" class=(icon_class) alt="Warning" title="Warning threshold" src="/icons/alert.svg";
}
}
}
}
fn render_warning_threshold_input() -> Markup {
// TODO: source threshold.warn value and replace below
html! {
div {
(PreEscaped("<!-- input for warning threshold -->"))
label id="warn" class="label-small font-near-black" {
input id="warnInput" class="alert-input" name="warn" placeholder="0" type="text" title="Warning threshold value" value="{{ threshold.warn }}" { "MB" }
}
label class="label-small font-gray" for="warn" style="padding-top: 0.25rem;" { "WARNING THRESHOLD" }
}
}
}
fn render_warning_threshold_checkbox() -> Markup {
let warning_enabled = true;
html! {
div {
(PreEscaped("<!-- checkbox for warning threshold flag -->"))
input id="warnCheck" name="warn_flag" title="Activate warning" type="checkbox" checked[warning_enabled];
}
}
}
fn render_critical_threshold_icon() -> Markup {
// threshold.cut_flag
let cutoff_enabled = true;
let icon_class = match cutoff_enabled {
true => "icon",
false => "icon icon-inactive",
};
html! {
div {
img id="cutIcon"
class=(icon_class)
alt="Cutoff"
title="Cutoff threshold"
src="/icons/scissor.svg";
}
}
}
fn render_critical_threshold_input() -> Markup {
// TODO: source threshold.cut value and replace below
html! {
div {
(PreEscaped("<!-- input for cutoff threshold -->"))
label id="cut" class="label-small font-near-black"><input id="cutInput" class="alert-input" name="cut" placeholder="0" type="text" title="Critical threshold value" value="{{ threshold.cut }}" { "MB" }
label class="label-small font-gray" for="cut" style="padding-top: 0.25rem;" { "CUTOFF THRESHOLD" }
}
}
}
fn render_critical_threshold_checkbox() -> Markup {
// threshold.cut_flag
let cutoff_enabled = true;
html! {
div {
(PreEscaped("<!-- checkbox for cutoff threshold flag -->"))
input id="cutCheck" name="cut_flag" title="Activate cutoff" type="checkbox" checked[cutoff_enabled];
}
}
}
fn render_buttons() -> Markup {
html! {
div id="buttonDiv" class="button-div" {
input id="updateAlerts" class="button button-primary center" title="Update" type="submit" value="Update";
a id="resetTotal" class="button button-warning center" href="/settings/network/wifi/usage/reset" title="Reset stored usage total to zero" { "Reset" }
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
}
}
}
/// WiFi data usage form template builder.
pub fn build_template(request: &Request) -> PreEscaped<String> {
let (flash_name, flash_msg) = request.retrieve_flash();
let wlan_data = Vnstat::get("wlan0");
// wlan_data.all_time_total
// wlan_data.all_time_total_unit
let form_template = html! {
(PreEscaped("<!-- NETWORK DATA ALERTS FORM -->"))
form id="wifiAlerts" action="/network/wifi/usage" class="card center" method="post" {
(render_data_usage_total_capsule())
(render_warning_threshold_icon())
(render_warning_threshold_input())
(render_warning_threshold_checkbox())
(render_critical_threshold_icon())
(render_critical_threshold_input())
(render_critical_threshold_checkbox())
(render_buttons())
}
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
};
}

View File

@ -0,0 +1,106 @@
use std::collections::HashMap;
use maud::{html, Markup, PreEscaped};
use peach_network::{network, network::AccessPoint};
use crate::{templates, utils::theme};
// ROUTE: /settings/network/wifi
/// Retrieve network state data required by the WiFi network list template.
fn get_network_state_data(ap: &str, wlan: &str) -> (String, String, HashMap<String, AccessPoint>) {
let ap_state = match network::state(ap) {
Ok(Some(state)) => state,
_ => "Interface unavailable".to_string(),
};
let wlan_ssid = match network::ssid(wlan) {
Ok(Some(ssid)) => ssid,
_ => "Not connected".to_string(),
};
let network_list = match network::all_networks(wlan) {
Ok(networks) => networks,
Err(_) => HashMap::new(),
};
(ap_state, wlan_ssid, network_list)
}
fn render_network_connected_elements(ssid: String) -> Markup {
let ap_detail_url = format!("/network/wifi?ssid={}", ssid);
html! {
a class="list-item link primary-bg" href=(ap_detail_url) {
img id="netStatus" class="icon icon-active icon-medium list-icon" src="/icons/wifi.svg" alt="WiFi online";
p class="list-text" { (ssid) }
label class="label-small list-label font-gray" for="netStatus" title="Status" { "Connected" }
}
}
}
fn render_network_available_elements(ssid: String, ap_state: String) -> Markup {
let ap_detail_url = format!("/network/wifi?ssid={}", ssid);
html! {
a class="list-item link light-bg" href=(ap_detail_url) {
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/icons/wifi.svg" alt="WiFi offline";
p class="list-text" { (ssid) }
label class="label-small list-label font-gray" for="netStatus" title="Status" { (ap_state) }
}
}
}
fn render_network_unavailable_elements(ssid: String, ap_state: String) -> Markup {
let ap_detail_url = format!("/network/wifi?ssid={}", ssid);
html! {
a class="list-item link" href=(ap_detail_url) {
img id="netStatus" class="icon icon-inactive icon-medium list-icon" src="/icons/wifi.svg" alt="WiFi offline";
p class="list-text" { (ssid) }
label class="label-small list-label font-gray" for="netStatus" title="Status" { (ap_state) }
}
}
}
/// WiFi network list template builder.
pub fn build_template() -> PreEscaped<String> {
let (ap_state, wlan_ssid, network_list) = get_network_state_data("ap0", "wlan0");
let list_template = html! {
div class="card center" {
div class="center list-container" {
ul class="list" {
@if ap_state == "up" {
li class="list-item light-bg warning-border" {
"Enable WiFi client mode to view saved and available networks."
}
} @else if network_list.is_empty() {
li class="list-item light-bg" {
"No saved or available networks found."
}
} @else {
@for (ssid, ap) in network_list {
li {
@if ssid == wlan_ssid {
(render_network_connected_elements(ssid))
} @else if ap.state == "Available" {
(render_network_available_elements(ssid, ap.state))
} @else {
(render_network_unavailable_elements(ssid, ap.state))
}
}
}
}
}
}
}
};
let body =
templates::nav::build_template(list_template, "WiFi Networks", Some("/settings/network"));
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}

View File

@ -0,0 +1,64 @@
use maud::{html, Markup, PreEscaped};
use peach_network::network;
use rouille::Request;
use crate::{
templates,
utils::{flash::FlashRequest, theme},
};
// ROUTE: /settings/network
/// Read the wireless interface mode (WiFi AP or client) and selectively render
/// the activation button for the deactivated mode.
fn render_mode_toggle_button() -> Markup {
match network::state("ap0") {
Ok(Some(state)) if state == "up" => {
html! {
a id="connectWifi" class="button button-primary center" href="/settings/network/wifi/activate" title="Enable WiFi" { "Enable WiFi" }
}
}
_ => html! {
a id="deployAccessPoint" class="button button-primary center" href="/settings/network/ap/activate" title="Deploy Access Point" { "Deploy Access Point" }
},
}
}
fn render_buttons() -> Markup {
html! {
(PreEscaped("<!-- BUTTONS -->"))
div id="buttons" {
a class="button button-primary center" href="/settings/network/wifi/add" title="Add WiFi Network" { "Add WiFi Network" }
a id="configureDNS" class="button button-primary center" href="/settings/network/dns" title="Configure DNS" { "Configure DNS" }
(PreEscaped("<!-- if ap is up, show 'Enable WiFi' button, else show 'Deplay Access Point' -->"))
(render_mode_toggle_button())
a id="listWifi" class="button button-primary center" href="/settings/network/wifi" title="List WiFi Networks" { "List WiFi Networks" }
// TODO: uncomment this once data usage feature is in place
// a id="viewUsage" class="button button-primary center" href="/settings/network/wifi/usage" title="View Data Usage" { "View Data Usage" }
a id="viewStatus" class="button button-primary center" href="/status/network" title="View Network Status" { "View Network Status" }
}
}
}
/// Network settings menu template builder.
pub fn build_template(request: &Request) -> PreEscaped<String> {
let (flash_name, flash_msg) = request.retrieve_flash();
let menu_template = html! {
(PreEscaped("<!-- NETWORK SETTINGS MENU -->"))
div class="card center" {
(render_buttons())
// render flash message if cookies were found in the request
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
};
let body = templates::nav::build_template(menu_template, "Network Settings", Some("/settings"));
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}

View File

@ -0,0 +1,8 @@
pub mod add_ap;
pub mod ap_details;
pub mod configure_dns;
// TODO: uncomment this once data usage feature is in place
// pub mod data_usage_limits;
pub mod list_aps;
pub mod menu;
pub mod modify_ap;

View File

@ -0,0 +1,104 @@
use maud::{html, Markup, PreEscaped};
use peach_network::network;
use rouille::{post_input, try_or_400, Request, Response};
use crate::{
templates,
utils::{
flash::{FlashRequest, FlashResponse},
theme,
},
};
// ROUTE: /settings/network/wifi/modify?<ssid>
fn render_ssid_input(selected_ap: Option<String>) -> Markup {
html! {
(PreEscaped("<!-- input for network ssid -->"))
input id="ssid" name="ssid" class="center input" type="text" placeholder="SSID" title="Network name (SSID) for WiFi access point" value=[selected_ap] autofocus;
}
}
fn render_password_input() -> Markup {
html! {
(PreEscaped("<!-- input for network password -->"))
input id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point";
}
}
fn render_buttons() -> Markup {
html! {
(PreEscaped("<!-- BUTTONS -->"))
div id="buttons" {
input id="savePassword" class="button button-primary center" title="Save" type="submit" value="Save";
a class="button button-secondary center" href="/settings/network" title="Cancel" { "Cancel" }
}
}
}
/// WiFi access point password modification form template builder.
pub fn build_template(request: &Request, selected_ap: Option<String>) -> PreEscaped<String> {
let (flash_name, flash_msg) = request.retrieve_flash();
let form_template = html! {
(PreEscaped("<!-- NETWORK MODIFY AP PASSWORD FORM -->"))
div class="card center" {
form id="wifiModify" action="/settings/network/wifi/modify" method="post" {
(render_ssid_input(selected_ap))
(render_password_input())
(render_buttons())
}
// render flash message if cookies were found in the request
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
};
let body = templates::nav::build_template(
form_template,
"Change WiFi Password",
Some("/settings/network"),
);
let theme = theme::get_theme();
templates::base::build_template(body, theme)
}
/// Parse the SSID and password for an access point and save the new password.
pub fn handle_form(request: &Request) -> Response {
let data = try_or_400!(post_input!(request, {
ssid: String,
pass: String,
}));
let (name, msg) = match network::id("wlan0", &data.ssid) {
Ok(Some(id)) => match network::modify(&id, &data.ssid, &data.pass) {
Ok(_) => ("success".to_string(), "WiFi password updated".to_string()),
Err(err) => (
"error".to_string(),
format!("Failed to update WiFi password: {}", err),
),
},
Ok(None) => (
"error".to_string(),
format!(
"Failed to update WiFi password: no saved credentials found for network {}",
&data.ssid
),
),
Err(err) => (
"error".to_string(),
format!(
"Failed to update WiFi password: no ID found for network {}: {}",
&data.ssid, err
),
),
};
let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg));
Response::redirect_303("/settings/network/wifi/modify").add_flash(flash_name, flash_msg)
}