From e3b118b227622135a415041a10a246a0c8e6fa98 Mon Sep 17 00:00:00 2001 From: Badr Date: Fri, 7 Nov 2025 18:21:40 +0100 Subject: [PATCH 01/11] Support WPA entreprise --- Cargo.lock | 4 +- Cargo.toml | 2 +- src/agent.rs | 41 +- src/app.rs | 2 + src/device.rs | 2 +- src/event.rs | 4 + src/handler.rs | 75 +++- src/main.rs | 21 + src/mode/station.rs | 145 ++++-- src/mode/station/auth.rs | 29 +- src/mode/station/auth/entreprise.rs | 422 +++++++++++++++++- src/mode/station/auth/entreprise/pap.rs | 253 +++++++++++ src/mode/station/auth/entreprise/peap.rs | 288 ++++++++++++ src/mode/station/auth/entreprise/pwd.rs | 190 ++++++++ src/mode/station/auth/entreprise/requests.rs | 2 + .../entreprise/requests/key_passphrase.rs | 142 ++++++ .../auth/entreprise/requests/password.rs | 159 +++++++ .../requests/username_and_password.rs | 0 src/mode/station/auth/entreprise/tls.rs | 353 +++++++++++++++ src/ui.rs | 22 +- 20 files changed, 2093 insertions(+), 63 deletions(-) create mode 100644 src/mode/station/auth/entreprise/pap.rs create mode 100644 src/mode/station/auth/entreprise/peap.rs create mode 100644 src/mode/station/auth/entreprise/pwd.rs create mode 100644 src/mode/station/auth/entreprise/requests.rs create mode 100644 src/mode/station/auth/entreprise/requests/key_passphrase.rs create mode 100644 src/mode/station/auth/entreprise/requests/password.rs create mode 100644 src/mode/station/auth/entreprise/requests/username_and_password.rs create mode 100644 src/mode/station/auth/entreprise/tls.rs diff --git a/Cargo.lock b/Cargo.lock index 9d40d22..1323852 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", 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/src/agent.rs b/src/agent.rs index ef1267d..bf3037f 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -17,6 +17,7 @@ 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 event_sender: UnboundedSender, } @@ -35,6 +36,7 @@ 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)), event_sender: sender, } } @@ -67,11 +69,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 { @@ -94,11 +101,33 @@ impl Agent for AuthAgent { std::future::ready(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..f4599b4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -26,6 +26,8 @@ pub enum FocusedBlock { AdapterInfos, AccessPointInput, AccessPointConnectedDevices, + RequestKeyPasshphrase, + RequestPassword, } #[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..4094a71 100644 --- a/src/event.rs +++ b/src/event.rs @@ -16,6 +16,10 @@ pub enum Event { Notification(Notification), Reset(Mode), Auth(String), + EapNeworkConfigured, + ConfigureNewEapNetwork(String), + AuthRequestPassword((String, Option)), + AuthReqKeyPassphrase(String), } #[allow(dead_code)] diff --git a/src/handler.rs b/src/handler.rs index 7144036..fec4bab 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,72 @@ 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::WpaEntrepriseAuth => match key_event.code { + KeyCode::Esc => { + app.focused_block = FocusedBlock::NewNetworks; + app.auth.reset(); + } + + _ => { + app.auth + .eap + .as_mut() + .unwrap() + .handle_key_events(key_event, sender) + .await? + } + }, FocusedBlock::AdapterInfos => { if key_event.code == KeyCode::Esc { app.focused_block = FocusedBlock::Device; @@ -344,7 +413,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..b749be7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,9 +55,30 @@ 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 => { + app.auth.reset(); + app.focused_block = impala::app::FocusedBlock::KnownNetworks; + } + + 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 + } _ => {} } } diff --git a/src/mode/station.rs b/src/mode/station.rs index 434a1b7..0ca191a 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,41 +628,84 @@ 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 <= 120 { + 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("󱊷 ").bold(), + Span::from(" Discard"), + 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"), - Span::from(" | "), - Span::from(config.station.start_scanning.to_string()).bold(), - Span::from(" Scan"), - 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::NewNetworks => Line::from(vec![ + 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("󱊷 ").bold(), + Span::from(" Discard"), + Span::from(" | "), + Span::from("ctrl+r").bold(), + Span::from(" Switch Mode"), + Span::from(" | "), + Span::from("⇄").bold(), + Span::from(" Nav"), + ])] + } + } + FocusedBlock::NewNetworks => vec![Line::from(vec![ Span::from("k,").bold(), Span::from(" Up"), Span::from(" | "), @@ -683,21 +726,37 @@ impl Station { Span::from(" | "), Span::from("⇄").bold(), Span::from(" Nav"), - ]), + ])], FocusedBlock::AdapterInfos => { - Line::from(vec![Span::from("󱊷 ").bold(), Span::from(" Discard")]) + vec![Line::from(vec![ + Span::from("󱊷 ").bold(), + Span::from(" Discard"), + ])] } - FocusedBlock::PskAuthKey => Line::from(vec![ + FocusedBlock::PskAuthKey => vec![Line::from(vec![ Span::from("⇄").bold(), Span::from(" Hide/Show password"), Span::from(" | "), Span::from("󱊷 ").bold(), Span::from(" Discard"), - ]), - _ => Line::from(""), + ])], + FocusedBlock::WpaEntrepriseAuth => vec![Line::from(vec![ + Span::from("h,l,←,→").bold(), + Span::from(" Switch EAP"), + Span::from(" | "), + Span::from(" ↵ ").bold(), + Span::from(" Connect"), + Span::from(" | "), + Span::from("󱊷 ").bold(), + Span::from(" Discard"), + Span::from(" | "), + Span::from("⇄").bold(), + Span::from(" Nav"), + ])], + _ => vec![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..8cb4400 100644 --- a/src/mode/station/auth.rs +++ b/src/mode/station/auth.rs @@ -1,9 +1,36 @@ 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}, + }, + psk::Psk, +}; #[derive(Debug, Default)] pub struct Auth { pub psk: Psk, + pub eap: Option, + pub request_key_passphrase: Option, + pub request_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)); + } } diff --git a/src/mode/station/auth/entreprise.rs b/src/mode/station/auth/entreprise.rs index 0d966c1..18fbfdb 100644 --- a/src/mode/station/auth/entreprise.rs +++ b/src/mode/station/auth/entreprise.rs @@ -1,13 +1,425 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use tokio::sync::mpsc::UnboundedSender; + +use ratatui::{ + Frame, + layout::{Constraint, Direction, Flex, Layout, Margin}, + style::{Style, Stylize}, + text::Text, + widgets::{Block, Borders, Clear}, +}; + +use crate::{app::AppResult, event::Event}; + +pub mod pap; +pub mod peap; +pub mod pwd; +pub mod requests; +pub mod tls; + +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::PAP(v) => { + v.focused_input = pap::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(); + } + }; + } + 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::PAP(v) => match v.focused_input { + pap::FocusedInput::CaCert => { + v.focused_input = pap::FocusedInput::ServerDomainMask; + v.next(); + } + pap::FocusedInput::ServerDomainMask => { + v.focused_input = pap::FocusedInput::Identity; + v.next(); + } + pap::FocusedInput::Identity => { + v.focused_input = pap::FocusedInput::Password; + v.next(); + } + pap::FocusedInput::Password => { + v.focused_input = pap::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(); + } + }, + }, + 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::PAP(v) => match v.focused_input { + pap::FocusedInput::CaCert => { + self.focused_section = FocusedSection::EapChoice; + v.previous(); + } + pap::FocusedInput::ServerDomainMask => { + v.focused_input = pap::FocusedInput::CaCert; + v.previous(); + } + pap::FocusedInput::Identity => { + v.focused_input = pap::FocusedInput::ServerDomainMask; + v.previous(); + } + pap::FocusedInput::Password => { + v.focused_input = pap::FocusedInput::Identity; + 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(); + } + }, + }, + FocusedSection::Apply => match &mut self.eap { + Eap::TLS(v) => { + v.focused_input = tls::FocusedInput::KeyPassphrase; + self.focused_section = FocusedSection::Eap; + v.set_last(); + } + Eap::PAP(v) => { + v.focused_input = pap::FocusedInput::Password; + 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(); + } + }, + }, + _ => match self.focused_section { + FocusedSection::EapChoice => match key_event.code { + KeyCode::Char('l') | KeyCode::Right => match self.eap { + Eap::PAP(_) => self.eap = Eap::PEAP(peap::PEAP::new()), + Eap::PEAP(_) => self.eap = Eap::PWD(pwd::PWD::new()), + Eap::PWD(_) => self.eap = Eap::TLS(tls::TLS::new()), + Eap::TLS(_) => self.eap = Eap::PAP(pap::PAP::new()), + }, + KeyCode::Char('h') | KeyCode::Left => {} + _ => {} + }, + FocusedSection::Eap => match &mut self.eap { + Eap::TLS(v) => v.handle_key_events(key_event, sender).await?, + Eap::PAP(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?, + }, + + 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::PAP(v) => v.apply(), + Eap::PEAP(v) => v.apply(), + Eap::PWD(v) => v.apply(), + }; + if result.is_ok() { + sender.send(Event::EapNeworkConfigured)?; + } + } + } + }, + } + Ok(()) + } + + pub fn render(&mut self, frame: &mut Frame) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(20), + Constraint::Fill(1), + ]) + .flex(ratatui::layout::Flex::SpaceBetween) + .split(frame.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]; + + frame.render_widget(Clear, block); + + let (eap_choice_block, eap_block, apply_block) = { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(5), + Constraint::Length(10), + Constraint::Length(4), + ]) + .flex(Flex::SpaceBetween) + .split(block); + + (chunks[0], chunks[1], chunks[2]) + }; + + frame.render_widget( + Block::default() + .title(format!(" Configure {} Network ", self.network_name)) + .title_alignment(ratatui::layout::Alignment::Center) + .title_style(Style::default().bold()) + .borders(Borders::ALL) + .border_type(ratatui::widgets::BorderType::Thick) + .border_style(Style::default().green()), + block, + ); + + let choice = match self.eap { + Eap::PAP(_) => Text::from(" < PAP >"), + Eap::PEAP(_) => Text::from(" < PEAP >"), + Eap::PWD(_) => Text::from(" < PWD >"), + Eap::TLS(_) => Text::from(" < TLS >"), + }; + + 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: 2, + }), + ); + + match &mut self.eap { + Eap::TLS(v) => { + // if self.focused_section == FocusedSection::Eap && !v.selected() { + // v.next(); + // } + v.render(frame, eap_block); + } + Eap::PWD(v) => { + // if self.focused_section == FocusedSection::Eap && !v.selected() { + // v.next(); + // } + v.render(frame, eap_block); + } + Eap::PAP(v) => { + // if self.focused_section == FocusedSection::Eap && !v.selected() { + // v.next(); + // } + v.render(frame, eap_block); + } + Eap::PEAP(v) => { + // if self.focused_section == FocusedSection::Eap && !v.selected() { + // v.next(); + // } + v.render(frame, eap_block); + } + } + + // Apply Block + let text = if self.focused_section == FocusedSection::Apply { + Text::from("Apply").bold().green().centered() + } else { + Text::from("Apply").centered() + }; + + frame.render_widget( + text, + apply_block.inner(Margin { + horizontal: 1, + vertical: 1, + }), + ); + } } diff --git a/src/mode/station/auth/entreprise/pap.rs b/src/mode/station/auth/entreprise/pap.rs new file mode 100644 index 0000000..77e9ad0 --- /dev/null +++ b/src/mode/station/auth/entreprise/pap.rs @@ -0,0 +1,253 @@ +use std::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, + Password, +} + +#[derive(Debug, Clone, Default)] +pub struct PAP { + ca_cert: UserInputField, + server_domain_mask: UserInputField, + identity: UserInputField, + password: UserInputField, + pub focused_input: FocusedInput, + state: ListState, +} + +#[derive(Debug, Clone, Default)] +struct UserInputField { + field: Input, + error: Option, +} + +impl PAP { + 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()); + } + if !Path::new(self.ca_cert.field.value()).exists() { + self.ca_cert.error = Some("The CA 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_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_ca_cert(); + self.validate_server_domain_mask(); + self.validate_identity(); + self.validate_password(); + if self.ca_cert.error.is_some() + | self.identity.error.is_some() + | self.server_domain_mask.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(6) => 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(6), + }; + + self.state.select(i); + } + + pub fn set_last(&mut self) { + self.state.select(Some(6)); + } + + pub fn selected(&self) -> bool { + self.state.selected().is_some() + } + + pub fn apply(&mut self) -> AppResult<()> { + self.validate()?; + 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::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)); + } + FocusedInput::ServerDomainMask => { + self.server_domain_mask + .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(" 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/peap.rs b/src/mode/station/auth/entreprise/peap.rs new file mode 100644 index 0000000..ebaf70d --- /dev/null +++ b/src/mode/station/auth/entreprise/peap.rs @@ -0,0 +1,288 @@ +use std::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()); + } + if !Path::new(self.ca_cert.field.value()).exists() { + self.ca_cert.error = Some("The CA 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) -> AppResult<()> { + self.validate()?; + 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..349b4cf --- /dev/null +++ b/src/mode/station/auth/entreprise/pwd.rs @@ -0,0 +1,190 @@ +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) -> AppResult<()> { + self.validate()?; + 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..ca738dc --- /dev/null +++ b/src/mode/station/auth/entreprise/requests.rs @@ -0,0 +1,2 @@ +pub mod key_passphrase; +pub mod 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..e69de29 diff --git a/src/mode/station/auth/entreprise/tls.rs b/src/mode/station/auth/entreprise/tls.rs new file mode 100644 index 0000000..cc21287 --- /dev/null +++ b/src/mode/station/auth/entreprise/tls.rs @@ -0,0 +1,353 @@ +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) + .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::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(" 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/ui.rs b/src/ui.rs index 9116677..9a7252a 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,21 @@ 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); + } + // Notifications for (index, notification) in app.notifications.iter().enumerate() { notification.render(index, frame); From 492cbd49468e6a4a87f3b313e5d7a996d572d4c6 Mon Sep 17 00:00:00 2001 From: Badr Date: Fri, 7 Nov 2025 22:57:45 +0100 Subject: [PATCH 02/11] handle request from agent for username and password --- src/agent.rs | 29 ++- src/app.rs | 1 + src/event.rs | 2 + src/handler.rs | 31 ++- src/main.rs | 13 ++ src/mode/station/auth.rs | 11 +- src/mode/station/auth/entreprise/requests.rs | 1 + .../requests/username_and_password.rs | 216 ++++++++++++++++++ src/ui.rs | 9 + 9 files changed, 301 insertions(+), 12 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index bf3037f..2fbbcc6 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -18,6 +18,7 @@ pub struct AuthAgent { pub psk_required: Arc, pub private_key_passphrase_required: Arc, pub password_required: Arc, + pub username_and_password_required: Arc, pub event_sender: UnboundedSender, } @@ -37,6 +38,7 @@ impl AuthAgent { 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, } } @@ -94,11 +96,30 @@ 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()) + } + + } } async fn request_user_password( diff --git a/src/app.rs b/src/app.rs index f4599b4..0290f74 100644 --- a/src/app.rs +++ b/src/app.rs @@ -28,6 +28,7 @@ pub enum FocusedBlock { AccessPointConnectedDevices, RequestKeyPasshphrase, RequestPassword, + RequestUsernameAndPassword, } #[derive(Debug)] diff --git a/src/event.rs b/src/event.rs index 4094a71..ce0a043 100644 --- a/src/event.rs +++ b/src/event.rs @@ -20,6 +20,8 @@ pub enum Event { ConfigureNewEapNetwork(String), AuthRequestPassword((String, Option)), AuthReqKeyPassphrase(String), + AuthReqUsernameAndPassword(String), + UsernameAndPasswordSubmit, } #[allow(dead_code)] diff --git a/src/handler.rs b/src/handler.rs index fec4bab..d156e64 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -261,20 +261,37 @@ pub async fn handle_key_events( } } } + 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.reset(); + app.auth.eap = None; } _ => { - app.auth - .eap - .as_mut() - .unwrap() - .handle_key_events(key_event, sender) - .await? + if let Some(eap) = &mut app.auth.eap { + eap.handle_key_events(key_event, sender).await? + } } }, FocusedBlock::AdapterInfos => { diff --git a/src/main.rs b/src/main.rs index b749be7..4b698af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,6 +65,14 @@ async fn main() -> AppResult<()> { app.focused_block = impala::app::FocusedBlock::KnownNetworks; } + 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; @@ -79,6 +87,11 @@ async fn main() -> AppResult<()> { 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/auth.rs b/src/mode/station/auth.rs index 8cb4400..5508efd 100644 --- a/src/mode/station/auth.rs +++ b/src/mode/station/auth.rs @@ -4,7 +4,10 @@ pub mod psk; use crate::mode::station::auth::{ entreprise::{ WPAEntreprise, - requests::{key_passphrase::RequestKeyPassphrase, password::RequestPassword}, + requests::{ + key_passphrase::RequestKeyPassphrase, password::RequestPassword, + username_and_password::RequestUsernameAndPassword, + }, }, psk::Psk, }; @@ -15,6 +18,7 @@ pub struct Auth { pub eap: Option, pub request_key_passphrase: Option, pub request_password: Option, + pub request_username_and_password: Option, } impl Auth { @@ -30,7 +34,12 @@ impl Auth { 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/requests.rs b/src/mode/station/auth/entreprise/requests.rs index ca738dc..1472083 100644 --- a/src/mode/station/auth/entreprise/requests.rs +++ b/src/mode/station/auth/entreprise/requests.rs @@ -1,2 +1,3 @@ pub mod key_passphrase; pub mod password; +pub mod username_and_password; diff --git a/src/mode/station/auth/entreprise/requests/username_and_password.rs b/src/mode/station/auth/entreprise/requests/username_and_password.rs index e69de29..ff30548 100644 --- a/src/mode/station/auth/entreprise/requests/username_and_password.rs +++ 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/ui.rs b/src/ui.rs index 9a7252a..9bdf5df 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -61,6 +61,15 @@ pub fn render(app: &mut App, frame: &mut Frame) { 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); From 29d028666a29e0d6e5bce3f8c3a462af1aa7fb0e Mon Sep 17 00:00:00 2001 From: Badr Date: Fri, 7 Nov 2025 23:22:04 +0100 Subject: [PATCH 03/11] support EAP-PWD --- src/mode/station/auth/entreprise.rs | 2 +- src/mode/station/auth/entreprise/pwd.rs | 26 ++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/mode/station/auth/entreprise.rs b/src/mode/station/auth/entreprise.rs index 18fbfdb..d77511b 100644 --- a/src/mode/station/auth/entreprise.rs +++ b/src/mode/station/auth/entreprise.rs @@ -299,7 +299,7 @@ impl WPAEntreprise { Eap::TLS(v) => v.apply(self.network_name.as_str()), Eap::PAP(v) => v.apply(), Eap::PEAP(v) => v.apply(), - Eap::PWD(v) => v.apply(), + Eap::PWD(v) => v.apply(self.network_name.as_str()), }; if result.is_ok() { sender.send(Event::EapNeworkConfigured)?; diff --git a/src/mode/station/auth/entreprise/pwd.rs b/src/mode/station/auth/entreprise/pwd.rs index 349b4cf..98a0132 100644 --- a/src/mode/station/auth/entreprise/pwd.rs +++ b/src/mode/station/auth/entreprise/pwd.rs @@ -1,3 +1,5 @@ +use std::{fs::OpenOptions, io::Write}; + use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ Frame, @@ -97,8 +99,30 @@ impl PWD { self.state.selected().is_some() } - pub fn apply(&mut self) -> AppResult<()> { + pub fn apply(&mut self, network_name: &str) -> AppResult<()> { self.validate()?; + + let mut file = OpenOptions::new() + .write(true) + .read(true) + .create(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(()) } From 776d79031268a17aed9d577a6969fbb49b3ed8b2 Mon Sep 17 00:00:00 2001 From: Badr Date: Fri, 7 Nov 2025 23:44:42 +0100 Subject: [PATCH 04/11] support PEAP --- src/mode/station/auth/entreprise.rs | 2 +- src/mode/station/auth/entreprise/peap.rs | 45 +++++++++++++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/mode/station/auth/entreprise.rs b/src/mode/station/auth/entreprise.rs index d77511b..23cec5e 100644 --- a/src/mode/station/auth/entreprise.rs +++ b/src/mode/station/auth/entreprise.rs @@ -298,7 +298,7 @@ impl WPAEntreprise { let result = match &mut self.eap { Eap::TLS(v) => v.apply(self.network_name.as_str()), Eap::PAP(v) => v.apply(), - Eap::PEAP(v) => v.apply(), + Eap::PEAP(v) => v.apply(self.network_name.as_str()), Eap::PWD(v) => v.apply(self.network_name.as_str()), }; if result.is_ok() { diff --git a/src/mode/station/auth/entreprise/peap.rs b/src/mode/station/auth/entreprise/peap.rs index ebaf70d..0d3ece0 100644 --- a/src/mode/station/auth/entreprise/peap.rs +++ b/src/mode/station/auth/entreprise/peap.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{fs::OpenOptions, io::Write, path::Path}; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ @@ -59,11 +59,20 @@ impl PEAP { self.ca_cert.error = None; if self.ca_cert.field.value().is_empty() { self.ca_cert.error = Some("Required field.".to_string()); + return; } - if !Path::new(self.ca_cert.field.value()).exists() { - self.ca_cert.error = Some("The CA file does not exist.".to_string()); + 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() { @@ -134,8 +143,36 @@ impl PEAP { self.state.selected().is_some() } - pub fn apply(&mut self) -> AppResult<()> { + pub fn apply(&mut self, network_name: &str) -> AppResult<()> { self.validate()?; + let mut file = OpenOptions::new() + .write(true) + .read(true) + .create(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(()) } From 499a55b208a6698be7b8112d47d9dfbd9bfa499a Mon Sep 17 00:00:00 2001 From: Badr Date: Sat, 8 Nov 2025 14:22:43 +0100 Subject: [PATCH 05/11] Support for TTLS --- src/mode/station/auth/entreprise.rs | 79 ++++++------ .../auth/entreprise/{pap.rs => ttls.rs} | 114 ++++++++++++++---- 2 files changed, 127 insertions(+), 66 deletions(-) rename src/mode/station/auth/entreprise/{pap.rs => ttls.rs} (68%) diff --git a/src/mode/station/auth/entreprise.rs b/src/mode/station/auth/entreprise.rs index 23cec5e..9f0a2c9 100644 --- a/src/mode/station/auth/entreprise.rs +++ b/src/mode/station/auth/entreprise.rs @@ -11,11 +11,11 @@ use ratatui::{ use crate::{app::AppResult, event::Event}; -pub mod pap; pub mod peap; pub mod pwd; pub mod requests; pub mod tls; +pub mod ttls; const ERROR_PADDING: &str = " "; @@ -44,7 +44,7 @@ pub struct WPAEntreprise { #[derive(Debug)] pub enum Eap { - PAP(pap::PAP), + TTLS(ttls::TTLS), PEAP(peap::PEAP), PWD(pwd::PWD), TLS(tls::TLS), @@ -83,8 +83,8 @@ impl WPAEntreprise { v.focused_input = tls::FocusedInput::CaCert; v.next(); } - Eap::PAP(v) => { - v.focused_input = pap::FocusedInput::CaCert; + Eap::TTLS(v) => { + v.focused_input = ttls::FocusedInput::CaCert; v.next(); } Eap::PEAP(v) => { @@ -120,21 +120,25 @@ impl WPAEntreprise { v.deselect(); } }, - Eap::PAP(v) => match v.focused_input { - pap::FocusedInput::CaCert => { - v.focused_input = pap::FocusedInput::ServerDomainMask; + Eap::TTLS(v) => match v.focused_input { + ttls::FocusedInput::CaCert => { + v.focused_input = ttls::FocusedInput::ServerDomainMask; v.next(); } - pap::FocusedInput::ServerDomainMask => { - v.focused_input = pap::FocusedInput::Identity; + ttls::FocusedInput::ServerDomainMask => { + v.focused_input = ttls::FocusedInput::Identity; v.next(); } - pap::FocusedInput::Identity => { - v.focused_input = pap::FocusedInput::Password; + ttls::FocusedInput::Identity => { + v.focused_input = ttls::FocusedInput::Phase2Identity; v.next(); } - pap::FocusedInput::Password => { - v.focused_input = pap::FocusedInput::CaCert; + 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(); } @@ -201,21 +205,25 @@ impl WPAEntreprise { v.previous(); } }, - Eap::PAP(v) => match v.focused_input { - pap::FocusedInput::CaCert => { + Eap::TTLS(v) => match v.focused_input { + ttls::FocusedInput::CaCert => { self.focused_section = FocusedSection::EapChoice; v.previous(); } - pap::FocusedInput::ServerDomainMask => { - v.focused_input = pap::FocusedInput::CaCert; + ttls::FocusedInput::ServerDomainMask => { + v.focused_input = ttls::FocusedInput::CaCert; + v.previous(); + } + ttls::FocusedInput::Identity => { + v.focused_input = ttls::FocusedInput::ServerDomainMask; v.previous(); } - pap::FocusedInput::Identity => { - v.focused_input = pap::FocusedInput::ServerDomainMask; + ttls::FocusedInput::Phase2Identity => { + v.focused_input = ttls::FocusedInput::Identity; v.previous(); } - pap::FocusedInput::Password => { - v.focused_input = pap::FocusedInput::Identity; + ttls::FocusedInput::Phase2Password => { + v.focused_input = ttls::FocusedInput::Phase2Identity; v.previous(); } }, @@ -258,8 +266,8 @@ impl WPAEntreprise { self.focused_section = FocusedSection::Eap; v.set_last(); } - Eap::PAP(v) => { - v.focused_input = pap::FocusedInput::Password; + Eap::TTLS(v) => { + v.focused_input = ttls::FocusedInput::Phase2Password; self.focused_section = FocusedSection::Eap; v.set_last(); } @@ -278,17 +286,17 @@ impl WPAEntreprise { _ => match self.focused_section { FocusedSection::EapChoice => match key_event.code { KeyCode::Char('l') | KeyCode::Right => match self.eap { - Eap::PAP(_) => self.eap = Eap::PEAP(peap::PEAP::new()), + Eap::TTLS(_) => self.eap = Eap::PEAP(peap::PEAP::new()), Eap::PEAP(_) => self.eap = Eap::PWD(pwd::PWD::new()), Eap::PWD(_) => self.eap = Eap::TLS(tls::TLS::new()), - Eap::TLS(_) => self.eap = Eap::PAP(pap::PAP::new()), + Eap::TLS(_) => self.eap = Eap::TTLS(ttls::TTLS::new()), }, KeyCode::Char('h') | KeyCode::Left => {} _ => {} }, FocusedSection::Eap => match &mut self.eap { Eap::TLS(v) => v.handle_key_events(key_event, sender).await?, - Eap::PAP(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?, }, @@ -297,7 +305,7 @@ impl WPAEntreprise { if let KeyCode::Enter = key_event.code { let result = match &mut self.eap { Eap::TLS(v) => v.apply(self.network_name.as_str()), - Eap::PAP(v) => v.apply(), + 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()), }; @@ -360,7 +368,7 @@ impl WPAEntreprise { ); let choice = match self.eap { - Eap::PAP(_) => Text::from(" < PAP >"), + Eap::TTLS(_) => Text::from(" < TTLS >"), Eap::PEAP(_) => Text::from(" < PEAP >"), Eap::PWD(_) => Text::from(" < PWD >"), Eap::TLS(_) => Text::from(" < TLS >"), @@ -382,32 +390,19 @@ impl WPAEntreprise { match &mut self.eap { Eap::TLS(v) => { - // if self.focused_section == FocusedSection::Eap && !v.selected() { - // v.next(); - // } v.render(frame, eap_block); } Eap::PWD(v) => { - // if self.focused_section == FocusedSection::Eap && !v.selected() { - // v.next(); - // } v.render(frame, eap_block); } - Eap::PAP(v) => { - // if self.focused_section == FocusedSection::Eap && !v.selected() { - // v.next(); - // } + Eap::TTLS(v) => { v.render(frame, eap_block); } Eap::PEAP(v) => { - // if self.focused_section == FocusedSection::Eap && !v.selected() { - // v.next(); - // } v.render(frame, eap_block); } } - // Apply Block let text = if self.focused_section == FocusedSection::Apply { Text::from("Apply").bold().green().centered() } else { diff --git a/src/mode/station/auth/entreprise/pap.rs b/src/mode/station/auth/entreprise/ttls.rs similarity index 68% rename from src/mode/station/auth/entreprise/pap.rs rename to src/mode/station/auth/entreprise/ttls.rs index 77e9ad0..a5526b1 100644 --- a/src/mode/station/auth/entreprise/pap.rs +++ b/src/mode/station/auth/entreprise/ttls.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::{fs::OpenOptions, io::Write, path::Path}; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ @@ -24,15 +24,17 @@ pub enum FocusedInput { CaCert, ServerDomainMask, Identity, - Password, + Phase2Identity, + Phase2Password, } #[derive(Debug, Clone, Default)] -pub struct PAP { +pub struct TTLS { ca_cert: UserInputField, server_domain_mask: UserInputField, identity: UserInputField, - password: UserInputField, + phase2_identity: UserInputField, + phase2_password: UserInputField, pub focused_input: FocusedInput, state: ListState, } @@ -43,7 +45,7 @@ struct UserInputField { error: Option, } -impl PAP { +impl TTLS { pub fn new() -> Self { Self::default() } @@ -52,9 +54,17 @@ impl PAP { self.ca_cert.error = None; if self.ca_cert.field.value().is_empty() { self.ca_cert.error = Some("Required field.".to_string()); + return; } - if !Path::new(self.ca_cert.field.value()).exists() { - self.ca_cert.error = Some("The CA file does not exist.".to_string()); + 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) { @@ -69,11 +79,17 @@ impl PAP { 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_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_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()); } } @@ -81,11 +97,13 @@ impl PAP { self.validate_ca_cert(); self.validate_server_domain_mask(); self.validate_identity(); - self.validate_password(); + 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.password.error.is_some() + | self.phase2_identity.error.is_some() + | self.phase2_password.error.is_some() { return Err("Valdidation Error".into()); } @@ -94,7 +112,7 @@ impl PAP { pub fn next(&mut self) { let i = match self.state.selected() { - Some(6) => None, + Some(8) => None, Some(i) => Some(i + 2), None => Some(0), }; @@ -105,22 +123,50 @@ impl PAP { let i = match self.state.selected() { Some(0) => None, Some(i) => Some(i.saturating_sub(2)), - None => Some(6), + None => Some(8), }; self.state.select(i); } pub fn set_last(&mut self) { - self.state.select(Some(6)); + self.state.select(Some(8)); } pub fn selected(&self) -> bool { self.state.selected().is_some() } - pub fn apply(&mut self) -> AppResult<()> { + pub fn apply(&mut self, network_name: &str) -> AppResult<()> { self.validate()?; + let mut file = OpenOptions::new() + .write(true) + .read(true) + .create(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(()) } @@ -139,18 +185,23 @@ impl PAP { .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::Password => { - self.password + FocusedInput::Phase2Identity => { + self.phase2_identity .field .handle_event(&crossterm::event::Event::Key(key_event)); } - FocusedInput::ServerDomainMask => { - self.server_domain_mask + FocusedInput::Phase2Password => { + self.phase2_password .field .handle_event(&crossterm::event::Event::Key(key_event)); } @@ -228,14 +279,29 @@ impl PAP { }]) .red(), Line::from(vec![ - Span::from(pad_string(" Password", 20)) + 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.password.field.value(), 50)).bg(Color::DarkGray), + 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.password.error { + if let Some(error) = &self.phase2_password.error { Span::from(error) } else { Span::from("") From f0a379a14e6e5876962ff631371b0d2de9e5feb4 Mon Sep 17 00:00:00 2001 From: Badr Date: Sat, 8 Nov 2025 14:37:34 +0100 Subject: [PATCH 06/11] make help newnetwork section responsive --- src/mode/station.rs | 80 +++++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/src/mode/station.rs b/src/mode/station.rs index 0ca191a..1bb2bcc 100644 --- a/src/mode/station.rs +++ b/src/mode/station.rs @@ -630,7 +630,7 @@ impl Station { Span::from(" Nav"), ])], FocusedBlock::KnownNetworks => { - if frame.area().width <= 120 { + if frame.area().width <= 110 { vec![ Line::from(vec![ Span::from(if config.station.toggle_connect == ' ' { @@ -658,9 +658,6 @@ impl Station { Span::from("j,").bold(), Span::from(" Down"), Span::from(" | "), - Span::from("󱊷 ").bold(), - Span::from(" Discard"), - Span::from(" | "), Span::from("ctrl+r").bold(), Span::from(" Switch Mode"), Span::from(" | "), @@ -694,8 +691,52 @@ impl Station { Span::from(config.station.start_scanning.to_string()).bold(), Span::from(" Scan"), Span::from(" | "), - Span::from("󱊷 ").bold(), - Span::from(" Discard"), + 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"), @@ -705,28 +746,6 @@ impl Station { ])] } } - FocusedBlock::NewNetworks => 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("󱊷 ").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 => { vec![Line::from(vec![ Span::from("󱊷 ").bold(), @@ -753,7 +772,10 @@ impl Station { Span::from("⇄").bold(), Span::from(" Nav"), ])], - _ => vec![Line::from("")], + _ => vec![Line::from(vec![ + Span::from("󱊷 ").bold(), + Span::from(" Discard"), + ])], }; let help_message = Paragraph::new(help_message).centered().blue(); From 6a0d147437e09c1526a3933fa880f563f44b803e Mon Sep 17 00:00:00 2001 From: Badr Date: Sat, 8 Nov 2025 15:09:14 +0100 Subject: [PATCH 07/11] support Eduroam --- src/mode/station/auth/entreprise.rs | 49 +++- src/mode/station/auth/entreprise/eduroam.rs | 249 ++++++++++++++++++++ src/mode/station/auth/entreprise/peap.rs | 1 + src/mode/station/auth/entreprise/pwd.rs | 1 + src/mode/station/auth/entreprise/tls.rs | 1 + src/mode/station/auth/entreprise/ttls.rs | 1 + 6 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 src/mode/station/auth/entreprise/eduroam.rs diff --git a/src/mode/station/auth/entreprise.rs b/src/mode/station/auth/entreprise.rs index 9f0a2c9..dcf7deb 100644 --- a/src/mode/station/auth/entreprise.rs +++ b/src/mode/station/auth/entreprise.rs @@ -11,6 +11,7 @@ use ratatui::{ use crate::{app::AppResult, event::Event}; +pub mod eduroam; pub mod peap; pub mod pwd; pub mod requests; @@ -48,6 +49,7 @@ pub enum Eap { PEAP(peap::PEAP), PWD(pwd::PWD), TLS(tls::TLS), + Eduroam(eduroam::Eduroam), } impl Default for Eap { @@ -95,6 +97,10 @@ impl WPAEntreprise { 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 { @@ -177,6 +183,21 @@ impl WPAEntreprise { 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, }, @@ -259,6 +280,20 @@ impl WPAEntreprise { 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) => { @@ -281,6 +316,11 @@ impl WPAEntreprise { 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 { @@ -289,7 +329,8 @@ impl WPAEntreprise { Eap::TTLS(_) => self.eap = Eap::PEAP(peap::PEAP::new()), Eap::PEAP(_) => self.eap = Eap::PWD(pwd::PWD::new()), Eap::PWD(_) => self.eap = Eap::TLS(tls::TLS::new()), - Eap::TLS(_) => self.eap = Eap::TTLS(ttls::TTLS::new()), + Eap::TLS(_) => self.eap = Eap::Eduroam(eduroam::Eduroam::new()), + Eap::Eduroam(_) => self.eap = Eap::TTLS(ttls::TTLS::new()), }, KeyCode::Char('h') | KeyCode::Left => {} _ => {} @@ -299,6 +340,7 @@ impl WPAEntreprise { 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 => { @@ -308,6 +350,7 @@ impl WPAEntreprise { 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)?; @@ -372,6 +415,7 @@ impl WPAEntreprise { Eap::PEAP(_) => Text::from(" < PEAP >"), Eap::PWD(_) => Text::from(" < PWD >"), Eap::TLS(_) => Text::from(" < TLS >"), + Eap::Eduroam(_) => Text::from(" < Eduroam >"), }; let choice = if self.focused_section == FocusedSection::EapChoice { @@ -401,6 +445,9 @@ impl WPAEntreprise { Eap::PEAP(v) => { v.render(frame, eap_block); } + Eap::Eduroam(v) => { + v.render(frame, eap_block); + } } let text = if self.focused_section == FocusedSection::Apply { 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 index 0d3ece0..3d0bc49 100644 --- a/src/mode/station/auth/entreprise/peap.rs +++ b/src/mode/station/auth/entreprise/peap.rs @@ -149,6 +149,7 @@ impl PEAP { .write(true) .read(true) .create(true) + .truncate(true) .open(format!("/var/lib/iwd/{}.8021x", network_name))?; let text = format!( " diff --git a/src/mode/station/auth/entreprise/pwd.rs b/src/mode/station/auth/entreprise/pwd.rs index 98a0132..d786f7c 100644 --- a/src/mode/station/auth/entreprise/pwd.rs +++ b/src/mode/station/auth/entreprise/pwd.rs @@ -106,6 +106,7 @@ impl PWD { .write(true) .read(true) .create(true) + .truncate(true) .open(format!("/var/lib/iwd/{}.8021x", network_name))?; let text = format!( " diff --git a/src/mode/station/auth/entreprise/tls.rs b/src/mode/station/auth/entreprise/tls.rs index cc21287..7149b94 100644 --- a/src/mode/station/auth/entreprise/tls.rs +++ b/src/mode/station/auth/entreprise/tls.rs @@ -169,6 +169,7 @@ impl TLS { .write(true) .read(true) .create(true) + .truncate(true) .open(format!("/var/lib/iwd/{}.8021x", network_name))?; let mut text = format!( " diff --git a/src/mode/station/auth/entreprise/ttls.rs b/src/mode/station/auth/entreprise/ttls.rs index a5526b1..90a2e74 100644 --- a/src/mode/station/auth/entreprise/ttls.rs +++ b/src/mode/station/auth/entreprise/ttls.rs @@ -143,6 +143,7 @@ impl TTLS { .write(true) .read(true) .create(true) + .truncate(true) .open(format!("/var/lib/iwd/{}.8021x", network_name))?; let text = format!( " From fa390952dca4c960487291a50f91cc66c7c83f8e Mon Sep 17 00:00:00 2001 From: Badr Date: Sat, 8 Nov 2025 15:44:04 +0100 Subject: [PATCH 08/11] add nav l/h for EAP choice --- src/mode/station/auth/entreprise.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/mode/station/auth/entreprise.rs b/src/mode/station/auth/entreprise.rs index dcf7deb..09f4e82 100644 --- a/src/mode/station/auth/entreprise.rs +++ b/src/mode/station/auth/entreprise.rs @@ -324,15 +324,23 @@ impl WPAEntreprise { }, }, _ => 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::TLS(tls::TLS::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()), - Eap::Eduroam(_) => self.eap = Eap::TTLS(ttls::TTLS::new()), }, - KeyCode::Char('h') | KeyCode::Left => {} + _ => {} }, FocusedSection::Eap => match &mut self.eap { From 539424f899edee7db35a4cff13525015eb266f03 Mon Sep 17 00:00:00 2001 From: Badr Date: Mon, 10 Nov 2025 21:24:09 +0100 Subject: [PATCH 09/11] update layout --- Cargo.lock | 80 ++++++++++++------------- src/mode/station.rs | 9 ++- src/mode/station/auth/entreprise.rs | 49 ++++++++------- src/mode/station/auth/entreprise/tls.rs | 2 +- 4 files changed, 75 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1323852..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]] @@ -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/src/mode/station.rs b/src/mode/station.rs index 1bb2bcc..0233ff0 100644 --- a/src/mode/station.rs +++ b/src/mode/station.rs @@ -753,6 +753,9 @@ impl Station { ])] } FocusedBlock::PskAuthKey => vec![Line::from(vec![ + Span::from(" ↵ ").bold(), + Span::from(" Apply"), + Span::from(" | "), Span::from("⇄").bold(), Span::from(" Hide/Show password"), Span::from(" | "), @@ -760,12 +763,12 @@ impl Station { Span::from(" Discard"), ])], FocusedBlock::WpaEntrepriseAuth => vec![Line::from(vec![ + Span::from(" ↵ ").bold(), + Span::from(" Apply"), + Span::from(" | "), Span::from("h,l,←,→").bold(), Span::from(" Switch EAP"), Span::from(" | "), - Span::from(" ↵ ").bold(), - Span::from(" Connect"), - Span::from(" | "), Span::from("󱊷 ").bold(), Span::from(" Discard"), Span::from(" | "), diff --git a/src/mode/station/auth/entreprise.rs b/src/mode/station/auth/entreprise.rs index 09f4e82..81b8b11 100644 --- a/src/mode/station/auth/entreprise.rs +++ b/src/mode/station/auth/entreprise.rs @@ -3,7 +3,7 @@ use tokio::sync::mpsc::UnboundedSender; use ratatui::{ Frame, - layout::{Constraint, Direction, Flex, Layout, Margin}, + layout::{Constraint, Direction, Layout, Margin}, style::{Style, Stylize}, text::Text, widgets::{Block, Borders, Clear}, @@ -375,7 +375,7 @@ impl WPAEntreprise { .direction(Direction::Vertical) .constraints([ Constraint::Fill(1), - Constraint::Length(20), + Constraint::Length(21), Constraint::Fill(1), ]) .flex(ratatui::layout::Flex::SpaceBetween) @@ -385,7 +385,7 @@ impl WPAEntreprise { .direction(Direction::Horizontal) .constraints([ Constraint::Fill(1), - Constraint::Percentage(80), + Constraint::Max(80), Constraint::Fill(1), ]) .flex(ratatui::layout::Flex::SpaceBetween) @@ -393,37 +393,44 @@ impl WPAEntreprise { frame.render_widget(Clear, block); - let (eap_choice_block, eap_block, apply_block) = { + let (title_block, eap_choice_block, eap_block, apply_block) = { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(5), - Constraint::Length(10), - Constraint::Length(4), + 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), ]) - .flex(Flex::SpaceBetween) .split(block); - (chunks[0], chunks[1], chunks[2]) + (chunks[1], chunks[3], chunks[5], chunks[7]) }; frame.render_widget( Block::default() - .title(format!(" Configure {} Network ", self.network_name)) - .title_alignment(ratatui::layout::Alignment::Center) - .title_style(Style::default().bold()) .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 >"), - Eap::PEAP(_) => Text::from(" < PEAP >"), - Eap::PWD(_) => Text::from(" < PWD >"), - Eap::TLS(_) => Text::from(" < TLS >"), - Eap::Eduroam(_) => Text::from(" < Eduroam >"), + 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 { @@ -436,7 +443,7 @@ impl WPAEntreprise { choice.centered(), eap_choice_block.inner(Margin { horizontal: 1, - vertical: 2, + vertical: 0, }), ); @@ -459,16 +466,16 @@ impl WPAEntreprise { } let text = if self.focused_section == FocusedSection::Apply { - Text::from("Apply").bold().green().centered() + Text::from("APPLY").centered().green().bold() } else { - Text::from("Apply").centered() + Text::from("APPLY").centered() }; frame.render_widget( text, apply_block.inner(Margin { horizontal: 1, - vertical: 1, + vertical: 0, }), ); } diff --git a/src/mode/station/auth/entreprise/tls.rs b/src/mode/station/auth/entreprise/tls.rs index 7149b94..7da8c90 100644 --- a/src/mode/station/auth/entreprise/tls.rs +++ b/src/mode/station/auth/entreprise/tls.rs @@ -261,7 +261,7 @@ AutoConnect=true", .direction(Direction::Horizontal) .constraints([ Constraint::Fill(1), - Constraint::Percentage(80), + Constraint::Max(70), Constraint::Fill(1), ]) .flex(ratatui::layout::Flex::SpaceBetween) From 28b8e4f5beb7c3cbb5d9628ee39d0714cbcaf3ca Mon Sep 17 00:00:00 2001 From: Badr Date: Mon, 10 Nov 2025 21:37:04 +0100 Subject: [PATCH 10/11] add notif when network configured --- src/event.rs | 2 +- src/main.rs | 8 +++++++- src/mode/station/auth/entreprise.rs | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/event.rs b/src/event.rs index ce0a043..8bffc4e 100644 --- a/src/event.rs +++ b/src/event.rs @@ -16,7 +16,7 @@ pub enum Event { Notification(Notification), Reset(Mode), Auth(String), - EapNeworkConfigured, + EapNeworkConfigured(String), ConfigureNewEapNetwork(String), AuthRequestPassword((String, Option)), AuthReqKeyPassphrase(String), diff --git a/src/main.rs b/src/main.rs index 4b698af..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, }; @@ -60,9 +61,14 @@ async fn main() -> AppResult<()> { app.network_name_requiring_auth = Some(network_name); } - Event::EapNeworkConfigured => { + 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 => { diff --git a/src/mode/station/auth/entreprise.rs b/src/mode/station/auth/entreprise.rs index 81b8b11..e32a557 100644 --- a/src/mode/station/auth/entreprise.rs +++ b/src/mode/station/auth/entreprise.rs @@ -361,7 +361,7 @@ impl WPAEntreprise { Eap::Eduroam(v) => v.apply(), }; if result.is_ok() { - sender.send(Event::EapNeworkConfigured)?; + sender.send(Event::EapNeworkConfigured(self.network_name.clone()))?; } } } From 1f496185470b16e576e0d45b7384392384b55fda Mon Sep 17 00:00:00 2001 From: Badr Date: Mon, 10 Nov 2025 21:44:43 +0100 Subject: [PATCH 11/11] update release file --- Release.md | 6 ++++++ 1 file changed, 6 insertions(+) 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