From 10e4f84d3f0bc6e3cd9395095f0c2384c5746e3d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 19 Dec 2025 12:02:48 -0500 Subject: [PATCH] refactor: use settings subscriptions and nm secret agent --- Cargo.lock | 210 ++- Cargo.toml | 5 + cosmic-applet-network/Cargo.toml | 15 + cosmic-applet-network/src/app.rs | 1278 ++++++++++++----- cosmic-applet-network/src/lib.rs | 2 +- .../src/network_manager/active_conns.rs | 66 - .../src/network_manager/available_vpns.rs | 48 - .../src/network_manager/available_wifi.rs | 106 -- .../src/network_manager/current_networks.rs | 193 --- .../src/network_manager/devices.rs | 65 - .../src/network_manager/hw_address.rs | 36 - .../src/network_manager/mod.rs | 822 ----------- .../src/network_manager/wireless_enabled.rs | 62 - cosmic-applet-network/src/utils.rs | 18 + 14 files changed, 1184 insertions(+), 1742 deletions(-) delete mode 100644 cosmic-applet-network/src/network_manager/active_conns.rs delete mode 100644 cosmic-applet-network/src/network_manager/available_vpns.rs delete mode 100644 cosmic-applet-network/src/network_manager/available_wifi.rs delete mode 100644 cosmic-applet-network/src/network_manager/current_networks.rs delete mode 100644 cosmic-applet-network/src/network_manager/devices.rs delete mode 100644 cosmic-applet-network/src/network_manager/hw_address.rs delete mode 100644 cosmic-applet-network/src/network_manager/mod.rs delete mode 100644 cosmic-applet-network/src/network_manager/wireless_enabled.rs create mode 100644 cosmic-applet-network/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 74051f6c..0007e06b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.12" @@ -336,6 +347,17 @@ dependencies = [ "slab", ] +[[package]] +name = "async-fn-stream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba0c4baf81a0d8ab31618ffa3ae29ceeb970a6d0d82f76130753462e39d0ea" +dependencies = [ + "futures-util", + "pin-project-lite", + "smallvec", +] + [[package]] name = "async-io" version = "1.13.0" @@ -582,7 +604,7 @@ dependencies = [ "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -636,6 +658,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -827,6 +858,15 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.51" @@ -906,6 +946,16 @@ dependencies = [ "phf 0.12.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1273,19 +1323,26 @@ name = "cosmic-applet-network" version = "1.0.0" dependencies = [ "anyhow", + "async-fn-stream", "cosmic-dbus-networkmanager", + "cosmic-settings-airplane-mode-subscription", + "cosmic-settings-network-manager-subscription", "cosmic-time", "futures", "futures-util", "i18n-embed", "i18n-embed-fl", + "indexmap 2.12.1", "libcosmic", + "nm-secret-agent-manager", "rust-embed", "rustc-hash 2.1.1", + "secure-string", "tokio", "tracing", "tracing-log", "tracing-subscriber", + "uuid", "zbus 5.12.0", ] @@ -1625,6 +1682,18 @@ dependencies = [ "zbus 5.12.0", ] +[[package]] +name = "cosmic-settings-airplane-mode-subscription" +version = "1.0.0-beta6" +source = "git+https://github.com/pop-os/cosmic-settings/?branch=nm-secret-agent#8bcec57132e0610cd7630b04c9be5e0661863b73" +dependencies = [ + "futures", + "iced_futures", + "log", + "rustix 1.1.3", + "tokio", +] + [[package]] name = "cosmic-settings-config" version = "0.1.0" @@ -1659,6 +1728,25 @@ dependencies = [ "zbus 5.12.0", ] +[[package]] +name = "cosmic-settings-network-manager-subscription" +version = "1.0.0-beta6" +source = "git+https://github.com/pop-os/cosmic-settings/?branch=nm-secret-agent#8bcec57132e0610cd7630b04c9be5e0661863b73" +dependencies = [ + "bitflags 2.10.0", + "cosmic-dbus-networkmanager", + "futures", + "iced_futures", + "itertools 0.14.0", + "nm-secret-agent-manager", + "secret-service", + "secure-string", + "thiserror 2.0.17", + "tokio", + "tracing", + "zbus 5.12.0", +] + [[package]] name = "cosmic-settings-sound-subscription" version = "1.0.0-beta6" @@ -2029,6 +2117,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -2968,6 +3057,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "i18n-config" version = "0.4.8" @@ -3775,6 +3882,16 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "input" version = "0.9.1" @@ -3848,6 +3965,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.16" @@ -4510,6 +4636,13 @@ dependencies = [ "memoffset 0.9.1", ] +[[package]] +name = "nm-secret-agent-manager" +version = "0.1.0" +dependencies = [ + "zbus 5.12.0", +] + [[package]] name = "nom" version = "7.1.3" @@ -4562,6 +4695,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -4572,6 +4719,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -4598,6 +4754,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -5915,6 +6082,35 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "secret-service" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a62d7f86047af0077255a29494136b9aaaf697c76ff70b8e49cded4e2623c14" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "getrandom 0.2.16", + "hkdf", + "num", + "once_cell", + "serde", + "sha2", + "zbus 5.12.0", +] + +[[package]] +name = "secure-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548ba8c9ff631f7bb3a64de1e8ad73fe20f6d04090724f2b496ed45314ad7488" +dependencies = [ + "libc", + "zeroize", +] + [[package]] name = "self_cell" version = "1.2.1" @@ -6320,6 +6516,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "svg_fmt" version = "0.4.5" @@ -8337,6 +8539,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index f09b9b18..a87400ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,3 +88,8 @@ sctk = { package = "smithay-client-toolkit", version = "0.20.0" } [patch."https://github.com/pop-os/cosmic-protocols"] cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" } cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" } + +[patch.'https://github.com/pop-os/dbus-settings-bindings'] +# cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" } +# upower_dbus = { path = "../dbus-settings-bindings/upower" } +nm-secret-agent-manager = { path = "../dbus-settings-bindings/nm-secret-agent-manager" } diff --git a/cosmic-applet-network/Cargo.toml b/cosmic-applet-network/Cargo.toml index 1b0190c2..9e2aa1fb 100644 --- a/cosmic-applet-network/Cargo.toml +++ b/cosmic-applet-network/Cargo.toml @@ -6,6 +6,7 @@ license = "GPL-3.0-or-later" [dependencies] anyhow.workspace = true +async-fn-stream = "0.3" cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" } cosmic-time.workspace = true futures.workspace = true @@ -26,3 +27,17 @@ tracing-log.workspace = true tracing-subscriber.workspace = true tracing.workspace = true zbus.workspace = true +nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings/", branch = "nm-secret-agent" } +indexmap = "2.12.1" +secure-string = "0.3.0" +uuid = { version = "1.19.0", features = ["v4"] } + + +[dependencies.cosmic-settings-network-manager-subscription] +git = "https://github.com/pop-os/cosmic-settings/" +branch = "nm-secret-agent" + + +[dependencies.cosmic-settings-airplane-mode-subscription] +git = "https://github.com/pop-os/cosmic-settings/" +branch = "nm-secret-agent" diff --git a/cosmic-applet-network/src/app.rs b/cosmic-applet-network/src/app.rs index be54c144..76f640f2 100644 --- a/cosmic-applet-network/src/app.rs +++ b/cosmic-applet-network/src/app.rs @@ -1,8 +1,24 @@ +use anyhow::Context; +use cosmic_dbus_networkmanager::settings::{NetworkManagerSettings, connection::Settings}; +use cosmic_settings_network_manager_subscription::{ + self as network_manager, NetworkManagerState, UUID, + active_conns::active_conns_subscription, + available_wifi::{AccessPoint, NetworkType}, + current_networks::ActiveConnectionInfo, + hw_address::HwAddress, + nm_secret_agent::{self, PasswordFlag, SecretSender}, +}; +use indexmap::IndexMap; use rustc_hash::FxHashSet; -use std::sync::LazyLock; +use secure_string::SecureString; +use std::{ + borrow::Cow, + collections::{BTreeMap, BTreeSet}, + sync::{Arc, LazyLock}, +}; use cosmic::{ - Element, Task, app, + Apply, Element, Task, app, applet::{ menu_button, menu_control_padding, padded_control, token::subscription::{TokenRequest, TokenUpdate, activation_token_subscription}, @@ -19,30 +35,19 @@ use cosmic::{ widget::{ Column, Row, button, container, divider, icon::{self, from_name}, - scrollable, text, text_input, + scrollable, secure_input, text, text_input, }, }; -use cosmic_dbus_networkmanager::interface::enums::{ - ActiveConnectionState, DeviceState, NmConnectivityState, +use cosmic_dbus_networkmanager::interface::{ + access_point, + enums::{ActiveConnectionState, DeviceState, NmConnectivityState, NmState}, }; use cosmic_time::{Instant, Timeline, anim, chain, id}; -use futures::channel::mpsc::UnboundedSender; -use zbus::Connection; +use futures::StreamExt; +use zbus::{Connection, zvariant::ObjectPath}; -use crate::{ - config, fl, - network_manager::{ - NetworkManagerEvent, NetworkManagerRequest, NetworkManagerState, - active_conns::active_conns_subscription, - available_wifi::{AccessPoint, NetworkType}, - current_networks::ActiveConnectionInfo, - devices::devices_subscription, - hw_address::HwAddress, - network_manager_subscription, - wireless_enabled::wireless_enabled_subscription, - }, -}; +use crate::{config, fl}; pub fn run() -> cosmic::iced::Result { cosmic::applet::run::(()) @@ -52,8 +57,9 @@ pub fn run() -> cosmic::iced::Result { enum NewConnectionState { EnterPassword { access_point: AccessPoint, + description: Option, identity: String, - password: String, + password: SecureString, password_hidden: bool, }, Waiting(AccessPoint), @@ -92,14 +98,71 @@ impl From for AccessPoint { static WIFI: LazyLock = LazyLock::new(id::Toggler::unique); static AIRPLANE_MODE: LazyLock = LazyLock::new(id::Toggler::unique); +#[derive(Default, Debug, Clone)] +pub struct MyNetworkState { + pub known_vpns: IndexMap, + pub ssid_to_uuid: BTreeMap, Box>, + pub devices: Vec>, + pub password: Option, + pub connecting: BTreeSet, + pub nm_state: NetworkManagerState, + pub requested_vpn: Option, +} + +#[derive(Debug, Clone)] +pub struct RequestedVpn { + name: String, + uuid: Arc, + description: Option, + password: SecureString, + password_hidden: bool, + tx: SecretSender, +} + +#[derive(Clone, Debug)] +pub enum ConnectionSettings { + Vpn(VpnConnectionSettings), + Wireguard { id: String }, +} + +#[derive(Clone, Debug, Default)] +pub struct VpnConnectionSettings { + id: String, + username: Option, + connection_type: Option, + password_flag: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum ConnectionType { + Password, +} + +impl VpnConnectionSettings { + fn password_flag(&self) -> Option { + self.connection_type + .as_ref() + .is_some_and(|ct| match ct { + ConnectionType::Password => true, + }) + .then_some(self.password_flag) + .flatten() + } +} + #[derive(Default)] struct CosmicNetworkApplet { core: cosmic::app::Core, icon_name: String, popup: Option, - nm_state: NetworkManagerState, + + // NM state + nm_sender: Option>, + nm_task: Option>, + secret_tx: Option>, + nm_state: MyNetworkState, + // UI state - nm_sender: Option>, show_visible_networks: bool, show_available_vpns: bool, new_connection: Option, @@ -107,7 +170,7 @@ struct CosmicNetworkApplet { timeline: Timeline, toggle_wifi_ctr: u128, token_tx: Option>, - failed_known_ssids: FxHashSet, + failed_known_ssids: FxHashSet>, hw_device_to_show: Option, } @@ -124,14 +187,14 @@ fn wifi_icon(strength: u8) -> &'static str { } fn vpn_section<'a>( - nm_state: &'a NetworkManagerState, + nm_state: &'a MyNetworkState, show_available_vpns: bool, space_xxs: u16, space_s: u16, ) -> Column<'a, Message> { let mut vpn_col = column![]; - if !nm_state.available_vpns.is_empty() { + if !nm_state.known_vpns.is_empty() { let dropdown_icon = if show_available_vpns { "go-up-symbolic" } else { @@ -141,6 +204,43 @@ fn vpn_section<'a>( vpn_col = vpn_col .push(padded_control(divider::horizontal::default()).padding([space_xxs, space_s])); + if let Some(requested_vpn) = nm_state.requested_vpn.as_ref() { + let column_content = vec![ + text::body( + requested_vpn + .description + .as_deref() + .unwrap_or(requested_vpn.uuid.as_ref()), + ) + .width(Length::Fill) + .into(), + secure_input( + "", + Cow::Borrowed(requested_vpn.password.unsecure()), + Some(Message::ToggleVPNPasswordVisibility), + requested_vpn.password_hidden, + ) + .on_input(|s| Message::VPNPasswordUpdate(s.into())) + .on_paste(|s| Message::VPNPasswordUpdate(s.into())) + .on_submit(|_| Message::ConnectVPNWithPassword) + .width(Length::Fill) + .into(), + row![ + button::standard(fl!("cancel")).on_press(Message::CancelVPNConnection), + button::suggested(fl!("connect")).on_press(Message::ConnectVPNWithPassword) + ] + .spacing(24) + .into(), + ]; + let col = padded_control( + Column::with_children(column_content) + .spacing(8) + .align_x(Alignment::Center), + ) + .align_x(Alignment::Center); + vpn_col = vpn_col.push(col); + } + let vpn_toggle_btn = menu_button(row![ text::body(fl!("vpn-connections")) .width(Length::Fill) @@ -154,18 +254,22 @@ fn vpn_section<'a>( vpn_col = vpn_col.push(vpn_toggle_btn); if show_available_vpns { - for vpn in &nm_state.available_vpns { + for (uuid, connection) in &nm_state.known_vpns { + let id = match connection { + ConnectionSettings::Vpn(connection) => connection.id.as_str(), + ConnectionSettings::Wireguard { id } => id.as_str(), + }; // Check if this VPN is currently active - let is_active = nm_state.active_conns.iter().any(|conn| { - matches!(conn, ActiveConnectionInfo::Vpn { name, .. } if name == &vpn.name) - }); + let is_active = nm_state.nm_state.active_conns.iter().any( + |conn| matches!(conn, ActiveConnectionInfo::Vpn { name, .. } if name == id), + ); let mut btn_content = vec![ icon::from_name("network-vpn-symbolic") .size(24) .symbolic(true) .into(), - text::body(&vpn.name).width(Length::Fill).into(), + text::body(id).width(Length::Fill).into(), ]; if is_active { @@ -179,9 +283,9 @@ fn vpn_section<'a>( ); btn = if is_active { - btn.on_press(Message::DeactivateVpn(vpn.name.clone())) + btn.on_press(Message::DeactivateVpn(uuid.clone())) } else { - btn.on_press(Message::ActivateVpn(vpn.uuid.clone())) + btn.on_press(Message::ActivateVpn(uuid.clone())) }; vpn_col = vpn_col.push(btn); @@ -202,7 +306,7 @@ impl CosmicNetworkApplet { }; if matches!(state, ActiveConnectionState::Activated) { - self.failed_known_ssids.remove(&new_s.name()); + self.failed_known_ssids.remove(new_s.name().as_str()); continue; } if matches!( @@ -212,7 +316,7 @@ impl CosmicNetworkApplet { continue; } - if self.nm_state.active_conns.iter().any(|old_s| { + if self.nm_state.nm_state.active_conns.iter().any(|old_s| { matches!( old_s, ActiveConnectionInfo::WiFi { @@ -221,15 +325,16 @@ impl CosmicNetworkApplet { } if new_s.name() == old_s.name() ) }) { - self.failed_known_ssids.insert(new_s.name()); + self.failed_known_ssids.insert(new_s.name().into()); } } - self.nm_state = new_state; + self.nm_state.nm_state = new_state; self.update_icon_name(); } fn update_icon_name(&mut self) { self.icon_name = self + .nm_state .nm_state .active_conns .iter() @@ -255,7 +360,8 @@ impl CosmicNetworkApplet { fn update_togglers(&mut self, state: &NetworkManagerState) { let timeline = &mut self.timeline; let mut changed = false; - if state.wifi_enabled != self.nm_state.wifi_enabled { + if self.nm_state.nm_state.wifi_enabled != state.wifi_enabled { + self.nm_state.nm_state.wifi_enabled = state.wifi_enabled; changed = true; let chain = if state.wifi_enabled { chain::Toggler::on(WIFI.clone(), 1.) @@ -265,7 +371,8 @@ impl CosmicNetworkApplet { timeline.set_chain(chain); } - if state.airplane_mode != self.nm_state.airplane_mode { + if self.nm_state.nm_state.airplane_mode != state.airplane_mode { + self.nm_state.nm_state.airplane_mode = state.airplane_mode; changed = true; let chain = if state.airplane_mode { chain::Toggler::on(AIRPLANE_MODE.clone(), 1.) @@ -292,23 +399,50 @@ impl CosmicNetworkApplet { .popup_container(content.padding([8, 0, 8, 0])) .into() } + + fn connect_vpn(&mut self, uuid: Arc) -> Task> { + if let Some((tx, conn)) = self.nm_sender.clone().zip(self.conn.clone()) { + cosmic::task::future(async move { + // Find the connection by UUID + if let Ok(nm_settings) = NetworkManagerSettings::new(&conn).await { + if let Ok(connections) = nm_settings.list_connections().await { + for connection in connections { + if let Ok(settings) = connection.get_settings().await { + let settings = Settings::new(settings); + if let Some(conn_settings) = &settings.connection { + if conn_settings.uuid.as_ref().is_some_and(|conn_uuid| { + conn_uuid.as_str() == uuid.as_ref() + }) { + let path = connection.inner().path().clone().to_owned(); + let _ = + tx.unbounded_send(network_manager::Request::Activate( + ObjectPath::try_from("/").unwrap(), + path, + )); + break; + } + } + } + } + } + } + Message::Refresh + }) + } else { + tracing::warn!("No sender available to activate VPN."); + Task::none() + } + } } #[derive(Debug, Clone)] pub(crate) enum Message { - ActivateKnownWifi(String, HwAddress), - Disconnect(String, HwAddress), TogglePopup, CloseRequested(window::Id), ToggleAirplaneMode(bool), - ToggleWiFi(bool), ToggleVisibleNetworks, - NetworkManagerEvent(NetworkManagerEvent), SelectWirelessAccessPoint(AccessPoint), CancelNewConnection, - Password(String), - Identity(String), - SubmitPassword, Frame(Instant), Token(TokenUpdate), OpenSettings, @@ -316,9 +450,264 @@ pub(crate) enum Message { OpenHwDevice(Option), TogglePasswordVisibility, Surface(surface::Action), - ActivateVpn(String), // UUID of VPN to activate - DeactivateVpn(String), // Name of VPN to deactivate - ToggleVpnList, // Show/hide available VPNs + ActivateVpn(Arc), // UUID of VPN to activate + DeactivateVpn(Arc), // UUID of VPN to deactivate + ToggleVpnList, // Show/hide available VPNs + /// An update from the secret agent + SecretAgent(network_manager::nm_secret_agent::Event), + /// Connect to a WiFi network access point. + Connect(network_manager::SSID, HwAddress), + /// Connect with a password + ConnectWithPassword, + KnownConnections(IndexMap), + /// Settings for known connections. + ConnectionSettings(BTreeMap, Box>), + /// Disconnect from an access point. + Disconnect(network_manager::SSID, HwAddress), + /// An error occurred. + Error(String), + /// Identity update from the dialog + IdentityUpdate(String), + /// An update from the network manager daemon + NetworkManager(network_manager::Event), + /// Successfully connected to the system dbus. + NetworkManagerConnect(zbus::Connection), + /// Update the password from the dialog + PasswordUpdate(SecureString), + /// Update NetworkManagerState + UpdateState(NetworkManagerState), + /// Update the devices lists + UpdateDevices(Vec), + /// Toggle WiFi access + WiFiEnable(bool), + /// Refresh state + Refresh, + ToggleVPNPasswordVisibility, + ConnectVPNWithPassword, + VPNPasswordUpdate(SecureString), + CancelVPNConnection, +} + +#[derive(Debug, Clone)] +struct Password { + ssid: network_manager::SSID, + hw_address: HwAddress, + identity: Option, + password: SecureString, + password_hidden: bool, + tx: SecretSender, +} + +fn connection_settings(conn: zbus::Connection) -> Task { + let settings = async move { + let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?; + + _ = settings.load_connections(&[]).await; + + let settings = settings + // Get a list of known connections. + .list_connections() + .await? + // Prepare for wrapping in a concurrent stream. + .into_iter() + .map(|conn| async move { conn }) + // Create a concurrent stream for each connection. + .apply(futures::stream::FuturesOrdered::from_iter) + // Concurrently fetch settings for each connection. + .filter_map(|conn| async move { + conn.get_settings() + .await + .map(network_manager::Settings::new) + .ok() + }) + // Reduce the settings list into a SSID->UUID map. + .fold(BTreeMap::new(), |mut set, settings| async move { + if let Some(ref wifi) = settings.wifi + && let Some(ssid) = wifi + .ssid + .clone() + .and_then(|ssid| String::from_utf8(ssid).ok()) + && let Some(ref connection) = settings.connection + && let Some(uuid) = connection.uuid.clone() + { + set.insert(ssid.into(), uuid.into()); + return set; + } + + set + }) + .await; + + Ok::<_, zbus::Error>(settings) + }; + + cosmic::task::future(async move { + settings + .await + .context("failed to get connection settings") + .map_or_else( + |why| Message::Error(why.to_string()), + Message::ConnectionSettings, + ) + }) +} + +pub fn update_state(conn: zbus::Connection) -> Task { + cosmic::task::future(async move { + match NetworkManagerState::new(&conn).await { + Ok(state) => Message::UpdateState(state), + Err(why) => Message::Error(why.to_string()), + } + }) +} + +pub fn update_devices(conn: zbus::Connection) -> Task { + cosmic::task::future(async move { + let filter = + |device_type| matches!(device_type, network_manager::devices::DeviceType::Wifi); + match network_manager::devices::list(&conn, filter).await { + Ok(devices) => Message::UpdateDevices(devices), + Err(why) => Message::Error(why.to_string()), + } + }) +} + +impl CosmicNetworkApplet { + fn connect(&mut self, conn: zbus::Connection) -> Task { + if self.nm_task.is_none() { + let (canceller, task) = crate::utils::forward_event_loop(move |emitter| async move { + let (tx, mut rx) = futures::channel::mpsc::channel(1); + + let watchers = std::pin::pin!(async move { + futures::join!( + network_manager::watch(conn.clone(), tx.clone()), + network_manager::active_conns::watch(conn.clone(), tx.clone()), + network_manager::wireless_enabled::watch(conn.clone(), tx.clone()), + network_manager::watch_connections_changed(conn, tx) + ); + }); + + let forwarder = std::pin::pin!(async move { + while let Some(message) = rx.next().await { + _ = emitter.emit(Message::NetworkManager(message)).await; + } + }); + + futures::future::select(watchers, forwarder).await; + }); + + self.nm_task = Some(canceller); + return task.map(Message::from); + } + + Task::none() + } +} + +fn load_vpns(conn: zbus::Connection) -> Task { + let settings = async move { + let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?; + + _ = settings.load_connections(&[]).await; + + let settings = settings + // Get a list of known connections. + .list_connections() + .await? + // Prepare for wrapping in a concurrent stream. + .into_iter() + .map(|conn| async move { conn }) + // Create a concurrent stream for each connection. + .apply(futures::stream::FuturesOrdered::from_iter) + // Concurrently fetch settings for each connection, and filter for VPN. + .filter_map(|conn| async move { + let settings = conn.get_settings().await.ok()?; + + let connection = settings.get("connection")?; + + match connection + .get("type")? + .downcast_ref::() + .ok()? + .as_str() + { + "vpn" => (), + + "wireguard" => { + let id = connection.get("id")?.downcast_ref::().ok()?; + let uuid = connection.get("uuid")?.downcast_ref::().ok()?; + return Some((Arc::from(uuid), ConnectionSettings::Wireguard { id })); + } + + _ => return None, + } + + let vpn = settings.get("vpn")?; + let id = connection.get("id")?.downcast_ref::().ok()?; + let uuid = connection.get("uuid")?.downcast_ref::().ok()?; + + let (connection_type, username, password_flag) = vpn + .get("data") + .and_then(|data| data.downcast_ref::().ok()) + .map(|dict| { + let (mut connection_type, mut password_flag) = (None, None); + let mut username = vpn + .get("user-name") + .and_then(|u| u.downcast_ref::().ok()); + if dict + .get::(&String::from("connection-type")) + .ok() + .flatten() + .as_deref() + // may be "password" or "password-tls" + .is_some_and(|p| p.starts_with("password")) + { + connection_type = Some(ConnectionType::Password); + username = Some(username.unwrap_or_default()); + + password_flag = dict + .get::(&String::from("password-flags")) + .ok() + .flatten() + .and_then(|value| match value.as_str() { + "0" => Some(PasswordFlag::None), + "1" => Some(PasswordFlag::AgentOwned), + "2" => Some(PasswordFlag::NotSaved), + "4" => Some(PasswordFlag::NotRequired), + _ => None, + }); + } + + (connection_type, username, password_flag) + }) + .unwrap_or_default(); + + Some(( + Arc::from(uuid), + ConnectionSettings::Vpn(VpnConnectionSettings { + id, + connection_type, + password_flag, + username, + }), + )) + }) + // Reduce the settings list into + .fold(IndexMap::new(), |mut set, (uuid, data)| async move { + set.insert(uuid, data); + set + }) + .await; + + Ok::<_, zbus::Error>(settings) + }; + + cosmic::task::future(async move { + settings.await.map_or_else( + |why| Message::Error(why.to_string()), + Message::KnownConnections, + ) + }) } impl cosmic::Application for CosmicNetworkApplet { @@ -328,14 +717,25 @@ impl cosmic::Application for CosmicNetworkApplet { const APP_ID: &'static str = config::APP_ID; fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task) { + let mut applet = Self { + core, + icon_name: "network-wired-disconnected-symbolic".to_string(), + token_tx: None, + ..Default::default() + }; + ( - Self { - core, - icon_name: "network-offline-symbolic".to_string(), - token_tx: None, - ..Default::default() - }, - Task::none(), + applet, + Task::batch(vec![cosmic::Task::future(async move { + zbus::Connection::system() + .await + .context("failed to create system dbus connection") + .map_or_else( + |why| Message::Error(why.to_string()), + Message::NetworkManagerConnect, + ) + })]) + .map(cosmic::Action::App), ) } @@ -355,6 +755,25 @@ impl cosmic::Application for CosmicNetworkApplet { self.show_visible_networks = false; return destroy_popup(p); } else { + let mut tasks = Vec::with_capacity(2); + if let Some(conn) = self.conn.clone() { + tasks.push(update_state(conn.clone())); + tasks.push(update_devices(conn.clone())); + tasks.push(load_vpns(conn)); + let (tx, rx) = tokio::sync::mpsc::channel(4); + self.secret_tx = Some(tx); + let my_id = format!( + "com.system76.CosmicSettings.Applet.{}.NetworkManager.SecretAgent", + uuid::Uuid::new_v4() + ); + tasks.push( + cosmic::Task::stream(nm_secret_agent::secret_agent_stream( + my_id.clone(), + rx, + )) + .map(Message::SecretAgent), + ); + } // TODO request update of state maybe let new_id = window::Id::unique(); self.popup.replace(new_id); @@ -369,143 +788,51 @@ impl cosmic::Application for CosmicNetworkApplet { ); if let Some(tx) = self.nm_sender.as_mut() { - let _ = tx.unbounded_send(NetworkManagerRequest::Reload); + let _ = tx.unbounded_send(network_manager::Request::Reload); } - return get_popup(popup_settings); + tasks.push(get_popup(popup_settings)); + + return Task::batch(tasks).map(cosmic::Action::App); } } Message::ToggleAirplaneMode(enabled) => { self.toggle_wifi_ctr += 1; if let Some(tx) = self.nm_sender.as_mut() { - let _ = tx.unbounded_send(NetworkManagerRequest::SetAirplaneMode(enabled)); + let _ = tx.unbounded_send(network_manager::Request::SetAirplaneMode(enabled)); } } - Message::ToggleWiFi(enabled) => { - self.toggle_wifi_ctr += 1; - - if let Some(tx) = self.nm_sender.as_mut() { - let _ = tx.unbounded_send(NetworkManagerRequest::SetWiFi(enabled)); - } - } - Message::NetworkManagerEvent(event) => match event { - NetworkManagerEvent::Init { - conn, - sender, - state, - } => { - self.nm_sender.replace(sender); - self.update_nm_state(state); - self.conn = Some(conn); - } - NetworkManagerEvent::WiFiEnabled(state) - | NetworkManagerEvent::WirelessAccessPoints(state) - | NetworkManagerEvent::ActiveConns(state) => { - self.update_nm_state(state); - } - NetworkManagerEvent::RequestResponse { - mut state, - success, - req, - } => { - if let NetworkManagerRequest::SelectAccessPoint( - ssid, - hw_address, - _network_type, - ) = &req - { - let conn_match = self - .new_connection - .as_ref() - .is_some_and(|c| c.ssid() == ssid && c.hw_address() == *hw_address); - - if conn_match && success { - if let Some(ActiveConnectionInfo::WiFi { state, .. }) = state - .active_conns - .iter_mut() - .find(|ap| &ap.name() == ssid && ap.hw_address() == *hw_address) - { - *state = ActiveConnectionState::Activated; - } - self.failed_known_ssids.remove(ssid); - self.new_connection = None; - self.show_visible_networks = false; - } else if !matches!( - &self.new_connection, - Some(NewConnectionState::EnterPassword { .. }) - ) { - self.failed_known_ssids.insert(ssid.clone()); - } - } else if let NetworkManagerRequest::Authenticate { - ssid, - identity: _, - password: _, - hw_address, - } = &req - { - if let Some(NewConnectionState::Waiting(access_point)) = - self.new_connection.as_ref() - { - if !success - && ssid == &access_point.ssid - && *hw_address == access_point.hw_address - { - self.new_connection = - Some(NewConnectionState::Failure(access_point.clone())); - } else { - self.new_connection = None; - self.show_visible_networks = false; - } - } else if let Some(NewConnectionState::EnterPassword { - access_point, .. - }) = self.new_connection.as_ref() - { - if success && ssid == &access_point.ssid && *hw_address == access_point.hw_address { - self.new_connection = None; - self.show_visible_networks = false; - } - } - } else if self - .new_connection - .as_ref() - .map(NewConnectionState::ssid).is_some_and(|ssid| { - state.active_conns.iter().any(|c| - matches!(c, ActiveConnectionInfo::WiFi { name, state: ActiveConnectionState::Activated, .. } if ssid == name) - ) - }) { - self.new_connection = None; - self.show_visible_networks = false; - } - - if !matches!(req, NetworkManagerRequest::Reload) - && matches!(state.connectivity, NmConnectivityState::Portal) - { - let mut browser = std::process::Command::new("xdg-open"); - browser.arg("http://204.pop-os.org/"); - - tokio::spawn(cosmic::process::spawn(browser)); - } - - self.update_nm_state(state); - } - }, Message::SelectWirelessAccessPoint(access_point) => { let Some(tx) = self.nm_sender.as_ref() else { return Task::none(); }; - let _ = tx.unbounded_send(NetworkManagerRequest::SelectAccessPoint( - access_point.ssid.clone(), - access_point.hw_address, - access_point.network_type, - )); - if matches!(access_point.network_type, NetworkType::Open) { + let _ = tx.unbounded_send(network_manager::Request::SelectAccessPoint( + access_point.ssid.clone(), + access_point.hw_address, + access_point.network_type, + self.secret_tx.clone(), + )); self.new_connection = Some(NewConnectionState::Waiting(access_point)); } else { + if self + .nm_state + .nm_state + .known_access_points + .contains(&access_point) + { + let _ = tx.unbounded_send(network_manager::Request::SelectAccessPoint( + access_point.ssid.clone(), + access_point.hw_address, + access_point.network_type, + self.secret_tx.clone(), + )); + } self.new_connection = Some(NewConnectionState::EnterPassword { access_point, + description: None, identity: String::new(), - password: String::new(), + password: String::new().into(), password_hidden: true, }); } @@ -514,13 +841,6 @@ impl cosmic::Application for CosmicNetworkApplet { self.new_connection = None; self.show_visible_networks = !self.show_visible_networks; } - Message::Password(entered_pw) => { - if let Some(NewConnectionState::EnterPassword { password, .. }) = - &mut self.new_connection - { - *password = entered_pw; - } - } Message::TogglePasswordVisibility => { if let Some(NewConnectionState::EnterPassword { password_hidden, .. @@ -529,78 +849,17 @@ impl cosmic::Application for CosmicNetworkApplet { *password_hidden = !*password_hidden; } } - Message::SubmitPassword => { - // save password - let Some(tx) = self.nm_sender.as_ref() else { - return Task::none(); - }; - - if let Some(NewConnectionState::EnterPassword { - password, - access_point, - identity, - .. - }) = self.new_connection.take() - { - let is_enterprise: bool = matches!(access_point.network_type, NetworkType::EAP); - - let _ = tx.unbounded_send(NetworkManagerRequest::Authenticate { - ssid: access_point.ssid.clone(), - identity: is_enterprise.then(|| identity.clone()), - password, - hw_address: access_point.hw_address, - }); - self.new_connection - .replace(NewConnectionState::Waiting(access_point)); - } - } - Message::ActivateKnownWifi(ssid, hw_address) => { - let mut network_type = NetworkType::Open; - let tx = if let Some(tx) = self.nm_sender.as_ref() { - if let Some(ap) = self - .nm_state - .known_access_points - .iter_mut() - .find(|c| c.ssid == ssid && c.hw_address == hw_address) - { - network_type = ap.network_type; - ap.working = true; - } - tx - } else { - return Task::none(); - }; - let _ = tx.unbounded_send(NetworkManagerRequest::SelectAccessPoint( - ssid, - hw_address, - network_type, - )); - } Message::CancelNewConnection => { self.new_connection = None; } - Message::Disconnect(ssid, hw_address) => { - self.new_connection = None; - let tx = if let Some(tx) = self.nm_sender.as_ref() { - if let Some(ActiveConnectionInfo::WiFi { state, .. }) = self - .nm_state - .active_conns - .iter_mut() - .find(|c| c.name() == ssid && c.hw_address() == hw_address) - { - *state = ActiveConnectionState::Deactivating; - } - tx - } else { - return Task::none(); - }; - let _ = tx.unbounded_send(NetworkManagerRequest::Disconnect(ssid, hw_address)); - } Message::CloseRequested(id) => { - self.hw_device_to_show = None; + if let Some(cancel) = self.nm_task.take() { + _ = cancel.send(()); + } if Some(id) == self.popup { self.popup = None; } + self.secret_tx = None; } Message::OpenSettings => { let exec = "cosmic-settings network".to_string(); @@ -631,33 +890,39 @@ impl cosmic::Application for CosmicNetworkApplet { Message::OpenHwDevice(hw_address) => self.hw_device_to_show = hw_address, Message::ResetFailedKnownSsid(ssid, hw_address) => { let ap = if let Some(pos) = self + .nm_state .nm_state .known_access_points .iter() - .position(|ap| ap.ssid == ssid && ap.hw_address == hw_address) + .position(|ap| ap.ssid.as_ref() == ssid.as_str() && ap.hw_address == hw_address) { - self.nm_state.known_access_points.remove(pos) + self.nm_state.nm_state.known_access_points.remove(pos) } else if let Some((pos, ap)) = self + .nm_state .nm_state .active_conns .iter() - .position(|conn| conn.name() == ssid && conn.hw_address() == hw_address) + .position(|conn| { + conn.name() == ssid && active_conn_hw_address(conn) == hw_address + }) .zip( self.nm_state + .nm_state .wireless_access_points .iter() - .find(|ap| ap.ssid == ssid && ap.hw_address == hw_address), + .find(|ap| { + ap.ssid.as_ref() == ssid.as_str() && ap.hw_address == hw_address + }), ) { - self.nm_state.active_conns.remove(pos); + self.nm_state.nm_state.active_conns.remove(pos); ap.clone() } else { tracing::warn!("Failed to find known access point with ssid: {}", ssid); return Task::none(); }; if let Some(tx) = self.nm_sender.as_ref() { - let _ = - tx.unbounded_send(NetworkManagerRequest::Forget(ssid.clone(), hw_address)); + let _ = tx.unbounded_send(network_manager::Request::Forget(ssid.into())); self.show_visible_networks = true; return self.update(Message::SelectWirelessAccessPoint(ap)); } @@ -667,25 +932,348 @@ impl cosmic::Application for CosmicNetworkApplet { cosmic::app::Action::Surface(a), )); } - Message::Identity(new_identity) => { + Message::ActivateVpn(uuid) => { + return self.connect_vpn(uuid.clone()); + } + Message::DeactivateVpn(name) => { + if let Some(tx) = self.nm_sender.as_ref() { + let _ = tx.unbounded_send(network_manager::Request::Deactivate(name)); + } + } + Message::ToggleVpnList => { + self.show_available_vpns = !self.show_available_vpns; + } + Message::Connect(ssid, hw_address) => { + let mut network_type = NetworkType::Open; + let tx = if let Some(tx) = self.nm_sender.as_ref() { + if let Some(ap) = self + .nm_state + .nm_state + .known_access_points + .iter_mut() + .find(|c| c.ssid == ssid && c.hw_address == hw_address) + { + network_type = ap.network_type; + ap.working = true; + } + tx + } else { + return Task::none(); + }; + let _ = tx.unbounded_send(network_manager::Request::SelectAccessPoint( + ssid, + hw_address, + network_type, + self.secret_tx.clone(), + )); + } + Message::ConnectWithPassword => { + // save password + let Some(tx) = self.nm_sender.as_ref() else { + return Task::none(); + }; + + if 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(), + }) { + tracing::error!("Failed to authenticate with network manager"); + } + self.new_connection + .replace(NewConnectionState::Waiting(access_point)); + } + } + Message::ConnectionSettings(btree_map) => { + self.nm_state.ssid_to_uuid = btree_map; + } + Message::Disconnect(ssid, hw_address) => { + self.new_connection = None; + let tx = if let Some(tx) = self.nm_sender.as_ref() { + if let Some(ActiveConnectionInfo::WiFi { state, .. }) = + self.nm_state.nm_state.active_conns.iter_mut().find(|c| { + let c_hw_address = match c { + ActiveConnectionInfo::Wired { hw_address, .. } + | ActiveConnectionInfo::WiFi { hw_address, .. } => { + HwAddress::from_str(hw_address).unwrap() + } + ActiveConnectionInfo::Vpn { .. } => HwAddress::default(), + }; + c.name().as_str() == ssid.as_ref() && c_hw_address == hw_address + }) + { + *state = ActiveConnectionState::Deactivating; + } + tx + } else { + return Task::none(); + }; + let _ = tx.unbounded_send(network_manager::Request::Disconnect(ssid)); + } + Message::Error(error) => { + tracing::error!("error: {error:?}") + } + Message::IdentityUpdate(new_identity) => { if let Some(NewConnectionState::EnterPassword { identity, .. }) = &mut self.new_connection { *identity = new_identity; } } - Message::ActivateVpn(uuid) => { - if let Some(tx) = self.nm_sender.as_ref() { - let _ = tx.unbounded_send(NetworkManagerRequest::ActivateVpn(uuid)); + Message::NetworkManager(event) => match event { + network_manager::Event::Init { + conn, + sender, + state, + } => { + self.nm_sender = Some(sender); + self.update_nm_state(state); + self.conn = Some(conn); + } + network_manager::Event::WiFiEnabled(_) + | network_manager::Event::WirelessAccessPoints + | network_manager::Event::ActiveConns => { + if let Some(conn) = self.conn.clone() { + return Task::future(async move { + let conn = conn.clone(); + NetworkManagerState::new(&conn).await + }) + .map(|res| match res { + Ok(s) => Message::UpdateState(s), + Err(err) => Message::Error(err.to_string()), + }) + .map(cosmic::Action::App); + } + } + network_manager::Event::RequestResponse { + mut state, + success, + req, + } => { + if let network_manager::Request::SelectAccessPoint( + ssid, + hw_address, + _network_type, + secret_tx, + ) = &req + { + let conn_match = self + .new_connection + .as_ref() + .is_some_and(|c| c.ssid() == ssid.as_ref() && c.hw_address() == *hw_address); + + if conn_match && success { + if let Some(ActiveConnectionInfo::WiFi { state, .. }) = state + .active_conns + .iter_mut() + .find(|ap| { + let ap_hw_address = match ap { + ActiveConnectionInfo::Wired { hw_address, .. } + | ActiveConnectionInfo::WiFi { hw_address, .. } => { + HwAddress::from_str(&hw_address).unwrap() + } + ActiveConnectionInfo::Vpn { .. } => HwAddress::default(), + }; + ap.name().as_str() == ssid.as_ref() && ap_hw_address == *hw_address}) + { + *state = ActiveConnectionState::Activated; + } + self.failed_known_ssids.remove(ssid); + self.new_connection = None; + self.show_visible_networks = false; + } else if !matches!( + &self.new_connection, + Some(NewConnectionState::EnterPassword { .. }) + ) && !success { + self.failed_known_ssids.insert(ssid.clone()); + } + } else if let network_manager::Request::Authenticate { + ssid, + identity: _, + password: _, + hw_address, + secret_tx + } = &req + { + if let Some(NewConnectionState::Waiting(access_point)) = + self.new_connection.as_ref() + { + if !success + && ssid.as_str() == access_point.ssid.as_ref() + && *hw_address == access_point.hw_address + { + self.new_connection = + Some(NewConnectionState::Failure(access_point.clone())); + } else { + self.show_visible_networks = false; + } + } else if let Some(NewConnectionState::EnterPassword { + access_point, .. + }) = self.new_connection.as_ref() + { + if success && ssid.as_str() == access_point.ssid.as_ref() && *hw_address == access_point.hw_address { + self.new_connection = None; + self.show_visible_networks = false; + } + } + } else if self + .new_connection + .as_ref() + .map(NewConnectionState::ssid).is_some_and(|ssid| { + state.active_conns.iter().any(|c| + matches!(c, ActiveConnectionInfo::WiFi { name, state: ActiveConnectionState::Activated, .. } if ssid == name) + ) + }) { + self.new_connection = None; + self.show_visible_networks = false; + } + + if !matches!(req, network_manager::Request::Reload) + && matches!(state.connectivity, NmConnectivityState::Portal) + { + let mut browser = std::process::Command::new("xdg-open"); + browser.arg("http://204.pop-os.org/"); + + tokio::spawn(cosmic::process::spawn(browser)); + } + + self.update_nm_state(state); + } + + cosmic_settings_network_manager_subscription::Event::Devices => { + if let Some(conn) = self.conn.clone() { + return update_devices(conn).map(cosmic::Action::App); + } + } + cosmic_settings_network_manager_subscription::Event::WiFiCredentials { + ssid, + password, + security_type, + } => {} + }, + Message::NetworkManagerConnect(connection) => { + return cosmic::task::batch(vec![ + self.connect(connection.clone()), + connection_settings(connection), + ]); + } + Message::PasswordUpdate(entered_pw) => { + if let Some(NewConnectionState::EnterPassword { password, .. }) = + &mut self.new_connection + { + *password = entered_pw; } } - Message::DeactivateVpn(name) => { - if let Some(tx) = self.nm_sender.as_ref() { - let _ = tx.unbounded_send(NetworkManagerRequest::DeactivateVpn(name)); + Message::UpdateState(network_manager_state) => { + self.update_nm_state(network_manager_state); + } + Message::UpdateDevices(device_infos) => { + self.nm_state.devices = device_infos.into_iter().map(Arc::new).collect(); + } + Message::WiFiEnable(enable) => { + if let Some(sender) = self.nm_sender.as_mut() { + _ = sender.unbounded_send(network_manager::Request::SetWiFi(enable)); + _ = sender.unbounded_send(network_manager::Request::Reload); } } - Message::ToggleVpnList => { - self.show_available_vpns = !self.show_available_vpns; + Message::SecretAgent(agent_event) => match agent_event { + nm_secret_agent::Event::RequestSecret { + uuid, + name, + description, + previous, + tx, + .. + } => { + if let Some(state) = self.new_connection.as_mut() { + match state { + NewConnectionState::EnterPassword { access_point, .. } + | NewConnectionState::Waiting(access_point) + | NewConnectionState::Failure(access_point) => { + if self + .nm_state + .ssid_to_uuid + .get(access_point.ssid.as_ref()) + .is_some_and(|ap_uuid| ap_uuid.as_ref() == uuid.as_str()) + { + *state = NewConnectionState::EnterPassword { + access_point: access_point.clone(), + description, + identity: String::new(), + password: String::new().into(), + password_hidden: true, + } + } + } + } + } else if self.nm_state.known_vpns.contains_key(uuid.as_str()) { + self.nm_state.requested_vpn = Some(RequestedVpn { + name, + uuid: uuid.into(), + description, + password: previous, + password_hidden: true, + tx, + }); + } + } + nm_secret_agent::Event::CancelGetSecrets { .. } => { + self.new_connection = None; + self.nm_state.requested_vpn = None; + } + nm_secret_agent::Event::Failed(error) => { + tracing::error!("Error from secret agent: {error:?}"); + } + }, + Message::KnownConnections(index_map) => { + self.nm_state.known_vpns = index_map; + } + Message::Refresh => { + if let Some(conn) = self.conn.clone() { + return Task::batch(vec![ + update_state(conn.clone()), + update_devices(conn.clone()), + load_vpns(conn), + ]) + .map(cosmic::Action::App); + } + } + Message::ToggleVPNPasswordVisibility => { + if let Some(requested_vpn) = self.nm_state.requested_vpn.as_mut() { + requested_vpn.password_hidden = !requested_vpn.password_hidden; + } + } + Message::ConnectVPNWithPassword => { + if let Some(RequestedVpn { password, tx, .. }) = self.nm_state.requested_vpn.take() + { + return Task::future(async move { + let mut guard = tx.lock().await; + if let Some(tx) = guard.take() { + let _ = tx.send(password); + } + Message::Refresh + }) + .map(cosmic::Action::App); + } + } + Message::VPNPasswordUpdate(secure_string) => { + if let Some(requested_vpn) = self.nm_state.requested_vpn.as_mut() { + requested_vpn.password = secure_string; + } + } + Message::CancelVPNConnection => { + self.nm_state.requested_vpn = None; } } Task::none() @@ -706,7 +1294,7 @@ impl cosmic::Application for CosmicNetworkApplet { let mut vpn_ethernet_col = column![]; let mut known_wifi = Vec::new(); - for conn in &self.nm_state.active_conns { + for conn in &self.nm_state.nm_state.active_conns { match conn { ActiveConnectionInfo::Vpn { name, ip_addresses } => { if self.hw_device_to_show.is_some() { @@ -744,7 +1332,7 @@ impl cosmic::Application for CosmicNetworkApplet { ip_addresses, } => { if self.hw_device_to_show.is_some() - && *hw_address != self.hw_device_to_show.unwrap() + && HwAddress::from_str(&hw_address) != self.hw_device_to_show { continue; } @@ -806,7 +1394,7 @@ impl cosmic::Application for CosmicNetworkApplet { hw_address, } => { if self.hw_device_to_show.is_some() - && hw_address != self.hw_device_to_show.as_ref().unwrap() + && HwAddress::from_str(&hw_address) != self.hw_device_to_show { continue; } @@ -840,13 +1428,16 @@ impl cosmic::Application for CosmicNetworkApplet { ), _ => {} } - if self.failed_known_ssids.contains(name) { + if self.failed_known_ssids.contains(name.as_str()) { btn_content.push( cosmic::widget::button::icon( from_name("view-refresh-symbolic").size(16), ) .icon_size(16) - .on_press(Message::ResetFailedKnownSsid(name.clone(), *hw_address)) + .on_press(Message::ResetFailedKnownSsid( + name.clone(), + HwAddress::from_str(&hw_address).unwrap(), + )) .into(), ); } @@ -858,7 +1449,10 @@ impl cosmic::Application for CosmicNetworkApplet { .align_y(Alignment::Center) .spacing(8) ) - .on_press(Message::Disconnect(name.clone(), *hw_address)) + .on_press(Message::Disconnect( + Arc::from(name.as_str()), + HwAddress::from_str(&hw_address).unwrap() + )) ] .align_x(Alignment::Center), )); @@ -898,7 +1492,7 @@ impl cosmic::Application for CosmicNetworkApplet { AIRPLANE_MODE, &self.timeline, fl!("airplane-mode"), - self.nm_state.airplane_mode, + self.nm_state.nm_state.airplane_mode, |_chain, enable| { Message::ToggleAirplaneMode(enable) }, ) .text_size(14) @@ -915,8 +1509,8 @@ impl cosmic::Application for CosmicNetworkApplet { WIFI, &self.timeline, fl!("wifi"), - self.nm_state.wifi_enabled, - |_chain, enable| { Message::ToggleWiFi(enable) }, + self.nm_state.nm_state.wifi_enabled, + |_chain, enable| { Message::WiFiEnable(enable) }, ) .text_size(14) .width(Length::Fill) @@ -924,8 +1518,7 @@ impl cosmic::Application for CosmicNetworkApplet { ] .align_x(Alignment::Center) }; - - if self.nm_state.airplane_mode { + if self.nm_state.nm_state.airplane_mode { content = content.push( column!( padded_control(divider::horizontal::default()).padding([space_xxs, space_s]), @@ -942,7 +1535,7 @@ impl cosmic::Application for CosmicNetworkApplet { ); // Show VPN connections even in airplane mode - if !self.nm_state.available_vpns.is_empty() { + if !self.nm_state.known_vpns.is_empty() { content = content.push(vpn_section( &self.nm_state, self.show_available_vpns, @@ -954,7 +1547,7 @@ impl cosmic::Application for CosmicNetworkApplet { return self.view_window_return(content); } - if !self.nm_state.wifi_enabled && !self.nm_state.available_vpns.is_empty() { + if !self.nm_state.nm_state.wifi_enabled && !self.nm_state.known_vpns.is_empty() { // Add VPN connections section when WiFi is disabled content = content.push(vpn_section( &self.nm_state, @@ -970,6 +1563,7 @@ impl cosmic::Application for CosmicNetworkApplet { .push(padded_control(divider::horizontal::default()).padding([space_xxs, space_s])); let wireless_hw_devices = self + .nm_state .nm_state .wireless_access_points .iter() @@ -980,11 +1574,10 @@ impl cosmic::Application for CosmicNetworkApplet { for hw_device in wireless_hw_devices { let display_name = hw_device.to_string(); - let is_connected = self - .nm_state - .active_conns - .iter() - .any(|conn| conn.hw_address() == hw_device); + let is_connected = self.nm_state.nm_state.active_conns.iter().any(|conn| { + let hw_address = active_conn_hw_address(conn); + hw_address == hw_device + }); let mut btn_content = vec![ column![ text::body(display_name), @@ -1020,14 +1613,14 @@ impl cosmic::Application for CosmicNetworkApplet { return self.view_window_return(content); } - for known in &self.nm_state.known_access_points { + for known in &self.nm_state.nm_state.known_access_points { if let Some(filter_hw_address) = self.hw_device_to_show { if filter_hw_address != known.hw_address { continue; } } let mut btn_content = Vec::with_capacity(2); - let ssid = text::body(&known.ssid).width(Length::Fill); + let ssid = text::body(known.ssid.as_ref()).width(Length::Fill); if known.working { btn_content.push( icon::from_name("network-wireless-acquiring-symbolic") @@ -1060,12 +1653,12 @@ impl cosmic::Application for CosmicNetworkApplet { btn_content.push(ssid.into()); } - if self.failed_known_ssids.contains(&known.ssid) { + if self.failed_known_ssids.contains(known.ssid.as_ref()) { btn_content.push( cosmic::widget::button::icon(from_name("view-refresh-symbolic").size(16)) .icon_size(16) .on_press(Message::ResetFailedKnownSsid( - known.ssid.clone(), + known.ssid.to_string(), known.hw_address, )) .into(), @@ -1082,10 +1675,9 @@ impl cosmic::Application for CosmicNetworkApplet { | DeviceState::Unknown | DeviceState::Unmanaged | DeviceState::Disconnected - | DeviceState::NeedAuth => btn.on_press(Message::ActivateKnownWifi( - known.ssid.clone(), - known.hw_address, - )), + | DeviceState::NeedAuth => { + btn.on_press(Message::Connect(known.ssid.clone(), known.hw_address)) + } DeviceState::Activated => { btn.on_press(Message::Disconnect(known.ssid.clone(), known.hw_address)) } @@ -1117,7 +1709,7 @@ impl cosmic::Application for CosmicNetworkApplet { content = content.push(available_connections_btn); if !self.show_visible_networks { - if !self.nm_state.available_vpns.is_empty() { + if !self.nm_state.known_vpns.is_empty() { content = content.push(vpn_section( &self.nm_state, self.show_available_vpns, @@ -1132,6 +1724,7 @@ impl cosmic::Application for CosmicNetworkApplet { match new_conn_state { NewConnectionState::EnterPassword { access_point, + description, identity, password, password_hidden, @@ -1141,7 +1734,7 @@ impl cosmic::Application for CosmicNetworkApplet { icon::from_name("network-wireless-acquiring-symbolic") .size(24) .symbolic(true), - text::body(&access_point.ssid), + text::body(access_point.ssid.as_ref()), ] .align_y(Alignment::Center) .spacing(12), @@ -1149,36 +1742,38 @@ impl cosmic::Application for CosmicNetworkApplet { content = content.push(id); let is_enterprise = matches!(access_point.network_type, NetworkType::EAP); - let enter_password_col = column![] - .push_maybe(is_enterprise.then(|| text::body(fl!("identity")))) - .push_maybe(is_enterprise.then(|| { - text_input::text_input("", identity).on_input(Message::Identity) - })) - .push(text::body(fl!("enter-password"))) - .push( - text_input::secure_input( - "", - password, - Some(Message::TogglePasswordVisibility), - *password_hidden, + let enter_password_col = + column![] + .push_maybe(is_enterprise.then(|| text::body(fl!("identity")))) + .push_maybe(is_enterprise.then(|| { + text_input::text_input("", identity) + .on_input(|i| Message::IdentityUpdate(i)) + })) + .push(text::body(fl!("enter-password"))) + .push_maybe(description.as_ref().map(|d| text::body(d.clone()))) + .push( + text_input::secure_input( + "", + password.unsecure(), + Some(Message::TogglePasswordVisibility), + *password_hidden, + ) + .on_input(|s| Message::PasswordUpdate(SecureString::from(s))) + .on_paste(|s| Message::PasswordUpdate(SecureString::from(s))) + .on_submit(|_| Message::ConnectWithPassword), ) - .on_input(Message::Password) - .on_paste(Message::Password) - .on_submit(|_| Message::SubmitPassword), - ) - .push_maybe( - access_point.wps_push.then(|| { + .push_maybe(access_point.wps_push.then(|| { container(text::body(fl!("router-wps-button"))).padding(8) - }), - ) - .push( - row![ - button::standard(fl!("cancel")) - .on_press(Message::CancelNewConnection), - button::suggested(fl!("connect")).on_press(Message::SubmitPassword) - ] - .spacing(24), - ); + })) + .push( + row![ + button::standard(fl!("cancel")) + .on_press(Message::CancelNewConnection), + button::suggested(fl!("connect")) + .on_press(Message::ConnectWithPassword) + ] + .spacing(24), + ); let col = padded_control(enter_password_col.spacing(8).align_x(Alignment::Center)) .align_x(Alignment::Center); @@ -1189,7 +1784,7 @@ impl cosmic::Application for CosmicNetworkApplet { icon::from_name("network-wireless-acquiring-symbolic") .size(24) .symbolic(true), - text::body(&access_point.ssid), + text::body(access_point.ssid.as_ref()), ] .align_y(Alignment::Center) .width(Length::Fill) @@ -1211,7 +1806,7 @@ impl cosmic::Application for CosmicNetworkApplet { icon::from_name("network-wireless-error-symbolic") .size(24) .symbolic(true), - text::body(&access_point.ssid), + text::body(access_point.ssid.as_ref()), ] .align_y(Alignment::Center) .spacing(12), @@ -1239,17 +1834,17 @@ impl cosmic::Application for CosmicNetworkApplet { } } } else { - let mut list_col = Vec::with_capacity(self.nm_state.wireless_access_points.len()); - for ap in &self.nm_state.wireless_access_points { + let mut list_col = + Vec::with_capacity(self.nm_state.nm_state.wireless_access_points.len()); + for ap in &self.nm_state.nm_state.wireless_access_points { if ap.hw_address != self.hw_device_to_show.unwrap_or(ap.hw_address) { continue; } - if self - .nm_state - .active_conns - .iter() - .any(|a| ap.ssid == a.name() && ap.hw_address == a.hw_address()) - { + + if self.nm_state.nm_state.active_conns.iter().any(|a| { + let hw_address = active_conn_hw_address(a); + ap.ssid.as_ref() == &a.name() && ap.hw_address == hw_address + }) { continue; } let button = menu_button( @@ -1257,7 +1852,7 @@ impl cosmic::Application for CosmicNetworkApplet { icon::from_name(wifi_icon(ap.strength)) .size(16) .symbolic(true), - text::body(&ap.ssid).align_y(Alignment::Center) + text::body(ap.ssid.as_ref()).align_y(Alignment::Center) ] .align_y(Alignment::Center) .spacing(12), @@ -1270,7 +1865,7 @@ impl cosmic::Application for CosmicNetworkApplet { } // Add VPN connections section after wireless networks when they are expanded - if !self.nm_state.available_vpns.is_empty() && self.nm_state.wifi_enabled { + if !self.nm_state.known_vpns.is_empty() && self.nm_state.nm_state.wifi_enabled { content = content.push(vpn_section( &self.nm_state, self.show_available_vpns, @@ -1283,28 +1878,19 @@ impl cosmic::Application for CosmicNetworkApplet { } fn subscription(&self) -> Subscription { - let network_sub = network_manager_subscription(0).map(Message::NetworkManagerEvent); let timeline = self .timeline .as_subscription() .map(|(_, now)| Message::Frame(now)); let token_sub = activation_token_subscription(0).map(Message::Token); - - if let Some(conn) = self.conn.as_ref() { - let has_popup = self.popup.is_some(); + if let Some((conn, id)) = self.conn.clone().zip(self.popup.as_ref()) { Subscription::batch([ + active_conns_subscription(*id, conn).map(|e| Message::NetworkManager(e)), timeline, - network_sub, token_sub, - active_conns_subscription(self.toggle_wifi_ctr, conn.clone()) - .map(Message::NetworkManagerEvent), - devices_subscription(self.toggle_wifi_ctr, has_popup, conn.clone()) - .map(Message::NetworkManagerEvent), - wireless_enabled_subscription(self.toggle_wifi_ctr, conn.clone()) - .map(Message::NetworkManagerEvent), ]) } else { - Subscription::batch([timeline, network_sub, token_sub]) + Subscription::batch([timeline, token_sub]) } } @@ -1316,3 +1902,11 @@ impl cosmic::Application for CosmicNetworkApplet { Some(Message::CloseRequested(id)) } } + +fn active_conn_hw_address(conn: &ActiveConnectionInfo) -> HwAddress { + match conn { + ActiveConnectionInfo::Wired { hw_address, .. } + | ActiveConnectionInfo::WiFi { hw_address, .. } => HwAddress::from_str(hw_address).unwrap(), + ActiveConnectionInfo::Vpn { .. } => HwAddress::default(), + } +} diff --git a/cosmic-applet-network/src/lib.rs b/cosmic-applet-network/src/lib.rs index 1a664d6f..7d5414a5 100644 --- a/cosmic-applet-network/src/lib.rs +++ b/cosmic-applet-network/src/lib.rs @@ -3,7 +3,7 @@ mod app; mod config; mod localize; -mod network_manager; +mod utils; use crate::localize::localize; diff --git a/cosmic-applet-network/src/network_manager/active_conns.rs b/cosmic-applet-network/src/network_manager/active_conns.rs deleted file mode 100644 index e4fa7140..00000000 --- a/cosmic-applet-network/src/network_manager/active_conns.rs +++ /dev/null @@ -1,66 +0,0 @@ -use super::{NetworkManagerEvent, NetworkManagerState}; -use cosmic::{ - iced::{self, Subscription}, - iced_futures::stream, -}; -use cosmic_dbus_networkmanager::nm::NetworkManager; -use futures::{SinkExt, StreamExt}; -use std::{fmt::Debug, hash::Hash}; -use zbus::Connection; - -pub fn active_conns_subscription( - id: I, - conn: Connection, -) -> iced::Subscription { - let initial = State::Continue(conn); - Subscription::run_with_id( - id, - stream::channel(50, move |mut output| { - let mut state = initial; - - async move { - loop { - state = start_listening(state, &mut output).await; - } - } - }), - ) -} - -#[derive(Debug, Clone)] -pub enum State { - Continue(Connection), - Error, -} - -async fn start_listening( - state: State, - output: &mut futures::channel::mpsc::Sender, -) -> State { - let conn = match state { - State::Continue(conn) => conn, - State::Error => iced::futures::future::pending().await, - }; - let network_manager = match NetworkManager::new(&conn).await { - Ok(n) => n, - Err(why) => { - tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager"); - return State::Error; - } - }; - - let mut active_conns_changed = network_manager.receive_active_connections_changed().await; - active_conns_changed.next().await; - - while let (Some(_change), ()) = tokio::join!( - active_conns_changed.next(), - tokio::time::sleep(tokio::time::Duration::from_secs(1)) - ) { - let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - _ = output - .send(NetworkManagerEvent::ActiveConns(new_state)) - .await; - } - - State::Continue(conn) -} diff --git a/cosmic-applet-network/src/network_manager/available_vpns.rs b/cosmic-applet-network/src/network_manager/available_vpns.rs deleted file mode 100644 index ae9a467e..00000000 --- a/cosmic-applet-network/src/network_manager/available_vpns.rs +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -use cosmic_dbus_networkmanager::settings::{NetworkManagerSettings, connection::Settings}; -use zbus::Connection; - -#[derive(Debug, Clone)] -pub struct VpnConnection { - pub name: String, - pub uuid: String, -} - -/// Load all available VPN connections from NetworkManager settings -pub async fn load_vpn_connections(conn: &Connection) -> anyhow::Result> { - let nm_settings = NetworkManagerSettings::new(conn).await?; - let connections = nm_settings.list_connections().await?; - - let mut vpn_connections = Vec::new(); - - for connection in connections { - let settings_map = match connection.get_settings().await { - Ok(s) => s, - Err(_) => continue, - }; - - let settings = Settings::new(settings_map); - - // Check if this is a VPN connection - if let Some(connection_settings) = &settings.connection { - if let Some(conn_type) = &connection_settings.type_ { - // VPN connections have type "vpn" or "wireguard" - if conn_type == "vpn" || conn_type == "wireguard" { - let name = connection_settings - .id - .clone() - .unwrap_or_else(|| "Unknown VPN".to_string()); - let uuid = connection_settings.uuid.clone().unwrap_or_default(); - - vpn_connections.push(VpnConnection { name, uuid }); - } - } - } - } - - // Sort by name for consistent UI - vpn_connections.sort_by(|a, b| a.name.cmp(&b.name)); - - Ok(vpn_connections) -} diff --git a/cosmic-applet-network/src/network_manager/available_wifi.rs b/cosmic-applet-network/src/network_manager/available_wifi.rs deleted file mode 100644 index 3fd7fae1..00000000 --- a/cosmic-applet-network/src/network_manager/available_wifi.rs +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -use cosmic_dbus_networkmanager::{ - device::wireless::WirelessDevice, - interface::{ - access_point::AccessPointProxy, - enums::{ApFlags, ApSecurityFlags, DeviceState}, - }, -}; - -use futures_util::StreamExt; -use rustc_hash::FxHashMap; -use std::collections::HashMap; -use zbus::zvariant::ObjectPath; - -use super::hw_address::HwAddress; - -pub async fn handle_wireless_device( - device: WirelessDevice<'_>, - hw_address: Option, -) -> zbus::Result> { - device.request_scan(HashMap::new()).await?; - let mut scan_changed = device.receive_last_scan_changed().await; - if let Some(t) = scan_changed.next().await { - if let Ok(-1) = t.get().await { - eprintln!("scan errored"); - return Ok(Vec::new()); - } - } - let access_points = device.get_access_points().await?; - let state: DeviceState = device - .upcast() - .await - .and_then(|dev| dev.cached_state()) - .unwrap_or_default() - .map_or(DeviceState::Unknown, |s| s.into()); - // Sort by strength and remove duplicates - let mut aps = FxHashMap::::default(); - for ap in access_points { - let ssid = String::from_utf8_lossy(ap.ssid().await?.as_slice()).into_owned(); - let wps_push = ap.flags().await?.contains(ApFlags::WPS_PBC); - let strength = ap.strength().await?; - if let Some(access_point) = aps.get(&ssid) { - if access_point.strength > strength { - continue; - } - } - let proxy: &AccessPointProxy = ≈ - let Ok(flags) = ap.rsn_flags().await else { - continue; - }; - - let network_type = if flags.intersects(ApSecurityFlags::KEY_MGMT_802_1X) { - NetworkType::EAP - } else if flags.intersects(ApSecurityFlags::KEY_MGMTPSK) { - NetworkType::PSK - } else if flags.is_empty() { - NetworkType::Open - } else { - continue; - }; - - aps.insert( - ssid.clone(), - AccessPoint { - ssid, - strength, - state, - working: false, - path: ap.inner().path().to_owned(), - hw_address: hw_address - .as_ref() - .and_then(|str_addr| HwAddress::from_str(str_addr)) - .unwrap_or_default(), - wps_push, - network_type, - }, - ); - } - let mut aps = aps.into_values().collect::>(); - aps.sort_unstable_by_key(|ap| ap.strength); - Ok(aps) -} - -#[derive(Debug, Clone)] -pub struct AccessPoint { - pub ssid: String, - pub strength: u8, - pub state: DeviceState, - pub working: bool, - pub path: ObjectPath<'static>, - pub hw_address: HwAddress, - pub wps_push: bool, - pub network_type: NetworkType, -} - -// TODO do we want to support eap methods other than peap in the applet? -// Then we'd need a dropdown for the eap method, -// and tls requires a cert instead of a password -#[allow(clippy::upper_case_acronyms)] -#[derive(Debug, Clone, Copy)] -pub enum NetworkType { - Open, - PSK, - EAP, -} diff --git a/cosmic-applet-network/src/network_manager/current_networks.rs b/cosmic-applet-network/src/network_manager/current_networks.rs deleted file mode 100644 index d063a54e..00000000 --- a/cosmic-applet-network/src/network_manager/current_networks.rs +++ /dev/null @@ -1,193 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -use cosmic_dbus_networkmanager::{ - active_connection::ActiveConnection, device::SpecificDevice, - interface::enums::ActiveConnectionState, -}; -use std::net::Ipv4Addr; - -use super::hw_address::HwAddress; - -/// Read network interface speed from sysfs -/// Returns speed in Mbps, or None if unable to read -fn read_speed_from_sysfs(interface: &str) -> Option { - let path = format!("/sys/class/net/{}/speed", interface); - std::fs::read_to_string(path) - .ok() - .and_then(|content| content.trim().parse::().ok()) - .and_then(|speed| if speed > 0 { Some(speed as u32) } else { None }) -} - -pub async fn active_connections( - active_connections: Vec>, -) -> zbus::Result> { - let mut info = Vec::::with_capacity(active_connections.len()); - for connection in active_connections { - let ipv4 = connection - .ip4_config() - .await? - .address_data() - .await - .unwrap_or_default(); - let addresses: Vec<_> = ipv4.iter().map(|d| d.address).collect(); - let state = connection - .state() - .await - .unwrap_or(ActiveConnectionState::Unknown); - - if connection.vpn().await.unwrap_or_default() { - info.push(ActiveConnectionInfo::Vpn { - name: connection.id().await?, - ip_addresses: addresses.clone(), - }); - continue; - } - for device in connection.devices().await.unwrap_or_default() { - let interface_name = device.interface().await.ok(); - - match device - .downcast_to_device() - .await - .ok() - .and_then(|inner| inner) - { - Some(SpecificDevice::Wired(wired_device)) => { - let mut speed = wired_device.speed().await?; - - // If NetworkManager returns 0, try to read from sysfs - if speed == 0 { - if let Some(interface) = interface_name.as_ref() { - speed = read_speed_from_sysfs(interface).unwrap_or(0); - } - } - - info.push(ActiveConnectionInfo::Wired { - name: connection.id().await?, - hw_address: HwAddress::from_str(&wired_device.hw_address().await?) - .unwrap_or_default(), - speed, - ip_addresses: addresses.clone(), - }); - } - Some(SpecificDevice::Wireless(wireless_device)) => { - if let Ok(access_point) = wireless_device.active_access_point().await { - info.push(ActiveConnectionInfo::WiFi { - name: String::from_utf8_lossy(&access_point.ssid().await?).into_owned(), - ip_addresses: addresses.clone(), - hw_address: HwAddress::from_str(&wireless_device.hw_address().await?) - .unwrap_or_default(), - state, - strength: access_point.strength().await.unwrap_or_default(), - }); - } - } - Some(SpecificDevice::WireGuard(_)) => { - info.push(ActiveConnectionInfo::Vpn { - name: connection.id().await?, - ip_addresses: addresses.clone(), - }); - } - _ => {} - } - } - } - - info.sort_unstable(); - Ok(info) -} - -#[derive(Debug, Clone)] -pub enum ActiveConnectionInfo { - Wired { - name: String, - hw_address: HwAddress, - speed: u32, - ip_addresses: Vec, - }, - WiFi { - name: String, - ip_addresses: Vec, - hw_address: HwAddress, - state: ActiveConnectionState, - strength: u8, - }, - Vpn { - name: String, - ip_addresses: Vec, - }, -} - -impl ActiveConnectionInfo { - pub fn name(&self) -> String { - match &self { - Self::Wired { name, .. } | Self::WiFi { name, .. } | Self::Vpn { name, .. } => { - name.clone() - } - } - } - pub fn hw_address(&self) -> HwAddress { - match &self { - Self::Wired { hw_address, .. } | Self::WiFi { hw_address, .. } => *hw_address, - Self::Vpn { .. } => HwAddress::default(), - } - } -} - -impl std::cmp::Ord for ActiveConnectionInfo { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match (self, other) { - (Self::Vpn { .. }, Self::Wired { .. } | Self::WiFi { .. }) - | (Self::Wired { .. }, Self::WiFi { .. }) => std::cmp::Ordering::Less, - - (Self::WiFi { .. }, Self::Wired { .. } | Self::Vpn { .. }) - | (Self::Wired { .. }, Self::Vpn { .. }) => std::cmp::Ordering::Greater, - - (Self::Vpn { name: n1, .. }, Self::Vpn { name: n2, .. }) - | (Self::Wired { name: n1, .. }, Self::Wired { name: n2, .. }) - | (Self::WiFi { name: n1, .. }, Self::WiFi { name: n2, .. }) => n1.cmp(n2), - } - } -} - -impl std::cmp::Eq for ActiveConnectionInfo {} - -impl std::cmp::PartialOrd for ActiveConnectionInfo { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl std::cmp::PartialEq for ActiveConnectionInfo { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - ( - Self::Wired { - name: n1, - hw_address: a1, - .. - }, - Self::Wired { - name: n2, - hw_address: a2, - .. - }, - ) - | ( - Self::WiFi { - name: n1, - hw_address: a1, - .. - }, - Self::WiFi { - name: n2, - hw_address: a2, - .. - }, - ) => n1 == n2 && a1 == a2, - - (Self::Vpn { name: n1, .. }, Self::Vpn { name: n2, .. }) => n1 == n2, - - _ => false, - } - } -} diff --git a/cosmic-applet-network/src/network_manager/devices.rs b/cosmic-applet-network/src/network_manager/devices.rs deleted file mode 100644 index 6d68e38d..00000000 --- a/cosmic-applet-network/src/network_manager/devices.rs +++ /dev/null @@ -1,65 +0,0 @@ -use super::{NetworkManagerEvent, NetworkManagerState}; -use cosmic::iced::{self, Subscription, stream}; -use cosmic_dbus_networkmanager::nm::NetworkManager; -use futures::{SinkExt, StreamExt}; -use std::{fmt::Debug, hash::Hash}; -use zbus::Connection; - -pub fn devices_subscription( - id: I, - has_popup: bool, - conn: Connection, -) -> iced::Subscription { - let initial = State::Continue(conn); - Subscription::run_with_id( - (id, has_popup), - stream::channel(50, move |mut output| { - let mut state = initial.clone(); - - async move { - loop { - state = start_listening(state, has_popup, &mut output).await; - } - } - }), - ) -} - -#[derive(Debug, Clone)] -pub enum State { - Continue(Connection), - Error, -} - -async fn start_listening( - state: State, - has_popup: bool, - output: &mut futures::channel::mpsc::Sender, -) -> State { - let conn = match state { - State::Continue(conn) => conn, - State::Error => iced::futures::future::pending().await, - }; - let network_manager = match NetworkManager::new(&conn).await { - Ok(n) => n, - Err(why) => { - tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager"); - return State::Error; - } - }; - - let mut devices_changed = network_manager.receive_devices_changed().await; - - let secs = if has_popup { 4 } else { 60 }; - while let (Some(_change), ()) = tokio::join!( - devices_changed.next(), - tokio::time::sleep(tokio::time::Duration::from_secs(secs)) - ) { - let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - _ = output - .send(NetworkManagerEvent::WirelessAccessPoints(new_state)) - .await; - } - - State::Continue(conn) -} diff --git a/cosmic-applet-network/src/network_manager/hw_address.rs b/cosmic-applet-network/src/network_manager/hw_address.rs deleted file mode 100644 index 4eb89ed9..00000000 --- a/cosmic-applet-network/src/network_manager/hw_address.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::fmt::Write; - -#[derive(Copy, Clone, PartialEq, Eq, Default, Debug, PartialOrd, Ord)] -pub struct HwAddress { - address: u64, -} - -impl HwAddress { - pub fn from_str(arg: &str) -> Option { - let columnless_vec = arg.split(':').collect::>(); - if columnless_vec.len() * 3 - 1 != arg.len() { - return None; - } - for byte in &columnless_vec { - if byte.len() != 2 { - return None; - } - } - u64::from_str_radix(columnless_vec.join("").as_str(), 16) - .ok() - .map(|address| HwAddress { address }) - } -} - -impl std::fmt::Display for HwAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for (index, c) in format!("{:x}", self.address).char_indices() { - if index != 0 && index % 2 == 0 { - f.write_char(':')?; - } - f.write_char(c)?; - } - - Ok(()) - } -} diff --git a/cosmic-applet-network/src/network_manager/mod.rs b/cosmic-applet-network/src/network_manager/mod.rs deleted file mode 100644 index f37af94f..00000000 --- a/cosmic-applet-network/src/network_manager/mod.rs +++ /dev/null @@ -1,822 +0,0 @@ -pub mod active_conns; -pub mod available_vpns; -pub mod available_wifi; -pub mod current_networks; -pub mod devices; -pub mod hw_address; -pub mod wireless_enabled; - -use std::{collections::HashMap, fmt::Debug, time::Duration}; - -use available_wifi::NetworkType; -use cosmic::{ - iced::{self, Subscription}, - iced_futures::stream, -}; -use cosmic_dbus_networkmanager::{ - active_connection::ActiveConnection, - device::SpecificDevice, - interface::{ - active_connection::ActiveConnectionProxy, - enums::{self, ActiveConnectionState, DeviceType, NmConnectivityState}, - }, - nm::NetworkManager, - settings::{NetworkManagerSettings, connection::Settings}, -}; -use futures::{ - SinkExt, StreamExt, - channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}, -}; -use hw_address::HwAddress; -use tokio::process::Command; -use zbus::{ - Connection, - zvariant::{self, Value}, -}; - -use self::{ - available_vpns::{VpnConnection, load_vpn_connections}, - available_wifi::{AccessPoint, handle_wireless_device}, - current_networks::{ActiveConnectionInfo, active_connections}, -}; - -// In some distros, rfkill is only in sbin, which isn't normally in PATH -// TODO: Directly access `/dev/rfkill` -fn rfkill_path_var() -> std::ffi::OsString { - let mut path = std::env::var_os("PATH").unwrap_or_default(); - path.push(":/usr/sbin"); - path -} - -#[derive(Debug)] -pub enum State { - Ready, - Waiting(Connection, UnboundedReceiver), - Finished, -} - -pub fn network_manager_subscription( - id: I, -) -> iced::Subscription { - Subscription::run_with_id( - id, - stream::channel(50, |mut output| async move { - let mut state = State::Ready; - - loop { - state = start_listening(state, &mut output).await; - } - }), - ) -} - -async fn start_listening( - state: State, - output: &mut futures::channel::mpsc::Sender, -) -> State { - match state { - State::Ready => { - let Ok(conn) = Connection::system().await else { - return State::Finished; - }; - - let (tx, rx) = unbounded(); - let nm_state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - if output - .send(NetworkManagerEvent::Init { - conn: conn.clone(), - sender: tx, - state: nm_state, - }) - .await - .is_ok() - { - State::Waiting(conn, rx) - } else { - State::Finished - } - } - State::Waiting(conn, mut rx) => { - let Ok(network_manager) = NetworkManager::new(&conn).await else { - return State::Finished; - }; - - match rx.next().await { - Some(NetworkManagerRequest::Disconnect(ssid, hw_address)) => { - let mut success = false; - for c in network_manager - .active_connections() - .await - .unwrap_or_default() - { - if c.id().await.unwrap_or_default() != ssid { - continue; - } - let mut is_there_device = false; - for device in c.devices().await.unwrap_or_default() { - if HwAddress::from_str(device.hw_address().await.as_ref().unwrap()) - == Some(hw_address) - { - is_there_device = true; - } - } - - if is_there_device - && network_manager.deactivate_connection(&c).await.is_ok() - { - success = true; - if let Ok(ActiveConnectionState::Deactivated) = c.state().await { - break; - } else { - let mut changed = c.receive_state_changed().await; - _ = tokio::time::timeout(Duration::from_secs(5), async move { - loop { - if let Some(next) = changed.next().await { - if let Ok(ActiveConnectionState::Deactivated) = - next.get().await.map(ActiveConnectionState::from) - { - break; - } - } - } - }) - .await; - } - break; - } - } - _ = output - .send(NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::Disconnect(ssid.clone(), hw_address), - success, - state: NetworkManagerState::new(&conn).await.unwrap_or_default(), - }) - .await; - } - Some(NetworkManagerRequest::SetAirplaneMode(airplane_mode)) => { - // wifi - let mut success = network_manager - .set_wireless_enabled(!airplane_mode) - .await - .is_ok(); - // bluetooth - success = success - && Command::new("rfkill") - .env("PATH", rfkill_path_var()) - .arg(if airplane_mode { "block" } else { "unblock" }) - .arg("bluetooth") - .output() - .await - .is_ok(); - let mut state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - state.airplane_mode = if success { - airplane_mode - } else { - !airplane_mode - }; - if state.airplane_mode { - state.wifi_enabled = false; - } - _ = output - .send(NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::SetAirplaneMode(airplane_mode), - success, - state, - }) - .await; - } - Some(NetworkManagerRequest::SetWiFi(enabled)) => { - let success = network_manager.set_wireless_enabled(enabled).await.is_ok(); - let mut state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - state.wifi_enabled = if success { enabled } else { !enabled }; - let response = NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::SetWiFi(enabled), - success, - state, - }; - _ = output.send(response).await; - } - Some(NetworkManagerRequest::Authenticate { - ssid, - identity, - password, - hw_address, - }) => { - let nm_state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - let mut success = true; - let err = nm_state - .connect_wifi( - &conn, - &ssid, - identity.as_deref(), - Some(&password), - hw_address, - ) - .await; - - if let Err(err) = err { - success = false; - tracing::error!("{:?}", &err); - } - - _ = output - .send(NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::Authenticate { - ssid: ssid.clone(), - identity: identity.clone(), - password: password.clone(), - hw_address, - }, - success, - state: NetworkManagerState::new(&conn).await.unwrap_or_default(), - }) - .await; - } - Some(NetworkManagerRequest::SelectAccessPoint(ssid, hw_address, network_type)) => { - if matches!(network_type, NetworkType::Open) { - attempt_wifi_connection(&conn, ssid, hw_address, network_type, output) - .await; - } else { - // For secured networks, check if we have saved credentials - if !has_saved_wifi_credentials(&conn, &ssid).await { - return State::Waiting(conn, rx); - } - - // We have saved credentials, attempt connection - attempt_wifi_connection(&conn, ssid, hw_address, network_type, output) - .await; - } - } - Some(NetworkManagerRequest::Reload) => { - let state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - _ = output - .send(NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::Reload, - success: true, - state, - }) - .await; - } - Some(NetworkManagerRequest::Forget(ssid, hw_address)) => { - let s = NetworkManagerSettings::new(&conn).await.unwrap(); - let known_conns = s.list_connections().await.unwrap_or_default(); - let mut success = false; - for c in known_conns { - let settings = c.get_settings().await.ok().unwrap_or_default(); - let s = Settings::new(settings); - if s.wifi - .as_ref() - .and_then(|w| w.ssid.as_deref()) - .is_some_and(|s| std::str::from_utf8(s).is_ok_and(|s| s == ssid)) - { - // todo most likely we can here forget ssid from wrong hw_address - _ = c.delete().await; - success = true; - break; - } - } - let state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - _ = output - .send(NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::Forget(ssid.clone(), hw_address), - success, - state, - }) - .await; - } - Some(NetworkManagerRequest::ActivateVpn(uuid)) => { - tracing::info!("Activating VPN with UUID: {}", uuid); - let network_manager = match NetworkManager::new(&conn).await { - Ok(n) => n, - Err(e) => { - tracing::error!("Failed to connect to NetworkManager: {:?}", e); - _ = output - .send(NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::ActivateVpn(uuid), - success: false, - state: NetworkManagerState::new(&conn) - .await - .unwrap_or_default(), - }) - .await; - return State::Waiting(conn, rx); - } - }; - - let mut success = false; - - // Find the connection by UUID - if let Ok(nm_settings) = NetworkManagerSettings::new(&conn).await { - if let Ok(connections) = nm_settings.list_connections().await { - for connection in connections { - if let Ok(settings) = connection.get_settings().await { - let settings = Settings::new(settings); - if let Some(conn_settings) = &settings.connection { - if conn_settings.uuid.as_ref() == Some(&uuid) { - // Activate the VPN connection without a specific device - // Call the D-Bus method directly since VPNs don't need a device - use zbus::zvariant::ObjectPath; - let empty_device = ObjectPath::try_from("/").unwrap(); - - match network_manager - .inner() - .call_method( - "ActivateConnection", - &( - connection.inner().path(), - empty_device.clone(), - empty_device, - ), - ) - .await - { - Ok(_) => { - tracing::info!( - "Successfully activated VPN: {}", - uuid - ); - success = true; - } - Err(e) => { - tracing::error!( - "Failed to activate VPN {}: {:?}", - uuid, - e - ); - } - } - break; - } - } - } - } - } - } - - if !success { - tracing::warn!( - "VPN connection with UUID {} not found or failed to activate", - uuid - ); - } - - let state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - _ = output - .send(NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::ActivateVpn(uuid), - success, - state, - }) - .await; - } - Some(NetworkManagerRequest::DeactivateVpn(name)) => { - tracing::info!("Deactivating VPN: {}", name); - let network_manager = match NetworkManager::new(&conn).await { - Ok(n) => n, - Err(e) => { - tracing::error!("Failed to connect to NetworkManager: {:?}", e); - _ = output - .send(NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::DeactivateVpn(name), - success: false, - state: NetworkManagerState::new(&conn) - .await - .unwrap_or_default(), - }) - .await; - return State::Waiting(conn, rx); - } - }; - - let mut success = false; - - // Find and deactivate the active VPN connection by name - if let Ok(active_connections) = network_manager.active_connections().await { - for active_conn in active_connections { - if let Ok(conn_id) = active_conn.id().await { - if conn_id == name && active_conn.vpn().await.unwrap_or(false) { - match network_manager.deactivate_connection(&active_conn).await - { - Ok(_) => { - tracing::info!( - "Successfully deactivated VPN: {}", - name - ); - success = true; - break; - } - Err(e) => { - tracing::error!( - "Failed to deactivate VPN {}: {:?}", - name, - e - ); - } - } - } - } - } - } - - if !success { - tracing::warn!( - "Active VPN connection '{}' not found or failed to deactivate", - name - ); - } - - let state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - _ = output - .send(NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::DeactivateVpn(name), - success, - state, - }) - .await; - } - _ => { - return State::Finished; - } - } - - State::Waiting(conn, rx) - } - State::Finished => iced::futures::future::pending().await, - } -} - -async fn has_saved_wifi_credentials(conn: &Connection, ssid: &str) -> bool { - let Ok(nm_settings) = NetworkManagerSettings::new(conn).await else { - return false; - }; - - let known_conns = nm_settings.list_connections().await.unwrap_or_default(); - - for connection in known_conns { - if let Ok(settings) = connection.get_settings().await { - let settings = Settings::new(settings); - if let Some(saved_ssid) = settings - .wifi - .and_then(|w| w.ssid) - .and_then(|ssid| String::from_utf8(ssid).ok()) - { - if saved_ssid == ssid { - return true; - } - } - } - } - - false -} - -async fn attempt_wifi_connection( - conn: &Connection, - ssid: String, - hw_address: HwAddress, - network_type: NetworkType, - output: &mut futures::channel::mpsc::Sender, -) { - let state = NetworkManagerState::new(conn).await.unwrap_or_default(); - - let success = if let Err(err) = state - .connect_wifi(conn, ssid.as_ref(), None, None, hw_address) - .await - { - tracing::error!("Failed to connect to access point: {:?}", err); - false - } else { - true - }; - - _ = output - .send(NetworkManagerEvent::RequestResponse { - req: NetworkManagerRequest::SelectAccessPoint(ssid, hw_address, network_type), - success, - state: NetworkManagerState::new(conn).await.unwrap_or_default(), - }) - .await; -} - -#[derive(Debug, Clone)] -pub enum NetworkManagerRequest { - SetAirplaneMode(bool), - SetWiFi(bool), - SelectAccessPoint(String, HwAddress, NetworkType), - Disconnect(String, HwAddress), - Authenticate { - ssid: String, - identity: Option, - password: String, - hw_address: HwAddress, - }, - Forget(String, HwAddress), - Reload, - ActivateVpn(String), // UUID of VPN connection to activate - DeactivateVpn(String), // Name of active VPN connection to deactivate -} - -#[derive(Debug, Clone)] -pub enum NetworkManagerEvent { - RequestResponse { - req: NetworkManagerRequest, - state: NetworkManagerState, - success: bool, - }, - Init { - conn: Connection, - sender: UnboundedSender, - state: NetworkManagerState, - }, - WiFiEnabled(NetworkManagerState), - WirelessAccessPoints(NetworkManagerState), - ActiveConns(NetworkManagerState), -} - -#[derive(Debug, Clone)] -pub struct NetworkManagerState { - pub wireless_access_points: Vec, - pub active_conns: Vec, - pub known_access_points: Vec, - pub available_vpns: Vec, - pub wifi_enabled: bool, - pub airplane_mode: bool, - pub connectivity: NmConnectivityState, -} - -impl Default for NetworkManagerState { - fn default() -> Self { - Self { - wireless_access_points: Vec::new(), - active_conns: Vec::new(), - known_access_points: Vec::new(), - available_vpns: Vec::new(), - wifi_enabled: false, - airplane_mode: false, - connectivity: NmConnectivityState::Unknown, - } - } -} - -impl NetworkManagerState { - pub async fn new(conn: &Connection) -> anyhow::Result { - let network_manager = NetworkManager::new(conn).await?; - let mut self_ = Self::default(); - // airplane mode - let airplaine_mode = Command::new("rfkill") - .env("PATH", rfkill_path_var()) - .arg("list") - .arg("bluetooth") - .output() - .await?; - let airplane_mode = std::str::from_utf8(&airplaine_mode.stdout).unwrap_or_default(); - self_.wifi_enabled = network_manager.wireless_enabled().await.unwrap_or_default(); - self_.airplane_mode = airplane_mode.contains("Soft blocked: yes") && !self_.wifi_enabled; - - let s = NetworkManagerSettings::new(conn).await?; - _ = s.load_connections(&[]).await; - let known_conns = s.list_connections().await.unwrap_or_default(); - let active_conns = active_connections( - network_manager - .active_connections() - .await - .unwrap_or_default(), - ) - .await - .unwrap_or_default(); - // active_conns.sort(); active_connections should have already sorted the vector - let devices = network_manager.devices().await.ok().unwrap_or_default(); - let wireless_access_point_futures: Vec<_> = devices - .into_iter() - .map(|device| async move { - if let Ok(Some(SpecificDevice::Wireless(wireless_device))) = - device.downcast_to_device().await - { - handle_wireless_device(wireless_device, device.hw_address().await.ok()) - .await - .unwrap_or_default() - } else { - Vec::new() - } - }) - .collect(); - let mut wireless_access_points = Vec::with_capacity(wireless_access_point_futures.len()); - for f in wireless_access_point_futures { - let mut access_points = f.await; - wireless_access_points.append(&mut access_points); - } - let mut known_ssid = Vec::with_capacity(known_conns.len()); - for c in known_conns { - let Ok(s) = c.get_settings().await else { - tracing::info!("Failed to get settings for known connection"); - continue; - }; - let s = Settings::new(s); - if let Some(cur_ssid) = s - .wifi - .clone() - .and_then(|w| w.ssid) - .and_then(|ssid| String::from_utf8(ssid).ok()) - { - known_ssid.push(cur_ssid); - } - } - let known_access_points: Vec<_> = wireless_access_points - .iter() - .filter(|a| { - known_ssid.contains(&a.ssid) - && !active_conns - .iter() - .any(|ac| ac.name() == a.ssid && ac.hw_address() == a.hw_address) - }) - .cloned() - .collect(); - wireless_access_points.sort_by(|a, b| b.strength.cmp(&a.strength)); - self_.wireless_access_points = wireless_access_points; - for ap in &self_.wireless_access_points { - tracing::info!( - "AP ssid: {},\ttype: {:?},\tworking: {},\tstate: {:?}", - ap.ssid, - ap.network_type, - ap.working, - ap.state - ); - } - self_.active_conns = active_conns; - self_.known_access_points = known_access_points; - self_.connectivity = network_manager.connectivity().await?; - - // Load available VPN connections - self_.available_vpns = load_vpn_connections(conn).await.unwrap_or_default(); - - Ok(self_) - } - - #[allow(dead_code)] - pub fn clear(&mut self) { - self.active_conns = Vec::new(); - self.known_access_points = Vec::new(); - self.wireless_access_points = Vec::new(); - self.available_vpns = Vec::new(); - } - - async fn connect_wifi( - &self, - conn: &Connection, - ssid: &str, - identity: Option<&str>, - password: Option<&str>, - hw_address: HwAddress, - ) -> anyhow::Result<()> { - let nm = NetworkManager::new(conn).await?; - - for c in nm.active_connections().await.unwrap_or_default() { - if self.wireless_access_points.iter().any(|w| { - c.cached_id() - .is_ok_and(|opt| opt.is_some_and(|id| id == w.ssid)) - && w.hw_address == hw_address - }) { - _ = nm.deactivate_connection(&c).await; - } - } - - let Some(ap) = self - .wireless_access_points - .iter() - .find(|ap| ap.ssid == ssid && ap.hw_address == hw_address) - else { - return Err(anyhow::anyhow!("Access point not found")); - }; - - let mut conn_settings: HashMap<&str, HashMap<&str, zvariant::Value>> = HashMap::from([ - ( - "802-11-wireless", - HashMap::from([("ssid", Value::Array(ssid.as_bytes().into()))]), - ), - ( - "connection", - HashMap::from([ - ("id", Value::Str(ssid.into())), - ("type", Value::Str("802-11-wireless".into())), - ]), - ), - ]); - - if let Some(identity) = identity { - conn_settings.insert( - "802-1x", - HashMap::from([ - ("identity", Value::Str(identity.into())), - // most common default - ("eap", Value::Array(["peap"].as_slice().into())), - // most common default - ("phase2-auth", Value::Str("mschapv2".into())), - ("password", Value::Str(password.unwrap_or("").into())), - ]), - ); - let wireless = conn_settings.get_mut("802-11-wireless").unwrap(); - wireless.insert("security", Value::Str("802-11-wireless-security".into())); - wireless.insert("mode", Value::Str("infrastructure".into())); - conn_settings.insert( - "802-11-wireless-security", - HashMap::from([("key-mgmt", Value::Str("wpa-eap".into()))]), - ); - } else if let Some(pass) = password { - conn_settings.insert( - "802-11-wireless-security", - HashMap::from([ - ("psk", Value::Str(pass.into())), - ("key-mgmt", Value::Str("wpa-psk".into())), - ]), - ); - } - - let devices = nm.devices().await?; - for device in devices { - let device_hw_address = device - .hw_address() - .await - .ok() - .and_then(|device_address| HwAddress::from_str(&device_address)) - .unwrap_or_default(); - if device_hw_address != hw_address { - continue; - } - if !matches!( - device.device_type().await.unwrap_or(DeviceType::Other), - DeviceType::Wifi - ) { - continue; - } - - let s = NetworkManagerSettings::new(conn).await?; - let known_conns = s.list_connections().await.unwrap_or_default(); - let mut known_conn = None; - for c in known_conns { - let settings = c.get_settings().await.ok().unwrap_or_default(); - - let s = Settings::new(settings); - // todo try to add hw_address comparing here if it changes anything - if s.wifi - .as_ref() - .and_then(|w| w.ssid.as_deref()) - .is_some_and(|s| std::str::from_utf8(s).is_ok_and(|cur_ssid| cur_ssid == ssid)) - { - known_conn = Some(c); - break; - } - } - - let active_conn = if let Some(known_conn) = known_conn.as_ref() { - // update settings if needed - if password.is_some() { - known_conn.update(conn_settings).await?; - } - - nm.activate_connection(known_conn, &device).await? - } else { - let (_, active_conn) = nm - .add_and_activate_connection(conn_settings, device.inner().path(), &ap.path) - .await?; - let dummy = ActiveConnectionProxy::new(conn, active_conn).await?; - let active = ActiveConnectionProxy::builder(conn) - .destination(dummy.inner().destination().to_owned()) - .unwrap() - .interface(dummy.inner().interface().to_owned()) - .unwrap() - .path(dummy.inner().path().to_owned()) - .unwrap() - .build() - .await - .unwrap(); - ActiveConnection::from(active) - }; - let mut changes = active_conn.receive_state_changed().await; - () = tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - let mut count = 5; - loop { - let state = active_conn.state().await; - if let Ok(enums::ActiveConnectionState::Activated) = state { - return Ok(()); - } else if let Ok(enums::ActiveConnectionState::Deactivated) = state { - anyhow::bail!("Failed to activate connection"); - } - if let Ok(Some(s)) = - tokio::time::timeout(Duration::from_secs(20), changes.next()).await - { - let state = s.get().await.unwrap_or_default().into(); - if matches!(state, enums::ActiveConnectionState::Activated) { - return Ok(()); - } - } - - count -= 1; - if count <= 0 { - anyhow::bail!("Failed to activate connection"); - } - } - } - - Err(anyhow::anyhow!("No wifi device found")) - } -} diff --git a/cosmic-applet-network/src/network_manager/wireless_enabled.rs b/cosmic-applet-network/src/network_manager/wireless_enabled.rs deleted file mode 100644 index 850ec36e..00000000 --- a/cosmic-applet-network/src/network_manager/wireless_enabled.rs +++ /dev/null @@ -1,62 +0,0 @@ -use super::{NetworkManagerEvent, NetworkManagerState}; -use cosmic::{ - iced::{self, Subscription}, - iced_futures::stream, -}; -use cosmic_dbus_networkmanager::nm::NetworkManager; -use futures::{SinkExt, StreamExt}; -use std::{fmt::Debug, hash::Hash}; -use zbus::Connection; - -pub fn wireless_enabled_subscription( - id: I, - conn: Connection, -) -> iced::Subscription { - let initial = State::Continue(conn); - Subscription::run_with_id( - id, - stream::channel(50, move |mut output| { - let mut state = initial; - - async move { - loop { - state = start_listening(state, &mut output).await; - } - } - }), - ) -} - -#[derive(Debug, Clone)] -pub enum State { - Continue(Connection), - Error, -} - -async fn start_listening( - state: State, - output: &mut futures::channel::mpsc::Sender, -) -> State { - let conn = match state { - State::Continue(conn) => conn, - State::Error => iced::futures::future::pending().await, - }; - - let network_manager = match NetworkManager::new(&conn).await { - Ok(n) => n, - Err(why) => { - tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager"); - return State::Error; - } - }; - - let mut wireless_enabled_changed = network_manager.receive_wireless_enabled_changed().await; - - while let Some(_change) = wireless_enabled_changed.next().await { - let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - _ = output - .send(NetworkManagerEvent::WiFiEnabled(new_state)) - .await; - } - State::Continue(conn) -} diff --git a/cosmic-applet-network/src/utils.rs b/cosmic-applet-network/src/utils.rs new file mode 100644 index 00000000..f802010f --- /dev/null +++ b/cosmic-applet-network/src/utils.rs @@ -0,0 +1,18 @@ +use futures_util::future::select; + +/// Spawn a background tasks and forward its messages +pub fn forward_event_loop + Send + 'static>( + event_loop: impl FnOnce(async_fn_stream::StreamEmitter) -> T + Send + 'static, +) -> (tokio::sync::oneshot::Sender<()>, cosmic::Task) { + let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>(); + + let task = cosmic::Task::stream(async_fn_stream::fn_stream(|emitter| async move { + select( + std::pin::pin!(cancel_rx), + std::pin::pin!(event_loop(emitter)), + ) + .await; + })); + + (cancel_tx, task) +}