diff --git a/Cargo.lock b/Cargo.lock index 12dce731..24d8c4c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1335,6 +1335,7 @@ dependencies = [ "indexmap 2.13.0", "libcosmic", "nm-secret-agent-manager", + "nmrs", "rust-embed", "rustc-hash 2.1.1", "secure-string", @@ -2758,6 +2759,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -4643,6 +4650,22 @@ dependencies = [ "zbus 5.13.1", ] +[[package]] +name = "nmrs" +version = "1.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b61a6ea9fa68a6bf6303834ed7457254f825632642c62c18931951ae6ad59" +dependencies = [ + "futures", + "futures-timer", + "log", + "serde", + "thiserror 2.0.17", + "uuid", + "zbus 5.13.1", + "zvariant 5.9.1", +] + [[package]] name = "nom" version = "7.1.3" @@ -6236,6 +6259,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -7218,6 +7247,7 @@ dependencies = [ "getrandom 0.3.4", "js-sys", "serde_core", + "sha1_smol", "wasm-bindgen", ] diff --git a/cosmic-applet-network/Cargo.toml b/cosmic-applet-network/Cargo.toml index c233d929..3da5775b 100644 --- a/cosmic-applet-network/Cargo.toml +++ b/cosmic-applet-network/Cargo.toml @@ -31,6 +31,7 @@ nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindi indexmap = "2.13.0" secure-string = "0.3.0" uuid = { version = "1.19.0", features = ["v4"] } +nmrs = "1.3.5" [dependencies.cosmic-settings-network-manager-subscription] diff --git a/cosmic-applet-network/src/app.rs b/cosmic-applet-network/src/app.rs index ae956bd1..5d342128 100644 --- a/cosmic-applet-network/src/app.rs +++ b/cosmic-applet-network/src/app.rs @@ -11,6 +11,7 @@ use cosmic_settings_network_manager_subscription::{ use indexmap::IndexMap; use rustc_hash::FxHashSet; use secure_string::SecureString; +use nmrs::{ConnectionError, EapMethod, EapOptions, Phase2, WifiSecurity}; use std::{ borrow::Cow, collections::{BTreeMap, BTreeSet}, @@ -162,6 +163,7 @@ struct CosmicNetworkApplet { nm_task: Option>, secret_tx: Option>, nm_state: MyNetworkState, + nm: Option, // UI state show_visible_networks: bool, @@ -502,6 +504,7 @@ pub(crate) enum Message { ConnectVPNWithPassword, VPNPasswordUpdate(SecureString), CancelVPNConnection, + NmrsReady(Option), } #[derive(Debug, Clone)] @@ -841,56 +844,39 @@ impl cosmic::Application for CosmicNetworkApplet { } } Message::SelectWirelessAccessPoint(access_point) => { - let Some(tx) = self.nm_sender.as_ref() else { - return Task::none(); + let Some(nm) = self.nm.clone() else { + return cosmic::task::message(Message::Error( + "Network manager not initialized".to_string() + )).map(cosmic::Action::App); }; - + + // Open networks - connect immediately if matches!(access_point.network_type, NetworkType::Open) { - if let Err(err) = - tx.unbounded_send(network_manager::Request::SelectAccessPoint( - access_point.ssid.clone(), - access_point.hw_address, - access_point.network_type, - self.secret_tx.clone(), - )) - { - if err.is_disconnected() { - return system_conn().map(cosmic::Action::App); - } - - tracing::error!("{err:?}"); - } + let ssid = access_point.ssid.to_string(); self.new_connection = Some(NewConnectionState::Waiting(access_point)); - } else { - if self - .nm_state - .nm_state - .known_access_points - .contains(&access_point) - { - if let Err(err) = - tx.unbounded_send(network_manager::Request::SelectAccessPoint( - access_point.ssid.clone(), - access_point.hw_address, - access_point.network_type, - self.secret_tx.clone(), - )) - { - if err.is_disconnected() { - return system_conn().map(cosmic::Action::App); + + return cosmic::task::future(async move { + match nm.connect(&ssid, WifiSecurity::Open).await { + Ok(()) => { + tracing::info!("Connected to open network {}", ssid); + Message::Refresh + } + Err(e) => { + tracing::error!("Failed to connect to {}: {}", ssid, e); + Message::Error(format!("Failed to connect to '{}': {}", ssid, e)) } - - tracing::error!("{err:?}"); } - } - self.new_connection = Some(NewConnectionState::EnterPassword { - access_point, - description: None, - identity: String::new(), - password: String::new().into(), - password_hidden: true, - }); + }).map(cosmic::Action::App); } + + // Secured networks - show password dialog + self.new_connection = Some(NewConnectionState::EnterPassword { + access_point, + description: None, + identity: String::new(), + password: String::new().into(), + password_hidden: true, + }); } Message::ToggleVisibleNetworks => { self.new_connection = None; @@ -978,19 +964,27 @@ impl cosmic::Application for CosmicNetworkApplet { tracing::warn!("Failed to find known access point with ssid: {}", ssid); return Task::none(); }; - if let Some(tx) = self.nm_sender.as_ref() { - if let Err(err) = - tx.unbounded_send(network_manager::Request::Forget(ssid.into())) - { - if err.is_disconnected() { - return system_conn().map(cosmic::Action::App); + + let Some(nm) = self.nm.clone() else { + tracing::warn!("nmrs not initialized for forget network"); + return Task::none(); + }; + + self.show_visible_networks = true; + let ssid_clone = ssid.clone(); + + return cosmic::task::future(async move { + match nm.forget(&ssid_clone).await { + Ok(()) => { + tracing::info!("Forgot network {}", ssid_clone); + Message::SelectWirelessAccessPoint(ap) + } + Err(e) => { + tracing::error!("Failed to forget network {}: {}", ssid_clone, e); + Message::Error(format!("Failed to forget network '{}'", ssid_clone)) } - - tracing::error!("{err:?}"); } - self.show_visible_networks = true; - return self.update(Message::SelectWirelessAccessPoint(ap)); - } + }).map(cosmic::Action::App); } Message::Surface(a) => { return cosmic::task::message(cosmic::Action::Cosmic( @@ -1046,35 +1040,73 @@ impl cosmic::Application for CosmicNetworkApplet { } } Message::ConnectWithPassword => { - // save password - let Some(tx) = self.nm_sender.as_ref() else { - return Task::none(); - }; - - if let Some(NewConnectionState::EnterPassword { + let Some(NewConnectionState::EnterPassword { password, access_point, identity, .. - }) = self.new_connection.take() - { - let is_enterprise: bool = matches!(access_point.network_type, NetworkType::EAP); - - if let Err(err) = tx.unbounded_send(network_manager::Request::Authenticate { - ssid: access_point.ssid.to_string(), - identity: is_enterprise.then(|| identity.clone()), - password, - hw_address: access_point.hw_address, - secret_tx: self.secret_tx.clone(), - }) { - if err.is_disconnected() { - return system_conn().map(cosmic::Action::App); + }) = self.new_connection.take() else { + return Task::none(); + }; + + let Some(nm) = self.nm.clone() else { + return cosmic::task::message(Message::Error( + "Network manager not initialized".to_string() + )).map(cosmic::Action::App); + }; + + let ssid = access_point.ssid.to_string(); + let password_str = password.unsecure().to_string(); + + self.new_connection = Some(NewConnectionState::Waiting(access_point.clone())); + + return cosmic::task::future(async move { + let security = match access_point.network_type { + NetworkType::Open => WifiSecurity::Open, + NetworkType::EAP => { + WifiSecurity::WpaEap { + opts: EapOptions { + identity: identity.clone(), + password: password_str, + anonymous_identity: None, + domain_suffix_match: None, + ca_cert_path: None, + system_ca_certs: true, + method: EapMethod::Peap, + phase2: Phase2::Mschapv2, + } + } + } + _ => { + // All other types (including secured networks) use WPA-PSK + WifiSecurity::WpaPsk { + psk: password_str, + } + } + }; + + match nm.connect(&ssid, security).await { + Ok(()) => { + tracing::info!("Connected to {}", ssid); + Message::Refresh + } + Err(e) => { + tracing::error!("Connection to {} failed: {}", ssid, e); + let error_msg = match e { + ConnectionError::AuthFailed => + format!("Wrong password for '{}'", ssid), + ConnectionError::NotFound => + format!("Network '{}' out of range", ssid), + ConnectionError::Timeout => + format!("Connection to '{}' timed out", ssid), + ConnectionError::DhcpFailed => + format!("Connected but failed to get IP address"), + _ => format!("Failed to connect: {}", e), + }; + Message::Error(error_msg) } - tracing::error!("Failed to authenticate with network manager"); } - self.new_connection - .replace(NewConnectionState::Waiting(access_point)); - } + }).map(cosmic::Action::App); } Message::ConnectionSettings(btree_map) => { self.nm_state.ssid_to_uuid = btree_map; @@ -1250,10 +1282,25 @@ impl cosmic::Application for CosmicNetworkApplet { } => {} }, Message::NetworkManagerConnect(connection) => { + // Initialize nmrs in a separate task + let init_task = cosmic::task::future(async { + match nmrs::NetworkManager::new().await { + Ok(nm) => { + tracing::info!("nmrs NetworkManager initialized"); + Message::NmrsReady(Some(nm)) + } + Err(e) => { + tracing::warn!("Failed to initialize nmrs: {}", e); + Message::NmrsReady(None) + } + } + }); + return cosmic::task::batch(vec![ self.connect(connection.clone()), connection_settings(connection), - ]); + init_task, + ]).map(cosmic::Action::App); } Message::PasswordUpdate(entered_pw) => { if let Some(NewConnectionState::EnterPassword { password, .. }) = @@ -1269,24 +1316,23 @@ impl cosmic::Application for CosmicNetworkApplet { self.nm_state.devices = device_infos.into_iter().map(Arc::new).collect(); } Message::WiFiEnable(enable) => { - if let Some(sender) = self.nm_sender.as_mut() { - if let Err(err) = - sender.unbounded_send(network_manager::Request::SetWiFi(enable)) - { - if err.is_disconnected() { - return system_conn().map(cosmic::Action::App); + let Some(nm) = self.nm.clone() else { + tracing::warn!("nmrs not initialized for WiFi toggle"); + return Task::none(); + }; + + return cosmic::task::future(async move { + match nm.set_wifi_enabled(enable).await { + Ok(()) => { + tracing::info!("WiFi {}", if enable { "enabled" } else { "disabled" }); + Message::Refresh } - - tracing::error!("{err:?}"); - } - if let Err(err) = sender.unbounded_send(network_manager::Request::Reload) { - if err.is_disconnected() { - return system_conn().map(cosmic::Action::App); + Err(e) => { + tracing::error!("Failed to {} WiFi: {}", if enable { "enable" } else { "disable" }, e); + Message::Error(format!("Failed to {} WiFi", if enable { "enable" } else { "disable" })) } - - tracing::error!("{err:?}"); } - } + }).map(cosmic::Action::App); } Message::SecretAgent(agent_event) => match agent_event { nm_secret_agent::Event::RequestSecret { @@ -1376,6 +1422,12 @@ impl cosmic::Application for CosmicNetworkApplet { Message::CancelVPNConnection => { self.nm_state.requested_vpn = None; } + Message::NmrsReady(nm) => { + self.nm = nm; + if self.nm.is_some() { + tracing::info!("nmrs ready for WiFi connections"); + } + } } Task::none() }