diff --git a/Cargo.toml b/Cargo.toml index 4c31c2d..d4242af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,9 +90,7 @@ cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", # iced_winit = { git = "https://github.com/pop-os/libcosmic//", branch = "theme-v2" } # [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 = { git = "https://github.com/pop-os/dbus-settings-bindings//", branch = "nm-secret-agent" } [patch.crates-io] atspi = { git = "https://github.com/wash2/atspi" } diff --git a/cosmic-settings/Cargo.toml b/cosmic-settings/Cargo.toml index 6bf0692..febae53 100644 --- a/cosmic-settings/Cargo.toml +++ b/cosmic-settings/Cargo.toml @@ -21,8 +21,6 @@ color-eyre = "0.6.5" cosmic-bg-config.workspace = true cosmic-comp-config = { workspace = true, optional = true } cosmic-config.workspace = true -cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } cosmic-idle-config.workspace = true cosmic-panel-config = { workspace = true, optional = true } cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", optional = true } @@ -34,7 +32,6 @@ cosmic-settings-accessibility-subscription = { path = "../subscriptions/accessib cosmic-settings-a11y-manager-subscription = { path = "../subscriptions/a11y-manager", optional = true } cosmic-settings-airplane-mode-subscription = { path = "../subscriptions/airplane-mode", optional = true } cosmic-settings-bluetooth-subscription = { path = "../subscriptions/bluetooth", optional = true } -cosmic-settings-network-manager-subscription = { path = "../subscriptions/network-manager", optional = true } cosmic-settings-sound = { path = "../pages/sound", optional = true } cosmic-settings-upower-subscription = { path = "../subscriptions/upower", optional = true } cosmic-settings-wallpaper = { path = "../pages/wallpapers" } @@ -57,11 +54,13 @@ locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = sysinfo = { version = "=0.38.0", optional = true } mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", features = ["tokio"], optional = true } notify = "8.2.0" +nmrs = { version = "3.4.0", optional = true } regex = "1.12.3" ron = "0.12" rust-embed = "8.11.0" sctk = { workspace = true, optional = true } secure-string = "0.3.0" +secret-service = { version = "5.1.0", features = ["rt-tokio-crypto-rust"], optional = true } serde = { version = "1.0.228", features = ["derive"] } slab = "0.4.12" slotmap = "1.1.1" @@ -169,10 +168,9 @@ page-input = [ ] page-legacy-applications = ["cosmic-comp-config", "dep:cosmic-randr"] page-networking = [ - "dep:cosmic-settings-network-manager-subscription", "xdg-portal", - "dep:cosmic-dbus-networkmanager", - "dep:nm-secret-agent-manager", + "dep:nmrs", + "dep:secret-service", "dep:zbus", ] page-power = ["dep:upower_dbus", "dep:zbus"] diff --git a/cosmic-settings/src/pages/networking/backend.rs b/cosmic-settings/src/pages/networking/backend.rs new file mode 100644 index 0000000..eec2e4e --- /dev/null +++ b/cosmic-settings/src/pages/networking/backend.rs @@ -0,0 +1,1812 @@ +// Copyright 2026 System76 +// SPDX-License-Identifier: GPL-3.0-only + +use std::collections::{BTreeMap, HashMap}; +use std::fmt; +use std::net::Ipv4Addr; +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; + +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}; +use futures::{FutureExt, SinkExt, StreamExt}; +use nmrs::agent::{SecretAgent, SecretAgentFlags, SecretRequest, SecretResponder, SecretSetting}; +use nmrs::raw::zbus; +use nmrs::raw::zvariant::{OwnedObjectPath, OwnedValue, Str, Value}; +use nmrs::{ + ActiveConnection, ConnectType, ConnectionOptions, EapOptions, NetworkEvent, NetworkManager, + SavedConnection, SettingsSummary, WifiKeyMgmt, WifiSecurity, +}; +use secure_string::SecureString; +use tokio::sync::oneshot; + +pub type SSID = Arc; +pub type UUID = Arc; + +const NM_DEST: &str = "org.freedesktop.NetworkManager"; +const NM_PATH: &str = "/org/freedesktop/NetworkManager"; +const NM_IFACE: &str = "org.freedesktop.NetworkManager"; +const SECRET_ID: &str = "com.system76.CosmicSettings.NetworkManager"; + +#[derive(Debug)] +pub enum Error { + Message(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Message(message) => f.write_str(message), + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(value: nmrs::ConnectionError) -> Self { + Self::Message(value.to_string()) + } +} + +impl From for Error { + fn from(value: zbus::Error) -> Self { + Self::Message(value.to_string()) + } +} + +pub mod hw_address { + #[derive(Clone, PartialEq, Eq, Default, Debug, PartialOrd, Ord)] + pub struct HwAddress { + octets: Vec, + } + + impl HwAddress { + pub fn from_str(arg: &str) -> Option { + let segments: Vec<&str> = arg.split(':').collect(); + if segments.len() != 6 && segments.len() != 8 { + return None; + } + + let mut octets = Vec::with_capacity(segments.len()); + for segment in segments { + if segment.len() != 2 { + return None; + } + octets.push(u8::from_str_radix(segment, 16).ok()?); + } + + Some(Self { octets }) + } + } + + impl std::fmt::Display for HwAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let hex_parts: Vec = self + .octets + .iter() + .map(|byte| format!("{byte:02x}")) + .collect(); + write!(f, "{}", hex_parts.join(":")) + } + } +} + +pub mod available_wifi { + use std::sync::Arc; + + use nmrs::raw::zvariant::OwnedObjectPath; + + use super::devices::DeviceState; + use super::hw_address::HwAddress; + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct AccessPoint { + pub ssid: Arc, + pub strength: u8, + pub state: DeviceState, + pub working: bool, + pub path: OwnedObjectPath, + pub hw_address: HwAddress, + pub secured: bool, + pub wps_push: bool, + pub network_type: NetworkType, + pub interface: Option, + pub bssid: Option, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum NetworkType { + Open, + PskOrSae, + Sae, + EAP, + } +} + +pub mod current_networks { + use std::net::Ipv4Addr; + + use super::devices::ActiveConnectionState; + + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum ActiveConnectionInfo { + Wired { + name: String, + hw_address: String, + speed: u32, + ip_addresses: Vec, + }, + WiFi { + name: String, + ip_addresses: Vec, + hw_address: String, + 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 mod devices { + use std::sync::Arc; + + use nmrs::raw::zvariant::OwnedObjectPath; + use nmrs::{NetworkManager, SavedConnection, SettingsSummary}; + + use super::{Error, active_connection_by_interface, owned_path, saved_matches_device}; + + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub enum DeviceType { + Ethernet, + Wifi, + WifiP2P, + Loopback, + Bluetooth, + Vlan, + WireGuard, + Other(u32), + } + + impl From for DeviceType { + fn from(value: nmrs::DeviceType) -> Self { + match value { + nmrs::DeviceType::Ethernet => Self::Ethernet, + nmrs::DeviceType::Wifi => Self::Wifi, + nmrs::DeviceType::WifiP2P => Self::WifiP2P, + nmrs::DeviceType::Loopback => Self::Loopback, + nmrs::DeviceType::Bluetooth => Self::Bluetooth, + nmrs::DeviceType::Vlan => Self::Vlan, + nmrs::DeviceType::Other(v) => Self::Other(v), + _ => Self::Other(0), + } + } + } + + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub enum DeviceState { + Unknown, + Unmanaged, + Unavailable, + Disconnected, + Prepare, + Config, + NeedAuth, + IpConfig, + IpCheck, + Secondaries, + Activated, + Deactivating, + Failed, + Other(u32), + } + + impl From for DeviceState { + fn from(value: nmrs::DeviceState) -> Self { + match value { + nmrs::DeviceState::Unmanaged => Self::Unmanaged, + nmrs::DeviceState::Unavailable => Self::Unavailable, + nmrs::DeviceState::Disconnected => Self::Disconnected, + nmrs::DeviceState::Prepare => Self::Prepare, + nmrs::DeviceState::Config => Self::Config, + nmrs::DeviceState::NeedAuth => Self::NeedAuth, + nmrs::DeviceState::IpConfig => Self::IpConfig, + nmrs::DeviceState::IpCheck => Self::IpCheck, + nmrs::DeviceState::Secondaries => Self::Secondaries, + nmrs::DeviceState::Activated => Self::Activated, + nmrs::DeviceState::Deactivating => Self::Deactivating, + nmrs::DeviceState::Failed => Self::Failed, + nmrs::DeviceState::Other(0) => Self::Unknown, + nmrs::DeviceState::Other(v) => Self::Other(v), + _ => Self::Unknown, + } + } + } + + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub enum ActiveConnectionState { + Unknown, + Activating, + Activated, + Deactivating, + Deactivated, + Other(u32), + } + + impl From for ActiveConnectionState { + fn from(value: nmrs::ActiveConnectionState) -> Self { + match value { + nmrs::ActiveConnectionState::Activating => Self::Activating, + nmrs::ActiveConnectionState::Activated => Self::Activated, + nmrs::ActiveConnectionState::Deactivating => Self::Deactivating, + nmrs::ActiveConnectionState::Deactivated => Self::Deactivated, + nmrs::ActiveConnectionState::Unknown => Self::Unknown, + nmrs::ActiveConnectionState::Other(v) => Self::Other(v), + _ => Self::Unknown, + } + } + } + + #[derive(Debug, Clone, Eq, PartialEq)] + pub struct DeviceInfo { + pub path: OwnedObjectPath, + pub device_type: DeviceType, + pub interface: String, + pub state: DeviceState, + pub active_connection: Option<(DeviceConnection, ActiveConnectionState)>, + pub available_connections: Vec, + pub known_connections: Vec, + } + + #[derive(Debug, Clone, Eq, PartialEq)] + pub struct DeviceConnection { + pub path: OwnedObjectPath, + pub id: String, + pub uuid: Arc, + } + + #[derive(Debug, Clone, Eq, PartialEq)] + pub struct KnownDeviceConnection { + pub id: String, + pub uuid: Arc, + } + + pub async fn list( + nm: &NetworkManager, + device_type_filter: fn(DeviceType) -> bool, + ) -> Result, Error> { + let (devices_res, saved_res, active_res) = futures::join!( + nm.list_devices(), + nm.list_saved_connections(), + nm.list_active_connections() + ); + + let saved = saved_res?; + let active = active_res.unwrap_or_default(); + let mut out = Vec::new(); + + for device in devices_res? { + let device_type = DeviceType::from(device.device_type); + if !device_type_filter(device_type) { + continue; + } + + let Ok(path) = owned_path(&device.path) else { + continue; + }; + + let matching_saved: Vec<&SavedConnection> = saved + .iter() + .filter(|connection| { + saved_matches_device(connection, device_type, &device.interface) + }) + .collect(); + + let available_connections = matching_saved + .iter() + .map(|connection| DeviceConnection { + path: connection.path.clone(), + id: connection.id.clone(), + uuid: Arc::from(connection.uuid.as_str()), + }) + .collect(); + + let known_connections = matching_saved + .iter() + .map(|connection| KnownDeviceConnection { + id: connection.id.clone(), + uuid: Arc::from(connection.uuid.as_str()), + }) + .collect(); + + let active_connection = + active_connection_by_interface(&active, &device.interface).map(|connection| { + ( + DeviceConnection { + path: connection.path.clone(), + id: connection.id.clone(), + uuid: Arc::from(connection.uuid.as_str()), + }, + connection.state, + ) + }); + + out.push(DeviceInfo { + path, + device_type, + interface: device.interface, + state: DeviceState::from(device.state), + active_connection, + available_connections, + known_connections, + }); + } + + Ok(out) + } + + pub fn is_wifi_summary(summary: &SettingsSummary) -> bool { + matches!(summary, SettingsSummary::Wifi { .. }) + } +} + +#[derive(Debug, Clone)] +pub struct ActiveConnectionRecord { + path: OwnedObjectPath, + id: String, + uuid: String, + state: devices::ActiveConnectionState, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NetworkManagerState { + pub wireless_access_points: Vec, + pub active_conns: Vec, + pub known_access_points: Vec, + pub wifi_enabled: bool, + pub airplane_mode: bool, +} + +impl Default for NetworkManagerState { + fn default() -> Self { + Self { + wireless_access_points: Vec::new(), + active_conns: Vec::new(), + known_access_points: Vec::new(), + wifi_enabled: false, + airplane_mode: false, + } + } +} + +impl NetworkManagerState { + pub async fn new(nm: &NetworkManager) -> Result { + let (wifi_state, airplane_mode, active_conns, access_points, saved_connections) = futures::join!( + nm.wifi_state(), + nm.airplane_mode_state(), + nm.list_active_connections(), + nm.list_access_points(None), + nm.list_saved_connections() + ); + + let active_conns = active_conns.unwrap_or_default(); + let saved_connections = saved_connections.unwrap_or_default(); + + let mut wireless_access_points = access_points + .unwrap_or_default() + .into_iter() + .filter(|ap| !ap.ssid.is_empty() && ap.ssid != "") + .map(access_point_from_nmrs) + .fold( + BTreeMap::<(String, SSID), available_wifi::AccessPoint>::new(), + |mut acc, ap| { + let key = (ap.interface.clone().unwrap_or_default(), ap.ssid.clone()); + let replace = acc + .get(&key) + .is_none_or(|existing| existing.strength < ap.strength); + if replace { + acc.insert(key, ap); + } + acc + }, + ) + .into_values() + .collect::>(); + wireless_access_points.sort_by(|a, b| b.strength.cmp(&a.strength)); + + let active_infos = active_connection_infos(active_conns); + let saved_wifi = saved_wifi_profiles(&saved_connections); + + let mut known_access_points = Vec::new(); + for saved in saved_wifi { + let Some((ssid, network_type)) = saved_wifi_identity(saved) else { + continue; + }; + if active_infos.iter().any(|active| active.name() == ssid) { + continue; + } + if let Some(visible) = wireless_access_points + .iter() + .find(|ap| ap.ssid.as_ref() == ssid) + .cloned() + { + known_access_points.push(visible); + continue; + } + + known_access_points.push(available_wifi::AccessPoint { + ssid: Arc::from(ssid), + strength: 0, + state: devices::DeviceState::Unknown, + working: false, + path: slash_path(), + hw_address: hw_address::HwAddress::default(), + secured: !matches!(network_type, available_wifi::NetworkType::Open), + wps_push: false, + network_type, + interface: saved.interface_name.clone(), + bssid: None, + }); + } + + known_access_points.sort_by(|a, b| a.ssid.cmp(&b.ssid)); + known_access_points.dedup_by(|a, b| a.ssid == b.ssid); + + Ok(Self { + wireless_access_points, + active_conns: active_infos, + known_access_points, + wifi_enabled: wifi_state.map(|state| state.enabled).unwrap_or(false), + airplane_mode: airplane_mode + .map(|state| state.is_airplane_mode()) + .unwrap_or(false), + }) + } + + #[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(); + } +} + +#[derive(Debug, Clone)] +pub enum Request { + Activate(OwnedObjectPath, OwnedObjectPath), + ActivateVpn(UUID), + Deactivate(UUID), + Disconnect(SSID), + Forget(SSID), + Authenticate { + ssid: String, + identity: Option, + password: SecureString, + network_type: available_wifi::NetworkType, + secret_tx: Option>, + interface: Option, + }, + GetWiFiCredentials( + SSID, + UUID, + available_wifi::NetworkType, + Option>, + ), + Reload, + Remove(UUID), + SelectAccessPoint( + SSID, + available_wifi::NetworkType, + Option>, + Option, + ), + SetAirplaneMode(bool), + SetWiFi(bool), +} + +#[derive(Debug, Clone)] +pub enum Event { + RequestResponse { + req: Request, + state: NetworkManagerState, + success: bool, + }, + Init { + conn: NetworkManager, + sender: UnboundedSender, + state: NetworkManagerState, + }, + Devices, + WiFiEnabled(bool), + WirelessAccessPoints, + ActiveConns, + WiFiCredentials { + ssid: SSID, + password: Option, + security_type: available_wifi::NetworkType, + }, +} + +pub async fn watch(nm: NetworkManager, mut output: futures::channel::mpsc::Sender) { + let (request_tx, mut request_rx): (UnboundedSender, UnboundedReceiver) = + unbounded(); + + let state = NetworkManagerState::new(&nm).await.unwrap_or_default(); + if output + .send(Event::Init { + conn: nm.clone(), + sender: request_tx, + state, + }) + .await + .is_err() + { + return; + } + + let Ok(events) = nm.network_events().await else { + while let Some(req) = request_rx.next().await { + let event = request_response(&nm, req, false).await; + _ = output.send(event).await; + } + return; + }; + + let mut events = events.fuse(); + loop { + futures::select! { + req = request_rx.next().fuse() => { + let Some(req) = req else { + break; + }; + let event = handle_request(&nm, req).await; + if output.send(event).await.is_err() { + break; + } + } + event = events.next() => { + let Some(event) = event else { + break; + }; + let message = match event { + Ok(NetworkEvent::AccessPointsChanged) => Some(Event::WirelessAccessPoints), + Ok(NetworkEvent::DeviceChanged { .. } | NetworkEvent::SettingsChanged(_)) => Some(Event::Devices), + Ok(NetworkEvent::ActiveConnectionsChanged) => Some(Event::ActiveConns), + Ok(NetworkEvent::WirelessEnabledChanged) => Some(Event::WiFiEnabled( + nm.wifi_state().await.map(|state| state.enabled).unwrap_or(false), + )), + Ok(NetworkEvent::ConnectivityChanged | NetworkEvent::NetworkManagerRestarted) => Some(Event::ActiveConns), + Ok(_) => None, + Err(err) => { + tracing::error!(%err, "network event stream error"); + None + } + }; + + if let Some(message) = message + && output.send(message).await.is_err() + { + break; + } + } + } + } +} + +async fn handle_request(nm: &NetworkManager, req: Request) -> Event { + let success = match &req { + Request::Activate(device_path, connection_path) => activate_connection( + nm, + connection_path.clone(), + device_path.clone(), + slash_path(), + ) + .await + .is_ok(), + Request::ActivateVpn(uuid) => nm.connect_vpn_by_uuid(uuid).await.is_ok(), + Request::Deactivate(uuid) => deactivate_by_uuid(nm, uuid).await.is_ok(), + Request::Disconnect(ssid) => disconnect_wifi_by_ssid(nm, ssid).await.is_ok(), + Request::Forget(ssid) => nm.forget(ssid).await.is_ok(), + Request::Authenticate { + ssid, + identity, + password, + network_type, + secret_tx, + interface, + } => { + let result = authenticate_wifi( + nm, + ssid, + identity.as_deref(), + password, + *network_type, + interface.as_deref(), + ) + .await; + if let Err(err) = &result { + tracing::error!( + %err, + %ssid, + ?network_type, + ?interface, + "wifi authentication failed" + ); + } + let success = result.is_ok(); + + if success + && let Some(secret_tx) = secret_tx + && let Ok(Some(uuid)) = nm.get_saved_connection_uuid(ssid).await + { + let (applied_tx, applied_rx) = oneshot::channel(); + let (setting_name, key) = if identity.is_some() { + ("802-1x", "password") + } else { + ("802-11-wireless-security", "psk") + }; + _ = secret_tx + .send(nm_secret_agent::Request::SetSecrets { + setting_name: setting_name.to_string(), + uuid, + secrets: HashMap::from([(key.to_string(), password.clone())]), + applied_tx, + }) + .await; + _ = tokio::time::timeout(Duration::from_secs(1), applied_rx).await; + } + + success + } + Request::GetWiFiCredentials(ssid, uuid, security_type, secret_tx) => { + let password = if matches!(security_type, available_wifi::NetworkType::Open) { + None + } else if let Some(secret_tx) = secret_tx { + let (resp_tx, resp_rx) = oneshot::channel(); + let (setting_name, key) = + if matches!(security_type, available_wifi::NetworkType::EAP) { + ("802-1x", "password") + } else { + ("802-11-wireless-security", "psk") + }; + _ = secret_tx + .send(nm_secret_agent::Request::GetSecrets { + setting_name: setting_name.to_string(), + uuid: uuid.to_string(), + resp_tx, + }) + .await; + tokio::time::timeout(Duration::from_secs(10), resp_rx) + .await + .ok() + .and_then(Result::ok) + .and_then(|mut secrets| secrets.remove(key)) + } else { + None + }; + + return Event::WiFiCredentials { + ssid: ssid.clone(), + password, + security_type: *security_type, + }; + } + Request::Reload => true, + Request::Remove(uuid) => nm.delete_saved_connection(uuid).await.is_ok(), + Request::SelectAccessPoint(ssid, network_type, _secret_tx, interface) => { + let result = select_access_point(nm, ssid, *network_type, interface.as_deref()).await; + if let Err(err) = &result { + tracing::error!( + %err, + %ssid, + ?network_type, + ?interface, + "wifi access point selection failed" + ); + } + result.is_ok() + } + Request::SetAirplaneMode(enabled) => nm.set_airplane_mode(*enabled).await.is_ok(), + Request::SetWiFi(enabled) => nm.set_wireless_enabled(*enabled).await.is_ok(), + }; + + request_response(nm, req, success).await +} + +async fn request_response(nm: &NetworkManager, req: Request, success: bool) -> Event { + Event::RequestResponse { + req, + success, + state: NetworkManagerState::new(nm).await.unwrap_or_default(), + } +} + +async fn select_access_point( + nm: &NetworkManager, + ssid: &str, + network_type: available_wifi::NetworkType, + interface: Option<&str>, +) -> Result<(), Error> { + match network_type { + available_wifi::NetworkType::Open => { + nm.connect(ssid, interface, WifiSecurity::Open).await?; + } + available_wifi::NetworkType::PskOrSae => { + nm.connect(ssid, interface, WifiSecurity::WpaPsk { psk: String::new() }) + .await?; + } + available_wifi::NetworkType::Sae | available_wifi::NetworkType::EAP => { + let Some(saved) = find_saved_wifi(nm, ssid, interface).await? else { + return Err(Error::Message("no saved Wi-Fi connection".to_string())); + }; + let device = saved + .interface_name + .as_deref() + .or(interface) + .map(|interface| nm.get_device_by_interface(interface)) + .ok_or_else(|| { + Error::Message("no Wi-Fi interface for saved connection".to_string()) + })? + .await?; + let ap = find_access_point_path(nm, ssid, interface) + .await + .unwrap_or_else(slash_path); + activate_connection(nm, saved.path, device, ap).await?; + } + } + + Ok(()) +} + +async fn authenticate_wifi( + nm: &NetworkManager, + ssid: &str, + identity: Option<&str>, + password: &SecureString, + network_type: available_wifi::NetworkType, + interface: Option<&str>, +) -> Result<(), Error> { + match network_type { + available_wifi::NetworkType::Open => { + nm.connect(ssid, interface, WifiSecurity::Open).await? + } + available_wifi::NetworkType::PskOrSae => { + nm.connect( + ssid, + interface, + WifiSecurity::WpaPsk { + psk: password.unsecure().to_string(), + }, + ) + .await?; + } + available_wifi::NetworkType::Sae => { + connect_sae(nm, ssid, password.unsecure(), interface).await?; + } + available_wifi::NetworkType::EAP => { + nm.connect( + ssid, + interface, + WifiSecurity::WpaEap { + opts: EapOptions::new(identity.unwrap_or_default(), password.unsecure()), + }, + ) + .await?; + } + } + + Ok(()) +} + +async fn connect_sae( + nm: &NetworkManager, + ssid: &str, + password: &str, + interface: Option<&str>, +) -> Result<(), Error> { + let mut settings = nmrs::builders::WifiConnectionBuilder::new(ssid) + .wpa_psk(password) + .autoconnect(true) + .ipv4_auto() + .ipv6_auto() + .build(); + + if let Some(security) = settings.get_mut("802-11-wireless-security") { + security.insert("key-mgmt", Value::from("sae")); + security.remove("auth-alg"); + } + + let ap_path = find_access_point_path(nm, ssid, interface) + .await + .ok_or_else(|| Error::Message(format!("access point {ssid} not found")))?; + nm.add_and_activate_connection(settings, interface, Some(ap_path.as_str())) + .await?; + + Ok(()) +} + +async fn disconnect_wifi_by_ssid(nm: &NetworkManager, ssid: &str) -> Result<(), Error> { + for active in nm.list_active_connections().await? { + if let ActiveConnection::Wifi(wifi) = active + && wifi.ssid == ssid + { + return deactivate_by_uuid(nm, &wifi.uuid).await; + } + } + + Ok(()) +} + +async fn deactivate_by_uuid(nm: &NetworkManager, uuid: &str) -> Result<(), Error> { + if nm.disconnect_vpn_by_uuid(uuid).await.is_ok() { + return Ok(()); + } + + let active_path = find_active_connection_path(nm, uuid).await?; + let proxy = zbus::Proxy::new(nm.dbus_connection(), NM_DEST, NM_PATH, NM_IFACE).await?; + proxy + .call_method("DeactivateConnection", &(active_path,)) + .await?; + + Ok(()) +} + +async fn activate_connection( + nm: &NetworkManager, + connection_path: OwnedObjectPath, + device_path: OwnedObjectPath, + specific_path: OwnedObjectPath, +) -> Result<(), Error> { + let proxy = zbus::Proxy::new(nm.dbus_connection(), NM_DEST, NM_PATH, NM_IFACE).await?; + proxy + .call_method( + "ActivateConnection", + &(connection_path, device_path, specific_path), + ) + .await?; + Ok(()) +} + +async fn find_active_connection_path( + nm: &NetworkManager, + uuid: &str, +) -> Result { + let proxy = zbus::Proxy::new(nm.dbus_connection(), NM_DEST, NM_PATH, NM_IFACE).await?; + let paths: Vec = proxy.get_property("ActiveConnections").await?; + + for path in paths { + let active = zbus::Proxy::new( + nm.dbus_connection(), + NM_DEST, + path.clone(), + "org.freedesktop.NetworkManager.Connection.Active", + ) + .await?; + let active_uuid: String = active.get_property("Uuid").await?; + if active_uuid == uuid { + return Ok(path); + } + } + + Err(Error::Message(format!( + "active connection {uuid} not found" + ))) +} + +async fn find_access_point_path( + nm: &NetworkManager, + ssid: &str, + interface: Option<&str>, +) -> Option { + nm.list_access_points(interface) + .await + .ok()? + .into_iter() + .filter(|ap| ap.ssid == ssid) + .max_by_key(|ap| ap.strength) + .map(|ap| ap.path) +} + +async fn find_saved_wifi( + nm: &NetworkManager, + ssid: &str, + interface: Option<&str>, +) -> Result, Error> { + Ok(nm + .list_saved_connections() + .await? + .into_iter() + .find(|connection| { + if let Some(interface) = interface + && connection + .interface_name + .as_deref() + .is_some_and(|i| i != interface) + { + return false; + } + saved_wifi_identity(connection).is_some_and(|(saved_ssid, _)| saved_ssid == ssid) + })) +} + +pub async fn wifi_connection_settings( + nm: NetworkManager, +) -> Result, Box>, Error> { + Ok(nm + .list_saved_connections() + .await? + .into_iter() + .filter_map(|connection| { + saved_wifi_identity(&connection) + .map(|(ssid, _)| (ssid.into_boxed_str(), connection.uuid.into_boxed_str())) + }) + .collect()) +} + +pub async fn import_openvpn(path: impl AsRef) -> Result<(), Error> { + use nmrs::builders::{OpenVpnBuilder, build_openvpn_connection}; + + let nm = NetworkManager::new().await?; + let config = OpenVpnBuilder::from_ovpn_file(path)?.build()?; + let settings = build_openvpn_connection(&config, &ConnectionOptions::new(false))?; + nm.add_connection(settings).await?; + Ok(()) +} + +pub async fn import_wireguard(path: impl AsRef, name: &str) -> Result<(), Error> { + use nmrs::builders::WireGuardBuilder; + + let nm = NetworkManager::new().await?; + let config = parse_wireguard_config(path.as_ref(), name)?; + let mut builder = WireGuardBuilder::new(&config.name) + .private_key(config.private_key) + .address(config.address) + .add_peers(config.peers) + .autoconnect(false); + + if !config.dns.is_empty() { + builder = builder.dns(config.dns); + } + if let Some(mtu) = config.mtu { + builder = builder.mtu(mtu); + } + + nm.add_connection(builder.build()?).await?; + Ok(()) +} + +struct ParsedWireGuard { + name: String, + private_key: String, + address: String, + dns: Vec, + mtu: Option, + peers: Vec, +} + +fn parse_wireguard_config(path: &Path, name: &str) -> Result { + let contents = std::fs::read_to_string(path) + .map_err(|err| Error::Message(format!("failed to read {}: {err}", path.display())))?; + let mut section = ""; + let mut private_key = None; + let mut address = None; + let mut dns = Vec::new(); + let mut mtu = None; + let mut peers = Vec::new(); + let mut peer_public_key: Option = None; + let mut peer_endpoint: Option = None; + let mut peer_allowed_ips: Vec = Vec::new(); + let mut peer_preshared_key: Option = None; + let mut peer_keepalive: Option = None; + + let flush_peer = |peers: &mut Vec, + public_key: &mut Option, + endpoint: &mut Option, + allowed_ips: &mut Vec, + preshared_key: &mut Option, + keepalive: &mut Option| + -> Result<(), Error> { + if public_key.is_none() + && endpoint.is_none() + && allowed_ips.is_empty() + && preshared_key.is_none() + && keepalive.is_none() + { + return Ok(()); + } + + let public_key = public_key + .take() + .ok_or_else(|| Error::Message("WireGuard peer is missing PublicKey".to_string()))?; + let endpoint = endpoint + .take() + .ok_or_else(|| Error::Message("WireGuard peer is missing Endpoint".to_string()))?; + if allowed_ips.is_empty() { + return Err(Error::Message( + "WireGuard peer is missing AllowedIPs".to_string(), + )); + } + + let mut peer = nmrs::WireGuardPeer::new(public_key, endpoint, std::mem::take(allowed_ips)); + if let Some(psk) = preshared_key.take() { + peer = peer.with_preshared_key(psk); + } + if let Some(interval) = keepalive.take() { + peer = peer.with_persistent_keepalive(interval); + } + peers.push(peer); + Ok(()) + }; + + for raw_line in contents.lines() { + let line = raw_line + .split_once('#') + .map_or(raw_line, |(line, _)| line) + .trim(); + if line.is_empty() { + continue; + } + + if line.starts_with('[') && line.ends_with(']') { + if section.eq_ignore_ascii_case("Peer") { + flush_peer( + &mut peers, + &mut peer_public_key, + &mut peer_endpoint, + &mut peer_allowed_ips, + &mut peer_preshared_key, + &mut peer_keepalive, + )?; + } + section = &line[1..line.len() - 1]; + continue; + } + + let Some((key, value)) = line.split_once('=') else { + continue; + }; + let key = key.trim(); + let value = value.trim(); + + if section.eq_ignore_ascii_case("Interface") { + match key.to_ascii_lowercase().as_str() { + "privatekey" => private_key = Some(value.to_string()), + "address" => { + address = value + .split(',') + .map(str::trim) + .find(|value| value.contains('.')) + .or_else(|| value.split(',').map(str::trim).next()) + .map(ToOwned::to_owned); + } + "dns" => dns.extend( + value + .split(',') + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned), + ), + "mtu" => mtu = value.parse().ok(), + _ => {} + } + } else if section.eq_ignore_ascii_case("Peer") { + match key.to_ascii_lowercase().as_str() { + "publickey" => peer_public_key = Some(value.to_string()), + "endpoint" => peer_endpoint = Some(value.to_string()), + "allowedips" => { + peer_allowed_ips.extend( + value + .split(',') + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned), + ); + } + "presharedkey" => peer_preshared_key = Some(value.to_string()), + "persistentkeepalive" => peer_keepalive = value.parse().ok(), + _ => {} + } + } + } + + if section.eq_ignore_ascii_case("Peer") { + flush_peer( + &mut peers, + &mut peer_public_key, + &mut peer_endpoint, + &mut peer_allowed_ips, + &mut peer_preshared_key, + &mut peer_keepalive, + )?; + } + + Ok(ParsedWireGuard { + name: name.to_string(), + private_key: private_key + .ok_or_else(|| Error::Message("WireGuard config is missing PrivateKey".to_string()))?, + address: address + .ok_or_else(|| Error::Message("WireGuard config is missing Address".to_string()))?, + dns, + mtu, + peers, + }) +} + +fn active_connection_infos( + active: Vec, +) -> Vec { + let mut infos = active + .into_iter() + .filter_map(|connection| match connection { + ActiveConnection::Wired(wired) => Some(current_networks::ActiveConnectionInfo::Wired { + name: wired.id, + hw_address: wired.hw_address.unwrap_or_default(), + speed: wired.speed_mbps.unwrap_or_default(), + ip_addresses: parse_ipv4_list([wired.ip4_address]), + }), + ActiveConnection::Wifi(wifi) => Some(current_networks::ActiveConnectionInfo::WiFi { + name: wifi.ssid, + ip_addresses: parse_ipv4_list([wifi.ip4_address]), + hw_address: wifi.bssid.unwrap_or_default(), + state: devices::ActiveConnectionState::from(wifi.state), + strength: wifi.strength.unwrap_or_default(), + }), + ActiveConnection::Vpn(vpn) => Some(current_networks::ActiveConnectionInfo::Vpn { + name: vpn.id, + ip_addresses: parse_ipv4_list([vpn.ip4_address]), + }), + ActiveConnection::Other(_) => None, + _ => None, + }) + .collect::>(); + + infos.sort_by(|a, b| { + let helper = |conn: ¤t_networks::ActiveConnectionInfo| match conn { + current_networks::ActiveConnectionInfo::Vpn { name, .. } => format!("0{name}"), + current_networks::ActiveConnectionInfo::Wired { name, .. } => format!("1{name}"), + current_networks::ActiveConnectionInfo::WiFi { name, .. } => format!("2{name}"), + }; + helper(a).cmp(&helper(b)) + }); + + infos +} + +fn access_point_from_nmrs(ap: nmrs::AccessPoint) -> available_wifi::AccessPoint { + let network_type = match ap.security.preferred_connect_type() { + ConnectType::Open | ConnectType::Owe => available_wifi::NetworkType::Open, + ConnectType::Eap => available_wifi::NetworkType::EAP, + ConnectType::Psk => available_wifi::NetworkType::PskOrSae, + ConnectType::Sae => available_wifi::NetworkType::Sae, + _ => available_wifi::NetworkType::PskOrSae, + }; + + available_wifi::AccessPoint { + ssid: Arc::from(ap.ssid.as_str()), + strength: ap.strength, + state: devices::DeviceState::from(ap.device_state), + working: false, + path: ap.path, + hw_address: hw_address::HwAddress::from_str(&ap.bssid).unwrap_or_default(), + secured: !ap.security.is_open(), + wps_push: ap.security.wps, + network_type, + interface: Some(ap.interface), + bssid: Some(ap.bssid), + } +} + +fn saved_wifi_profiles(saved: &[SavedConnection]) -> impl Iterator { + saved.iter().filter(|connection| { + connection.connection_type == "802-11-wireless" + && matches!(connection.summary, SettingsSummary::Wifi { .. }) + }) +} + +fn saved_wifi_identity(saved: &SavedConnection) -> Option<(String, available_wifi::NetworkType)> { + let SettingsSummary::Wifi { ssid, security, .. } = &saved.summary else { + return None; + }; + if ssid.is_empty() { + return None; + } + + let network_type = match security.as_ref().map(|s| s.key_mgmt) { + None | Some(WifiKeyMgmt::None | WifiKeyMgmt::Owe | WifiKeyMgmt::OweTransitionMode) => { + available_wifi::NetworkType::Open + } + Some(WifiKeyMgmt::WpaEap) => available_wifi::NetworkType::EAP, + Some(WifiKeyMgmt::Wep | WifiKeyMgmt::WpaPsk) => available_wifi::NetworkType::PskOrSae, + Some(WifiKeyMgmt::Sae) => available_wifi::NetworkType::Sae, + Some(_) => available_wifi::NetworkType::PskOrSae, + }; + + Some((ssid.clone(), network_type)) +} + +fn saved_matches_device( + connection: &SavedConnection, + device_type: devices::DeviceType, + interface: &str, +) -> bool { + let type_matches = match device_type { + devices::DeviceType::Ethernet => connection.connection_type == "802-3-ethernet", + devices::DeviceType::Wifi => connection.connection_type == "802-11-wireless", + devices::DeviceType::WireGuard => connection.connection_type == "wireguard", + _ => false, + }; + + type_matches + && connection + .interface_name + .as_deref() + .is_none_or(|name| name == interface) +} + +fn active_connection_by_interface( + active: &[ActiveConnection], + interface: &str, +) -> Option { + for connection in active { + match connection { + ActiveConnection::Wired(wired) if wired.interface.as_deref() == Some(interface) => { + return Some(ActiveConnectionRecord { + path: slash_path(), + id: wired.id.clone(), + uuid: wired.uuid.clone(), + state: devices::ActiveConnectionState::from(wired.state), + }); + } + ActiveConnection::Wifi(wifi) if wifi.interface.as_deref() == Some(interface) => { + return Some(ActiveConnectionRecord { + path: slash_path(), + id: wifi.id.clone(), + uuid: wifi.uuid.clone(), + state: devices::ActiveConnectionState::from(wifi.state), + }); + } + ActiveConnection::Vpn(vpn) if vpn.interface.as_deref() == Some(interface) => { + return Some(ActiveConnectionRecord { + path: slash_path(), + id: vpn.id.clone(), + uuid: vpn.uuid.clone(), + state: devices::ActiveConnectionState::from(vpn.state), + }); + } + _ => {} + } + } + + None +} + +fn parse_ipv4_list(addresses: impl IntoIterator>) -> Vec { + addresses + .into_iter() + .flatten() + .filter_map(|address| { + address + .split_once('/') + .map_or(address.as_str(), |(address, _)| address) + .parse() + .ok() + }) + .collect() +} + +fn slash_path() -> OwnedObjectPath { + OwnedObjectPath::try_from("/").expect("slash is a valid object path") +} + +fn owned_path(path: &str) -> Result { + OwnedObjectPath::try_from(path) + .map_err(|err| Error::Message(format!("invalid object path {path}: {err}"))) +} + +pub mod nm_secret_agent { + use super::*; + + pub type SecretSender = + Arc>>>; + + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub enum PasswordFlag { + None = 0, + AgentOwned = 1, + NotSaved = 2, + NotRequired = 4, + } + + #[derive(Clone, Debug)] + pub struct Error(pub String); + + impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } + } + + impl std::error::Error for Error {} + + #[derive(Debug, Clone)] + pub enum Event { + RequestSecret { + uuid: String, + name: String, + description: Option, + previous: SecureString, + tx: SecretSender, + }, + CancelGetSecrets { + uuid: String, + name: String, + }, + Failed(Error), + } + + #[derive(Debug)] + pub enum Request { + SetSecrets { + setting_name: String, + uuid: String, + secrets: HashMap, + applied_tx: oneshot::Sender<()>, + }, + GetSecrets { + setting_name: String, + uuid: String, + resp_tx: oneshot::Sender>, + }, + } + + pub fn secret_agent_stream( + identifier: impl AsRef, + rx: tokio::sync::mpsc::Receiver, + ) -> impl futures::Stream { + let identifier = identifier.as_ref().to_string(); + cosmic::iced::stream::channel( + 4, + move |mut msg_tx: futures::channel::mpsc::Sender| async move { + if let Err(error) = secret_agent_stream_impl(&identifier, msg_tx.clone(), rx).await + { + _ = msg_tx.send(Event::Failed(error)).await; + } + }, + ) + } + + async fn secret_agent_stream_impl( + identifier: &str, + mut msg_tx: futures::channel::mpsc::Sender, + mut rx: tokio::sync::mpsc::Receiver, + ) -> Result<(), Error> { + unlock_collection().await?; + + let (_handle, mut requests) = SecretAgent::builder() + .with_identifier(identifier) + .register() + .await + .map_err(|err| Error(err.to_string()))?; + + loop { + futures::select! { + request = rx.recv().fuse() => { + let Some(request) = request else { + break; + }; + handle_control_request(request).await?; + } + request = requests.next().fuse() => { + let Some(request) = request else { + break; + }; + handle_secret_request(&mut msg_tx, request).await?; + } + } + } + + Ok(()) + } + + async fn handle_control_request(request: Request) -> Result<(), Error> { + match request { + Request::SetSecrets { + setting_name, + uuid, + secrets, + applied_tx, + } => { + if secrets.is_empty() { + delete_secrets(&uuid).await?; + } else { + store_secrets(&uuid, &setting_name, &secrets).await?; + } + _ = applied_tx.send(()); + } + Request::GetSecrets { + setting_name, + uuid, + resp_tx, + } => { + let secrets = get_secrets(&uuid, &setting_name).await?; + _ = resp_tx.send(secrets); + } + } + + Ok(()) + } + + async fn handle_secret_request( + msg_tx: &mut futures::channel::mpsc::Sender, + request: SecretRequest, + ) -> Result<(), Error> { + let setting_name = setting_name(&request.setting); + if !request.flags.contains(SecretAgentFlags::REQUEST_NEW) { + let stored = get_secrets(&request.connection_uuid, setting_name).await?; + if !stored.is_empty() { + return respond_with_stored(request, stored).await; + } + } + + if !request + .flags + .intersects(SecretAgentFlags::ALLOW_INTERACTION | SecretAgentFlags::REQUEST_NEW) + { + request + .responder + .no_secrets() + .await + .map_err(|err| Error(err.to_string()))?; + return Ok(()); + } + + let (tx, rx) = oneshot::channel(); + let tx = Arc::new(tokio::sync::Mutex::new(Some(tx))); + let description = match &request.setting { + SecretSetting::WifiPsk { ssid } => Some(ssid.clone()), + SecretSetting::WifiEap { identity, method } => Some( + format!( + "{} {}", + identity.clone().unwrap_or_default(), + method.clone().unwrap_or_default() + ) + .trim() + .to_string(), + ), + SecretSetting::Vpn { + service_type, + user_name, + } => Some(user_name.clone().unwrap_or_else(|| service_type.clone())), + SecretSetting::Other(setting) => Some(setting.clone()), + _ => None, + } + .filter(|value| !value.is_empty()); + + let event_name = match &request.setting { + SecretSetting::WifiPsk { .. } | SecretSetting::WifiEap { .. } => { + request.connection_uuid.clone() + } + _ => request.connection_id.clone(), + }; + + let response_kind = ResponseKind::from(&request); + let uuid = request.connection_uuid.clone(); + msg_tx + .send(Event::RequestSecret { + uuid: uuid.clone(), + name: event_name, + description, + previous: SecureString::from(""), + tx, + }) + .await + .map_err(|err| Error(err.to_string()))?; + + match rx.await { + Ok(secret) => { + let mut stored = HashMap::new(); + let key = response_kind.key(); + stored.insert(key.to_string(), secret.clone()); + store_secrets(&uuid, setting_name, &stored).await?; + response_kind.respond(request.responder, secret).await?; + } + Err(_) => { + request + .responder + .cancel() + .await + .map_err(|err| Error(err.to_string()))?; + } + } + + Ok(()) + } + + enum ResponseKind { + WifiPsk, + WifiEap { identity: Option }, + Vpn { key: String }, + Raw { setting_name: String, key: String }, + } + + impl ResponseKind { + fn from(request: &SecretRequest) -> Self { + match &request.setting { + SecretSetting::WifiPsk { .. } => Self::WifiPsk, + SecretSetting::WifiEap { identity, .. } => Self::WifiEap { + identity: identity.clone(), + }, + SecretSetting::Vpn { .. } => Self::Vpn { + key: request + .hints + .iter() + .find(|hint| !hint.contains(':')) + .cloned() + .unwrap_or_else(|| "password".to_string()), + }, + SecretSetting::Other(setting_name) => Self::Raw { + setting_name: setting_name.clone(), + key: request + .hints + .first() + .cloned() + .unwrap_or_else(|| "password".to_string()), + }, + _ => Self::Raw { + setting_name: setting_name(&request.setting).to_string(), + key: "password".to_string(), + }, + } + } + + fn key(&self) -> &str { + match self { + Self::WifiPsk => "psk", + Self::WifiEap { .. } => "password", + Self::Vpn { key } | Self::Raw { key, .. } => key.as_str(), + } + } + + async fn respond( + self, + responder: SecretResponder, + secret: SecureString, + ) -> Result<(), Error> { + match self { + Self::WifiPsk => responder + .wifi_psk(secret.unsecure().to_string()) + .await + .map_err(|err| Error(err.to_string())), + Self::WifiEap { identity } => responder + .wifi_eap(identity, secret.unsecure().to_string()) + .await + .map_err(|err| Error(err.to_string())), + Self::Vpn { key } => responder + .vpn_secrets(HashMap::from([(key, secret.unsecure().to_string())])) + .await + .map_err(|err| Error(err.to_string())), + Self::Raw { setting_name, key } => { + let mut inner = HashMap::new(); + inner.insert( + key, + OwnedValue::from(Str::from(secret.unsecure().to_string())), + ); + responder + .raw(setting_name, inner) + .await + .map_err(|err| Error(err.to_string())) + } + } + } + } + + async fn respond_with_stored( + request: SecretRequest, + stored: HashMap, + ) -> Result<(), Error> { + match &request.setting { + SecretSetting::WifiPsk { .. } => { + if let Some(psk) = stored.get("psk") { + request + .responder + .wifi_psk(psk.unsecure().to_string()) + .await + .map_err(|err| Error(err.to_string()))?; + } else { + request + .responder + .no_secrets() + .await + .map_err(|err| Error(err.to_string()))?; + } + } + SecretSetting::WifiEap { identity, .. } => { + if let Some(password) = stored.get("password") { + request + .responder + .wifi_eap(identity.clone(), password.unsecure().to_string()) + .await + .map_err(|err| Error(err.to_string()))?; + } else { + request + .responder + .no_secrets() + .await + .map_err(|err| Error(err.to_string()))?; + } + } + SecretSetting::Vpn { .. } => { + let secrets = stored + .into_iter() + .map(|(key, value)| (key, value.unsecure().to_string())) + .collect(); + request + .responder + .vpn_secrets(secrets) + .await + .map_err(|err| Error(err.to_string()))?; + } + _ => { + let setting_name = setting_name(&request.setting).to_string(); + let inner = stored + .into_iter() + .map(|(key, value)| { + ( + key, + OwnedValue::from(Str::from(value.unsecure().to_string())), + ) + }) + .collect(); + request + .responder + .raw(setting_name, inner) + .await + .map_err(|err| Error(err.to_string()))?; + } + } + + Ok(()) + } + + fn setting_name(setting: &SecretSetting) -> &'static str { + match setting { + SecretSetting::WifiPsk { .. } => "802-11-wireless-security", + SecretSetting::WifiEap { .. } => "802-1x", + SecretSetting::Vpn { .. } => "vpn", + SecretSetting::Gsm => "gsm", + SecretSetting::Cdma => "cdma", + SecretSetting::Pppoe => "pppoe", + SecretSetting::Other(_) => "connection", + _ => "connection", + } + } + + async fn unlock_collection() -> Result<(), Error> { + let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) + .await + .map_err(|err| Error(err.to_string()))?; + let collection = ss + .get_default_collection() + .await + .map_err(|err| Error(err.to_string()))?; + if collection + .is_locked() + .await + .map_err(|err| Error(err.to_string()))? + { + collection + .unlock() + .await + .map_err(|err| Error(err.to_string()))?; + } + Ok(()) + } + + async fn store_secrets( + uuid: &str, + setting_name: &str, + secrets: &HashMap, + ) -> Result<(), Error> { + let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) + .await + .map_err(|err| Error(err.to_string()))?; + let collection = ss + .get_default_collection() + .await + .map_err(|err| Error(err.to_string()))?; + + for (name, secret) in secrets { + let mut attributes = HashMap::new(); + attributes.insert("application", SECRET_ID); + attributes.insert("uuid", uuid); + attributes.insert("setting_name", setting_name); + attributes.insert("name", name.as_str()); + collection + .create_item( + "NetworkManager Secret", + attributes, + secret.unsecure().as_bytes(), + true, + "text/plain", + ) + .await + .map_err(|err| Error(err.to_string()))?; + } + + Ok(()) + } + + async fn get_secrets( + uuid: &str, + setting_name: &str, + ) -> Result, Error> { + let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) + .await + .map_err(|err| Error(err.to_string()))?; + let collection = ss + .get_default_collection() + .await + .map_err(|err| Error(err.to_string()))?; + let mut attributes = HashMap::new(); + attributes.insert("application", SECRET_ID); + attributes.insert("uuid", uuid); + attributes.insert("setting_name", setting_name); + + let search_items = collection + .search_items(attributes) + .await + .map_err(|err| Error(err.to_string()))?; + let mut secrets = HashMap::new(); + for item in &search_items { + let name = item + .get_attributes() + .await + .map_err(|err| Error(err.to_string()))? + .get("name") + .cloned() + .unwrap_or_else(|| "unknown".to_string()); + let secret = item + .get_secret() + .await + .map_err(|err| Error(err.to_string()))?; + let secret = String::from_utf8(secret).map_err(|err| Error(err.to_string()))?; + secrets.insert(name, SecureString::from(secret)); + } + Ok(secrets) + } + + async fn delete_secrets(uuid: &str) -> Result<(), Error> { + let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) + .await + .map_err(|err| Error(err.to_string()))?; + let collection = ss + .get_default_collection() + .await + .map_err(|err| Error(err.to_string()))?; + let mut attributes = HashMap::new(); + attributes.insert("application", SECRET_ID); + attributes.insert("uuid", uuid); + + let search_items = collection + .search_items(attributes) + .await + .map_err(|err| Error(err.to_string()))?; + for item in &search_items { + item.delete().await.map_err(|err| Error(err.to_string()))?; + } + Ok(()) + } +} diff --git a/cosmic-settings/src/pages/networking/mod.rs b/cosmic-settings/src/pages/networking/mod.rs index f9b4a4c..84748ce 100644 --- a/cosmic-settings/src/pages/networking/mod.rs +++ b/cosmic-settings/src/pages/networking/mod.rs @@ -1,24 +1,24 @@ // Copyright 2024 System76 // SPDX-License-Identifier: GPL-3.0-only +pub mod backend; pub mod vpn; pub mod wifi; pub mod wired; -use std::ffi::OsStr; use std::process::Stdio; use std::sync::Arc; use anyhow::Context; use cosmic::{Apply, Element, Task, widget}; -use cosmic_dbus_networkmanager::interface::enums::{DeviceState, DeviceType}; -use cosmic_dbus_networkmanager::nm::NetworkManager; -use cosmic_settings_network_manager_subscription as network_manager; use cosmic_settings_page::{self as page, Section, section}; use futures::{SinkExt, StreamExt}; use secure_string::SecureString; use slotmap::SlotMap; +use self::backend as network_manager; +use self::backend::devices::{DeviceState, DeviceType}; + pub type SecretSender = Arc>>>; static NM_CONNECTION_EDITOR: &str = "nm-connection-editor"; @@ -37,8 +37,8 @@ pub struct Page { pub enum Message { /// An error occurred. Error(String), - /// Successfully connected to the system dbus. - NetworkManagerConnect(zbus::Connection), + /// Successfully connected to NetworkManager. + NetworkManagerConnect(nmrs::NetworkManager), /// Open the wifi settings page with the selected device. OpenPage { page: page::Entity, @@ -143,6 +143,7 @@ impl page::Page for Page { fl!("network-device-state", "unavailable") } DeviceState::Unknown => fl!("network-device-state", "unknown"), + DeviceState::Other(_) => fl!("network-device-state", "unknown"), DeviceState::Unmanaged => fl!("network-device-state", "unmanaged"), }, "preferences-wireless-symbolic", @@ -190,6 +191,7 @@ impl page::Page for Page { fl!("network-device-state", "unplugged") } DeviceState::Unknown => fl!("network-device-state", "unknown"), + DeviceState::Other(_) => fl!("network-device-state", "unknown"), DeviceState::Unmanaged => fl!("network-device-state", "unmanaged"), }, "preferences-wired-symbolic", @@ -225,9 +227,9 @@ impl page::Page for Page { fn on_enter(&mut self) -> cosmic::Task { if self.nm_task.is_none() { return cosmic::Task::future(async move { - zbus::Connection::system() + nmrs::NetworkManager::new() .await - .context("failed to create system dbus connection") + .context("failed to connect to NetworkManager") .map_or_else( |why| Message::Error(why.to_string()), Message::NetworkManagerConnect, @@ -310,33 +312,47 @@ impl Page { Task::none() } - fn connect(&mut self, conn: zbus::Connection) -> Task { + fn connect(&mut self, nm: nmrs::NetworkManager) -> Task { if self.nm_task.is_none() { let (canceller, task) = crate::utils::forward_event_loop(move |mut sender| async move { - let network_manager = match NetworkManager::new(&conn).await { - Ok(n) => n, - Err(why) => { - tracing::error!( - why = why.to_string(), - "failed to connect to network_manager" - ); - - return futures::future::pending().await; + match network_manager::devices::list(&nm, |_| true).await { + Ok(devices) => { + _ = sender + .send(crate::pages::Message::Networking(Message::UpdateDevices( + devices.into_iter().map(Arc::new).collect(), + ))) + .await; } - }; + Err(why) => { + _ = sender + .send(crate::pages::Message::Networking(Message::Error( + why.to_string(), + ))) + .await; + } + } - let mut devices_changed = - std::pin::pin!(network_manager.receive_devices_changed().await.then( - |_| async { - match network_manager::devices::list(&conn, |_| true).await { - Ok(devices) => Message::UpdateDevices( + let Ok(events) = nm.network_events().await else { + return futures::future::pending().await; + }; + let devices_changed = events.filter_map(|event| async { + match event { + Ok(nmrs::NetworkEvent::DeviceChanged { .. }) + | Ok(nmrs::NetworkEvent::SettingsChanged(_)) + | Ok(nmrs::NetworkEvent::NetworkManagerRestarted) => { + match network_manager::devices::list(&nm, |_| true).await { + Ok(devices) => Some(Message::UpdateDevices( devices.into_iter().map(Arc::new).collect(), - ), - Err(why) => Message::Error(why.to_string()), + )), + Err(why) => Some(Message::Error(why.to_string())), } } - )); + Ok(_) => None, + Err(why) => Some(Message::Error(why.to_string())), + } + }); + futures::pin_mut!(devices_changed); while let Some(message) = devices_changed.next().await { _ = sender @@ -353,16 +369,6 @@ impl Page { } } -async fn nm_add_vpn_file>(type_: &str, path: P) -> Result<(), String> { - tokio::process::Command::new("nmcli") - .args(["connection", "import", "type", type_, "file"]) - .arg(path) - .stderr(Stdio::piped()) - .output() - .await - .apply(crate::utils::map_stderr_output) -} - async fn nm_add_wired() -> Result<(), String> { nm_connection_editor(&["--type=802-3-ethernet", "-c"]).await } diff --git a/cosmic-settings/src/pages/networking/vpn/mod.rs b/cosmic-settings/src/pages/networking/vpn/mod.rs index ff85a8f..6c2c9e7 100644 --- a/cosmic-settings/src/pages/networking/vpn/mod.rs +++ b/cosmic-settings/src/pages/networking/vpn/mod.rs @@ -1,8 +1,6 @@ // Copyright 2024 System76 // SPDX-License-Identifier: GPL-3.0-only -pub mod nmcli; - use std::collections::HashMap; use std::sync::{Arc, LazyLock}; @@ -14,17 +12,16 @@ use cosmic::widget::space::horizontal as horizontal_space; use cosmic::widget::text_input::focus; use cosmic::widget::{self, icon}; use cosmic::{Apply, Element, Task, task}; -use cosmic_settings_network_manager_subscription::current_networks::ActiveConnectionInfo; -use cosmic_settings_network_manager_subscription::nm_secret_agent::{self, PasswordFlag}; -use cosmic_settings_network_manager_subscription::{ - self as network_manager, NetworkManagerState, UUID, -}; use cosmic_settings_page::{self as page, Section, section}; use futures::{FutureExt, SinkExt, StreamExt}; use indexmap::IndexMap; use secure_string::SecureString; use tokio::sync::Mutex; +use super::backend as network_manager; +use super::backend::current_networks::ActiveConnectionInfo; +use super::backend::nm_secret_agent::{self, PasswordFlag}; +use super::backend::{NetworkManagerState, UUID}; use crate::pages::networking::SecretSender; pub static SECURE_INPUT_VPN: LazyLock = LazyLock::new(widget::Id::unique); @@ -60,8 +57,8 @@ pub enum Message { NetworkManager(network_manager::Event), /// An update from the secret agent SecretAgent(network_manager::nm_secret_agent::Event), - /// Successfully connected to the system dbus. - NetworkManagerConnect(zbus::Connection), + /// Successfully connected to NetworkManager. + NetworkManagerConnect(nmrs::NetworkManager), /// Updates the password text input PasswordUpdate(SecureString), /// Refresh devices and their connection profiles @@ -181,7 +178,7 @@ pub enum VpnDialog { #[derive(Debug)] pub struct NmState { - conn: zbus::Connection, + conn: nmrs::NetworkManager, sender: futures::channel::mpsc::UnboundedSender, active_conns: Vec, devices: Vec, @@ -371,20 +368,22 @@ impl page::Page for Page { let (tx, rx) = tokio::sync::mpsc::channel(4); self.secret_tx = Some(tx); if self.nm_task.is_none() { - return cosmic::Task::batch([cosmic::task::future(async move { - zbus::Connection::system() - .await - .context("failed to create system dbus connection") - .map_or_else( - |why| Message::Error(ErrorKind::DbusConnection, why.to_string()), - Message::NetworkManagerConnect, - ) - }), - cosmic::Task::stream( - cosmic_settings_network_manager_subscription::nm_secret_agent::secret_agent_stream("com.system76.CosmicSettings.VPN.NetworkManager.SecretAgent", rx), - ) - .map(|m| crate::pages::Message::Vpn(Message::SecretAgent(m))), - ]); + return cosmic::Task::batch([ + cosmic::task::future(async move { + nmrs::NetworkManager::new() + .await + .context("failed to connect to NetworkManager") + .map_or_else( + |why| Message::Error(ErrorKind::DbusConnection, why.to_string()), + Message::NetworkManagerConnect, + ) + }), + cosmic::Task::stream(nm_secret_agent::secret_agent_stream( + "com.system76.CosmicSettings.VPN.NetworkManager.SecretAgent", + rx, + )) + .map(|m| crate::pages::Message::Vpn(Message::SecretAgent(m))), + ]); } cosmic::Task::none() @@ -495,9 +494,8 @@ impl Page { Message::WireGuardConfig => { if let Some(VpnDialog::WireGuardName(device, filename, path)) = self.dialog.take() { return cosmic::task::future(async move { - let new_path = path.replace(&filename, &device); - _ = std::fs::rename(&path, &new_path); - match super::nm_add_vpn_file("wireguard", new_path).await { + let _ = filename; + match network_manager::import_wireguard(path, &device).await { Ok(_) => Message::Refresh, Err(why) => Message::Error(ErrorKind::Config, why.to_string()), } @@ -510,18 +508,12 @@ impl Page { if let Some(settings) = self.known_connections.get(&uuid) { let settings = match settings { ConnectionSettings::Vpn(settings) => settings, - ConnectionSettings::Wireguard { id } => { - let connection_name = id.clone(); - return cosmic::task::future(async move { - if let Err(why) = nmcli::connect(&connection_name).await { - return Message::Error( - ErrorKind::Connect, - format!("failed to connect to WireGuard VPN: {why}"), - ); - } - - Message::Refresh - }); + ConnectionSettings::Wireguard { .. } => { + if let Some(NmState { ref sender, .. }) = self.nm_state { + _ = sender + .unbounded_send(network_manager::Request::ActivateVpn(uuid)); + } + return Task::none(); } }; @@ -541,28 +533,10 @@ impl Page { return task::message(Message::FocusSecureInput); } _ => { - let connection_name = settings.id.clone(); - let username = settings.username.clone(); - return cosmic::task::future(async move { - if let Err(why) = nmcli::connect(&connection_name).await { - return Message::VpnDialogError(VpnDialog::Password { - error: Some(( - ErrorKind::Connect, - format!("failed to connect to VPN: {why}"), - )), - id: connection_name.clone(), - uuid, - username: username.clone(), - description: None, - password: SecureString::from(""), - password_hidden: true, - // TODO grab from the current dialog - tx: Arc::new(Mutex::new(None)), - }); - } - - Message::Refresh - }); + if let Some(NmState { ref sender, .. }) = self.nm_state { + _ = sender + .unbounded_send(network_manager::Request::ActivateVpn(uuid)); + } } } } @@ -629,89 +603,55 @@ impl Page { }; if let VpnDialog::Password { - id, + id: _, uuid, username, password, tx, .. } = dialog + && let Some(NmState { ref sender, .. }) = self.nm_state { let username_unwrapped = username.clone().unwrap_or_default(); - let task = self.activate_with_password( - id.clone(), - uuid.clone(), - username_unwrapped.clone(), - password.clone(), - ); let sec_tx = self.secret_tx.clone(); - return task - .then(move |_| { - let sec_tx = sec_tx.clone(); - let uuid = uuid.clone(); - let username = username.clone(); - let password = password.clone(); - let tx = tx.clone(); - let id = id.clone(); - Task::future(async move { - let mut guard = tx.lock().await; - if let Some(sender) = guard.take() { - let _ = sender.send(password); - } else { - // apply password and username then - if let Some(sec_tx) = sec_tx { - let (applied_tx, applied_rx) = - tokio::sync::oneshot::channel(); - if let Err(err) = sec_tx - .send(nm_secret_agent::Request::SetSecrets { - setting_name: "vpn".to_string(), - uuid: uuid.to_string(), - secrets: HashMap::from_iter([ - // username and password - ( - "username".to_string(), - username.clone().unwrap_or_default().into(), - ), - ("password".to_string(), password.clone()), - ]), - applied_tx, - }) - .await - { - tracing::error!(%err, "failed to apply secret"); - } - // wait max 1s for the applied signal - if let Err(err) = tokio::time::timeout( - std::time::Duration::from_secs(1), - applied_rx, - ) - .await - { - tracing::error!(%err, "failed to apply secret"); - } - } - // activate - if let Err(why) = nmcli::connect(&id).await { - return Message::VpnDialogError(VpnDialog::Password { - error: Some(( - ErrorKind::Connect, - format!("failed to connect to VPN: {why}"), - )), - id: id.clone(), - uuid, - username: username.clone(), - description: None, - password, - password_hidden: true, - tx: Arc::new(Mutex::new(None)), - }); - } + let nm_sender = sender.clone(); + return Task::future(async move { + let mut guard = tx.lock().await; + if let Some(sender) = guard.take() { + let _ = sender.send(password); + } else { + if let Some(sec_tx) = sec_tx { + let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); + if let Err(err) = sec_tx + .send(nm_secret_agent::Request::SetSecrets { + setting_name: "vpn".to_string(), + uuid: uuid.to_string(), + secrets: HashMap::from_iter([ + ("username".to_string(), username_unwrapped.into()), + ("password".to_string(), password), + ]), + applied_tx, + }) + .await + { + tracing::error!(%err, "failed to apply secret"); } + if let Err(err) = tokio::time::timeout( + std::time::Duration::from_secs(1), + applied_rx, + ) + .await + { + tracing::error!(%err, "failed to apply secret"); + } + } + _ = nm_sender + .unbounded_send(network_manager::Request::ActivateVpn(uuid)); + } - Message::Refresh - }) - }) - .map(crate::app::Message::from); + Message::Refresh + }) + .map(crate::app::Message::from); } } Message::RetryWithPassword => { @@ -720,53 +660,39 @@ impl Page { }; if let VpnDialog::Password { - id, + id: _, uuid, username, password, .. } = dialog + && let Some(NmState { ref sender, .. }) = self.nm_state { let username_unwrapped = username.unwrap_or_default(); let sec_tx = self.secret_tx.clone(); - let task = self.activate_with_password( - id.clone(), - uuid.clone(), - username_unwrapped.clone(), - password.clone(), - ); - return task - .then(move |_| { - let sec_tx = sec_tx.clone(); - let uuid = uuid.clone(); - let username = username_unwrapped.clone(); - let password = password.clone(); - Task::future(async move { - if let Some(sec_tx) = sec_tx { - let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); - let _ = sec_tx - .send(nm_secret_agent::Request::SetSecrets { - setting_name: "vpn".to_string(), - uuid: uuid.to_string(), - secrets: HashMap::from_iter([ - // username and password - ("username".to_string(), username.clone().into()), - ("password".to_string(), password.clone()), - ]), - applied_tx, - }) - .await; - // wait max 1s for the applied signal - let _ = tokio::time::timeout( - std::time::Duration::from_secs(1), - applied_rx, - ) + let nm_sender = sender.clone(); + return Task::future(async move { + if let Some(sec_tx) = sec_tx { + let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); + let _ = sec_tx + .send(nm_secret_agent::Request::SetSecrets { + setting_name: "vpn".to_string(), + uuid: uuid.to_string(), + secrets: HashMap::from_iter([ + ("username".to_string(), username_unwrapped.into()), + ("password".to_string(), password), + ]), + applied_tx, + }) + .await; + let _ = + tokio::time::timeout(std::time::Duration::from_secs(1), applied_rx) .await; - } - Message::Activate(uuid) - }) - }) - .map(crate::app::Message::from); + } + _ = nm_sender.unbounded_send(network_manager::Request::ActivateVpn(uuid)); + Message::Refresh + }) + .map(crate::app::Message::from); } } Message::UsernameUpdate(user) => { @@ -873,56 +799,14 @@ impl Page { Task::none() } - fn activate_with_password( - &mut self, - connection_name: String, - uuid: Arc, - username: String, - password: SecureString, - ) -> Task { - cosmic::task::future(async move { - if let Err(why) = nmcli::set_username(&connection_name, &username).await { - return Message::VpnDialogError(VpnDialog::Password { - error: Some((ErrorKind::WithPassword("username"), why.to_string())), - id: connection_name.clone(), - uuid, - username: Some(username), - description: None, - password, - password_hidden: true, - tx: Arc::new(Mutex::new(None)), - }); - } - - if let Err(why) = nmcli::add_fallback(&connection_name).await { - return Message::VpnDialogError(VpnDialog::Password { - error: Some((ErrorKind::Config, why.to_string())), - id: connection_name.clone(), - uuid, - username: Some(username), - password, - description: None, - password_hidden: true, - tx: Arc::new(Mutex::new(None)), - }); - } - - Message::Refresh - }) - } - - fn connect(&mut self, conn: zbus::Connection) -> Task { + fn connect(&mut self, conn: nmrs::NetworkManager) -> Task { if self.nm_task.is_none() { let (canceller, task) = crate::utils::forward_event_loop(move |mut sender| 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::devices::watch(conn, true, tx) - ) + network_manager::watch(conn, tx).await; }); let forwarder = std::pin::pin!(async move { @@ -1141,7 +1025,7 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> { .into() } -fn update_state(conn: zbus::Connection) -> Task { +fn update_state(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { match NetworkManagerState::new(&conn).await { Ok(state) => Message::UpdateState(state), @@ -1150,7 +1034,7 @@ fn update_state(conn: zbus::Connection) -> Task { }) } -fn update_devices(conn: zbus::Connection) -> Task { +fn update_devices(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { let filter = |device_type| matches!(device_type, network_manager::devices::DeviceType::WireGuard); @@ -1208,7 +1092,9 @@ fn add_network() -> Task { ); }; - super::nm_add_vpn_file("openvpn", path).await + network_manager::import_openvpn(path) + .await + .map_err(|why| why.to_string()) }; match result { @@ -1223,108 +1109,96 @@ fn add_network() -> Task { .apply(cosmic::task::future) } -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, 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) - }; - +fn connection_settings(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { + let settings = async move { + let vpns = conn.list_vpn_connections().await?; + let mut settings = IndexMap::new(); + + for vpn in vpns { + let uuid = Arc::from(vpn.uuid.as_str()); + let data = match vpn.vpn_type { + nmrs::VpnType::WireGuard { .. } => ConnectionSettings::Wireguard { id: vpn.id }, + nmrs::VpnType::OpenVpn { + connection_type, + user_name, + password_flags, + .. + } => ConnectionSettings::Vpn(VpnConnectionSettings { + id: vpn.id, + username: user_name, + connection_type: connection_type + .filter(|ct| { + matches!( + ct, + nmrs::OpenVpnConnectionType::Password + | nmrs::OpenVpnConnectionType::PasswordTls + ) + }) + .map(|_| ConnectionType::Password), + password_flag: Some(password_flag(password_flags.0)), + }), + nmrs::VpnType::OpenConnect { + user_name, + password_flags, + .. + } + | nmrs::VpnType::StrongSwan { + user_name, + password_flags, + .. + } + | nmrs::VpnType::Pptp { + user_name, + password_flags, + .. + } + | nmrs::VpnType::L2tp { + user_name, + password_flags, + .. + } => ConnectionSettings::Vpn(VpnConnectionSettings { + id: vpn.id, + username: user_name, + connection_type: Some(ConnectionType::Password), + password_flag: Some(password_flag(password_flags.0)), + }), + nmrs::VpnType::Generic { + user_name, + password_flags, + .. + } => ConnectionSettings::Vpn(VpnConnectionSettings { + id: vpn.id, + username: user_name, + connection_type: Some(ConnectionType::Password), + password_flag: Some(password_flag(password_flags.0)), + }), + _ => ConnectionSettings::Vpn(VpnConnectionSettings { + id: vpn.id, + username: None, + connection_type: None, + password_flag: None, + }), + }; + settings.insert(uuid, data); + } + + Ok::<_, nmrs::ConnectionError>(settings) + }; + settings.await.map_or_else( |why| Message::Error(ErrorKind::ConnectionSettings, why.to_string()), Message::KnownConnections, ) }) } + +fn password_flag(value: u32) -> PasswordFlag { + match value { + 0 => PasswordFlag::None, + 1 => PasswordFlag::AgentOwned, + 2 => PasswordFlag::NotSaved, + 4 => PasswordFlag::NotRequired, + _ => PasswordFlag::AgentOwned, + } +} diff --git a/cosmic-settings/src/pages/networking/vpn/nmcli.rs b/cosmic-settings/src/pages/networking/vpn/nmcli.rs deleted file mode 100644 index c759c14..0000000 --- a/cosmic-settings/src/pages/networking/vpn/nmcli.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: GPL-3.0-only - -use cosmic::Apply; -use std::process::Stdio; - -pub async fn set_username(connection_name: &str, username: &str) -> Result<(), String> { - tokio::process::Command::new("nmcli") - .args(["con", "mod", connection_name, "vpn.user-name", username]) - .stderr(Stdio::piped()) - .output() - .await - .apply(crate::utils::map_stderr_output) -} - -pub async fn add_fallback(connection_name: &str) -> Result<(), String> { - tokio::process::Command::new("nmcli") - .args([ - "con", - "mod", - connection_name, - "+vpn.data", - "data-ciphers=AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305:AES-256-CBC:AES-128-CBC", - ]) - .stderr(Stdio::piped()) - .output() - .await - .apply(crate::utils::map_stderr_output) -} - -pub async fn connect(connection_name: &str) -> Result<(), String> { - tokio::process::Command::new("nmcli") - .args(["con", "up", connection_name]) - .stderr(Stdio::piped()) - .output() - .await - .apply(crate::utils::map_stderr_output) -} diff --git a/cosmic-settings/src/pages/networking/wifi.rs b/cosmic-settings/src/pages/networking/wifi.rs index 53c0a37..a715fda 100644 --- a/cosmic-settings/src/pages/networking/wifi.rs +++ b/cosmic-settings/src/pages/networking/wifi.rs @@ -13,16 +13,15 @@ use cosmic::widget::space::horizontal; use cosmic::widget::text_input::focus; use cosmic::widget::{self, column, icon}; use cosmic::{Apply, Element, Task, task}; -use cosmic_settings_network_manager_subscription::available_wifi::{AccessPoint, NetworkType}; -use cosmic_settings_network_manager_subscription::current_networks::ActiveConnectionInfo; -use cosmic_settings_network_manager_subscription::{ - self as network_manager, NetworkManagerState, nm_secret_agent, -}; use cosmic_settings_page::{self as page, Section, section}; use futures::{SinkExt, StreamExt}; use secure_string::SecureString; use tokio::sync::Mutex; +use super::backend as network_manager; +use super::backend::available_wifi::{AccessPoint, NetworkType}; +use super::backend::current_networks::ActiveConnectionInfo; +use super::backend::{NetworkManagerState, nm_secret_agent}; use crate::pages::networking::SecretSender; pub static SECURE_INPUT_WIFI: LazyLock = LazyLock::new(widget::Id::unique); @@ -55,8 +54,8 @@ pub enum Message { NetworkManager(network_manager::Event), /// An update from the secret agent SecretAgent(network_manager::nm_secret_agent::Event), - /// Successfully connected to the system dbus. - NetworkManagerConnect(zbus::Connection), + /// Successfully connected to NetworkManager. + NetworkManagerConnect(nmrs::NetworkManager), /// Request an auth dialog PasswordRequest(network_manager::SSID), /// Update the password from the dialog @@ -102,6 +101,7 @@ enum WiFiDialog { ssid: network_manager::SSID, identity: Option, password: SecureString, + network_type: NetworkType, password_hidden: bool, tx: SecretSender, }, @@ -141,7 +141,7 @@ pub struct Page { #[derive(Debug)] pub struct NmState { - conn: zbus::Connection, + conn: nmrs::NetworkManager, sender: futures::channel::mpsc::UnboundedSender, state: network_manager::NetworkManagerState, devices: Vec, @@ -294,19 +294,23 @@ impl page::Page for Page { let (tx, rx) = tokio::sync::mpsc::channel(4); self.secret_tx = Some(tx); if self.nm_task.is_none() { - return 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, - ) - .apply(crate::pages::Message::WiFi) - }), cosmic::Task::stream( - cosmic_settings_network_manager_subscription::nm_secret_agent::secret_agent_stream("com.system76.CosmicSettings.WiFi.NetworkManager.SecretAgent", rx), - ) - .map(|m| crate::pages::Message::WiFi(Message::SecretAgent(m)))]); + return Task::batch(vec![ + cosmic::Task::future(async move { + nmrs::NetworkManager::new() + .await + .context("failed to connect to NetworkManager") + .map_or_else( + |why| Message::Error(why.to_string()), + Message::NetworkManagerConnect, + ) + .apply(crate::pages::Message::WiFi) + }), + cosmic::Task::stream(nm_secret_agent::secret_agent_stream( + "com.system76.CosmicSettings.WiFi.NetworkManager.SecretAgent", + rx, + )) + .map(|m| crate::pages::Message::WiFi(Message::SecretAgent(m))), + ]); } Task::none() @@ -352,15 +356,22 @@ impl Page { } match req { - network_manager::Request::Authenticate { ssid, identity, .. } => { + network_manager::Request::Authenticate { + ssid, + identity, + network_type, + .. + } => { if success { self.connecting.remove(ssid.as_str()); } else { + self.connecting.remove(ssid.as_str()); // Request to retry self.dialog = Some(WiFiDialog::Password { ssid: ssid.into(), identity, password: SecureString::from(""), + network_type, password_hidden: true, tx: Arc::new(Mutex::new(None)), }); @@ -377,11 +388,13 @@ impl Page { if success || matches!(network_type, NetworkType::Open) { self.connecting.remove(ssid.as_ref()); } else { + self.connecting.remove(ssid.as_ref()); self.dialog = Some(WiFiDialog::Password { ssid, identity: matches!(network_type, NetworkType::EAP) .then(String::new), password: SecureString::from(""), + network_type, password_hidden: true, tx: Arc::new(Mutex::new(None)), }); @@ -449,6 +462,7 @@ impl Page { let qr_string = if let Some(ref pass) = password { let security = match security_type { NetworkType::PskOrSae => "WPA", + NetworkType::Sae => "WPA", NetworkType::EAP => "WPA", NetworkType::Open => "", }; @@ -521,6 +535,7 @@ impl Page { ssid, identity: matches!(ap.network_type, NetworkType::EAP).then(String::new), password: SecureString::from(""), + network_type: ap.network_type, password_hidden: true, tx: Arc::new(Mutex::new(None)), }); @@ -544,6 +559,7 @@ impl Page { ssid, identity, password, + network_type, tx, .. } = dialog @@ -562,6 +578,7 @@ impl Page { ssid: ssid.to_string(), identity, password, + network_type, secret_tx, interface, }); @@ -614,6 +631,7 @@ impl Page { } Message::Disconnect(ssid) => { self.close_popup_and_apply_updates(); + self.connecting.remove(ssid.as_ref()); if let Some(nm) = self.nm_state.as_mut() { _ = nm .sender @@ -627,6 +645,7 @@ impl Page { Message::Forget(ssid) => { self.dialog = None; self.close_popup_and_apply_updates(); + self.connecting.remove(ssid.as_ref()); if let Some(nm) = self.nm_state.as_mut() { _ = nm .sender @@ -648,6 +667,9 @@ impl Page { } } Message::WiFiEnable(enable) => { + if !enable { + self.connecting.clear(); + } if let Some(nm) = self.nm_state.as_mut() { _ = nm .sender @@ -656,7 +678,9 @@ impl Page { } } Message::CancelDialog => { - self.dialog = None; + if let Some(WiFiDialog::Password { ssid, .. }) = self.dialog.take() { + self.connecting.remove(ssid.as_ref()); + } } Message::Error(why) => { tracing::error!(why); @@ -715,15 +739,20 @@ impl Page { password: previous, password_hidden: true, identity: matches!(ap.network_type, NetworkType::EAP).then(String::new), + network_type: ap.network_type, tx, }); return task::message(Message::FocusSecureInput); } nm_secret_agent::Event::CancelGetSecrets { uuid: _, name: _ } => { - self.dialog = self - .dialog - .take() - .filter(|d| !matches!(d, &WiFiDialog::Password { .. })); + match self.dialog.take() { + Some(WiFiDialog::Password { ssid, .. }) => { + self.connecting.remove(ssid.as_ref()); + } + other => { + self.dialog = other; + } + } } nm_secret_agent::Event::Failed(error) => { tracing::error!(%error, "secret agent failure"); @@ -731,15 +760,18 @@ impl Page { ssid, password, identity, + network_type, .. }) = self.dialog.take() { + self.connecting.remove(ssid.as_ref()); self.dialog = Some(WiFiDialog::Password { password, password_hidden: true, tx: Arc::new(Mutex::new(None)), ssid, identity, + network_type, }); return task::message(Message::FocusSecureInput); } @@ -770,19 +802,14 @@ impl Page { Task::none() } - fn connect(&mut self, conn: zbus::Connection) -> Task { + fn connect(&mut self, conn: nmrs::NetworkManager) -> Task { if self.nm_task.is_none() { let (canceller, task) = crate::utils::forward_event_loop(move |mut sender| 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) - ); + network_manager::watch(conn, tx).await; }); let forwarder = std::pin::pin!(async move { @@ -832,6 +859,12 @@ impl Page { /// Withholds updates if the view more popup is displayed. fn update_state(&mut self, state: NetworkManagerState) { + for active in &state.active_conns { + if let ActiveConnectionInfo::WiFi { name, .. } = active { + self.connecting.remove(name.as_str()); + } + } + if let Some(ref mut nm_state) = self.nm_state { if self.view_more_popup.is_some() { self.withheld_state = Some(state); @@ -1271,51 +1304,9 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> { .into() } -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) - }; - +fn connection_settings(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { - settings + network_manager::wifi_connection_settings(conn) .await .context("failed to get connection settings") .map_or_else( @@ -1326,7 +1317,7 @@ fn connection_settings(conn: zbus::Connection) -> Task { }) } -pub fn update_state(conn: zbus::Connection) -> Task { +pub fn update_state(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { match NetworkManagerState::new(&conn).await { Ok(state) => Message::UpdateState(state), @@ -1335,7 +1326,7 @@ pub fn update_state(conn: zbus::Connection) -> Task { }) } -pub fn update_devices(conn: zbus::Connection) -> Task { +pub fn update_devices(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { let filter = |device_type| matches!(device_type, network_manager::devices::DeviceType::Wifi); diff --git a/cosmic-settings/src/pages/networking/wired.rs b/cosmic-settings/src/pages/networking/wired.rs index 104d4a0..3a1bf86 100644 --- a/cosmic-settings/src/pages/networking/wired.rs +++ b/cosmic-settings/src/pages/networking/wired.rs @@ -10,12 +10,14 @@ use cosmic::iced::{Alignment, Length}; use cosmic::widget::space::horizontal as horizontal_space; use cosmic::widget::{self, icon}; use cosmic::{Apply, Element, Task}; -use cosmic_dbus_networkmanager::interface::enums::DeviceState; -use cosmic_settings_network_manager_subscription::current_networks::ActiveConnectionInfo; -use cosmic_settings_network_manager_subscription::{self as network_manager, NetworkManagerState}; use cosmic_settings_page::{self as page, Section, section}; use futures::{SinkExt, StreamExt}; +use super::backend as network_manager; +use super::backend::NetworkManagerState; +use super::backend::current_networks::ActiveConnectionInfo; +use super::backend::devices::DeviceState; + pub type ConnectionId = Arc; #[derive(Clone, Debug)] @@ -32,8 +34,8 @@ pub enum Message { Error(String), /// An update from the network manager daemon NetworkManager(network_manager::Event), - /// Successfully connected to the system dbus. - NetworkManagerConnect(zbus::Connection), + /// Successfully connected to NetworkManager. + NetworkManagerConnect(nmrs::NetworkManager), /// Refresh devices and their connection profiles Refresh, /// Create a dialog to ask for confirmation of removal. @@ -91,7 +93,7 @@ pub struct Page { #[derive(Debug)] pub struct NmState { - conn: zbus::Connection, + conn: nmrs::NetworkManager, sender: futures::channel::mpsc::UnboundedSender, active_conns: Vec, devices: Vec>, @@ -154,9 +156,9 @@ impl page::Page for Page { fn on_enter(&mut self) -> cosmic::Task { if self.nm_task.is_none() { return cosmic::task::future(async move { - zbus::Connection::system() + nmrs::NetworkManager::new() .await - .context("failed to create system dbus connection") + .context("failed to connect to NetworkManager") .map_or_else( |why| Message::Error(why.to_string()), Message::NetworkManagerConnect, @@ -356,18 +358,14 @@ impl Page { Task::none() } - fn connect(&mut self, conn: zbus::Connection) -> Task { + fn connect(&mut self, conn: nmrs::NetworkManager) -> Task { if self.nm_task.is_none() { let (canceller, task) = crate::utils::forward_event_loop(move |mut sender| 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::devices::watch(conn, true, tx) - ) + network_manager::watch(conn, tx).await; }); let forwarder = std::pin::pin!(async move { @@ -622,7 +620,7 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> { .into() } -fn update_state(conn: zbus::Connection) -> Task { +fn update_state(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { match NetworkManagerState::new(&conn).await { Ok(state) => Message::UpdateState(state), @@ -631,7 +629,7 @@ fn update_state(conn: zbus::Connection) -> Task { }) } -fn update_devices(conn: zbus::Connection) -> Task { +fn update_devices(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { let filter = |device_type| matches!(device_type, network_manager::devices::DeviceType::Ethernet); diff --git a/subscriptions/network-manager/Cargo.toml b/subscriptions/network-manager/Cargo.toml deleted file mode 100644 index e40a2a8..0000000 --- a/subscriptions/network-manager/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "cosmic-settings-network-manager-subscription" -version = "1.0.7" -edition = "2024" -license = "MPL-2.0" -rust-version.workspace = true -publish = true - -[dependencies] -cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" } -secret-service = { version = "5.1.0", features = ["rt-tokio-crypto-rust"] } -nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings" } -futures = "0.3.32" -iced_futures = { git = "https://github.com/pop-os/libcosmic" } -itertools = "0.14.0" -secure-string = "0.3.0" -thiserror = "2.0.18" -tokio = "1.49.0" -tracing = "0.1.44" -zbus = { version = "5.13.2", features = ["tokio"] } -bitflags = "2.11.0" diff --git a/subscriptions/network-manager/LICENSE.md b/subscriptions/network-manager/LICENSE.md deleted file mode 100644 index 8dc5b15..0000000 --- a/subscriptions/network-manager/LICENSE.md +++ /dev/null @@ -1,359 +0,0 @@ -Mozilla Public License Version 2.0 -================================== - -## 1. Definitions - -### 1.1. "Contributor" -means each individual or legal entity that creates, contributes to -the creation of, or owns Covered Software. - -### 1.2. "Contributor Version" -means the combination of the Contributions of others (if any) used -by a Contributor and that particular Contributor's Contribution. - -### 1.3. "Contribution" -means Covered Software of a particular Contributor. - -### 1.4. "Covered Software" -means Source Code Form to which the initial Contributor has attached -the notice in Exhibit A, the Executable Form of such Source Code -Form, and Modifications of such Source Code Form, in each case -including portions thereof. - -### 1.5. "Incompatible With Secondary Licenses" -means - -+ (a) that the initial Contributor has attached the notice described -in Exhibit B to the Covered Software; or - -+ (b) that the Covered Software was made available under the terms of -version 1.1 or earlier of the License, but not also under the -terms of a Secondary License. - -### 1.6. "Executable Form" -means any form of the work other than Source Code Form. - -### 1.7. "Larger Work" -means a work that combines Covered Software with other material, in -a separate file or files, that is not Covered Software. - -### 1.8. "License" -means this document. - -### 1.9. "Licensable" -means having the right to grant, to the maximum extent possible, -whether at the time of the initial grant or subsequently, any and -all of the rights conveyed by this License. - -### 1.10. "Modifications" -means any of the following: - -+ (a) any file in Source Code Form that results from an addition to, -deletion from, or modification of the contents of Covered -Software; or - -+ (b) any new file in Source Code Form that contains any Covered -Software. - -### 1.11. "Patent Claims" of a Contributor -means any patent claim(s), including without limitation, method, -process, and apparatus claims, in any patent Licensable by such -Contributor that would be infringed, but for the grant of the -License, by the making, using, selling, offering for sale, having -made, import, or transfer of either its Contributions or its -Contributor Version. - -### 1.12. "Secondary License" -means either the GNU General Public License, Version 2.0, the GNU -Lesser General Public License, Version 2.1, the GNU Affero General -Public License, Version 3.0, or any later versions of those -licenses. - -### 1.13. "Source Code Form" -means the form of the work preferred for making modifications. - -### 1.14. "You" (or "Your") -means an individual or a legal entity exercising rights under this -License. For legal entities, "You" includes any entity that -controls, is controlled by, or is under common control with You. For -purposes of this definition, "control" means (a) the power, direct -or indirect, to cause the direction or management of such entity, -whether by contract or otherwise, or (b) ownership of more than -fifty percent (50%) of the outstanding shares or beneficial -ownership of such entity. - -## 2. License Grants and Conditions - -### 2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -+ (a) under intellectual property rights (other than patent or trademark) -Licensable by such Contributor to use, reproduce, make available, -modify, display, perform, distribute, and otherwise exploit its -Contributions, either on an unmodified basis, with Modifications, or -as part of a Larger Work; and - -+ (b) under Patent Claims of such Contributor to make, use, sell, offer -for sale, have made, import, and otherwise transfer either its -Contributions or its Contributor Version. - -### 2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -### 2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -+ (a) for any code that a Contributor has removed from Covered Software; -or - -+ (b) for infringements caused by: (i) Your and any other third party's -modifications of Covered Software, or (ii) the combination of its -Contributions with other software (except as part of its Contributor -Version); or - -+ (c) under Patent Claims infringed by Covered Software in the absence of -its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -### 2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -### 2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -### 2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -### 2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -## 3. Responsibilities - -### 3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -### 3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -+ (a) such Covered Software must also be made available in Source Code -Form, as described in Section 3.1, and You must inform recipients of -the Executable Form how they can obtain a copy of such Source Code -Form by reasonable means in a timely manner, at a charge no more -than the cost of distribution to the recipient; and - -+ (b) You may distribute such Executable Form under the terms of this -License, or sublicense it under different terms, provided that the -license for the Executable Form does not attempt to limit or alter -the recipients' rights in the Source Code Form under this License. - -### 3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -### 3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -### 3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -## 4. Inability to Comply Due to Statute or Regulation - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -## 5. Termination - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - - -## 6. Disclaimer of Warranty - -**Covered Software is provided under this License on an "as is" -basis, without warranty of any kind, either expressed, implied, or -statutory, including, without limitation, warranties that the -Covered Software is free of defects, merchantable, fit for a -particular purpose or non-infringing. The entire risk as to the -quality and performance of the Covered Software is with You. -Should any Covered Software prove defective in any respect, You -(not any Contributor) assume the cost of any necessary servicing, -repair, or correction. This disclaimer of warranty constitutes an -essential part of this License. No use of any Covered Software is -authorized under this License except under this disclaimer.** - - -#7. Limitation of Liability - -**Under no circumstances and under no legal theory, whether tort -(including negligence), contract, or otherwise, shall any -Contributor, or anyone who distributes Covered Software as -permitted above, be liable to You for any direct, indirect, -special, incidental, or consequential damages of any character -including, without limitation, damages for lost profits, loss of -goodwill, work stoppage, computer failure or malfunction, or any -and all other commercial damages or losses, even if such party -shall have been informed of the possibility of such damages. This -limitation of liability shall not apply to liability for death or -personal injury resulting from such party's negligence to the -extent applicable law prohibits such limitation. Some -jurisdictions do not allow the exclusion or limitation of -incidental or consequential damages, so this exclusion and -limitation may not apply to You.** - - -## 8. Litigation - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -## 9. Miscellaneous - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -## 10. Versions of the License - -### 10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -### 10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -### 10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -### 10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -## Exhibit A - Source Code Form License Notice - - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -## Exhibit B - "Incompatible With Secondary Licenses" Notice - - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. - diff --git a/subscriptions/network-manager/src/active_conns.rs b/subscriptions/network-manager/src/active_conns.rs deleted file mode 100644 index 63af460..0000000 --- a/subscriptions/network-manager/src/active_conns.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::Wrapper; - -use super::Event; -use cosmic_dbus_networkmanager::nm::NetworkManager; -use futures::{SinkExt, StreamExt}; -use iced_futures::{Subscription, stream}; -use std::fmt::Debug; -use std::hash::Hash; -use zbus::Connection; - -#[derive(Debug, Clone)] -pub enum State { - Continue(Connection), - Error, -} - -pub fn active_conns_subscription( - id: I, - conn: Connection, -) -> iced_futures::Subscription { - Subscription::run_with(Wrapper { id, conn: conn }, |Wrapper { id: _id, conn }| { - let conn = conn.clone(); - stream::channel(50, move |output| async move { - watch(conn, output).await; - futures::future::pending().await - }) - }) -} - -pub async fn watch(conn: zbus::Connection, mut output: futures::channel::mpsc::Sender) { - let mut state = State::Continue(conn); - - loop { - state = start_listening(state, &mut output).await; - } -} - -async fn start_listening( - state: State, - output: &mut futures::channel::mpsc::Sender, -) -> State { - let conn = match state { - State::Continue(conn) => conn, - State::Error => 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), _) = futures::future::join( - active_conns_changed.next(), - tokio::time::sleep(tokio::time::Duration::from_secs(1)), - ) - .await - { - _ = output.send(Event::ActiveConns).await; - } - - State::Continue(conn) -} diff --git a/subscriptions/network-manager/src/available_wifi.rs b/subscriptions/network-manager/src/available_wifi.rs deleted file mode 100644 index 0653ae1..0000000 --- a/subscriptions/network-manager/src/available_wifi.rs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -use cosmic_dbus_networkmanager::device::wireless::WirelessDevice; -use cosmic_dbus_networkmanager::interface::enums::{ApFlags, ApSecurityFlags, DeviceState}; - -use futures::StreamExt; -use itertools::Itertools; -use std::collections::HashMap; -use std::sync::Arc; -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 - && let Ok(-1) = t.get().await - { - tracing::error!("wireless device scan errored"); - return Ok(Default::default()); - } - - let access_points = device.get_access_points().await?; - - let state: DeviceState = device - .upcast() - .await - .and_then(|dev| dev.cached_state()) - .unwrap_or_default() - .map(|s| s.into()) - .unwrap_or_else(|| DeviceState::Unknown); - - // Sort by strength and remove duplicates - let mut aps = HashMap::::new(); - for ap in access_points { - let (ssid_res, strength_res) = futures::join!(ap.ssid(), ap.strength()); - - if let Some((ssid, strength)) = ssid_res.ok().zip(strength_res.ok()) { - let ssid = String::from_utf8_lossy(&ssid.clone()).into_owned(); - if let Some(access_point) = aps.get(&ssid) - && access_point.strength > strength - { - continue; - } - - 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 | ApSecurityFlags::KEY_MGMT_SAE) - { - NetworkType::PskOrSae - } else if flags.intersects(ApSecurityFlags::KEY_MGMT_OWE) || flags.is_empty() { - NetworkType::Open - } else { - continue; - }; - - aps.insert( - ssid.clone(), - AccessPoint { - ssid: Arc::from(ssid), - strength, - state, - working: false, - path: ap.inner().path().to_owned(), - secured: !ap.wpa_flags().await?.is_empty(), - wps_push: ap.flags().await?.contains(ApFlags::WPS_PBC), - network_type, - hw_address: hw_address - .as_ref() - .and_then(|str_addr| HwAddress::from_str(str_addr)) - .unwrap_or_default(), - }, - ); - } - } - - let aps = aps - .into_values() - .sorted_by(|a, b| b.strength.cmp(&a.strength)) - .collect(); - - Ok(aps) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AccessPoint { - pub ssid: Arc, - pub strength: u8, - pub state: DeviceState, - pub working: bool, - pub path: ObjectPath<'static>, - pub hw_address: HwAddress, - pub secured: bool, - 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 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NetworkType { - Open, - PskOrSae, - EAP, -} diff --git a/subscriptions/network-manager/src/current_networks.rs b/subscriptions/network-manager/src/current_networks.rs deleted file mode 100644 index 45ac95d..0000000 --- a/subscriptions/network-manager/src/current_networks.rs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -use cosmic_dbus_networkmanager::active_connection::ActiveConnection; -use cosmic_dbus_networkmanager::device::SpecificDevice; -use cosmic_dbus_networkmanager::interface::enums::ActiveConnectionState; -use std::net::Ipv4Addr; - -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() { - match device - .downcast_to_device() - .await - .ok() - .and_then(|inner| inner) - { - Some(SpecificDevice::Wired(wired_device)) => { - info.push(ActiveConnectionInfo::Wired { - name: connection.id().await?, - hw_address: wired_device.hw_address().await?, - speed: wired_device.speed().await?, - 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: wireless_device.hw_address().await?, - 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_by(|a, b| { - let helper = |conn: &ActiveConnectionInfo| match conn { - ActiveConnectionInfo::Vpn { name, .. } => format!("0{name}"), - ActiveConnectionInfo::Wired { name, .. } => format!("1{name}"), - ActiveConnectionInfo::WiFi { name, .. } => format!("2{name}"), - }; - helper(a).cmp(&helper(b)) - }); - - Ok(info) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ActiveConnectionInfo { - Wired { - name: String, - hw_address: String, - speed: u32, - ip_addresses: Vec, - }, - WiFi { - name: String, - ip_addresses: Vec, - hw_address: String, - state: ActiveConnectionState, - strength: u8, - }, - Vpn { - name: String, - ip_addresses: Vec, - }, -} - -impl ActiveConnectionInfo { - pub fn name(&self) -> String { - match &self { - Self::Wired { name, .. } => name.clone(), - Self::WiFi { name, .. } => name.clone(), - Self::Vpn { name, .. } => name.clone(), - } - } -} diff --git a/subscriptions/network-manager/src/devices.rs b/subscriptions/network-manager/src/devices.rs deleted file mode 100644 index 4dbcdb6..0000000 --- a/subscriptions/network-manager/src/devices.rs +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -use super::Event; -pub use cosmic_dbus_networkmanager::interface::enums::{ - ActiveConnectionState, DeviceState, DeviceType, -}; - -use core::hash; -use cosmic_dbus_networkmanager::nm::NetworkManager; -use futures::{SinkExt, StreamExt}; -use iced_futures::{self, Subscription, stream}; -use std::fmt::Debug; -use std::hash::Hash; -use std::sync::Arc; -use zbus::Connection; -use zbus::zvariant::ObjectPath; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct DeviceInfo { - pub path: ObjectPath<'static>, - pub device_type: DeviceType, - pub interface: String, - pub state: DeviceState, - pub active_connection: Option<(DeviceConnection, ActiveConnectionState)>, - pub available_connections: Vec, - pub known_connections: Vec, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct DeviceConnection { - pub path: ObjectPath<'static>, - pub id: String, - pub uuid: Arc, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct KnownDeviceConnection { - pub id: String, - pub uuid: Arc, -} - -pub async fn list( - conn: &zbus::Connection, - device_type_filter: fn(DeviceType) -> bool, -) -> zbus::Result> { - let nm = NetworkManager::new(conn).await?; - - let (devices, nm_settings) = futures::try_join!(nm.devices(), nm.settings())?; - - let connection_settings: &Vec<_> = &futures::stream::FuturesOrdered::from_iter( - nm_settings - .list_connections() - .await? - .into_iter() - .map(|connection| async move { connection.get_settings().await }), - ) - .filter_map(|res| async move { res.ok() }) - .collect() - .await; - - let device_iter = devices.into_iter().map(|device| async move { - let (interface, hw_address, device_type, state, available_connections) = - futures::try_join!( - device.interface(), - device.hw_address(), - device.device_type(), - device.state(), - device.available_connections() - ) - .ok()?; - - if !device_type_filter(device_type) { - return None; - } - - if hw_address.is_empty() { - return None; - } - - let (active_connection, available_connections) = futures::join!( - async { - let connection = device.active_connection().await?; - - let (id, uuid, state) = - futures::try_join!(connection.id(), connection.uuid(), connection.state())?; - - Ok::<_, zbus::Error>(( - DeviceConnection { - id, - uuid: Arc::from(uuid), - path: connection.inner().path().to_owned(), - }, - state, - )) - }, - futures::stream::FuturesOrdered::from_iter(available_connections.into_iter().map( - |conn| async move { - let path = conn.inner().path().to_owned(); - - let settings = conn.get_settings().await.ok()?; - - let id = settings - .get("connection")? - .get("id")? - .downcast_ref::() - .ok()?; - - let uuid = settings["connection"] - .get("uuid")? - .downcast_ref::() - .ok()?; - - Some(DeviceConnection { - id, - uuid: Arc::from(uuid), - path, - }) - } - ),) - .filter_map(|res| async move { res }) - .collect::>() - ); - - let known_connections = connection_settings - .iter() - .flat_map(|conn_settings| { - let connection = conn_settings.get("connection")?; - - let interface_name = connection - .get("interface-name")? - .downcast_ref::() - .ok()?; - - if interface_name != interface { - return None; - } - - let id = connection.get("id")?.downcast_ref::().ok()?; - let uuid = connection.get("uuid")?.downcast_ref::().ok()?; - - Some(KnownDeviceConnection { - uuid: Arc::from(uuid), - id, - }) - }) - .collect(); - - Some(DeviceInfo { - path: device.inner().path().to_owned(), - device_type, - interface, - state, - active_connection: active_connection.ok(), - known_connections, - available_connections, - }) - }); - - let devices_info = futures::stream::FuturesOrdered::from_iter(device_iter) - .filter_map(|res| async move { res }) - .collect::>() - .await; - - Ok(devices_info) -} - -pub fn subscription( - id: I, - has_popup: bool, - conn: Connection, -) -> iced_futures::Subscription { - struct Wrapper { - id: I, - has_popup: bool, - conn: Connection, - } - impl Hash for Wrapper { - fn hash(&self, state: &mut H) { - self.id.hash(state); - self.has_popup.hash(state); - } - } - Subscription::run_with( - Wrapper { - id, - has_popup, - conn, - }, - |Wrapper { - id, - has_popup, - conn, - }| { - let conn = conn.clone(); - let has_popup = *has_popup; - stream::channel(50, move |output| async move { - watch(conn, has_popup, output).await; - futures::future::pending().await - }) - }, - ) -} - -pub async fn watch( - conn: zbus::Connection, - has_popup: bool, - mut output: futures::channel::mpsc::Sender, -) { - let mut state = State::Continue(conn); - - 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 => 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 network_manager" - ); - 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), _) = futures::future::join( - devices_changed.next(), - tokio::time::sleep(tokio::time::Duration::from_secs(secs)), - ) - .await - { - _ = output.send(Event::Devices).await; - } - - State::Continue(conn) -} diff --git a/subscriptions/network-manager/src/hw_address.rs b/subscriptions/network-manager/src/hw_address.rs deleted file mode 100644 index eb49769..0000000 --- a/subscriptions/network-manager/src/hw_address.rs +++ /dev/null @@ -1,102 +0,0 @@ -#[derive(Clone, PartialEq, Eq, Default, Debug, PartialOrd, Ord)] -pub struct HwAddress { - octets: Vec, -} - -impl HwAddress { - pub fn from_str(arg: &str) -> Option { - let segments: Vec<&str> = arg.split(":").collect(); - - // Only accept 6-byte (EUI-48) or 8-byte (EUI-64) addresses - if segments.len() != 6 && segments.len() != 8 { - return None; - } - - let mut octets: Vec = Vec::new(); - for segment in segments { - if segment.len() != 2 { - return None; - } - let byte: u8 = u8::from_str_radix(segment, 16).ok()?; - octets.push(byte); - } - - Some(HwAddress { octets }) - } -} - -impl std::fmt::Display for HwAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let hex_parts: Vec = self - .octets - .iter() - .map(|byte| format!("{:02x}", byte)) - .collect(); - write!(f, "{}", hex_parts.join(":")) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_valid_6_byte_mac() { - let mac: &str = "00:11:22:33:44:55"; - let hw_addr: HwAddress = HwAddress::from_str(mac).expect("should parse valid MAC"); - - // Access the internal octets field - assert_eq!(hw_addr.octets.len(), 6); - assert_eq!(hw_addr.octets, vec![0x00, 0x11, 0x22, 0x33, 0x44, 0x55]); - } - - #[test] - fn test_display_6_byte_mac() { - let hw_addr: HwAddress = HwAddress { - octets: vec![0x00, 0x11, 0x22, 0x33, 0x44, 0x55], - }; - - assert_eq!(format!("{}", hw_addr), "00:11:22:33:44:55"); - } - - #[test] - fn test_parse_valid_8_byte_mac() { - let mac: &str = "00:11:22:33:44:55:66:77"; - let hw_addr: HwAddress = HwAddress::from_str(mac).expect("should parse valid EUI-64 MAC"); - - assert_eq!(hw_addr.octets.len(), 8); - assert_eq!( - hw_addr.octets, - vec![0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77] - ); - } - - #[test] - fn test_display_8_byte_mac() { - let hw_addr: HwAddress = HwAddress { - octets: vec![0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77], - }; - - assert_eq!(format!("{}", hw_addr), "00:11:22:33:44:55:66:77"); - } - - #[test] - fn test_reject_invalid_length_macs() { - let invalid_macs: Vec<(&str, &str)> = vec![ - ("00", "1-byte MAC"), - ("00:11:22:33", "4-byte MAC"), - ("00:11:22:33:44", "5-byte MAC"), - ("00:11:22:33:44:55:66", "7-byte MAC"), - ("00:11:22:33:44:55:66:77:88", "9-byte MAC"), - ("00:11:22:33:44:55:66:77:88:99:aa:bb", "12-byte MAC"), - ]; - - for (mac, description) in invalid_macs { - assert!( - HwAddress::from_str(mac).is_none(), - "should reject {}", - description - ); - } - } -} diff --git a/subscriptions/network-manager/src/lib.rs b/subscriptions/network-manager/src/lib.rs deleted file mode 100644 index 675b7bf..0000000 --- a/subscriptions/network-manager/src/lib.rs +++ /dev/null @@ -1,1169 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -pub mod active_conns; -pub mod available_wifi; -pub mod current_networks; -pub mod devices; -pub mod hw_address; -pub mod nm_secret_agent; -pub mod wireless_enabled; - -use std::collections::HashMap; -use std::fmt::Debug; -use std::hash::Hash; -use std::sync::Arc; -use std::time::Duration; - -use available_wifi::NetworkType; -pub use cosmic_dbus_networkmanager as dbus; -pub use dbus::settings::connection::Settings; - -use cosmic_dbus_networkmanager::device::SpecificDevice; -use cosmic_dbus_networkmanager::interface::enums::{ - self, ActiveConnectionState, DeviceType, NmConnectivityState, -}; -use cosmic_dbus_networkmanager::interface::settings::connection::ConnectionSettingsProxy; -use cosmic_dbus_networkmanager::nm::NetworkManager; -use cosmic_dbus_networkmanager::settings::NetworkManagerSettings; -use cosmic_dbus_networkmanager::settings::connection::Connection; -use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}; -use futures::{FutureExt, SinkExt, StreamExt}; -use iced_futures::{Subscription, stream}; -use secure_string::SecureString; -use tokio::process::Command; -use zbus::zvariant::{self, ObjectPath, Value}; - -use self::available_wifi::{AccessPoint, handle_wireless_device}; -use self::current_networks::{ActiveConnectionInfo, active_connections}; - -pub type SSID = Arc; -pub type UUID = Arc; - -pub(crate) struct Wrapper { - id: I, - conn: zbus::Connection, -} - -impl Hash for Wrapper { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("failed to list bluetooth devices with rfkill")] - BluetoothRfkillList(std::io::Error), - #[error("failed to activate connection")] - ConnectionActivate, - #[error("no wifi devices found")] - NoWiFiDevices, - #[error("zbus error")] - Zbus(#[from] zbus::Error), - #[error("missing connection field")] - MissingField(&'static str), -} - -#[derive(Debug)] -pub enum State { - Ready(zbus::Connection), - Waiting(zbus::Connection, UnboundedReceiver), - Finished, -} - -/// Reloads state on available connection changes. -pub async fn watch_connections_changed( - conn: zbus::Connection, - mut output: futures::channel::mpsc::Sender, -) { - let Ok(nm) = NetworkManager::new(&conn).await else { - return; - }; - - let mut device_stream = nm.receive_devices_changed().await; - - loop { - // Emits updates when available connections changes. - let connections_changed = std::pin::pin!(async { - let devices = nm.devices().await.unwrap_or_default(); - - let mut connection_streams = - futures::stream::FuturesUnordered::from_iter(devices.into_iter().map( - |device| async move { device.receive_available_connections_changed().await }, - )) - .collect::>() - .await; - - let mut available_connections = futures::stream::FuturesUnordered::from_iter( - connection_streams - .iter_mut() - .map(|stream| async { stream.next().await }), - ); - - loop { - _ = futures::join!( - available_connections.next(), - tokio::time::sleep(Duration::from_secs(3)) - ); - - // TODO: although it should consume the stream, the stream is never empty. - // while available_connections.next().now_or_never().is_some() {} - - let state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - - _ = output - .send(Event::RequestResponse { - req: Request::Reload, - state, - success: true, - }) - .await; - } - }); - - // Reload the connection streams whenever devices change. - futures::future::select(connections_changed, device_stream.next()).await; - } -} - -pub fn subscription( - id: I, - conn: zbus::Connection, -) -> iced_futures::Subscription { - struct Wrapper { - id: I, - conn: zbus::Connection, - } - impl Hash for Wrapper { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } - } - Subscription::run_with(Wrapper { id, conn }, |Wrapper { id, conn }| { - let conn = conn.clone(); - stream::channel( - 50, - |output: futures::channel::mpsc::Sender| async move { - watch(conn, output).await; - futures::future::pending().await - }, - ) - }) -} - -pub async fn watch(conn: zbus::Connection, mut output: futures::channel::mpsc::Sender) { - let mut state = State::Ready(conn); - - 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(conn) => { - let (tx, rx) = unbounded(); - - if output - .send(Event::Init { - conn: conn.clone(), - sender: tx, - state: NetworkManagerState::new(&conn).await.unwrap_or_default(), - }) - .await - .is_ok() - { - State::Waiting(conn, rx) - } else { - State::Finished - } - } - State::Waiting(conn, mut rx) => { - let network_manager = match NetworkManager::new(&conn).await { - Ok(n) => n, - Err(_) => return State::Finished, - }; - - match rx.next().await { - Some(Request::Deactivate(uuid)) => { - let mut success = false; - for c in network_manager - .active_connections() - .await - .unwrap_or_default() - { - if c.uuid().await.unwrap_or_default().as_str() == uuid.as_ref() - && 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 - && let Ok(ActiveConnectionState::Deactivated) = - next.get().await.map(ActiveConnectionState::from) - { - break; - } - } - }) - .await; - } - break; - } - } - - _ = request_response(&conn, Request::Deactivate(uuid.clone()), success) - .then(|event| output.send(event)) - .await; - } - - Some(Request::Disconnect(ssid)) => { - let mut success = false; - for c in network_manager - .active_connections() - .await - .unwrap_or_default() - { - if c.id().await.unwrap_or_default().as_str() == ssid.as_ref() - && 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 - && let Ok(ActiveConnectionState::Deactivated) = - next.get().await.map(ActiveConnectionState::from) - { - break; - } - } - }) - .await; - } - break; - } - } - - _ = request_response(&conn, Request::Disconnect(ssid.clone()), success) - .then(|event| output.send(event)) - .await; - } - - Some(Request::SetAirplaneMode(airplane_mode)) => { - // wifi - let mut success = network_manager - .set_wireless_enabled(!airplane_mode) - .await - .is_ok(); - // bluetooth - success = success - && Command::new("rfkill") - .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(Event::RequestResponse { - req: Request::SetAirplaneMode(airplane_mode), - success, - state, - }) - .await; - } - - Some(Request::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 }; - - if state.wifi_enabled { - tokio::time::sleep(Duration::from_secs(3)).await; - } - - _ = request_response(&conn, Request::SetWiFi(enabled), success) - .then(|event| output.send(event)) - .await; - } - - Some(Request::Authenticate { - ssid, - identity, - password, - secret_tx, - interface, - }) => { - let nm_state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - - let success = nm_state - .connect_wifi( - &conn, - &ssid, - identity.as_deref(), - Some(password.unsecure()), - secret_tx.clone(), - if identity.is_some() { - NetworkType::EAP - } else { - NetworkType::PskOrSae - }, - interface.clone(), - ) - .await - .is_ok(); - - _ = output - .send(Event::RequestResponse { - req: Request::Authenticate { - ssid: ssid.clone(), - identity: identity.clone(), - password: password.clone(), - secret_tx: secret_tx.clone(), - interface, - }, - success, - state: NetworkManagerState::new(&conn).await.unwrap_or_default(), - }) - .await; - } - - Some(Request::SelectAccessPoint(ssid, network_type, secret_tx, interface)) => { - if matches!(network_type, NetworkType::Open) { - attempt_wifi_connection(&conn, ssid, network_type, output, None, interface) - .await; - } else { - // For secured networks, check if we have saved credentials - let has_saved = has_saved_wifi_credentials(&conn, &ssid).await; - - if !has_saved { - return State::Waiting(conn, rx); - } - - // We have saved credentials, attempt connection - attempt_wifi_connection( - &conn, - ssid, - network_type, - output, - secret_tx, - interface, - ) - .await; - } - } - - Some(Request::Activate(device_path, connection_path)) => { - let mut success = true; - - if let Err(why) = network_manager - .activate_connection_by_paths(&connection_path, &device_path) - .await - { - tracing::error!( - ?why, - "failed to activate connection on {device_path:?} to {connection_path}" - ); - success = false; - }; - - _ = request_response( - &conn, - Request::Activate(device_path, connection_path), - success, - ) - .then(|event| output.send(event)) - .await; - } - - Some(Request::Reload) => { - _ = output - .send(request_response(&conn, Request::Reload, true).await) - .await; - } - - Some(Request::Remove(uuid)) => { - let s = match NetworkManagerSettings::new(&conn).await { - Ok(s) => s, - Err(why) => { - tracing::error!(?why, "error getting network manager settings"); - _ = output - .send(Event::RequestResponse { - req: Request::Forget(uuid.clone()), - success: false, - state: NetworkManagerState::new(&conn) - .await - .unwrap_or_default(), - }) - .await; - - return State::Waiting(conn, rx); - } - }; - - 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 c_uuid = settings - .get("connection") - .and_then(|conn| conn.get("uuid")) - .and_then(|uuid| uuid.downcast_ref::().ok()) - .unwrap_or_default(); - - if uuid.as_ref() == c_uuid.as_str() { - _ = c.delete().await; - success = true; - } - } - - _ = request_response(&conn, Request::Remove(uuid.clone()), success) - .then(|event| output.send(event)) - .await; - } - - Some(Request::Forget(ssid)) => { - let s = match NetworkManagerSettings::new(&conn).await { - Ok(s) => s, - Err(why) => { - tracing::error!(?why, "error getting network manager settings"); - _ = output - .send(Event::RequestResponse { - req: Request::Forget(ssid.clone()), - success: false, - state: NetworkManagerState::new(&conn) - .await - .unwrap_or_default(), - }) - .await; - - return State::Waiting(conn, rx); - } - }; - - 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 - .clone() - .and_then(|w| w.ssid) - .and_then(|ssid| String::from_utf8(ssid).ok()) - .is_some_and(|s| s == ssid.as_ref()) - { - _ = c.delete().await; - success = true; - break; - } - } - - _ = request_response(&conn, Request::Forget(ssid.clone()), success) - .then(|event| output.send(event)) - .await; - } - - Some(Request::GetWiFiCredentials(ssid, uuid, security_type, secret_tx)) => { - let s = match NetworkManagerSettings::new(&conn).await { - Ok(s) => s, - Err(why) => { - tracing::error!(?why, "error getting network manager settings"); - return State::Waiting(conn, rx); - } - }; - - match security_type { - NetworkType::Open => { - _ = output - .send(Event::WiFiCredentials { - ssid: ssid.clone(), - password: None, - security_type, - }) - .await; - } - t => { - let (tx, rx) = tokio::sync::oneshot::channel(); - let setting_name = if matches!(t, NetworkType::PskOrSae) { - "802-11-wireless-security" - } else { - "802-1x" - }; - let pw_key = if matches!(t, NetworkType::PskOrSae) { - "psk" - } else { - "password" - }; - if let Some(secret_tx) = secret_tx { - let _ = secret_tx - .send(nm_secret_agent::Request::GetSecrets { - setting_name: setting_name.to_string(), - uuid: uuid.to_string(), - resp_tx: tx, - }) - .await; - let _ = tokio::time::timeout(Duration::from_secs(10), async move { - if let Some(password) = - rx.await.ok().and_then(|mut secrets| secrets.remove(pw_key)) - { - _ = output - .send(Event::WiFiCredentials { - ssid: ssid.clone(), - password: Some(password), - security_type, - }) - .await; - } else { - _ = output - .send(Event::WiFiCredentials { - ssid: ssid.clone(), - password: None, - security_type, - }) - .await; - } - }) - .await; - } else { - let known_conns = s.list_connections().await.unwrap_or_default(); - for c in known_conns { - let settings = c.get_settings().await.ok().unwrap_or_default(); - let settings_parsed = Settings::new(settings.clone()); - - if let Some(saved_ssid) = settings_parsed - .wifi - .clone() - .and_then(|w| w.ssid) - .and_then(|s| String::from_utf8(s).ok()) - && saved_ssid == ssid.as_ref() - { - let password = c - .get_secrets("802-11-wireless-security") - .await - .ok() - .and_then(|secrets| { - // Look for PSK password - secrets - .get("802-11-wireless-security") - .and_then(|sec| sec.get("psk")) - .and_then(|v| { - v.downcast_ref::().ok() - }) - .map(|s| s.to_string()) - .or_else(|| { - // Fallback to WEP key - secrets - .get("802-11-wireless-security") - .and_then(|sec| sec.get("wep-key0")) - .and_then(|v| { - v.downcast_ref::().ok() - }) - .map(|s| s.to_string()) - }) - }); - - _ = output - .send(Event::WiFiCredentials { - ssid: ssid.clone(), - password: password.map(SecureString::from), - security_type, - }) - .await; - break; - } - } - } - } - } - } - - None => { - return State::Finished; - } - }; - - State::Waiting(conn, rx) - } - State::Finished => futures::future::pending().await, - } -} - -async fn request_response(conn: &zbus::Connection, req: Request, success: bool) -> Event { - Event::RequestResponse { - req, - success, - state: NetworkManagerState::new(conn).await.unwrap_or_default(), - } -} - -async fn has_saved_wifi_credentials(conn: &zbus::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()) - && saved_ssid == ssid - { - return true; - } - } - } - - false -} - -async fn attempt_wifi_connection( - conn: &zbus::Connection, - ssid: SSID, - network_type: NetworkType, - output: &mut futures::channel::mpsc::Sender, - secret_tx: Option>, - interface: Option, -) { - let state = NetworkManagerState::new(conn).await.unwrap_or_default(); - let success = if let Err(err) = state - .connect_wifi( - conn, - ssid.as_ref(), - None, - None, - secret_tx, - network_type, - interface.clone(), - ) - .await - { - tracing::error!("Failed to connect to access point: {:?}", err); - false - } else { - true - }; - - _ = request_response( - conn, - Request::SelectAccessPoint(ssid, network_type, None, interface), - success, - ) - .then(|event| output.send(event)) - .await; -} - -#[derive(Debug, Clone)] -pub enum Request { - /// Activate a device's connection profile - Activate(ObjectPath<'static>, ObjectPath<'static>), - /// Deactivate a connection - Deactivate(UUID), - /// Disconnect from an access point. - Disconnect(SSID), - /// Forget a known access point. - Forget(SSID), - /// Create a connection to a new access point. - Authenticate { - ssid: String, - identity: Option, - password: SecureString, - secret_tx: Option>, - interface: Option, - }, - /// Get WiFi credentials for a known access point. - GetWiFiCredentials( - SSID, - UUID, - NetworkType, - Option>, - ), - /// Signal to reload the service. - Reload, - /// Remove a connection profile. - Remove(UUID), - /// Connect to a known access point. - SelectAccessPoint( - SSID, - NetworkType, - Option>, - Option, - ), - /// Toggle airplaine mode. - SetAirplaneMode(bool), - /// Toggle WiFi enablement. - SetWiFi(bool), -} - -#[derive(Debug, Clone)] -pub enum Event { - RequestResponse { - req: Request, - state: NetworkManagerState, - success: bool, - }, - Init { - conn: zbus::Connection, - sender: UnboundedSender, - state: NetworkManagerState, - }, - Devices, - WiFiEnabled(bool), - WirelessAccessPoints, - ActiveConns, - WiFiCredentials { - ssid: SSID, - password: Option, - security_type: NetworkType, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NetworkManagerState { - pub wireless_access_points: Vec, - pub active_conns: Vec, - pub known_access_points: 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(), - wifi_enabled: false, - airplane_mode: false, - connectivity: NmConnectivityState::Unknown, - } - } -} - -impl NetworkManagerState { - pub async fn new(conn: &zbus::Connection) -> Result { - let network_manager = NetworkManager::new(conn).await?; - let mut this = Self::default(); - - this.refresh_wifi_state(conn, &network_manager).await?; - - Ok(this) - } - - pub async fn refresh_wifi_state( - &mut self, - conn: &zbus::Connection, - network_manager: &NetworkManager<'_>, - ) -> Result<(), Error> { - let (airplane_mode, wireless_enabled, settings_res) = futures::join!( - Command::new("rfkill") - .arg("list") - .arg("bluetooth") - .output() - .then(|res| async move { - let Ok(output) = res else { - return false; - }; - - std::str::from_utf8(&output.stdout) - .ok() - .is_some_and(|stdout| stdout.contains("Soft blocked: yes")) - }), - network_manager - .wireless_enabled() - .then(|res| async move { res.unwrap_or_default() }), - NetworkManagerSettings::new(conn) - ); - - self.wifi_enabled = wireless_enabled; - self.airplane_mode = airplane_mode && !self.wifi_enabled; - - let settings = settings_res?; - - _ = settings.load_connections(&[]).await; - - let (known_conns, active_conns, devices, connectivity) = futures::join!( - settings.list_connections(), - network_manager.active_connections(), - network_manager.devices(), - network_manager.connectivity(), - ); - - let devices = devices.unwrap_or_default(); - let known_conns = known_conns.unwrap_or_default(); - - let (active_conns, wireless_access_points) = futures::join!( - // Retrieve active connections. - async move { - let mut active_conns = active_connections(active_conns.unwrap_or_default()) - .await - .unwrap_or_default(); - - active_conns.sort_by(|a, b| { - let helper = |conn: &ActiveConnectionInfo| match conn { - ActiveConnectionInfo::Vpn { name, .. } => format!("0{name}"), - ActiveConnectionInfo::Wired { name, .. } => format!("1{name}"), - ActiveConnectionInfo::WiFi { name, .. } => format!("2{name}"), - }; - helper(a).cmp(&helper(b)) - }); - - active_conns - }, - // Retrieve all access points, and sort by strength. - async move { - let mut wireless_access_points = futures::stream::FuturesUnordered::from_iter( - devices.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() - } - }), - ) - .fold( - Vec::with_capacity(devices.len()), - |mut access_points, mut f| async move { - access_points.append(&mut f); - access_points - }, - ) - .await; - - wireless_access_points.sort_by(|a, b| b.strength.cmp(&a.strength)); - wireless_access_points - } - ); - - // Concurrently get - let known_ssid: Vec> = futures::stream::FuturesOrdered::from_iter( - known_conns.into_iter().map(|c| async move { - let s = c.get_settings().await.ok()?; - let s = Settings::new(s); - let curr_ssid = s - .wifi - .clone() - .and_then(|w| w.ssid) - .and_then(|ssid| String::from_utf8(ssid).ok())?; - - Some(Arc::from(curr_ssid)) - }), - ) - .filter_map(|c| async move { c }) - .collect() - .await; - - self.known_access_points = wireless_access_points - .iter() - .filter(|a| { - known_ssid.contains(&a.ssid) - && !active_conns.iter().any(|ac| ac.name() == a.ssid.as_ref()) - }) - .cloned() - .collect(); - - self.wireless_access_points = wireless_access_points; - self.active_conns = active_conns; - self.connectivity = connectivity?; - - Ok(()) - } - - #[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(); - } - - async fn connect_wifi( - &self, - conn: &zbus::Connection, - ssid: &str, - identity: Option<&str>, - password: Option<&str>, - mut secret_tx: Option>, - network_type: NetworkType, - interface: Option, - ) -> Result<(), Error> { - secret_tx = secret_tx.filter(|tx| !tx.is_closed()); - let nm = NetworkManager::new(conn).await?; - - for c in nm.active_connections().await.unwrap_or_default() { - if self - .wireless_access_points - .iter() - .any(|w| Ok(Some(w.ssid.as_ref())) == c.cached_id().as_ref().map(|v| v.as_deref())) - { - _ = nm.deactivate_connection(&c).await; - break; - } - } - - 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(vec!["peap"].into())), - // most common default - ("phase2-auth", Value::Str("mschapv2".into())), - ]), - ); - if secret_tx.is_none() { - conn_settings - .get_mut("802-1x") - .unwrap() - .insert("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 { - let entry = conn_settings.entry("802-11-wireless-security").or_default(); - _ = entry.insert("key-mgmt", Value::Str("wpa-psk".into())); - if secret_tx.is_none() { - _ = entry.insert("psk", Value::Str(pass.into())); - } - } - - let devices = nm.devices().await?; - for device in devices { - if !matches!( - device.device_type().await.unwrap_or(DeviceType::Other), - DeviceType::Wifi - ) || (interface.is_some() && interface != device.interface().await.ok()) - { - 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); - if let Some(cur_ssid) = s - .wifi - .clone() - .and_then(|w| w.ssid) - .and_then(|ssid| String::from_utf8(ssid).ok()) - && cur_ssid == ssid - { - known_conn = Some(c); - break; - } - } - - let known_conn = if let Some(known_conn) = known_conn { - if (secret_tx.is_none() && password.is_some()) || identity.is_some() { - known_conn.update(conn_settings).await.unwrap(); - } - known_conn - } else { - let settings = nm.settings().await?; - let object_path = settings.add_connection(conn_settings).await?; - let known_connection = Connection::from( - ConnectionSettingsProxy::builder(settings.inner().connection()) - .path(object_path)? - .build() - .await?, - ); - let settings = known_connection.get_settings().await?; - let uuid = String::try_from( - settings - .get("connection") - .ok_or_else(|| Error::MissingField("connection"))? - .get("uuid") - .ok_or_else(|| Error::MissingField("uuid"))? - .clone(), - ) - .map_err(zbus::Error::Variant)?; - - if let Some((pass, secret_tx)) = password.zip(secret_tx.as_ref()) { - let pass = SecureString::from(pass); - let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); - - let _ = secret_tx - .send(nm_secret_agent::Request::SetSecrets { - setting_name: if identity.is_some() { - "802-1x".into() - } else { - "802-11-wireless-security".into() - }, - uuid, - secrets: if identity.is_some() { - HashMap::from([("password".to_string(), pass)]) - } else { - HashMap::from([("psk".to_string(), pass)]) - }, - applied_tx, - }) - .await; - if let Err(err) = applied_rx.await { - tracing::error!("Failed to set secret. {err:?}"); - } - } - - known_connection - }; - - if let Some(pass) = password { - let pass = SecureString::from(pass); - if let Some(secret_tx) = secret_tx.as_ref() { - let settings = known_conn.get_settings().await?; - let uuid = String::try_from( - settings - .get("connection") - .ok_or_else(|| Error::MissingField("connection"))? - .get("uuid") - .ok_or_else(|| Error::MissingField("uuid"))? - .clone(), - ) - .map_err(zbus::Error::Variant)?; - let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); - let setting_name: String = if identity.is_some() { - "802-1x".into() - } else { - "802-11-wireless-security".into() - }; - let _ = secret_tx - .send(nm_secret_agent::Request::SetSecrets { - setting_name, - uuid, - secrets: if identity.is_some() { - HashMap::from([("password".to_string(), pass)]) - } else { - HashMap::from([("psk".to_string(), pass)]) - }, - applied_tx, - }) - .await; - if let Err(err) = applied_rx.await { - tracing::error!("Failed to set secret. {err:?}"); - } - } - } - - let active_conn = nm.activate_connection(&known_conn, &device).await?; - 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 { - break; - } - - 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 { - break; - } - } - if let Some(secret_tx) = secret_tx - && !matches!(network_type, NetworkType::Open) - { - let settings = known_conn.get_settings().await?; - let uuid = String::try_from( - settings - .get("connection") - .ok_or_else(|| Error::MissingField("connection"))? - .get("uuid") - .ok_or_else(|| Error::MissingField("uuid"))? - .clone(), - ) - .map_err(zbus::Error::Variant)?; - let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); - let setting_name: String = if identity.is_some() { - "802-1x".into() - } else { - "802-11-wireless-security".into() - }; - if let Err(err) = secret_tx - .send(nm_secret_agent::Request::SetSecrets { - setting_name, - uuid, - secrets: Default::default(), - applied_tx, - }) - .await - { - tracing::error!( - "Failed to reset access point secrets after failed activation: {err:?}" - ); - } else if let Err(err) = applied_rx.await { - tracing::error!( - "Failed to reset access point secrets after failed activation: {err:?}" - ); - } - } - return Err(Error::ConnectionActivate); - } - - Err(Error::NoWiFiDevices) - } -} diff --git a/subscriptions/network-manager/src/nm_secret_agent.rs b/subscriptions/network-manager/src/nm_secret_agent.rs deleted file mode 100644 index 330c594..0000000 --- a/subscriptions/network-manager/src/nm_secret_agent.rs +++ /dev/null @@ -1,738 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::fmt::Debug; -use std::sync::Arc; - -use bitflags::bitflags; -use cosmic_dbus_networkmanager::interface::settings::connection::ConnectionSettingsProxy; -use futures::{SinkExt, Stream}; -use secure_string::SecureString; -use tokio::sync::oneshot; -use zbus::zvariant::{OwnedValue, Str}; -use zbus::{ObjectServer, fdo}; - -pub type SecretSender = Arc>>>; - -pub const SECRET_ID: &str = "com.system76.CosmicSettings.NetworkManager"; -pub const DBUS_PATH: &str = "/org/freedesktop/NetworkManager/SecretAgent"; - -bitflags! { - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct GetSecretsFlags: u32 { - /// No special behavior. - /// By default no user interaction is allowed and secrets must come - /// from persistent storage, otherwise an error is returned. - const NONE = 0x0; - - /// Allows interaction with the user (eg. prompt via UI). - const ALLOW_INTERACTION = 0x1; - - /// Explicitly request new secrets from the user. - /// Implies ALLOW_INTERACTION. - const REQUEST_NEW = 0x2; - - /// Request was initiated by a user action (via D-Bus). - const USER_REQUESTED = 0x4; - - /// Internal flag, not part of the public D-Bus API. - const ONLY_SYSTEM = 0x8000_0000; - - /// Internal flag, not part of the public D-Bus API. - const NO_ERRORS = 0x4000_0000; - } -} - -#[derive(thiserror::Error, Clone, Debug)] -pub enum Error { - #[error("zbus error")] - Zbus(#[from] zbus::Error), - #[error("listening for secret agent closed")] - RecvError(#[from] oneshot::error::RecvError), - #[error("secret service error")] - SecretService(#[from] Arc), - #[error("no password found for identifier: {0}")] - NoPasswordForIdentifier(String), - #[error("utf8 error")] - Utf8Error(#[from] std::string::FromUtf8Error), -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum PasswordFlag { - /// The system is responsible for providing and storing this secret. - None = 0, - /// A user-session secret agent is responsible for providing and storing - /// this secret; when it is required, agents will be asked to provide it. - AgentOwned = 1, - /// This secret should not be saved but should be requested from the user - /// each time it is required. This flag should be used for One-Time-Pad - /// secrets, PIN codes from hardware tokens, or if the user simply does not - /// want to save the secret. - NotSaved = 2, - /// in some situations it cannot be automatically determined that a secret is required or not. This flag hints that the secret is not required and should not be requested from the user. - NotRequired = 4, -} - -#[derive(Debug, Clone)] -pub struct SecretHint { - pub key: String, - pub message: Option, -} - -fn parse_hints(hints: Vec) -> Vec { - hints - .into_iter() - // fold message hints into previous hints - .fold(Vec::new(), |mut acc, hint| { - if let Some((key, msg)) = hint.split_once(':') { - if let Some(last) = acc.last_mut() { - last.message = Some(format!("{}: {}", key, msg)); - } - } else { - acc.push(SecretHint { - key: hint, - message: None, - }); - } - acc - }) -} - -#[derive(Debug, Clone)] -pub enum Event { - RequestSecret { - uuid: String, - name: String, - description: Option, - previous: SecureString, - tx: SecretSender, - }, - CancelGetSecrets { - uuid: String, - name: String, - }, - Failed(Error), -} - -#[derive(Debug)] -pub enum Request { - SetSecrets { - setting_name: String, - uuid: String, - secrets: HashMap, - applied_tx: oneshot::Sender<()>, - }, - GetSecrets { - setting_name: String, - uuid: String, - resp_tx: oneshot::Sender>, - }, -} - -pub fn secret_agent_stream( - identifier: impl AsRef, - rx: tokio::sync::mpsc::Receiver, -) -> impl Stream { - iced_futures::stream::channel( - 4, - move |mut msg_tx: futures::channel::mpsc::Sender| async move { - if let Err(e) = secret_agent_stream_impl(identifier.as_ref(), msg_tx.clone(), rx).await - { - let _ = msg_tx.send(Event::Failed(e)).await; - } - }, - ) -} - -async fn secret_agent_stream_impl( - identifier: &str, - msg_tx: futures::channel::mpsc::Sender, - mut rx: tokio::sync::mpsc::Receiver, -) -> Result<(), Error> { - // fail early if we can't connect, closing the channel - { - let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) - .await - .map_err(Arc::new)?; - let collection = ss.get_default_collection().await.map_err(Arc::new)?; - if collection.is_locked().await.map_err(Arc::new)? { - collection.unlock().await.map_err(Arc::new)?; - } - } - // register the secret agent with NetworkManager - let proxy = - nm_secret_agent_manager::AgentManagerProxy::builder(&zbus::Connection::system().await?) - .path("/org/freedesktop/NetworkManager/AgentManager")? - .build() - .await?; - - let _ = ObjectServer::at( - proxy.inner().connection().object_server(), - DBUS_PATH, - SettingsSecretAgent { tx: msg_tx }, - ) - .await?; - - proxy.register_with_capabilities(identifier, 1).await?; - - while let Some(request) = rx.recv().await { - match request { - Request::SetSecrets { - setting_name, - uuid, - secrets, - applied_tx, - } => { - let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) - .await - .map_err(Arc::new)?; - let collection = ss.get_default_collection().await.map_err(Arc::new)?; - - if secrets.is_empty() { - let mut attributes = std::collections::HashMap::new(); - attributes.insert("application", SECRET_ID); - attributes.insert("uuid", &uuid); - let search_items = collection - .search_items(attributes) - .await - .map_err(Arc::new)?; - - for item in &search_items { - item.delete().await.map_err(Arc::new)?; - } - let _ = applied_tx.send(()); - - continue; - } - - for (name, secret) in &secrets { - let mut attributes = std::collections::HashMap::new(); - attributes.insert("application", SECRET_ID); - attributes.insert("uuid", &uuid); - attributes.insert("setting_name", &setting_name); - attributes.insert("name", name); - let _item = collection - .create_item( - "NetworkManager Secret", - attributes, - secret.unsecure().as_bytes(), - true, - "text/plain", - ) - .await - .map_err(Arc::new)?; - } - let _ = applied_tx.send(()); - } - Request::GetSecrets { - setting_name, - uuid, - resp_tx, - } => { - let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) - .await - .map_err(Arc::new)?; - let collection = ss.get_default_collection().await.map_err(Arc::new)?; - - let mut attributes = std::collections::HashMap::new(); - attributes.insert("application", SECRET_ID); - attributes.insert("uuid", &uuid); - attributes.insert("setting_name", &setting_name); - - let search_items = collection - .search_items(attributes) - .await - .map_err(Arc::new)?; - - let mut secrets = HashMap::new(); - for item in &search_items { - let name = item - .get_attributes() - .await - .map_err(Arc::new)? - .get("name") - .cloned() - .unwrap_or_else(|| "unknown".to_string()); - let secret = item.get_secret().await.map_err(Arc::new)?; - let secret: String = String::from_utf8(secret)?; - secrets.insert(name, SecureString::from(secret)); - } - let _ = resp_tx.send(secrets); - } - } - } - Ok(()) -} - -fn parse_secret_flag(value: &str) -> PasswordFlag { - match value { - "0" => PasswordFlag::None, - "1" => PasswordFlag::AgentOwned, - "2" => PasswordFlag::NotSaved, - "4" => PasswordFlag::NotRequired, - _ => PasswordFlag::AgentOwned, - } -} - -fn setting_has_always_ask(setting: zbus::zvariant::Dict) -> bool { - for (key, value) in setting.iter() { - let Ok(key) = key.downcast_ref::() else { - continue; - }; - let Ok(value) = value.downcast_ref::() else { - continue; - }; - // we only care about "-flags" - if !key.ends_with("-flags") { - continue; - } - - if parse_secret_flag(value.as_str()) == PasswordFlag::NotSaved { - return true; - } - } - - false -} - -fn has_always_ask(setting: Option) -> bool { - setting.map(setting_has_always_ask).unwrap_or(false) -} - -fn is_connection_always_ask(connection: &HashMap>) -> bool { - let conn_setting = match connection.get("connection") { - Some(s) => s, - None => return false, - }; - - let conn_type = match conn_setting - .get("type") - .and_then(|v| v.downcast_ref::().ok()) - { - Some(t) => t, - None => return false, - }; - - // Primary setting (vpn, wifi, ethernet, etc) - if has_always_ask( - connection - .get(&conn_type) - .and_then(|d| d.get("data")) - .and_then(|data| data.downcast_ref::().ok()), - ) { - return true; - } - - match conn_type.as_str() { - "802-11-wireless" => { - if has_always_ask( - connection - .get("802-11-wireless-security") - .and_then(|d| d.get("data")) - .and_then(|data| data.downcast_ref::().ok()), - ) { - return true; - } - if has_always_ask( - connection - .get("802-1x") - .and_then(|d| d.get("data")) - .and_then(|data| data.downcast_ref::().ok()), - ) { - return true; - } - } - - "802-3-ethernet" => { - if has_always_ask( - connection - .get("pppoe") - .and_then(|d| d.get("data")) - .and_then(|data| data.downcast_ref::().ok()), - ) { - return true; - } - if has_always_ask( - connection - .get("802-1x") - .and_then(|d| d.get("data")) - .and_then(|data| data.downcast_ref::().ok()), - ) { - return true; - } - } - - _ => {} - } - - false -} - -#[derive(Debug)] -pub struct SettingsSecretAgent { - tx: futures::channel::mpsc::Sender, -} - -#[zbus::interface(name = "org.freedesktop.NetworkManager.SecretAgent")] -impl SettingsSecretAgent { - /// CancelGetSecrets method - async fn cancel_get_secrets( - &mut self, - connection_path: zbus::zvariant::ObjectPath<'_>, - setting_name: String, - ) -> fdo::Result<()> { - let conn = ConnectionSettingsProxy::builder( - &zbus::Connection::system() - .await - .map_err(|_| fdo::Error::Failed("failed to get uuid".to_string()))?, - ) - .path(connection_path)? - .build() - .await - .map_err(|e| fdo::Error::Failed(e.to_string()))?; - - let uuid = conn - .get_settings() - .await - .map_err(|e| fdo::Error::Failed(e.to_string()))? - .get("connection") - .and_then(|m| m.get("uuid")) - .and_then(|v| v.downcast_ref::().ok()) - .ok_or_else(|| fdo::Error::Failed("failed to get uuid".to_string()))? - .to_string(); - if let Err(e) = self - .tx - .clone() - .send(Event::CancelGetSecrets { - uuid, - name: setting_name, - }) - .await - && e.is_disconnected() - { - return Err(fdo::Error::Failed( - "failed to send cancel message".to_string(), - )); - } - - Ok(()) - } - - /// DeleteSecrets method - async fn delete_secrets( - &self, - connection: HashMap>, - connection_path: zbus::zvariant::ObjectPath<'_>, - ) -> fdo::Result<()> { - match self.delete_secrets_inner(connection, connection_path).await { - Ok(_) => Ok(()), - Err(err) => Err(fdo::Error::Failed(err.to_string())), - } - } - - /// GetSecrets method - async fn get_secrets( - &mut self, - connection: HashMap>, - connection_path: zbus::zvariant::ObjectPath<'_>, - setting_name: String, - hints: Vec, - flags: u32, - ) -> HashMap> { - self.get_secrets_inner(connection, connection_path, setting_name, hints, flags) - .await - .unwrap_or_default() - } - - /// SaveSecrets method - async fn save_secrets( - &self, - connection: HashMap>, - _connection_path: zbus::zvariant::ObjectPath<'_>, - ) -> fdo::Result<()> { - match self.save_secrets_inner(connection).await { - Ok(_) => Ok(()), - Err(err) => Err(fdo::Error::Failed(err.to_string())), - } - } -} - -impl SettingsSecretAgent { - pub async fn get_secrets_inner( - &mut self, - connection: HashMap>, - connection_path: zbus::zvariant::ObjectPath<'_>, - setting_name: String, - hints: Vec, - flags: u32, - ) -> Result>, Error> { - let flags = GetSecretsFlags::from_bits_truncate(flags); - - let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) - .await - .map_err(Arc::new)?; - - let collection = ss.get_default_collection().await.map_err(Arc::new)?; - - let conn_uuid = connection - .get("connection") - .and_then(|m| m.get("uuid")) - .and_then(|v| v.downcast_ref::().ok()) - .ok_or_else(|| Error::NoPasswordForIdentifier(setting_name.clone()))? - .to_string(); - - let conn = - ConnectionSettingsProxy::builder(&zbus::Connection::system().await.map_err(|_| { - Error::Zbus(fdo::Error::Failed("failed to get uuid".to_string()).into()) - })?) - .path(connection_path)? - .build() - .await - .map_err(|e| Error::Zbus(fdo::Error::Failed(e.to_string()).into()))?; - let settings = conn.get_settings().await?; - let is_vpn = settings - .get("connection") - .and_then(|m| m.get("type")) - .and_then(|v| v.downcast_ref::().ok()) - .is_some_and(|t| t == "vpn"); - let is_always_ask = is_connection_always_ask(&settings); - - let mut setting_attributes = std::collections::HashMap::new(); - setting_attributes.insert("application", SECRET_ID); - setting_attributes.insert("uuid", &conn_uuid); - setting_attributes.insert("setting_name", &setting_name); - - let search_items = collection - .search_items(setting_attributes.clone()) - .await - .map_err(Arc::new)?; - let mut result = HashMap::new(); - let mut setting = HashMap::new(); - - if hints.is_empty() { - for item in &search_items { - let name = item - .get_attributes() - .await - .map_err(Arc::new)? - .get("name") - .cloned() - .unwrap_or_else(|| "unknown".to_string()); - let secret = item.get_secret().await.map_err(Arc::new)?; - let secret: String = String::from_utf8(secret)?; - setting.insert(name, zbus::zvariant::OwnedValue::from(Str::from(secret))); - } - result.insert(setting_name, setting); - Ok(result) - } else { - let hints = parse_hints(hints); - let mut requested = HashSet::new(); - - for SecretHint { key, message } in &hints { - if requested.contains(key) { - continue; - } - requested.insert(key); - if flags.contains(GetSecretsFlags::REQUEST_NEW) - && flags.contains(GetSecretsFlags::ALLOW_INTERACTION) - || is_always_ask - { - // request the secret via the message channel - let (resp_tx, resp_rx) = oneshot::channel(); - // msg begins after ":" - let actual_hint = message.as_ref().map(|m| { - m.split_once(":") - .map(|(_, msg)| msg.trim().to_string()) - .unwrap_or(m.clone()) - }); - if let Err(e) = self - .tx - .clone() - .send(Event::RequestSecret { - uuid: conn_uuid.clone(), - name: setting_name.clone(), - description: actual_hint.clone(), - previous: String::new().into(), - tx: Arc::new(tokio::sync::Mutex::new(Some(resp_tx))), - }) - .await - && e.is_disconnected() - { - continue; - } else if let Ok(secret) = resp_rx.await { - let mut named_attribute = setting_attributes.clone(); - named_attribute.insert("name", key); - let _item = collection - .create_item( - "NetworkManager Secret", - named_attribute, - secret.unsecure().as_bytes(), - true, - "text/plain", - ) - .await - .map_err(Arc::new)?; - - setting.insert( - key.clone(), - zbus::zvariant::OwnedValue::from(Str::from(secret.unsecure())), - ); - } - } else if !is_always_ask { - let mut pos = None; - let mut pos_with_message = None; - for item in &search_items { - let attributes = item.get_attributes().await.map_err(Arc::new)?; - if let Some(value) = attributes.get("name") - && value == key - { - if let Some(saved_message) = attributes.get("message") { - if message.as_ref().is_some_and(|msg| msg == saved_message) { - pos_with_message = Some(item); - } - break; - } else { - pos = Some(item); - } - } - } - - if let Some(item) = pos_with_message.or(pos) { - let secret = item.get_secret().await.map_err(Arc::new)?; - let secret: String = String::from_utf8(secret)?; - if is_vpn { - // ask anyway, but offer the previous one as a hint - let (resp_tx, resp_rx) = oneshot::channel(); - let actual_hint = message.as_ref().map(|m| { - m.split_once(":") - .map(|(_, msg)| msg.trim().to_string()) - .unwrap_or(m.clone()) - }); - if let Err(e) = self - .tx - .clone() - .send(Event::RequestSecret { - uuid: conn_uuid.clone(), - name: setting_name.clone(), - description: actual_hint.clone(), - previous: SecureString::from(secret.clone()), - tx: Arc::new(tokio::sync::Mutex::new(Some(resp_tx))), - }) - .await - && e.is_disconnected() - { - continue; - } else if let Ok(secret) = resp_rx.await { - let mut named_attribute = setting_attributes.clone(); - named_attribute.insert("name", key); - let _item = collection - .create_item( - "NetworkManager Secret", - named_attribute, - secret.unsecure().as_bytes(), - true, - "text/plain", - ) - .await - .map_err(Arc::new)?; - - setting.insert( - key.clone(), - zbus::zvariant::OwnedValue::from(Str::from(secret.unsecure())), - ); - } - } else { - setting.insert( - key.clone(), - zbus::zvariant::OwnedValue::from(Str::from(secret)), - ); - } - } - } else { - // can't find the secret, and we can't request it, so we just skip it - continue; - } - } - result.insert(setting_name, setting); - Ok(result) - } - } - - pub async fn delete_secrets_inner( - &self, - connection: HashMap>, - _connection_path: zbus::zvariant::ObjectPath<'_>, - ) -> Result<(), Error> { - let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) - .await - .map_err(Arc::new)?; - let collection = ss.get_default_collection().await.map_err(Arc::new)?; - - let conn_uuid = connection - .get("connection") - .and_then(|m| m.get("uuid")) - .and_then(|v| v.downcast_ref::().ok()) - .ok_or_else(|| Error::NoPasswordForIdentifier("unknown".to_string()))? - .to_string(); - - let mut attributes = std::collections::HashMap::new(); - attributes.insert("application", SECRET_ID); - attributes.insert("uuid", &conn_uuid); - - let search_items = collection - .search_items(attributes) - .await - .map_err(Arc::new)?; - - for item in &search_items { - item.delete().await.map_err(Arc::new)?; - } - Ok(()) - } - - pub async fn save_secrets_inner( - &self, - connection: HashMap>, - ) -> Result<(), Error> { - let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) - .await - .map_err(Arc::new)?; - let collection = ss.get_default_collection().await.map_err(Arc::new)?; - let conn_uuid = connection - .get("connection") - .and_then(|m| m.get("uuid")) - .and_then(|v| v.downcast_ref::().ok()) - .ok_or_else(|| Error::NoPasswordForIdentifier("unknown".to_string()))? - .to_string(); - - let secret: Option<(String, String)> = connection - .get("802-11-wireless-security") - .and_then(|m| m.get("psk")) - .and_then(|v| v.downcast_ref::().ok()) - .map(|password| ("psk".to_string(), password.clone())) - .or_else(|| { - connection - .get("802-1x") - .and_then(|s| s.get("password")) - .and_then(|v| v.downcast_ref::().ok()) - .map(|password| ("802-1x-password".to_string(), password.clone())) - }); - if let Some((name, secret)) = secret { - let mut attributes = std::collections::HashMap::new(); - attributes.insert("application", SECRET_ID); - attributes.insert("uuid", &conn_uuid); - attributes.insert("setting_name", &name); - let _item = collection - .create_item( - "NetworkManager Secret", - attributes, - secret.as_bytes(), - true, - "text/plain", - ) - .await - .map_err(Arc::new)?; - Ok(()) - } else { - Err(Error::NoPasswordForIdentifier("unknown".to_string())) - } - } -} diff --git a/subscriptions/network-manager/src/wireless_enabled.rs b/subscriptions/network-manager/src/wireless_enabled.rs deleted file mode 100644 index 039bbf3..0000000 --- a/subscriptions/network-manager/src/wireless_enabled.rs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::Wrapper; - -use super::Event; -use cosmic_dbus_networkmanager::nm::NetworkManager; -use futures::{SinkExt, StreamExt}; -use iced_futures::{Subscription, stream}; -use std::fmt::Debug; -use std::hash::Hash; -use zbus::Connection; - -#[derive(Debug, Clone)] -pub enum State { - Continue(Connection), - Error, -} - -pub fn wireless_enabled_subscription( - id: I, - conn: Connection, -) -> iced_futures::Subscription { - Subscription::run_with(Wrapper { id, conn: conn }, |Wrapper { id: _id, conn }| { - let conn = conn.clone(); - stream::channel(50, move |output| async move { - watch(conn, output).await; - futures::future::pending().await - }) - }) -} - -pub async fn watch(conn: zbus::Connection, mut output: futures::channel::mpsc::Sender) { - let mut state = State::Continue(conn); - - loop { - state = start_listening(state, &mut output).await; - } -} - -async fn start_listening( - state: State, - output: &mut futures::channel::mpsc::Sender, -) -> State { - let conn = match state { - State::Continue(conn) => conn, - State::Error => 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 { - if let Ok(enable) = change.get().await { - _ = output.send(Event::WiFiEnabled(enable)).await; - } - } - State::Continue(conn) -}