diff --git a/Cargo.lock b/Cargo.lock index 9d40d22..8e78194 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,7 +175,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -210,7 +210,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -324,9 +324,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.44" +version = "1.2.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" dependencies = [ "find-msvc-tools", "shlex", @@ -388,7 +388,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -555,7 +555,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -566,7 +566,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -602,7 +602,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -675,7 +675,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -879,7 +879,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1065,7 +1065,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1100,9 +1100,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "iwdrs" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "979ecc8ed4efa88abca5d88a76c5e89935b4ed3ceaf7ccdb666a5672238d9fd3" +checksum = "0d514273c403cf58f6b97bd60af44c5dcb6ce2ef9e3e5b916b53710b07383629" dependencies = [ "futures-lite", "strum 0.27.2", @@ -1114,26 +1114,26 @@ dependencies = [ [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1148,9 +1148,9 @@ dependencies = [ [[package]] name = "kasuari" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a3d6645acdef96d256c1f9fd3be7ecfa60d8457520a50bbd1600b6053f8173" +checksum = "012b421f57b19b6dca64a11f6703087faa71aaef81c6e7cae57dcf7cdf286303" dependencies = [ "hashbrown 0.16.0", "portable-atomic", @@ -1344,7 +1344,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1467,7 +1467,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1520,7 +1520,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1610,9 +1610,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -1864,7 +1864,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -1875,7 +1875,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2002,7 +2002,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2014,7 +2014,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2030,9 +2030,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -2141,7 +2141,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2152,7 +2152,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2201,7 +2201,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2274,7 +2274,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2446,7 +2446,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", "wasm-bindgen-shared", ] @@ -2574,7 +2574,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2585,7 +2585,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", ] [[package]] @@ -2826,7 +2826,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", "zbus_names", "zvariant", "zvariant_utils", @@ -2867,7 +2867,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.110", "zvariant_utils", ] @@ -2880,6 +2880,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.108", + "syn 2.0.110", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 4c74ca8..7671173 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ serde = { version = "1", features = ["derive"] } toml = { version = "0.9" } clap = { version = "4", features = ["derive", "cargo"] } anyhow = "1" -iwdrs = "0.2" +iwdrs = "0.2.3" chrono = "0.4" log = "0.4" env_logger = "0.11" diff --git a/Release.md b/Release.md index d5fd82f..efc6ecc 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,9 @@ +## 0.5.0 - TBA + +- Support for WPA2 Entreprise +- Support for Eduroam +- Make help banner responsive + ## v0.4.1 - 2025-10-23 - Fix display issue on light mode background diff --git a/src/agent.rs b/src/agent.rs index ef1267d..2fbbcc6 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -17,6 +17,8 @@ pub struct AuthAgent { pub rx_username_password: Receiver<(String, String)>, pub psk_required: Arc, pub private_key_passphrase_required: Arc, + pub password_required: Arc, + pub username_and_password_required: Arc, pub event_sender: UnboundedSender, } @@ -35,6 +37,8 @@ impl AuthAgent { rx_username_password, psk_required: Arc::new(AtomicBool::new(false)), private_key_passphrase_required: Arc::new(AtomicBool::new(false)), + password_required: Arc::new(AtomicBool::new(false)), + username_and_password_required: Arc::new(AtomicBool::new(false)), event_sender: sender, } } @@ -67,11 +71,16 @@ impl Agent for AuthAgent { async fn request_private_key_passphrase( &self, - _network: &Network, + network: &Network, ) -> Result { self.private_key_passphrase_required .store(true, std::sync::atomic::Ordering::Relaxed); + let network_name = network.name().await.map_err(|_| Canceled())?; + self.event_sender + .send(Event::AuthReqKeyPassphrase(network_name)) + .map_err(|_| Canceled())?; + tokio::select! { r = self.rx_passphrase.recv() => { match r { @@ -87,18 +96,59 @@ impl Agent for AuthAgent { } } - fn request_user_name_and_passphrase( + async fn request_user_name_and_passphrase( &self, - _network: &Network, - ) -> impl Future> + Send { - std::future::ready(Err(Canceled())) + network: &Network, + ) -> Result<(String, String), iwdrs::error::agent::Canceled> { + self.username_and_password_required + .store(true, std::sync::atomic::Ordering::Relaxed); + let network_name = network.name().await.map_err(|_| Canceled())?; + self.event_sender + .send(Event::AuthReqUsernameAndPassword(network_name)) + .map_err(|_| Canceled())?; + + tokio::select! { + r = self.rx_username_password.recv() => { + match r { + Ok((username, password)) => Ok((username, password)), + Err(_) => Err(Canceled()), + } + } + + _ = self.rx_cancel.recv() => { + Err(Canceled()) + } + + } } - fn request_user_password( + async fn request_user_password( &self, - _network: &Network, - _user_name: Option<&String>, - ) -> impl Future> + Send { - std::future::ready(Err(Canceled())) + network: &Network, + user_name: Option<&String>, + ) -> Result { + self.password_required + .store(true, std::sync::atomic::Ordering::Relaxed); + let network_name = network.name().await.map_err(|_| Canceled())?; + self.event_sender + .send(Event::AuthRequestPassword(( + network_name, + user_name.cloned(), + ))) + .map_err(|_| Canceled())?; + + tokio::select! { + r = self.rx_passphrase.recv() => { + match r { + Ok(password) => Ok(password), + Err(_) => Err(Canceled()), + } + } + + _ = self.rx_cancel.recv() => { + Err(Canceled()) + } + + } } } diff --git a/src/app.rs b/src/app.rs index db15ff6..0290f74 100644 --- a/src/app.rs +++ b/src/app.rs @@ -26,6 +26,9 @@ pub enum FocusedBlock { AdapterInfos, AccessPointInput, AccessPointConnectedDevices, + RequestKeyPasshphrase, + RequestPassword, + RequestUsernameAndPassword, } #[derive(Debug)] diff --git a/src/device.rs b/src/device.rs index b8bd499..552700c 100644 --- a/src/device.rs +++ b/src/device.rs @@ -89,7 +89,7 @@ impl Device { match self.mode { Mode::Station => { if let Some(station) = &mut self.station { - if station.diagnostic.is_none() { + if station.diagnostic.is_none() && station.connected_network.is_some() { sender.send(Event::Reset(Mode::Station))?; } else { station.refresh().await?; diff --git a/src/event.rs b/src/event.rs index 579da7b..8bffc4e 100644 --- a/src/event.rs +++ b/src/event.rs @@ -16,6 +16,12 @@ pub enum Event { Notification(Notification), Reset(Mode), Auth(String), + EapNeworkConfigured(String), + ConfigureNewEapNetwork(String), + AuthRequestPassword((String, Option)), + AuthReqKeyPassphrase(String), + AuthReqUsernameAndPassword(String), + UsernameAndPasswordSubmit, } #[allow(dead_code)] diff --git a/src/handler.rs b/src/handler.rs index 7144036..d156e64 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -9,6 +9,7 @@ use crate::notification::Notification; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use iwdrs::modes::Mode; +use iwdrs::network::NetworkType; use tokio::sync::mpsc::UnboundedSender; use tui_input::backend::crossterm::EventHandler; @@ -18,6 +19,11 @@ async fn toggle_connect(app: &mut App, sender: UnboundedSender) -> AppRes FocusedBlock::NewNetworks => { if let Some(net_index) = station.new_networks_state.selected() { let (net, _) = station.new_networks[net_index].clone(); + + if net.network_type == NetworkType::Eap { + sender.send(Event::ConfigureNewEapNetwork(net.name.clone()))?; + return Ok(()); + } tokio::spawn(async move { net.connect(sender.clone()).await.unwrap(); }); @@ -205,9 +211,89 @@ pub async fn handle_key_events( } }, - FocusedBlock::WpaEntrepriseAuth => { - unimplemented!() + FocusedBlock::RequestKeyPasshphrase => { + if let Some(req) = &mut app.auth.request_key_passphrase { + match key_event.code { + KeyCode::Enter => { + req.submit(&app.agent).await?; + app.focused_block = FocusedBlock::KnownNetworks; + } + + KeyCode::Esc => { + req.cancel(&app.agent).await?; + app.auth.request_key_passphrase = None; + app.focused_block = FocusedBlock::KnownNetworks; + } + + KeyCode::Tab => { + req.show_password = !req.show_password; + } + + _ => { + req.passphrase + .handle_event(&crossterm::event::Event::Key(key_event)); + } + } + } } + FocusedBlock::RequestPassword => { + if let Some(req) = &mut app.auth.request_password { + match key_event.code { + KeyCode::Enter => { + req.submit(&app.agent).await?; + app.focused_block = FocusedBlock::KnownNetworks; + } + + KeyCode::Esc => { + req.cancel(&app.agent).await?; + app.auth.request_password = None; + app.focused_block = FocusedBlock::KnownNetworks; + } + + KeyCode::Tab => { + req.show_password = !req.show_password; + } + + _ => { + req.password + .handle_event(&crossterm::event::Event::Key(key_event)); + } + } + } + } + FocusedBlock::RequestUsernameAndPassword => { + if let Some(req) = &mut app.auth.request_username_and_password { + match key_event.code { + KeyCode::Enter => { + req.submit(&app.agent).await?; + app.focused_block = FocusedBlock::KnownNetworks; + } + + KeyCode::Esc => { + req.cancel(&app.agent).await?; + app.auth.request_username_and_password = None; + app.focused_block = FocusedBlock::KnownNetworks; + } + + _ => { + req.handle_key_events(key_event, sender).await?; + } + } + } + } + + FocusedBlock::WpaEntrepriseAuth => match key_event.code { + KeyCode::Esc => { + app.focused_block = FocusedBlock::NewNetworks; + app.auth.eap = None; + } + + _ => { + if let Some(eap) = &mut app.auth.eap { + eap.handle_key_events(key_event, sender).await? + } + } + }, FocusedBlock::AdapterInfos => { if key_event.code == KeyCode::Esc { app.focused_block = FocusedBlock::Device; @@ -344,7 +430,7 @@ pub async fn handle_key_events( toggle_connect(app, sender).await? } KeyCode::Char('j') | KeyCode::Down => { - if !station.known_networks.is_empty() { + if !station.new_networks.is_empty() { let i = match station.new_networks_state.selected() { Some(i) => { if i < station.new_networks.len() - 1 { diff --git a/src/main.rs b/src/main.rs index 7a6df5c..6963b1f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use impala::{ config::Config, event::{Event, EventHandler}, handler::handle_key_events, + notification::{Notification, NotificationLevel}, rfkill, tui::Tui, }; @@ -55,9 +56,48 @@ async fn main() -> AppResult<()> { } app = App::new(tui.events.sender.clone(), config.clone(), mode).await?; } + Event::Auth(network_name) => { app.network_name_requiring_auth = Some(network_name); } + + Event::EapNeworkConfigured(network_name) => { + app.auth.reset(); + app.focused_block = impala::app::FocusedBlock::KnownNetworks; + Notification::send( + format!("Network {} configured", network_name), + NotificationLevel::Info, + &tui.events.sender.clone(), + )?; + } + + Event::UsernameAndPasswordSubmit => { + if let Some(req) = &mut app.auth.request_username_and_password { + req.submit(&app.agent).await?; + app.focused_block = impala::app::FocusedBlock::KnownNetworks; + app.auth.request_username_and_password = None; + } + } + + Event::ConfigureNewEapNetwork(network_name) => { + app.auth.init_eap(network_name); + app.focused_block = impala::app::FocusedBlock::WpaEntrepriseAuth; + } + + Event::AuthReqKeyPassphrase(network_name) => { + app.auth.init_request_key_passphrase(network_name.clone()); + app.focused_block = impala::app::FocusedBlock::RequestKeyPasshphrase; + } + + Event::AuthRequestPassword((network_name, user_name)) => { + app.auth.init_request_password(network_name, user_name); + app.focused_block = impala::app::FocusedBlock::RequestPassword + } + + Event::AuthReqUsernameAndPassword(network_name) => { + app.auth.init_request_username_and_password(network_name); + app.focused_block = impala::app::FocusedBlock::RequestUsernameAndPassword + } _ => {} } } diff --git a/src/mode/station.rs b/src/mode/station.rs index 434a1b7..0233ff0 100644 --- a/src/mode/station.rs +++ b/src/mode/station.rs @@ -14,7 +14,7 @@ use ratatui::{ layout::{Constraint, Direction, Flex, Layout}, style::{Color, Style, Stylize}, text::{Line, Span}, - widgets::{Block, BorderType, Borders, Padding, Row, Table, TableState}, + widgets::{Block, BorderType, Borders, Padding, Paragraph, Row, Table, TableState}, }; use tokio::sync::mpsc::UnboundedSender; @@ -271,7 +271,7 @@ impl Station { Constraint::Min(5), Constraint::Min(5), Constraint::Length(5), - Constraint::Length(1), + Constraint::Length(2), ]) .margin(1) .split(frame.area()); @@ -613,7 +613,7 @@ impl Station { ); let help_message = match focused_block { - FocusedBlock::Device => Line::from(vec![ + FocusedBlock::Device => vec![Line::from(vec![ Span::from(config.station.start_scanning.to_string()).bold(), Span::from(" Scan"), Span::from(" | "), @@ -628,76 +628,160 @@ impl Station { Span::from(" | "), Span::from("⇄").bold(), Span::from(" Nav"), - ]), - FocusedBlock::KnownNetworks => Line::from(vec![ - Span::from("k,").bold(), - Span::from(" Up"), - Span::from(" | "), - Span::from("j,").bold(), - Span::from(" Down"), - Span::from(" | "), - Span::from(if config.station.toggle_connect == ' ' { - "󱁐 or ↵ ".to_string() + ])], + FocusedBlock::KnownNetworks => { + if frame.area().width <= 110 { + vec![ + Line::from(vec![ + Span::from(if config.station.toggle_connect == ' ' { + "󱁐 or ↵ ".to_string() + } else { + config.station.toggle_connect.to_string() + }) + .bold(), + Span::from(" Dis/connect"), + Span::from(" | "), + Span::from(config.station.known_network.remove.to_string()).bold(), + Span::from(" Remove"), + Span::from(" | "), + Span::from(config.station.known_network.toggle_autoconnect.to_string()) + .bold(), + Span::from(" Autoconnect"), + Span::from(" | "), + Span::from(config.station.start_scanning.to_string()).bold(), + Span::from(" Scan"), + ]), + Line::from(vec![ + Span::from("k,").bold(), + Span::from(" Up"), + Span::from(" | "), + Span::from("j,").bold(), + Span::from(" Down"), + Span::from(" | "), + Span::from("ctrl+r").bold(), + Span::from(" Switch Mode"), + Span::from(" | "), + Span::from("⇄").bold(), + Span::from(" Nav"), + ]), + ] } else { - config.station.toggle_connect.to_string() - }) - .bold(), - Span::from(" Dis/connect"), - Span::from(" | "), - Span::from(config.station.known_network.remove.to_string()).bold(), - Span::from(" Remove"), - Span::from(" | "), - Span::from(config.station.known_network.toggle_autoconnect.to_string()).bold(), - Span::from(" Autoconnect"), + vec![Line::from(vec![ + Span::from("k,").bold(), + Span::from(" Up"), + Span::from(" | "), + Span::from("j,").bold(), + Span::from(" Down"), + Span::from(" | "), + Span::from(if config.station.toggle_connect == ' ' { + "󱁐 or ↵ ".to_string() + } else { + config.station.toggle_connect.to_string() + }) + .bold(), + Span::from(" Dis/connect"), + Span::from(" | "), + Span::from(config.station.known_network.remove.to_string()).bold(), + Span::from(" Remove"), + Span::from(" | "), + Span::from(config.station.known_network.toggle_autoconnect.to_string()) + .bold(), + Span::from(" Autoconnect"), + Span::from(" | "), + Span::from(config.station.start_scanning.to_string()).bold(), + Span::from(" Scan"), + Span::from(" | "), + Span::from("ctrl+r").bold(), + Span::from(" Switch Mode"), + Span::from(" | "), + Span::from("⇄").bold(), + Span::from(" Nav"), + ])] + } + } + FocusedBlock::NewNetworks => { + if frame.area().width < 80 { + vec![ + Line::from(vec![ + Span::from("󱁐 or ↵ ").bold(), + Span::from(" Connect"), + Span::from(" | "), + Span::from(config.station.start_scanning.to_string()).bold(), + Span::from(" Scan"), + Span::from(" | "), + ]), + Line::from(vec![ + Span::from("k,").bold(), + Span::from(" Up"), + Span::from(" | "), + Span::from("j,").bold(), + Span::from(" Down"), + Span::from(" | "), + Span::from("ctrl+r").bold(), + Span::from(" Switch Mode"), + Span::from(" | "), + Span::from("⇄").bold(), + Span::from(" Nav"), + ]), + ] + } else { + vec![Line::from(vec![ + Span::from("k,").bold(), + Span::from(" Up"), + Span::from(" | "), + Span::from("j,").bold(), + Span::from(" Down"), + Span::from(" | "), + Span::from("󱁐 or ↵ ").bold(), + Span::from(" Connect"), + Span::from(" | "), + Span::from(config.station.start_scanning.to_string()).bold(), + Span::from(" Scan"), + Span::from(" | "), + Span::from("ctrl+r").bold(), + Span::from(" Switch Mode"), + Span::from(" | "), + Span::from("⇄").bold(), + Span::from(" Nav"), + ])] + } + } + FocusedBlock::AdapterInfos => { + vec![Line::from(vec![ + Span::from("󱊷 ").bold(), + Span::from(" Discard"), + ])] + } + FocusedBlock::PskAuthKey => vec![Line::from(vec![ + Span::from(" ↵ ").bold(), + Span::from(" Apply"), Span::from(" | "), - Span::from(config.station.start_scanning.to_string()).bold(), - Span::from(" Scan"), + Span::from("⇄").bold(), + Span::from(" Hide/Show password"), Span::from(" | "), Span::from("󱊷 ").bold(), Span::from(" Discard"), + ])], + FocusedBlock::WpaEntrepriseAuth => vec![Line::from(vec![ + Span::from(" ↵ ").bold(), + Span::from(" Apply"), Span::from(" | "), - Span::from("ctrl+r").bold(), - Span::from(" Switch Mode"), - Span::from(" | "), - Span::from("⇄").bold(), - Span::from(" Nav"), - ]), - FocusedBlock::NewNetworks => Line::from(vec![ - Span::from("k,").bold(), - Span::from(" Up"), - Span::from(" | "), - Span::from("j,").bold(), - Span::from(" Down"), - Span::from(" | "), - Span::from("󱁐 or ↵ ").bold(), - Span::from(" Connect"), - Span::from(" | "), - Span::from(config.station.start_scanning.to_string()).bold(), - Span::from(" Scan"), + Span::from("h,l,←,→").bold(), + Span::from(" Switch EAP"), Span::from(" | "), Span::from("󱊷 ").bold(), Span::from(" Discard"), Span::from(" | "), - Span::from("ctrl+r").bold(), - Span::from(" Switch Mode"), - Span::from(" | "), Span::from("⇄").bold(), Span::from(" Nav"), - ]), - FocusedBlock::AdapterInfos => { - Line::from(vec![Span::from("󱊷 ").bold(), Span::from(" Discard")]) - } - FocusedBlock::PskAuthKey => Line::from(vec![ - Span::from("⇄").bold(), - Span::from(" Hide/Show password"), - Span::from(" | "), + ])], + _ => vec![Line::from(vec![ Span::from("󱊷 ").bold(), Span::from(" Discard"), - ]), - _ => Line::from(""), + ])], }; - let help_message = help_message.centered().blue(); + let help_message = Paragraph::new(help_message).centered().blue(); frame.render_widget(help_message, help_block); } diff --git a/src/mode/station/auth.rs b/src/mode/station/auth.rs index 0436f65..5508efd 100644 --- a/src/mode/station/auth.rs +++ b/src/mode/station/auth.rs @@ -1,9 +1,45 @@ pub mod entreprise; pub mod psk; -use crate::mode::station::auth::psk::Psk; +use crate::mode::station::auth::{ + entreprise::{ + WPAEntreprise, + requests::{ + key_passphrase::RequestKeyPassphrase, password::RequestPassword, + username_and_password::RequestUsernameAndPassword, + }, + }, + psk::Psk, +}; #[derive(Debug, Default)] pub struct Auth { pub psk: Psk, + pub eap: Option, + pub request_key_passphrase: Option, + pub request_password: Option, + pub request_username_and_password: Option, +} + +impl Auth { + pub fn init_eap(&mut self, network_name: String) { + self.eap = Some(WPAEntreprise::new(network_name)); + } + + pub fn reset(&mut self) { + self.psk = Psk::default(); + self.eap = None; + } + + pub fn init_request_key_passphrase(&mut self, network_name: String) { + self.request_key_passphrase = Some(RequestKeyPassphrase::new(network_name)); + } + + pub fn init_request_password(&mut self, network_name: String, user_name: Option) { + self.request_password = Some(RequestPassword::new(network_name, user_name)); + } + + pub fn init_request_username_and_password(&mut self, network_name: String) { + self.request_username_and_password = Some(RequestUsernameAndPassword::new(network_name)); + } } diff --git a/src/mode/station/auth/entreprise.rs b/src/mode/station/auth/entreprise.rs index 0d966c1..e32a557 100644 --- a/src/mode/station/auth/entreprise.rs +++ b/src/mode/station/auth/entreprise.rs @@ -1,13 +1,482 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use tokio::sync::mpsc::UnboundedSender; + +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout, Margin}, + style::{Style, Stylize}, + text::Text, + widgets::{Block, Borders, Clear}, +}; + +use crate::{app::AppResult, event::Event}; + +pub mod eduroam; +pub mod peap; +pub mod pwd; +pub mod requests; +pub mod tls; +pub mod ttls; + +const ERROR_PADDING: &str = " "; + +fn pad_string(input: &str, length: usize) -> String { + let current_length = input.chars().count(); + if current_length >= length { + input.to_string() + } else { + format!("{: Self { + Self::new() + } +} +impl Eap { + pub fn new() -> Self { + Self::TLS(tls::TLS::new()) + } +} + +impl WPAEntreprise { + pub fn new(network_name: String) -> Self { + Self { + eap: Eap::new(), + network_name, + focused_section: FocusedSection::EapChoice, + } + } + pub async fn handle_key_events( + &mut self, + key_event: KeyEvent, + sender: UnboundedSender, + ) -> AppResult<()> { + match key_event.code { + KeyCode::Tab => match self.focused_section { + FocusedSection::EapChoice => { + self.focused_section = FocusedSection::Eap; + match &mut self.eap { + Eap::TLS(v) => { + v.focused_input = tls::FocusedInput::CaCert; + v.next(); + } + Eap::TTLS(v) => { + v.focused_input = ttls::FocusedInput::CaCert; + v.next(); + } + Eap::PEAP(v) => { + v.focused_input = peap::FocusedInput::CaCert; + v.next(); + } + Eap::PWD(v) => { + v.focused_input = pwd::FocusedInput::Identity; + v.next(); + } + Eap::Eduroam(v) => { + v.focused_input = eduroam::FocusedInput::Identity; + v.next(); + } + }; + } + FocusedSection::Eap => match &mut self.eap { + Eap::TLS(v) => match v.focused_input { + tls::FocusedInput::CaCert => { + v.focused_input = tls::FocusedInput::Identity; + v.next(); + } + tls::FocusedInput::Identity => { + v.focused_input = tls::FocusedInput::ClientCert; + v.next(); + } + tls::FocusedInput::ClientCert => { + v.focused_input = tls::FocusedInput::ClientKey; + v.next(); + } + tls::FocusedInput::ClientKey => { + v.focused_input = tls::FocusedInput::KeyPassphrase; + v.next(); + } + tls::FocusedInput::KeyPassphrase => { + self.focused_section = FocusedSection::Apply; + v.deselect(); + } + }, + Eap::TTLS(v) => match v.focused_input { + ttls::FocusedInput::CaCert => { + v.focused_input = ttls::FocusedInput::ServerDomainMask; + v.next(); + } + ttls::FocusedInput::ServerDomainMask => { + v.focused_input = ttls::FocusedInput::Identity; + v.next(); + } + ttls::FocusedInput::Identity => { + v.focused_input = ttls::FocusedInput::Phase2Identity; + v.next(); + } + ttls::FocusedInput::Phase2Identity => { + v.focused_input = ttls::FocusedInput::Phase2Password; + v.next(); + } + ttls::FocusedInput::Phase2Password => { + v.focused_input = ttls::FocusedInput::CaCert; + self.focused_section = FocusedSection::Apply; + v.next(); + } + }, + Eap::PEAP(v) => match v.focused_input { + peap::FocusedInput::CaCert => { + v.focused_input = peap::FocusedInput::ServerDomainMask; + v.next(); + } + peap::FocusedInput::ServerDomainMask => { + v.focused_input = peap::FocusedInput::Identity; + v.next(); + } + peap::FocusedInput::Identity => { + v.focused_input = peap::FocusedInput::Phase2Identity; + v.next(); + } + peap::FocusedInput::Phase2Identity => { + v.focused_input = peap::FocusedInput::Phase2Password; + v.next(); + } + peap::FocusedInput::Phase2Password => { + v.focused_input = peap::FocusedInput::CaCert; + self.focused_section = FocusedSection::Apply; + v.next(); + } + }, + Eap::PWD(v) => match v.focused_input { + pwd::FocusedInput::Identity => { + v.focused_input = pwd::FocusedInput::Password; + v.next(); + } + pwd::FocusedInput::Password => { + v.focused_input = pwd::FocusedInput::Identity; + self.focused_section = FocusedSection::Apply; + v.next(); + } + }, + Eap::Eduroam(v) => match v.focused_input { + eduroam::FocusedInput::Identity => { + v.focused_input = eduroam::FocusedInput::Phase2Identity; + v.next(); + } + eduroam::FocusedInput::Phase2Identity => { + v.focused_input = eduroam::FocusedInput::Phase2Password; + v.next(); + } + eduroam::FocusedInput::Phase2Password => { + v.focused_input = eduroam::FocusedInput::Identity; + self.focused_section = FocusedSection::Apply; + v.next(); + } + }, + }, + FocusedSection::Apply => self.focused_section = FocusedSection::EapChoice, + }, + KeyCode::BackTab => match self.focused_section { + FocusedSection::EapChoice => self.focused_section = FocusedSection::Apply, + FocusedSection::Eap => match &mut self.eap { + Eap::TLS(v) => match v.focused_input { + tls::FocusedInput::CaCert => { + self.focused_section = FocusedSection::EapChoice; + v.previous(); + } + tls::FocusedInput::Identity => { + v.focused_input = tls::FocusedInput::CaCert; + v.previous(); + } + tls::FocusedInput::ClientCert => { + v.focused_input = tls::FocusedInput::Identity; + v.previous(); + } + tls::FocusedInput::ClientKey => { + v.focused_input = tls::FocusedInput::ClientCert; + v.previous(); + } + tls::FocusedInput::KeyPassphrase => { + v.focused_input = tls::FocusedInput::ClientKey; + v.previous(); + } + }, + Eap::TTLS(v) => match v.focused_input { + ttls::FocusedInput::CaCert => { + self.focused_section = FocusedSection::EapChoice; + v.previous(); + } + ttls::FocusedInput::ServerDomainMask => { + v.focused_input = ttls::FocusedInput::CaCert; + v.previous(); + } + ttls::FocusedInput::Identity => { + v.focused_input = ttls::FocusedInput::ServerDomainMask; + v.previous(); + } + ttls::FocusedInput::Phase2Identity => { + v.focused_input = ttls::FocusedInput::Identity; + v.previous(); + } + ttls::FocusedInput::Phase2Password => { + v.focused_input = ttls::FocusedInput::Phase2Identity; + v.previous(); + } + }, + Eap::PEAP(v) => match v.focused_input { + peap::FocusedInput::CaCert => { + self.focused_section = FocusedSection::EapChoice; + v.previous(); + } + peap::FocusedInput::ServerDomainMask => { + v.focused_input = peap::FocusedInput::CaCert; + v.previous(); + } + peap::FocusedInput::Identity => { + v.focused_input = peap::FocusedInput::ServerDomainMask; + v.previous(); + } + peap::FocusedInput::Phase2Identity => { + v.focused_input = peap::FocusedInput::Identity; + v.previous(); + } + peap::FocusedInput::Phase2Password => { + v.focused_input = peap::FocusedInput::Phase2Identity; + v.previous(); + } + }, + Eap::PWD(v) => match v.focused_input { + pwd::FocusedInput::Identity => { + self.focused_section = FocusedSection::EapChoice; + v.previous(); + } + pwd::FocusedInput::Password => { + v.focused_input = pwd::FocusedInput::Identity; + v.previous(); + } + }, + Eap::Eduroam(v) => match v.focused_input { + eduroam::FocusedInput::Identity => { + self.focused_section = FocusedSection::EapChoice; + v.previous(); + } + eduroam::FocusedInput::Phase2Identity => { + v.focused_input = eduroam::FocusedInput::Identity; + v.previous(); + } + eduroam::FocusedInput::Phase2Password => { + v.focused_input = eduroam::FocusedInput::Phase2Identity; + v.previous(); + } + }, + }, + FocusedSection::Apply => match &mut self.eap { + Eap::TLS(v) => { + v.focused_input = tls::FocusedInput::KeyPassphrase; + self.focused_section = FocusedSection::Eap; + v.set_last(); + } + Eap::TTLS(v) => { + v.focused_input = ttls::FocusedInput::Phase2Password; + self.focused_section = FocusedSection::Eap; + v.set_last(); + } + Eap::PEAP(v) => { + v.focused_input = peap::FocusedInput::Phase2Password; + self.focused_section = FocusedSection::Eap; + v.set_last(); + } + Eap::PWD(v) => { + v.focused_input = pwd::FocusedInput::Password; + self.focused_section = FocusedSection::Eap; + v.set_last(); + } + Eap::Eduroam(v) => { + v.focused_input = eduroam::FocusedInput::Phase2Password; + self.focused_section = FocusedSection::Eap; + v.set_last(); + } + }, + }, + _ => match self.focused_section { + // TLS => TTLS => PEAP => PWD => Eduroam + FocusedSection::EapChoice => match key_event.code { + KeyCode::Char('l') | KeyCode::Right => match self.eap { + Eap::TLS(_) => self.eap = Eap::TTLS(ttls::TTLS::new()), + Eap::TTLS(_) => self.eap = Eap::PEAP(peap::PEAP::new()), + Eap::PEAP(_) => self.eap = Eap::PWD(pwd::PWD::new()), + Eap::PWD(_) => self.eap = Eap::Eduroam(eduroam::Eduroam::new()), + Eap::Eduroam(_) => self.eap = Eap::TLS(tls::TLS::new()), + }, + KeyCode::Char('h') | KeyCode::Left => match self.eap { + Eap::Eduroam(_) => self.eap = Eap::PWD(pwd::PWD::new()), + Eap::PWD(_) => self.eap = Eap::PEAP(peap::PEAP::new()), + Eap::PEAP(_) => self.eap = Eap::TTLS(ttls::TTLS::new()), + Eap::TTLS(_) => self.eap = Eap::TLS(tls::TLS::new()), + Eap::TLS(_) => self.eap = Eap::Eduroam(eduroam::Eduroam::new()), + }, + + _ => {} + }, + FocusedSection::Eap => match &mut self.eap { + Eap::TLS(v) => v.handle_key_events(key_event, sender).await?, + Eap::TTLS(v) => v.handle_key_events(key_event, sender).await?, + Eap::PEAP(v) => v.handle_key_events(key_event, sender).await?, + Eap::PWD(v) => v.handle_key_events(key_event, sender).await?, + Eap::Eduroam(v) => v.handle_key_events(key_event, sender).await?, + }, + + FocusedSection::Apply => { + if let KeyCode::Enter = key_event.code { + let result = match &mut self.eap { + Eap::TLS(v) => v.apply(self.network_name.as_str()), + Eap::TTLS(v) => v.apply(self.network_name.as_str()), + Eap::PEAP(v) => v.apply(self.network_name.as_str()), + Eap::PWD(v) => v.apply(self.network_name.as_str()), + Eap::Eduroam(v) => v.apply(), + }; + if result.is_ok() { + sender.send(Event::EapNeworkConfigured(self.network_name.clone()))?; + } + } + } + }, + } + Ok(()) + } + + pub fn render(&mut self, frame: &mut Frame) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(21), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(frame.area()); + + let block = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Max(80), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(layout[1])[1]; + + frame.render_widget(Clear, block); + + let (title_block, eap_choice_block, eap_block, apply_block) = { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(2), + Constraint::Length(1), // Title + Constraint::Length(2), + Constraint::Length(1), // Eap choice + Constraint::Length(1), + Constraint::Length(10), // Form + Constraint::Length(2), + Constraint::Length(1), // Submit + Constraint::Length(2), + ]) + .split(block); + + (chunks[1], chunks[3], chunks[5], chunks[7]) + }; + + frame.render_widget( + Block::default() + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Thick) + .border_style(Style::default().green()), + block, + ); + + let title = Text::from(format!("Configure the network {}", self.network_name)) + .centered() + .bold(); + frame.render_widget(title, title_block); + + let choice = match self.eap { + Eap::TTLS(_) => Text::from("< TTLS >").centered(), + Eap::PEAP(_) => Text::from("< PEAP >").centered(), + Eap::PWD(_) => Text::from("< PWD >").centered(), + Eap::TLS(_) => Text::from("< TLS >").centered(), + Eap::Eduroam(_) => Text::from("< Eduroam >").centered(), + }; + + let choice = if self.focused_section == FocusedSection::EapChoice { + choice.bold().green() + } else { + choice + }; + + frame.render_widget( + choice.centered(), + eap_choice_block.inner(Margin { + horizontal: 1, + vertical: 0, + }), + ); + + match &mut self.eap { + Eap::TLS(v) => { + v.render(frame, eap_block); + } + Eap::PWD(v) => { + v.render(frame, eap_block); + } + Eap::TTLS(v) => { + v.render(frame, eap_block); + } + Eap::PEAP(v) => { + v.render(frame, eap_block); + } + Eap::Eduroam(v) => { + v.render(frame, eap_block); + } + } + + let text = if self.focused_section == FocusedSection::Apply { + Text::from("APPLY").centered().green().bold() + } else { + Text::from("APPLY").centered() + }; + + frame.render_widget( + text, + apply_block.inner(Margin { + horizontal: 1, + vertical: 0, + }), + ); + } } diff --git a/src/mode/station/auth/entreprise/eduroam.rs b/src/mode/station/auth/entreprise/eduroam.rs new file mode 100644 index 0000000..7cf752d --- /dev/null +++ b/src/mode/station/auth/entreprise/eduroam.rs @@ -0,0 +1,249 @@ +use std::{fs::OpenOptions, io::Write}; + +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Stylize}, + text::{Line, Span}, + widgets::{HighlightSpacing, List, ListState}, +}; + +use tokio::sync::mpsc::UnboundedSender; +use tui_input::{Input, backend::crossterm::EventHandler}; + +use crate::{app::AppResult, event::Event, mode::station::auth::entreprise::ERROR_PADDING}; + +fn pad_string(input: &str, length: usize) -> String { + let current_length = input.chars().count(); + if current_length >= length { + input.to_string() + } else { + format!("{:, +} + +impl Eduroam { + pub fn new() -> Self { + Self::default() + } + + pub fn validate_identity(&mut self) { + self.identity.error = None; + if self.identity.field.value().is_empty() { + self.identity.error = Some("Required field.".to_string()); + } + } + + pub fn validate_phase2_identity(&mut self) { + self.phase2_identity.error = None; + if self.phase2_identity.field.value().is_empty() { + self.phase2_identity.error = Some("Required field.".to_string()); + } + } + pub fn validate_phase2_password(&mut self) { + self.phase2_password.error = None; + if self.phase2_password.field.value().is_empty() { + self.phase2_password.error = Some("Required field.".to_string()); + } + } + + pub fn validate(&mut self) -> AppResult<()> { + self.validate_identity(); + self.validate_phase2_identity(); + self.validate_phase2_password(); + if self.identity.error.is_some() + | self.phase2_identity.error.is_some() + | self.phase2_password.error.is_some() + { + return Err("Valdidation Error".into()); + } + Ok(()) + } + + pub fn next(&mut self) { + let i = match self.state.selected() { + Some(4) => None, + Some(i) => Some(i + 2), + None => Some(0), + }; + + self.state.select(i); + } + pub fn previous(&mut self) { + let i = match self.state.selected() { + Some(0) => None, + Some(i) => Some(i.saturating_sub(2)), + None => Some(4), + }; + + self.state.select(i); + } + + pub fn set_last(&mut self) { + self.state.select(Some(4)); + } + + pub fn selected(&self) -> bool { + self.state.selected().is_some() + } + + pub fn apply(&mut self) -> AppResult<()> { + self.validate()?; + let mut file = OpenOptions::new() + .write(true) + .read(true) + .create(true) + .truncate(true) + .open("/var/lib/iwd/eduroam.8021x")?; + let text = format!( + " +[Security] +EAP-Method=PEAP +EAP-Identity={} +EAP-PEAP-Phase2-Method=MSCHAPV2 +EAP-PEAP-Phase2-Identity={} +EAP-PEAP-Phase2-Password={} + +[Settings] +AutoConnect=true", + self.identity.field.value(), + self.phase2_identity.field.value(), + self.phase2_password.field.value(), + ); + + let text = text.trim_start(); + file.write_all(text.as_bytes())?; + + Ok(()) + } + + pub async fn handle_key_events( + &mut self, + key_event: KeyEvent, + _sender: UnboundedSender, + ) -> AppResult<()> { + match key_event.code { + KeyCode::Enter => { + let _ = self.validate(); + } + _ => match self.focused_input { + FocusedInput::Identity => { + self.identity + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::Phase2Identity => { + self.phase2_identity + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::Phase2Password => { + self.phase2_password + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + }, + } + Ok(()) + } + + pub fn render(&mut self, frame: &mut Frame, area: Rect) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(9), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(area); + + let block = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Percentage(80), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(layout[1])[1]; + + let items = [ + Line::from(vec![ + Span::from(pad_string(" Identity", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.identity.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.identity.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Phase2 Identity", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.phase2_identity.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.phase2_identity.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Phase2 Password", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.phase2_password.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.phase2_password.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + ]; + + let list = List::new(items) + .highlight_symbol("> ") + .highlight_spacing(HighlightSpacing::Always); + + frame.render_stateful_widget(list, block, &mut self.state); + } +} diff --git a/src/mode/station/auth/entreprise/peap.rs b/src/mode/station/auth/entreprise/peap.rs new file mode 100644 index 0000000..3d0bc49 --- /dev/null +++ b/src/mode/station/auth/entreprise/peap.rs @@ -0,0 +1,326 @@ +use std::{fs::OpenOptions, io::Write, path::Path}; + +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Stylize}, + text::{Line, Span}, + widgets::{HighlightSpacing, List, ListState}, +}; + +use tokio::sync::mpsc::UnboundedSender; +use tui_input::{Input, backend::crossterm::EventHandler}; + +use crate::{app::AppResult, event::Event, mode::station::auth::entreprise::ERROR_PADDING}; + +fn pad_string(input: &str, length: usize) -> String { + let current_length = input.chars().count(); + if current_length >= length { + input.to_string() + } else { + format!("{:, +} + +impl PEAP { + pub fn new() -> Self { + Self::default() + } + + pub fn validate_ca_cert(&mut self) { + self.ca_cert.error = None; + if self.ca_cert.field.value().is_empty() { + self.ca_cert.error = Some("Required field.".to_string()); + return; + } + let path = Path::new(self.ca_cert.field.value()); + + if !path.is_absolute() { + self.ca_cert.error = Some("The file path should be absolute.".to_string()); + return; + } + + if !path.exists() { + self.ca_cert.error = Some("The file does not exist.".to_string()); + } + } + + pub fn validate_server_domain_mask(&mut self) { + self.server_domain_mask.error = None; + if self.server_domain_mask.field.value().is_empty() { + self.server_domain_mask.error = Some("Required field.".to_string()); + } + } + pub fn validate_identity(&mut self) { + self.identity.error = None; + if self.identity.field.value().is_empty() { + self.identity.error = Some("Required field.".to_string()); + } + } + + pub fn validate_phase2_identity(&mut self) { + self.phase2_identity.error = None; + if self.phase2_identity.field.value().is_empty() { + self.phase2_identity.error = Some("Required field.".to_string()); + } + } + pub fn validate_phase2_password(&mut self) { + self.phase2_password.error = None; + if self.phase2_password.field.value().is_empty() { + self.phase2_password.error = Some("Required field.".to_string()); + } + } + + pub fn validate(&mut self) -> AppResult<()> { + self.validate_ca_cert(); + self.validate_server_domain_mask(); + self.validate_identity(); + self.validate_phase2_identity(); + self.validate_phase2_password(); + if self.ca_cert.error.is_some() + | self.identity.error.is_some() + | self.server_domain_mask.error.is_some() + | self.phase2_identity.error.is_some() + | self.phase2_password.error.is_some() + { + return Err("Valdidation Error".into()); + } + Ok(()) + } + + pub fn next(&mut self) { + let i = match self.state.selected() { + Some(8) => None, + Some(i) => Some(i + 2), + None => Some(0), + }; + + self.state.select(i); + } + pub fn previous(&mut self) { + let i = match self.state.selected() { + Some(0) => None, + Some(i) => Some(i.saturating_sub(2)), + None => Some(8), + }; + + self.state.select(i); + } + + pub fn set_last(&mut self) { + self.state.select(Some(8)); + } + + pub fn selected(&self) -> bool { + self.state.selected().is_some() + } + + pub fn apply(&mut self, network_name: &str) -> AppResult<()> { + self.validate()?; + let mut file = OpenOptions::new() + .write(true) + .read(true) + .create(true) + .truncate(true) + .open(format!("/var/lib/iwd/{}.8021x", network_name))?; + let text = format!( + " +[Security] +EAP-Method=PEAP +EAP-PEAP-CACert={} +EAP-Identity={} +EAP-PEAP-ServerDomainMask={} +EAP-PEAP-Phase2-Method=MSCHAPV2 +EAP-PEAP-Phase2-Identity={} +EAP-PEAP-Phase2-Password={} + +[Settings] +AutoConnect=true", + self.ca_cert.field.value(), + self.identity.field.value(), + self.server_domain_mask.field.value(), + self.phase2_identity.field.value(), + self.phase2_password.field.value(), + ); + + let text = text.trim_start(); + file.write_all(text.as_bytes())?; + + Ok(()) + } + + pub async fn handle_key_events( + &mut self, + key_event: KeyEvent, + _sender: UnboundedSender, + ) -> AppResult<()> { + match key_event.code { + KeyCode::Enter => { + let _ = self.validate(); + } + _ => match self.focused_input { + FocusedInput::CaCert => { + self.ca_cert + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::ServerDomainMask => { + self.server_domain_mask + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::Identity => { + self.identity + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::Phase2Identity => { + self.phase2_identity + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::Phase2Password => { + self.phase2_password + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + }, + } + Ok(()) + } + + pub fn render(&mut self, frame: &mut Frame, area: Rect) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(9), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(area); + + let block = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Percentage(80), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(layout[1])[1]; + + let items = [ + Line::from(vec![ + Span::from(pad_string(" CA Cert", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.ca_cert.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.ca_cert.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Server Domain Mask", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.server_domain_mask.field.value(), 50)) + .bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.server_domain_mask.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Identity", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.identity.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.identity.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Phase2 Identity", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.phase2_identity.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.phase2_identity.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Phase2 Password", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.phase2_password.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.phase2_password.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + ]; + + let list = List::new(items) + .highlight_symbol("> ") + .highlight_spacing(HighlightSpacing::Always); + + frame.render_stateful_widget(list, block, &mut self.state); + } +} diff --git a/src/mode/station/auth/entreprise/pwd.rs b/src/mode/station/auth/entreprise/pwd.rs new file mode 100644 index 0000000..d786f7c --- /dev/null +++ b/src/mode/station/auth/entreprise/pwd.rs @@ -0,0 +1,215 @@ +use std::{fs::OpenOptions, io::Write}; + +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Stylize}, + text::{Line, Span}, + widgets::{HighlightSpacing, List, ListState}, +}; + +use tokio::sync::mpsc::UnboundedSender; +use tui_input::{Input, backend::crossterm::EventHandler}; + +use crate::{app::AppResult, event::Event, mode::station::auth::entreprise::ERROR_PADDING}; + +fn pad_string(input: &str, length: usize) -> String { + let current_length = input.chars().count(); + if current_length >= length { + input.to_string() + } else { + format!("{:, +} + +impl PWD { + pub fn new() -> Self { + Self::default() + } + + pub fn validate_identity(&mut self) { + self.identity.error = None; + if self.identity.field.value().is_empty() { + self.identity.error = Some("Required field.".to_string()); + } + } + + pub fn validate_password(&mut self) { + self.password.error = None; + if self.password.field.value().is_empty() { + self.password.error = Some("Required field.".to_string()); + } + } + + pub fn validate(&mut self) -> AppResult<()> { + self.validate_identity(); + self.validate_password(); + if self.identity.error.is_some() | self.password.error.is_some() { + return Err("Valdidation Error".into()); + } + Ok(()) + } + + pub fn next(&mut self) { + let i = match self.state.selected() { + Some(2) => None, + Some(_) => Some(2), + None => Some(0), + }; + + self.state.select(i); + } + pub fn previous(&mut self) { + let i = match self.state.selected() { + Some(0) => None, + Some(_) => Some(0), + None => Some(2), + }; + + self.state.select(i); + } + + pub fn set_last(&mut self) { + self.state.select(Some(2)); + } + + pub fn selected(&self) -> bool { + self.state.selected().is_some() + } + + pub fn apply(&mut self, network_name: &str) -> AppResult<()> { + self.validate()?; + + let mut file = OpenOptions::new() + .write(true) + .read(true) + .create(true) + .truncate(true) + .open(format!("/var/lib/iwd/{}.8021x", network_name))?; + let text = format!( + " +[Security] +EAP-Method=PWD +EAP-Identity={} +EAP-Password={} + +[Settings] +AutoConnect=true", + self.identity.field.value(), + self.password.field.value(), + ); + + let text = text.trim_start(); + file.write_all(text.as_bytes())?; + + Ok(()) + } + + pub async fn handle_key_events( + &mut self, + key_event: KeyEvent, + _sender: UnboundedSender, + ) -> AppResult<()> { + match key_event.code { + KeyCode::Enter => { + let _ = self.validate(); + } + _ => match self.focused_input { + FocusedInput::Identity => { + self.identity + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::Password => { + self.password + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + }, + } + Ok(()) + } + + pub fn render(&mut self, frame: &mut Frame, area: Rect) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(9), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(area); + + let block = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Percentage(80), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(layout[1])[1]; + + let items = [ + Line::from(vec![ + Span::from(pad_string(" Identity", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.identity.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.identity.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Password", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.password.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.password.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + ]; + + let list = List::new(items) + .highlight_symbol("> ") + .highlight_spacing(HighlightSpacing::Always); + + frame.render_stateful_widget(list, block, &mut self.state); + } +} diff --git a/src/mode/station/auth/entreprise/requests.rs b/src/mode/station/auth/entreprise/requests.rs new file mode 100644 index 0000000..1472083 --- /dev/null +++ b/src/mode/station/auth/entreprise/requests.rs @@ -0,0 +1,3 @@ +pub mod key_passphrase; +pub mod password; +pub mod username_and_password; diff --git a/src/mode/station/auth/entreprise/requests/key_passphrase.rs b/src/mode/station/auth/entreprise/requests/key_passphrase.rs new file mode 100644 index 0000000..61a0228 --- /dev/null +++ b/src/mode/station/auth/entreprise/requests/key_passphrase.rs @@ -0,0 +1,142 @@ +use crate::{agent::AuthAgent, app::AppResult}; + +use ratatui::{ + Frame, + layout::{Alignment, Constraint, Direction, Layout}, + style::{Color, Style, Stylize}, + text::{Line, Span, Text}, + widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph}, +}; +use tui_input::Input; + +#[derive(Debug)] +pub struct RequestKeyPassphrase { + pub passphrase: Input, + pub show_password: bool, + network_name: String, +} + +impl RequestKeyPassphrase { + pub fn new(network_name: String) -> Self { + Self { + passphrase: Input::default(), + show_password: true, + network_name, + } + } + pub async fn submit(&mut self, agent: &AuthAgent) -> AppResult<()> { + let passkey: String = self.passphrase.value().into(); + agent.tx_passphrase.send(passkey).await?; + agent + .private_key_passphrase_required + .store(false, std::sync::atomic::Ordering::Relaxed); + self.passphrase.reset(); + Ok(()) + } + + pub async fn cancel(&mut self, agent: &AuthAgent) -> AppResult<()> { + agent.tx_cancel.send(()).await?; + agent + .private_key_passphrase_required + .store(false, std::sync::atomic::Ordering::Relaxed); + self.passphrase.reset(); + Ok(()) + } + pub fn render(&self, frame: &mut Frame) { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(8), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(frame.area()); + + let area = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Length(80), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(popup_layout[1])[1]; + + let (text_area, passkey_area, show_password_area) = { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(1), + Constraint::Length(1), + ]) + .split(area); + + let area1 = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Length(1), + Constraint::Fill(1), + Constraint::Length(1), + ]) + .flex(ratatui::layout::Flex::Center) + .split(chunks[1]); + + let area2 = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(20), + Constraint::Fill(1), + Constraint::Length(5), + Constraint::Percentage(20), + ]) + .flex(ratatui::layout::Flex::Center) + .split(chunks[2]); + + (area1[1], area2[1], area2[2]) + }; + + let text = Line::from(vec![ + Span::raw("Enter the key passphrase for "), + Span::from(&self.network_name).bold(), + ]); + + let text = Paragraph::new(text.centered()) + .alignment(Alignment::Center) + .style(Style::default().fg(Color::White)) + .block(Block::new().padding(Padding::uniform(1))); + + let passkey = Paragraph::new({ + if self.show_password { + self.passphrase.value().to_string() + } else { + "*".repeat(self.passphrase.value().len()) + } + }) + .alignment(Alignment::Center) + .style(Style::default().fg(Color::White)) + .block(Block::new().style(Style::default().bg(Color::DarkGray))); + + let show_password_icon = if self.show_password { + Text::from(" ").centered() + } else { + Text::from(" ").centered() + }; + + frame.render_widget(Clear, area); + + frame.render_widget( + Block::new() + .borders(Borders::ALL) + .border_type(BorderType::Thick) + .style(Style::default().green()) + .border_style(Style::default().fg(Color::Green)), + area, + ); + frame.render_widget(text, text_area); + frame.render_widget(passkey, passkey_area); + frame.render_widget(show_password_icon, show_password_area); + } +} diff --git a/src/mode/station/auth/entreprise/requests/password.rs b/src/mode/station/auth/entreprise/requests/password.rs new file mode 100644 index 0000000..a4d1d95 --- /dev/null +++ b/src/mode/station/auth/entreprise/requests/password.rs @@ -0,0 +1,159 @@ +use crate::{agent::AuthAgent, app::AppResult}; + +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout, Margin}, + style::{Color, Style, Stylize}, + text::{Line, Span, Text}, + widgets::{Block, BorderType, Borders, Clear, List}, +}; +use tui_input::Input; + +#[derive(Debug)] +pub struct RequestPassword { + pub password: Input, + pub show_password: bool, + network_name: String, + user_name: Option, +} + +impl RequestPassword { + pub fn new(network_name: String, user_name: Option) -> Self { + Self { + password: Input::default(), + show_password: true, + network_name, + user_name, + } + } + pub async fn submit(&mut self, agent: &AuthAgent) -> AppResult<()> { + let passkey: String = self.password.value().into(); + agent.tx_passphrase.send(passkey).await?; + agent + .password_required + .store(false, std::sync::atomic::Ordering::Relaxed); + self.password.reset(); + Ok(()) + } + + pub async fn cancel(&mut self, agent: &AuthAgent) -> AppResult<()> { + agent.tx_cancel.send(()).await?; + agent + .password_required + .store(false, std::sync::atomic::Ordering::Relaxed); + self.password.reset(); + Ok(()) + } + pub fn render(&self, frame: &mut Frame) { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(12), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(frame.area()); + + let area = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Length(80), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(popup_layout[1])[1]; + + let (title_area, form_area, show_password_area) = { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(5), Constraint::Length(5)]) + .split(area); + + let area2 = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(20), + Constraint::Fill(1), + Constraint::Length(5), + Constraint::Percentage(20), + ]) + .flex(ratatui::layout::Flex::Center) + .split(chunks[1]); + + (chunks[0], area2[1], area2[2]) + }; + + let title = Line::from(vec![ + Span::raw("Authentication Required for "), + Span::from(&self.network_name).bold(), + ]) + .centered(); + + let items = vec![ + { + if let Some(user_name) = &self.user_name { + Line::from(vec![ + Span::raw(" Username ").bold().bg(Color::DarkGray), + Span::from(" "), + Span::from(user_name), + ]) + } else { + Line::from("") + } + }, + Line::from(""), + Line::from(vec![ + Span::raw(" Password ").bold().bg(Color::DarkGray), + Span::from(" "), + Span::from({ + if self.show_password { + self.password.value().to_string() + } else { + "*".repeat(self.password.value().len()) + } + }), + ]), + ]; + + let form = List::new(items); + + let show_password_icon = if self.show_password { + Text::from("\n\n ").centered() + } else { + Text::from("\n\n ").centered() + }; + + frame.render_widget(Clear, area); + + frame.render_widget( + Block::new() + .borders(Borders::ALL) + .border_type(BorderType::Thick) + .border_style(Style::default().fg(Color::Green)), + area, + ); + frame.render_widget( + title, + title_area.inner(Margin { + horizontal: 1, + vertical: 2, + }), + ); + frame.render_widget( + form, + form_area.inner(Margin { + horizontal: 1, + vertical: 1, + }), + ); + frame.render_widget( + show_password_icon, + show_password_area.inner(Margin { + horizontal: 1, + vertical: 1, + }), + ); + } +} diff --git a/src/mode/station/auth/entreprise/requests/username_and_password.rs b/src/mode/station/auth/entreprise/requests/username_and_password.rs new file mode 100644 index 0000000..ff30548 --- /dev/null +++ b/src/mode/station/auth/entreprise/requests/username_and_password.rs @@ -0,0 +1,216 @@ +use crate::event::Event; +use crate::{agent::AuthAgent, app::AppResult}; +use tokio::sync::mpsc::UnboundedSender; + +use crossterm::event::{KeyCode, KeyEvent}; + +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout, Margin}, + style::{Color, Style, Stylize}, + text::{Line, Span, Text}, + widgets::{Block, BorderType, Borders, Clear, List}, +}; +use tui_input::Input; +use tui_input::backend::crossterm::EventHandler; + +#[derive(Debug, PartialEq)] +enum FocusedSection { + Username, + Password, + Submit, +} + +#[derive(Debug)] +pub struct RequestUsernameAndPassword { + pub password: Input, + pub username: Input, + pub show_password: bool, + focused_section: FocusedSection, + network_name: String, +} + +impl RequestUsernameAndPassword { + pub fn new(network_name: String) -> Self { + Self { + password: Input::default(), + username: Input::default(), + show_password: true, + focused_section: FocusedSection::Username, + network_name, + } + } + + pub async fn handle_key_events( + &mut self, + key_event: KeyEvent, + sender: UnboundedSender, + ) -> AppResult<()> { + match key_event.code { + KeyCode::Tab => match self.focused_section { + FocusedSection::Username => { + self.focused_section = FocusedSection::Password; + } + FocusedSection::Password => { + self.focused_section = FocusedSection::Submit; + } + FocusedSection::Submit => { + self.focused_section = FocusedSection::Username; + } + }, + KeyCode::BackTab => match self.focused_section { + FocusedSection::Username => { + self.focused_section = FocusedSection::Submit; + } + FocusedSection::Password => { + self.focused_section = FocusedSection::Username; + } + FocusedSection::Submit => { + self.focused_section = FocusedSection::Password; + } + }, + _ => match self.focused_section { + FocusedSection::Username => { + self.username + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedSection::Password => { + self.password + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedSection::Submit => { + sender.send(Event::UsernameAndPasswordSubmit)?; + } + }, + } + Ok(()) + } + pub async fn submit(&mut self, agent: &AuthAgent) -> AppResult<()> { + let username: String = self.username.value().into(); + let password: String = self.password.value().into(); + agent + .tx_username_password + .send((username, password)) + .await?; + agent + .username_and_password_required + .store(false, std::sync::atomic::Ordering::Relaxed); + Ok(()) + } + + pub async fn cancel(&mut self, agent: &AuthAgent) -> AppResult<()> { + agent.tx_cancel.send(()).await?; + agent + .username_and_password_required + .store(false, std::sync::atomic::Ordering::Relaxed); + self.username.reset(); + self.password.reset(); + Ok(()) + } + pub fn render(&self, frame: &mut Frame) { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(12), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(frame.area()); + + let area = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Length(80), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(popup_layout[1])[1]; + + let (title_area, form_area, show_password_area) = { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(5), Constraint::Length(5)]) + .split(area); + + let area2 = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(20), + Constraint::Fill(1), + Constraint::Length(5), + Constraint::Percentage(20), + ]) + .flex(ratatui::layout::Flex::Center) + .split(chunks[1]); + + (chunks[0], area2[1], area2[2]) + }; + + let title = Line::from(vec![ + Span::raw("Authentication Required for "), + Span::from(&self.network_name).bold(), + ]) + .centered(); + + let items = vec![ + Line::from(vec![ + Span::raw(" Username ").bold().bg(Color::DarkGray), + Span::from(" "), + Span::from(self.username.value()), + ]), + Line::from(""), + Line::from(vec![ + Span::raw(" Password ").bold().bg(Color::DarkGray), + Span::from(" "), + Span::from({ + if self.show_password { + self.password.value().to_string() + } else { + "*".repeat(self.password.value().len()) + } + }), + ]), + ]; + + let form = List::new(items); + + let show_password_icon = if self.show_password { + Text::from("\n\n ").centered() + } else { + Text::from("\n\n ").centered() + }; + + frame.render_widget(Clear, area); + + frame.render_widget( + Block::new() + .borders(Borders::ALL) + .border_type(BorderType::Thick) + .border_style(Style::default().fg(Color::Green)), + area, + ); + frame.render_widget( + title, + title_area.inner(Margin { + horizontal: 1, + vertical: 2, + }), + ); + frame.render_widget( + form, + form_area.inner(Margin { + horizontal: 1, + vertical: 1, + }), + ); + frame.render_widget( + show_password_icon, + show_password_area.inner(Margin { + horizontal: 1, + vertical: 1, + }), + ); + } +} diff --git a/src/mode/station/auth/entreprise/tls.rs b/src/mode/station/auth/entreprise/tls.rs new file mode 100644 index 0000000..7da8c90 --- /dev/null +++ b/src/mode/station/auth/entreprise/tls.rs @@ -0,0 +1,354 @@ +use std::{fs::OpenOptions, io::Write, path::Path}; + +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Stylize}, + text::{Line, Span}, + widgets::{HighlightSpacing, List, ListState}, +}; + +use tokio::sync::mpsc::UnboundedSender; +use tui_input::{Input, backend::crossterm::EventHandler}; + +use crate::{app::AppResult, event::Event, mode::station::auth::entreprise::ERROR_PADDING}; + +fn pad_string(input: &str, length: usize) -> String { + let current_length = input.chars().count(); + if current_length >= length { + input.to_string() + } else { + format!("{:, +} + +impl TLS { + pub fn new() -> Self { + Self::default() + } + + pub fn validate_ca_cert(&mut self) { + self.ca_cert.error = None; + if self.ca_cert.field.value().is_empty() { + self.ca_cert.error = Some("Required field.".to_string()); + return; + } + let path = Path::new(self.ca_cert.field.value()); + + if !path.is_absolute() { + self.ca_cert.error = Some("The file path should be absolute.".to_string()); + return; + } + + if !path.exists() { + self.ca_cert.error = Some("The file does not exist.".to_string()); + } + } + + pub fn validate_identity(&mut self) { + self.identity.error = None; + if self.identity.field.value().is_empty() { + self.identity.error = Some("Required field.".to_string()); + } + } + + pub fn validate_client_cert(&mut self) { + self.client_cert.error = None; + if self.client_cert.field.value().is_empty() { + self.client_cert.error = Some("Required field.".to_string()); + return; + } + + let path = Path::new(self.client_cert.field.value()); + + if !path.is_absolute() { + self.client_cert.error = Some("The file path should be absolute.".to_string()); + return; + } + + if !path.exists() { + self.client_cert.error = Some("The file does not exist.".to_string()); + } + } + pub fn validate_client_key(&mut self) { + self.client_key.error = None; + if self.client_key.field.value().is_empty() { + self.client_key.error = Some("Required field.".to_string()); + return; + } + + let path = Path::new(self.client_key.field.value()); + + if !path.is_absolute() { + self.client_key.error = Some("The file path should be absolute.".to_string()); + return; + } + + if !path.exists() { + self.client_key.error = Some("The file does not exist.".to_string()); + } + } + + pub fn validate(&mut self) -> AppResult<()> { + self.validate_ca_cert(); + self.validate_identity(); + self.validate_client_cert(); + self.validate_client_key(); + if self.ca_cert.error.is_some() + | self.identity.error.is_some() + | self.client_cert.error.is_some() + | self.client_key.error.is_some() + { + return Err("Valdidation Error".into()); + } + Ok(()) + } + + pub fn next(&mut self) { + let i = match self.state.selected() { + Some(8) => None, + Some(i) => Some(i + 2), + None => Some(0), + }; + + self.state.select(i); + } + pub fn previous(&mut self) { + let i = match self.state.selected() { + Some(0) => None, + Some(i) => Some(i.saturating_sub(2)), + None => Some(8), + }; + + self.state.select(i); + } + + pub fn set_last(&mut self) { + self.state.select(Some(8)); + } + + pub fn deselect(&mut self) { + self.state.select(None); + } + + pub fn selected(&self) -> bool { + self.state.selected().is_some() + } + + pub fn apply(&mut self, network_name: &str) -> AppResult<()> { + self.validate()?; + let mut file = OpenOptions::new() + .write(true) + .read(true) + .create(true) + .truncate(true) + .open(format!("/var/lib/iwd/{}.8021x", network_name))?; + let mut text = format!( + " +[Security] +EAP-Method=TLS +EAP-TLS-CACert={} +EAP-Identity={} +EAP-TLS-ClientCert={} +EAP-TLS-ClientKey={} +", + self.ca_cert.field.value(), + self.identity.field.value(), + self.client_cert.field.value(), + self.client_key.field.value(), + ); + + if !self.key_passphrase.field.value().is_empty() { + text.push_str( + format!( + "EAP-TLS-ClientKeyPassphrase={}", + self.key_passphrase.field.value() + ) + .as_str(), + ); + } + + text.push_str( + " + +[Settings] +AutoConnect=true", + ); + let text = text.trim_start(); + file.write_all(text.as_bytes())?; + + Ok(()) + } + + pub async fn handle_key_events( + &mut self, + key_event: KeyEvent, + _sender: UnboundedSender, + ) -> AppResult<()> { + match key_event.code { + KeyCode::Enter => {} + _ => match self.focused_input { + FocusedInput::CaCert => { + self.ca_cert + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::Identity => { + self.identity + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::ClientCert => { + self.client_cert + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::ClientKey => { + self.client_key + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::KeyPassphrase => { + self.key_passphrase + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + }, + } + Ok(()) + } + + pub fn render(&mut self, frame: &mut Frame, area: Rect) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(9), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(area); + + let block = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Max(70), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(layout[1])[1]; + + let items = [ + Line::from(vec![ + Span::from(pad_string(" CA Cert", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.ca_cert.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.ca_cert.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Identity", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.identity.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.identity.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Client Cert", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.client_cert.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.client_cert.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Client Key", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.client_key.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.client_key.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Key Passphrase", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.key_passphrase.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.key_passphrase.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + ]; + + let list = List::new(items) + .highlight_symbol("> ") + .highlight_spacing(HighlightSpacing::Always); + + frame.render_stateful_widget(list, block, &mut self.state); + } +} diff --git a/src/mode/station/auth/entreprise/ttls.rs b/src/mode/station/auth/entreprise/ttls.rs new file mode 100644 index 0000000..90a2e74 --- /dev/null +++ b/src/mode/station/auth/entreprise/ttls.rs @@ -0,0 +1,320 @@ +use std::{fs::OpenOptions, io::Write, path::Path}; + +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Stylize}, + text::{Line, Span}, + widgets::{HighlightSpacing, List, ListState}, +}; + +use tokio::sync::mpsc::UnboundedSender; +use tui_input::{Input, backend::crossterm::EventHandler}; + +use crate::{ + app::AppResult, + event::Event, + mode::station::auth::entreprise::{ERROR_PADDING, pad_string}, +}; + +#[derive(Debug, Clone, PartialEq, Default)] +pub enum FocusedInput { + #[default] + CaCert, + ServerDomainMask, + Identity, + Phase2Identity, + Phase2Password, +} + +#[derive(Debug, Clone, Default)] +pub struct TTLS { + ca_cert: UserInputField, + server_domain_mask: UserInputField, + identity: UserInputField, + phase2_identity: UserInputField, + phase2_password: UserInputField, + pub focused_input: FocusedInput, + state: ListState, +} + +#[derive(Debug, Clone, Default)] +struct UserInputField { + field: Input, + error: Option, +} + +impl TTLS { + pub fn new() -> Self { + Self::default() + } + + pub fn validate_ca_cert(&mut self) { + self.ca_cert.error = None; + if self.ca_cert.field.value().is_empty() { + self.ca_cert.error = Some("Required field.".to_string()); + return; + } + let path = Path::new(self.ca_cert.field.value()); + + if !path.is_absolute() { + self.ca_cert.error = Some("The file path should be absolute.".to_string()); + return; + } + + if !path.exists() { + self.ca_cert.error = Some("The file does not exist.".to_string()); + } + } + pub fn validate_server_domain_mask(&mut self) { + self.server_domain_mask.error = None; + if self.server_domain_mask.field.value().is_empty() { + self.server_domain_mask.error = Some("Required field.".to_string()); + } + } + pub fn validate_identity(&mut self) { + self.identity.error = None; + if self.identity.field.value().is_empty() { + self.identity.error = Some("Required field.".to_string()); + } + } + pub fn validate_phase2_identity(&mut self) { + self.identity.error = None; + if self.phase2_identity.field.value().is_empty() { + self.phase2_identity.error = Some("Required field.".to_string()); + } + } + + pub fn validate_phase2_password(&mut self) { + self.phase2_password.error = None; + if self.phase2_password.field.value().is_empty() { + self.phase2_password.error = Some("Required field.".to_string()); + } + } + + pub fn validate(&mut self) -> AppResult<()> { + self.validate_ca_cert(); + self.validate_server_domain_mask(); + self.validate_identity(); + self.validate_phase2_identity(); + self.validate_phase2_password(); + if self.ca_cert.error.is_some() + | self.identity.error.is_some() + | self.server_domain_mask.error.is_some() + | self.phase2_identity.error.is_some() + | self.phase2_password.error.is_some() + { + return Err("Valdidation Error".into()); + } + Ok(()) + } + + pub fn next(&mut self) { + let i = match self.state.selected() { + Some(8) => None, + Some(i) => Some(i + 2), + None => Some(0), + }; + + self.state.select(i); + } + pub fn previous(&mut self) { + let i = match self.state.selected() { + Some(0) => None, + Some(i) => Some(i.saturating_sub(2)), + None => Some(8), + }; + + self.state.select(i); + } + + pub fn set_last(&mut self) { + self.state.select(Some(8)); + } + + pub fn selected(&self) -> bool { + self.state.selected().is_some() + } + + pub fn apply(&mut self, network_name: &str) -> AppResult<()> { + self.validate()?; + let mut file = OpenOptions::new() + .write(true) + .read(true) + .create(true) + .truncate(true) + .open(format!("/var/lib/iwd/{}.8021x", network_name))?; + let text = format!( + " +[Security] +EAP-Method=TTLS +EAP-TTLS-CACert={} +EAP-Identity={} +EAP-TTLS-ServerDomainMask={} +EAP-TTLS-Phase2-Method=Tunneled-PAP +EAP-TTLS-Phase2-Identity={} +EAP-TTLS-Phase2-Password={} + +[Settings] +AutoConnect=true", + self.ca_cert.field.value(), + self.identity.field.value(), + self.server_domain_mask.field.value(), + self.phase2_identity.field.value(), + self.phase2_password.field.value(), + ); + + let text = text.trim_start(); + file.write_all(text.as_bytes())?; + + Ok(()) + } + + pub async fn handle_key_events( + &mut self, + key_event: KeyEvent, + _sender: UnboundedSender, + ) -> AppResult<()> { + match key_event.code { + KeyCode::Enter => { + let _ = self.validate(); + } + _ => match self.focused_input { + FocusedInput::CaCert => { + self.ca_cert + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::ServerDomainMask => { + self.server_domain_mask + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::Identity => { + self.identity + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::Phase2Identity => { + self.phase2_identity + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + FocusedInput::Phase2Password => { + self.phase2_password + .field + .handle_event(&crossterm::event::Event::Key(key_event)); + } + }, + } + Ok(()) + } + + pub fn render(&mut self, frame: &mut Frame, area: Rect) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(9), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(area); + + let block = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Percentage(80), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(layout[1])[1]; + + let items = [ + Line::from(vec![ + Span::from(pad_string(" CA Cert", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.ca_cert.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.ca_cert.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Server Domain Mask", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.server_domain_mask.field.value(), 50)) + .bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.server_domain_mask.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Identity", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.identity.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.identity.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Phase2 Identity", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.phase2_identity.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.phase2_identity.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + Line::from(vec![ + Span::from(pad_string(" Phase2 Password", 20)) + .bold() + .bg(Color::DarkGray), + Span::from(" "), + Span::from(pad_string(self.phase2_password.field.value(), 50)).bg(Color::DarkGray), + ]), + Line::from(vec![Span::from(ERROR_PADDING), { + if let Some(error) = &self.phase2_password.error { + Span::from(error) + } else { + Span::from("") + } + }]) + .red(), + ]; + + let list = List::new(items) + .highlight_symbol("> ") + .highlight_spacing(HighlightSpacing::Always); + + frame.render_stateful_widget(list, block, &mut self.state); + } +} diff --git a/src/ui.rs b/src/ui.rs index 9116677..9bdf5df 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -28,11 +28,16 @@ pub fn render(app: &mut App, frame: &mut Frame) { } }; + if app.focused_block == FocusedBlock::WpaEntrepriseAuth + && let Some(eap) = &mut app.auth.eap + { + eap.render(frame); + } + if app.focused_block == FocusedBlock::AdapterInfos { app.adapter.render(frame, app.device.address.clone()); } - // Auth Popup if app.agent.psk_required.load(Ordering::Relaxed) { app.focused_block = FocusedBlock::PskAuthKey; @@ -41,6 +46,30 @@ pub fn render(app: &mut App, frame: &mut Frame) { .render(frame, app.network_name_requiring_auth.clone()); } + if app + .agent + .private_key_passphrase_required + .load(Ordering::Relaxed) + && let Some(req) = &app.auth.request_key_passphrase + { + req.render(frame); + } + + if app.agent.password_required.load(Ordering::Relaxed) + && let Some(req) = &app.auth.request_password + { + req.render(frame); + } + + if app + .agent + .username_and_password_required + .load(Ordering::Relaxed) + && let Some(req) = &app.auth.request_username_and_password + { + req.render(frame); + } + // Notifications for (index, notification) in app.notifications.iter().enumerate() { notification.render(index, frame);