diff --git a/Cargo.lock b/Cargo.lock index f327027..99e1f25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/peach-web/Cargo.toml b/peach-web/Cargo.toml index 9379556..c3a34ab 100644 --- a/peach-web/Cargo.toml +++ b/peach-web/Cargo.toml @@ -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" diff --git a/peach-web/src/private_router.rs b/peach-web/src/private_router.rs index 5bc7665..248e375 100644 --- a/peach-web/src/private_router.rs +++ b/peach-web/src/private_router.rs @@ -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) }, diff --git a/peach-web/src/routes/settings/mod.rs b/peach-web/src/routes/settings/mod.rs index d910549..b864d03 100644 --- a/peach-web/src/routes/settings/mod.rs +++ b/peach-web/src/routes/settings/mod.rs @@ -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; diff --git a/peach-web/src/routes/settings/network.rs b/peach-web/src/routes/settings/network.rs deleted file mode 100644 index 0e67c15..0000000 --- a/peach-web/src/routes/settings/network.rs +++ /dev/null @@ -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 { - 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 = "")] -pub fn connect_wifi(network: Form, _auth: Authenticated) -> Flash { - 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 = "")] -pub fn disconnect_wifi(network: Form, _auth: Authenticated) -> Flash { - 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 = "")] -pub fn forget_wifi(network: Form, _auth: Authenticated) -> Flash { - 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?")] -pub fn wifi_password(ssid: &str, flash: Option, _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 = "")] -pub fn wifi_set_password(wifi: Form, _auth: Authenticated) -> Flash { - 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, _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 { - // 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, _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 - -#[get("/wifi?")] -pub fn network_detail(ssid: &str, flash: Option, _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 { - // 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, _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?")] -pub fn add_ssid(ssid: &str, flash: Option, _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 = "")] -pub fn add_credentials(wifi: Form, _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, _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 = "")] -pub fn wifi_usage_alerts(thresholds: Form, _auth: Authenticated) -> Flash { - 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", - ) - } - } -} diff --git a/peach-web/src/routes/settings/network/add_ap.rs b/peach-web/src/routes/settings/network/add_ap.rs new file mode 100644 index 0000000..1efc972 --- /dev/null +++ b/peach-web/src/routes/settings/network/add_ap.rs @@ -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) -> Markup { + html! { + (PreEscaped("")) + 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 id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point"; + } +} + +fn render_buttons() -> Markup { + html! { + (PreEscaped("")) + 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) -> PreEscaped { + let (flash_name, flash_msg) = request.retrieve_flash(); + + let form_template = html! { + (PreEscaped("")) + 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("")) + (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) +} diff --git a/peach-web/src/routes/settings/network/ap_details.rs b/peach-web/src/routes/settings/network/ap_details.rs new file mode 100644 index 0000000..6a67847 --- /dev/null +++ b/peach-web/src/routes/settings/network/ap_details.rs @@ -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? + +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("")) + 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) -> Markup { + let ap_signal_value = match ap_signal { + Some(signal) => signal.to_string(), + None => "Unknown".to_string(), + }; + + html! { + (PreEscaped("")) + 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("")) + 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("")) + 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("")) + 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, +) -> Markup { + html! { + (PreEscaped("")) + 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, NetworkError>, + Vec, + 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 { + 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("")) + 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("")) + div class=(network_info_box_class) title="PeachCloud network mode and status" { + (PreEscaped("")) + (render_network_status_icon(ssid, &wlan_ssid, &ap.state)) + (PreEscaped("")) + (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("")) + (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) +} diff --git a/peach-web/src/routes/settings/network/configure_dns.rs b/peach-web/src/routes/settings/network/configure_dns.rs new file mode 100644 index 0000000..a619234 --- /dev/null +++ b/peach-web/src/routes/settings/network/configure_dns.rs @@ -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("")) + 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("")) + 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("")) + 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("")) + 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 { + 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("")) + 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("")) + (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) +} diff --git a/peach-web/src/routes/settings/network/data_usage_limits.rs b/peach-web/src/routes/settings/network/data_usage_limits.rs new file mode 100644 index 0000000..efa5038 --- /dev/null +++ b/peach-web/src/routes/settings/network/data_usage_limits.rs @@ -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("")) + 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("")) + 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("")) + label id="cut" class="label-small font-near-black"> Markup { + // threshold.cut_flag + let cutoff_enabled = true; + + html! { + div { + (PreEscaped("")) + 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 { + 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("")) + 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("")) + (templates::flash::build_template(name, msg)) + } + }; +} diff --git a/peach-web/src/routes/settings/network/list_aps.rs b/peach-web/src/routes/settings/network/list_aps.rs new file mode 100644 index 0000000..9a78b0f --- /dev/null +++ b/peach-web/src/routes/settings/network/list_aps.rs @@ -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) { + 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 { + 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) +} diff --git a/peach-web/src/routes/settings/network/menu.rs b/peach-web/src/routes/settings/network/menu.rs new file mode 100644 index 0000000..e4ae934 --- /dev/null +++ b/peach-web/src/routes/settings/network/menu.rs @@ -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("")) + 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("")) + (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 { + let (flash_name, flash_msg) = request.retrieve_flash(); + + let menu_template = html! { + (PreEscaped("")) + 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("")) + (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) +} diff --git a/peach-web/src/routes/settings/network/mod.rs b/peach-web/src/routes/settings/network/mod.rs new file mode 100644 index 0000000..666bc39 --- /dev/null +++ b/peach-web/src/routes/settings/network/mod.rs @@ -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; diff --git a/peach-web/src/routes/settings/network/modify_ap.rs b/peach-web/src/routes/settings/network/modify_ap.rs new file mode 100644 index 0000000..8e7b6fd --- /dev/null +++ b/peach-web/src/routes/settings/network/modify_ap.rs @@ -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? + +fn render_ssid_input(selected_ap: Option) -> Markup { + html! { + (PreEscaped("")) + 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 id="pass" name="pass" class="center input" type="password" placeholder="Password" title="Password for WiFi access point"; + } +} + +fn render_buttons() -> Markup { + html! { + (PreEscaped("")) + 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) -> PreEscaped { + let (flash_name, flash_msg) = request.retrieve_flash(); + + let form_template = html! { + (PreEscaped("")) + 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("")) + (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) +}