peach-workspace/peach-web/src/routes/settings/network.rs

759 lines
27 KiB
Rust

use log::{debug, warn};
use rocket::{
form::{Form, FromForm},
get, post,
request::FlashMessage,
response::{Flash, Redirect},
serde::{Deserialize, Serialize},
uri, UriDisplayQuery,
};
use rocket_dyn_templates::Template;
use std::collections::HashMap;
use peach_lib::network_client;
use peach_lib::network_client::{AccessPoint, Networks, Scan};
use peach_lib::stats_client::Traffic;
use crate::routes::authentication::Authenticated;
use crate::utils::monitor;
use crate::utils::monitor::{Alert, Data, Threshold};
// STRUCTS USED BY NETWORK ROUTES
#[derive(Debug, Deserialize, FromForm, UriDisplayQuery)]
pub struct Ssid {
pub ssid: String,
}
#[derive(Debug, Deserialize, 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_client::id("wlan0", ssid) {
Ok(id) => match network_client::connect(&id, "wlan0") {
Ok(_) => Flash::success(Redirect::to(url), "Connected to chosen network"),
Err(_) => Flash::error(Redirect::to(url), "Failed to connect to chosen network"),
},
Err(_) => 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_client::disable("wlan0", 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_client::forget("wlan0", 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 = NetworkAddContext {
back: Some("/settings/network/wifi".to_string()),
flash_name: None,
flash_msg: None,
selected: Some(ssid.to_string()),
title: Some("Update WiFi Password".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/modify_ap", &context)
}
#[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_client::update("wlan0", 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
#[derive(Debug, Serialize)]
pub struct NetworkContext {
pub ap_ip: String,
pub ap_ssid: String,
pub ap_state: String,
pub ap_traffic: Option<Traffic>,
pub wlan_ip: String,
pub wlan_rssi: Option<String>,
pub wlan_scan: Option<Vec<Scan>>,
pub wlan_ssid: String,
pub wlan_state: String,
pub wlan_status: String,
pub wlan_traffic: Option<Traffic>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
// allows for passing in the ssid of a chosen access point
// this is used in the network_detail template
pub selected: Option<String>,
// page title for header in navbar
pub title: Option<String>,
// url for back-arrow link
pub back: Option<String>,
}
impl NetworkContext {
pub fn build() -> NetworkContext {
let ap_ip = match network_client::ip("ap0") {
Ok(ip) => ip,
Err(_) => "x.x.x.x".to_string(),
};
let ap_ssid = match network_client::ssid("ap0") {
Ok(ssid) => ssid,
Err(_) => "Not currently activated".to_string(),
};
let ap_state = match network_client::state("ap0") {
Ok(state) => state,
Err(_) => "Interface unavailable".to_string(),
};
let ap_traffic = match network_client::traffic("ap0") {
Ok(traffic) => {
let mut t = traffic;
// modify traffic values & assign measurement unit
// based on received and transmitted values
// if received > 999 MB, convert it to GB
if t.received > 1_047_527_424 {
t.received /= 1_073_741_824;
t.rx_unit = Some("GB".to_string());
} else if t.received > 0 {
// otherwise, convert it to MB
t.received = (t.received / 1024) / 1024;
t.rx_unit = Some("MB".to_string());
} else {
t.received = 0;
t.rx_unit = Some("MB".to_string());
}
if t.transmitted > 1_047_527_424 {
t.transmitted /= 1_073_741_824;
t.tx_unit = Some("GB".to_string());
} else if t.transmitted > 0 {
t.transmitted = (t.transmitted / 1024) / 1024;
t.tx_unit = Some("MB".to_string());
} else {
t.transmitted = 0;
t.tx_unit = Some("MB".to_string());
}
Some(t)
}
Err(_) => None,
};
let wlan_ip = match network_client::ip("wlan0") {
Ok(ip) => ip,
Err(_) => "x.x.x.x".to_string(),
};
let wlan_rssi = match network_client::rssi_percent("wlan0") {
Ok(rssi) => Some(rssi),
Err(_) => None,
};
let wlan_scan = match network_client::available_networks("wlan0") {
Ok(networks) => {
let scan: Vec<Scan> = serde_json::from_str(networks.as_str())
.expect("Failed to deserialize scan_networks response");
Some(scan)
}
Err(_) => None,
};
let wlan_ssid = match network_client::ssid("wlan0") {
Ok(ssid) => ssid,
Err(_) => "Not connected".to_string(),
};
let wlan_state = match network_client::state("wlan0") {
Ok(state) => state,
Err(_) => "Interface unavailable".to_string(),
};
let wlan_status = match network_client::status("wlan0") {
Ok(status) => status,
Err(_) => "Interface unavailable".to_string(),
};
let wlan_traffic = match network_client::traffic("wlan0") {
Ok(traffic) => {
let mut t = traffic;
// modify traffic values & assign measurement unit
// based on received and transmitted values
// if received > 999 MB, convert it to GB
if t.received > 1_047_527_424 {
t.received /= 1_073_741_824;
t.rx_unit = Some("GB".to_string());
} else if t.received > 0 {
// otherwise, convert it to MB
t.received = (t.received / 1024) / 1024;
t.rx_unit = Some("MB".to_string());
} else {
t.received = 0;
t.rx_unit = Some("MB".to_string());
}
if t.transmitted > 1_047_527_424 {
t.transmitted /= 1_073_741_824;
t.tx_unit = Some("GB".to_string());
} else if t.transmitted > 0 {
t.transmitted = (t.transmitted / 1024) / 1024;
t.tx_unit = Some("MB".to_string());
} else {
t.transmitted = 0;
t.tx_unit = Some("MB".to_string());
}
Some(t)
}
Err(_) => None,
};
NetworkContext {
ap_ip,
ap_ssid,
ap_state,
ap_traffic,
wlan_ip,
wlan_rssi,
wlan_scan,
wlan_ssid,
wlan_state,
wlan_status,
wlan_traffic,
flash_name: None,
flash_msg: None,
selected: None,
title: None,
back: None,
}
}
}
#[get("/")]
pub fn network_home(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
// assign context through context_builder call
let mut context = NetworkContext::build();
// set back button (nav) url
context.back = Some("/settings".to_string());
// set page title
context.title = Some("Network Configuration".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/menu", &context)
}
// 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_client::activate_ap() {
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
#[derive(Debug, Serialize)]
pub struct NetworkListContext {
pub ap_state: String,
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
pub wlan_networks: HashMap<String, String>,
pub wlan_ssid: String,
}
impl NetworkListContext {
pub fn build() -> NetworkListContext {
// list of networks saved in the wpa_supplicant.conf
let wlan_list = match network_client::saved_networks() {
Ok(ssids) => {
let networks: Vec<Networks> = serde_json::from_str(ssids.as_str())
.expect("Failed to deserialize scan_list response");
networks
}
Err(_) => Vec::new(),
};
// list of networks currently in range (online & accessible)
let wlan_scan = match network_client::available_networks("wlan0") {
Ok(networks) => {
let scan: Vec<Networks> = serde_json::from_str(networks.as_str())
.expect("Failed to deserialize scan_networks response");
scan
}
Err(_) => Vec::new(),
};
let wlan_ssid = match network_client::ssid("wlan0") {
Ok(ssid) => ssid,
Err(_) => "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.ssid)
.or_insert_with(|| "Not in range".to_string());
}
let ap_state = match network_client::state("ap0") {
Ok(state) => state,
Err(_) => "Interface unavailable".to_string(),
};
NetworkListContext {
ap_state,
back: None,
flash_msg: None,
flash_name: None,
title: None,
wlan_networks,
wlan_ssid,
}
}
}
#[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_dir is set in Rocket.toml
Template::render("settings/network/list_aps", &context)
}
// HELPERS AND ROUTES FOR /settings/network/wifi<ssid>
#[derive(Debug, Serialize)]
pub struct NetworkDetailContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub saved_aps: Vec<Networks>,
pub selected: Option<String>,
pub title: Option<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: String,
pub wlan_traffic: Option<Traffic>,
}
impl NetworkDetailContext {
pub fn build() -> NetworkDetailContext {
let wlan_ip = match network_client::ip("wlan0") {
Ok(ip) => ip,
Err(_) => "x.x.x.x".to_string(),
};
// list of networks saved in wpa_supplicant.conf
let wlan_list = match network_client::saved_networks() {
Ok(ssids) => {
let networks: Vec<Networks> = serde_json::from_str(ssids.as_str())
.expect("Failed to deserialize scan_list response");
networks
}
Err(_) => Vec::new(),
};
// list of networks saved in wpa_supplicant.conf
// HACK: we're running the same function twice (wlan_list)
// see if we can implement clone for Vec<Networks> instead
let saved_aps = match network_client::saved_networks() {
Ok(ssids) => {
let networks: Vec<Networks> = serde_json::from_str(ssids.as_str())
.expect("Failed to deserialize scan_list response");
networks
}
Err(_) => Vec::new(),
};
let wlan_rssi = match network_client::rssi_percent("wlan0") {
Ok(rssi) => Some(rssi),
Err(_) => None,
};
// list of networks currently in range (online & accessible)
let wlan_scan = match network_client::available_networks("wlan0") {
Ok(networks) => {
let scan: Vec<Scan> = serde_json::from_str(networks.as_str())
.expect("Failed to deserialize scan_networks response");
scan
}
Err(_) => Vec::new(),
};
let wlan_ssid = match network_client::ssid("wlan0") {
Ok(ssid) => ssid,
Err(_) => "Not connected".to_string(),
};
let wlan_state = match network_client::state("wlan0") {
Ok(state) => state,
Err(_) => "Interface unavailable".to_string(),
};
let wlan_status = match network_client::status("wlan0") {
Ok(status) => status,
Err(_) => "Interface unavailable".to_string(),
};
let wlan_traffic = match network_client::traffic("wlan0") {
Ok(traffic) => {
let mut t = traffic;
// modify traffic values & assign measurement unit
// based on received and transmitted values
// if received > 999 MB, convert it to GB
if t.received > 1_047_527_424 {
t.received /= 1_073_741_824;
t.rx_unit = Some("GB".to_string());
} else if t.received > 0 {
// otherwise, convert it to MB
t.received = (t.received / 1024) / 1024;
t.rx_unit = Some("MB".to_string());
} else {
t.received = 0;
t.rx_unit = Some("MB".to_string());
}
if t.transmitted > 1_047_527_424 {
t.transmitted /= 1_073_741_824;
t.tx_unit = Some("GB".to_string());
} else if t.transmitted > 0 {
t.transmitted = (t.transmitted / 1024) / 1024;
t.tx_unit = Some("MB".to_string());
} else {
t.transmitted = 0;
t.tx_unit = Some("MB".to_string());
}
Some(t)
}
Err(_) => None,
};
// 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.ssid) {
let ssid = network.ssid.clone();
let net_detail = AccessPoint {
detail: None,
state: "Not in range".to_string(),
signal: None,
};
wlan_networks.insert(ssid, net_detail);
}
}
NetworkDetailContext {
back: None,
flash_name: None,
flash_msg: None,
saved_aps,
selected: None,
title: None,
wlan_ip,
wlan_networks,
wlan_rssi,
wlan_ssid,
wlan_state,
wlan_status,
wlan_traffic,
}
}
}
#[get("/wifi?<ssid>")]
pub fn network_detail(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
// assign context through context_builder call
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());
// 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/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_client::activate_client() {
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 = NetworkContext::build();
// set back icon link to network route
context.back = Some("/settings/network".to_string());
context.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.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/add_ap", &context)
}
// used in /settings/network/wifi/add?<ssid>
#[derive(Debug, Serialize)]
pub struct NetworkAddContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub selected: Option<String>,
pub title: Option<String>,
}
impl NetworkAddContext {
pub fn build() -> NetworkAddContext {
NetworkAddContext {
back: None,
flash_name: None,
flash_msg: None,
selected: None,
title: None,
}
}
}
#[get("/wifi/add?<ssid>")]
pub fn add_ssid(ssid: &str, flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
let mut context = NetworkAddContext::build();
context.back = Some("/settings/network/wifi".to_string());
context.selected = Some(ssid.to_string());
context.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.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/add_ap", &context)
}
#[post("/wifi/add", data = "<wifi>")]
pub fn add_credentials(wifi: Form<WiFi>, _auth: Authenticated) -> Template {
// 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_client::saved_ap(&wifi.ssid).unwrap_or(false);
if creds_exist {
let mut context = NetworkAddContext::build();
context.back = Some("/settings/network".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg =
Some("Network credentials already exist for this access point".to_string());
context.title = Some("Add WiFi Network".to_string());
// return early from handler with "creds already exist" message
return Template::render("settings/network/add_ap", &context);
};
// if credentials not found, generate and write wifi config to wpa_supplicant
match network_client::add(&wifi.ssid, &wifi.pass) {
Ok(_) => {
debug!("Added WiFi credentials.");
// force reread of wpa_supplicant.conf file with new credentials
match network_client::reconfigure() {
Ok(_) => debug!("Successfully reconfigured wpa_supplicant"),
Err(_) => warn!("Failed to reconfigure wpa_supplicant"),
}
let mut context = NetworkAddContext::build();
context.back = Some("/settings/network".to_string());
context.flash_name = Some("success".to_string());
context.flash_msg = Some("Added WiFi credentials".to_string());
context.title = Some("Add WiFi Network".to_string());
Template::render("settings/network/add_ap", &context)
}
Err(_) => {
debug!("Failed to add WiFi credentials.");
let mut context = NetworkAddContext::build();
context.back = Some("/settings/network".to_string());
context.flash_name = Some("error".to_string());
context.flash_msg = Some("Failed to add WiFi credentials".to_string());
context.title = Some("Add WiFi Network".to_string());
Template::render("settings/network/add_ap", &context)
}
}
}
// HELPERS AND ROUTES FOR WIFI USAGE
#[derive(Debug, Serialize)]
pub struct NetworkAlertContext {
pub alert: Alert,
pub back: Option<String>,
pub data_total: Data, // combined stored and current wifi traffic in bytes
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub threshold: Threshold,
pub title: Option<String>,
pub traffic: Traffic, // current wifi traffic in bytes (since boot)
}
impl NetworkAlertContext {
pub fn build() -> NetworkAlertContext {
let alert = monitor::get_alerts().unwrap();
// stored wifi data values as bytes
let stored_traffic = monitor::get_data().unwrap();
let threshold = monitor::get_thresholds().unwrap();
// current wifi traffic values as bytes
let traffic = match network_client::traffic("wlan0") {
Ok(t) => t,
Err(_) => Traffic {
received: 0,
transmitted: 0,
rx_unit: None,
tx_unit: None,
},
};
let current_traffic = traffic.received + traffic.transmitted;
let total = stored_traffic.total + current_traffic;
let data_total = Data { total };
NetworkAlertContext {
alert,
back: None,
data_total,
flash_name: None,
flash_msg: None,
threshold,
title: None,
traffic,
}
}
}
#[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",
)
}
}
}