feat(networking): replace custom NetworkManager backend with nmrs
Switch the networking pages to use nmrs 3.4.0 for NetworkManager state, device lists, Wi-Fi operations, VPN imports/activation, and secret-agent handling. Remove the old cosmic-settings-network-manager-subscription crate, drop the direct dbus-settings NetworkManager dependencies, and delete the unused nmcli VPN helper. Keep nm-connection-editor for profile creation/editing where the UI still depends on it. Carry Wi-Fi security metadata through the password flow so SAE/WPA3 personal networks can be activated with SAE key management, and make failed/cancelled auth attempts clear pending connection state
This commit is contained in:
parent
fff1485876
commit
04d5f46d0d
18 changed files with 2134 additions and 3504 deletions
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
1812
cosmic-settings/src/pages/networking/backend.rs
Normal file
1812
cosmic-settings/src/pages/networking/backend.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,24 +1,24 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// 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<tokio::sync::Mutex<Option<tokio::sync::oneshot::Sender<SecureString>>>>;
|
||||
|
||||
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<crate::pages::Message> 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<crate::pages::Message> 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<crate::pages::Message> for Page {
|
|||
fn on_enter(&mut self) -> cosmic::Task<crate::pages::Message> {
|
||||
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<crate::app::Message> {
|
||||
fn connect(&mut self, nm: nmrs::NetworkManager) -> Task<crate::app::Message> {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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(
|
||||
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(),
|
||||
),
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
)))
|
||||
.await;
|
||||
}
|
||||
Err(why) => {
|
||||
_ = sender
|
||||
.send(crate::pages::Message::Networking(Message::Error(
|
||||
why.to_string(),
|
||||
)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
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) => 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<P: AsRef<OsStr>>(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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// 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<widget::Id> = 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<network_manager::Request>,
|
||||
active_conns: Vec<ActiveConnectionInfo>,
|
||||
devices: Vec<network_manager::devices::DeviceInfo>,
|
||||
|
|
@ -371,18 +368,20 @@ impl page::Page<crate::pages::Message> 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()
|
||||
return cosmic::Task::batch([
|
||||
cosmic::task::future(async move {
|
||||
nmrs::NetworkManager::new()
|
||||
.await
|
||||
.context("failed to create system dbus connection")
|
||||
.context("failed to connect to NetworkManager")
|
||||
.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),
|
||||
)
|
||||
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))),
|
||||
]);
|
||||
}
|
||||
|
|
@ -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}"),
|
||||
);
|
||||
ConnectionSettings::Wireguard { .. } => {
|
||||
if let Some(NmState { ref sender, .. }) = self.nm_state {
|
||||
_ = sender
|
||||
.unbounded_send(network_manager::Request::ActivateVpn(uuid));
|
||||
}
|
||||
|
||||
Message::Refresh
|
||||
});
|
||||
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)),
|
||||
});
|
||||
if let Some(NmState { ref sender, .. }) = self.nm_state {
|
||||
_ = sender
|
||||
.unbounded_send(network_manager::Request::ActivateVpn(uuid));
|
||||
}
|
||||
|
||||
Message::Refresh
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -629,50 +603,32 @@ 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 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 {
|
||||
// apply password and username then
|
||||
if let Some(sec_tx) = sec_tx {
|
||||
let (applied_tx, applied_rx) =
|
||||
tokio::sync::oneshot::channel();
|
||||
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()),
|
||||
("username".to_string(), username_unwrapped.into()),
|
||||
("password".to_string(), password),
|
||||
]),
|
||||
applied_tx,
|
||||
})
|
||||
|
|
@ -680,7 +636,6 @@ impl Page {
|
|||
{
|
||||
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,
|
||||
|
|
@ -690,27 +645,12 @@ impl Page {
|
|||
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)),
|
||||
});
|
||||
}
|
||||
_ = nm_sender
|
||||
.unbounded_send(network_manager::Request::ActivateVpn(uuid));
|
||||
}
|
||||
|
||||
Message::Refresh
|
||||
})
|
||||
})
|
||||
.map(crate::app::Message::from);
|
||||
}
|
||||
}
|
||||
|
|
@ -720,28 +660,18 @@ 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 {
|
||||
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
|
||||
|
|
@ -749,22 +679,18 @@ impl Page {
|
|||
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()),
|
||||
("username".to_string(), username_unwrapped.into()),
|
||||
("password".to_string(), password),
|
||||
]),
|
||||
applied_tx,
|
||||
})
|
||||
.await;
|
||||
// wait max 1s for the applied signal
|
||||
let _ = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(1),
|
||||
applied_rx,
|
||||
)
|
||||
let _ =
|
||||
tokio::time::timeout(std::time::Duration::from_secs(1), applied_rx)
|
||||
.await;
|
||||
}
|
||||
Message::Activate(uuid)
|
||||
})
|
||||
_ = nm_sender.unbounded_send(network_manager::Request::ActivateVpn(uuid));
|
||||
Message::Refresh
|
||||
})
|
||||
.map(crate::app::Message::from);
|
||||
}
|
||||
|
|
@ -873,56 +799,14 @@ impl Page {
|
|||
Task::none()
|
||||
}
|
||||
|
||||
fn activate_with_password(
|
||||
&mut self,
|
||||
connection_name: String,
|
||||
uuid: Arc<str>,
|
||||
username: String,
|
||||
password: SecureString,
|
||||
) -> Task<Message> {
|
||||
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<crate::app::Message> {
|
||||
fn connect(&mut self, conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
|
||||
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<crate::app::Message> {
|
||||
fn update_state(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
|
||||
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<crate::app::Message> {
|
|||
})
|
||||
}
|
||||
|
||||
fn update_devices(conn: zbus::Connection) -> Task<crate::app::Message> {
|
||||
fn update_devices(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
|
||||
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<crate::app::Message> {
|
|||
);
|
||||
};
|
||||
|
||||
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<crate::app::Message> {
|
|||
.apply(cosmic::task::future)
|
||||
}
|
||||
|
||||
fn connection_settings(conn: zbus::Connection) -> Task<crate::app::Message> {
|
||||
fn connection_settings(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
|
||||
cosmic::task::future(async move {
|
||||
let settings = async move {
|
||||
let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?;
|
||||
let vpns = conn.list_vpn_connections().await?;
|
||||
let mut settings = IndexMap::new();
|
||||
|
||||
_ = 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::<String>()
|
||||
.ok()?
|
||||
.as_str()
|
||||
{
|
||||
"vpn" => (),
|
||||
|
||||
"wireguard" => {
|
||||
let id = connection.get("id")?.downcast_ref::<String>().ok()?;
|
||||
let uuid = connection.get("uuid")?.downcast_ref::<String>().ok()?;
|
||||
return Some((Arc::from(uuid), ConnectionSettings::Wireguard { id }));
|
||||
}
|
||||
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
let vpn = settings.get("vpn")?;
|
||||
let id = connection.get("id")?.downcast_ref::<String>().ok()?;
|
||||
let uuid = connection.get("uuid")?.downcast_ref::<String>().ok()?;
|
||||
|
||||
let (connection_type, username, password_flag) = vpn
|
||||
.get("data")
|
||||
.and_then(|data| data.downcast_ref::<zbus::zvariant::Dict>().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::<String>().ok());
|
||||
if dict
|
||||
.get::<String, String>(&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, String>(&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,
|
||||
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,
|
||||
password_flag,
|
||||
username,
|
||||
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)),
|
||||
}),
|
||||
))
|
||||
})
|
||||
// Reduce the settings list into
|
||||
.fold(IndexMap::new(), |mut set, (uuid, data)| async move {
|
||||
set.insert(uuid, data);
|
||||
set
|
||||
})
|
||||
.await;
|
||||
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::<_, zbus::Error>(settings)
|
||||
Ok::<_, nmrs::ConnectionError>(settings)
|
||||
};
|
||||
|
||||
cosmic::task::future(async move {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// 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)
|
||||
}
|
||||
|
|
@ -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<widget::Id> = 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<String>,
|
||||
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<network_manager::Request>,
|
||||
state: network_manager::NetworkManagerState,
|
||||
devices: Vec<network_manager::devices::DeviceInfo>,
|
||||
|
|
@ -294,19 +294,23 @@ impl page::Page<crate::pages::Message> 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()
|
||||
return Task::batch(vec![
|
||||
cosmic::Task::future(async move {
|
||||
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,
|
||||
)
|
||||
.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)))]);
|
||||
}),
|
||||
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<crate::app::Message> {
|
||||
fn connect(&mut self, conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
|
||||
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<crate::app::Message> {
|
||||
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<crate::app::Message> {
|
||||
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<crate::app::Message> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
|
||||
pub fn update_state(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
|
||||
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<crate::app::Message> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn update_devices(conn: zbus::Connection) -> Task<crate::app::Message> {
|
||||
pub fn update_devices(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
|
||||
cosmic::task::future(async move {
|
||||
let filter =
|
||||
|device_type| matches!(device_type, network_manager::devices::DeviceType::Wifi);
|
||||
|
|
|
|||
|
|
@ -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<str>;
|
||||
|
||||
#[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<network_manager::Request>,
|
||||
active_conns: Vec<ActiveConnectionInfo>,
|
||||
devices: Vec<Arc<network_manager::devices::DeviceInfo>>,
|
||||
|
|
@ -154,9 +156,9 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
fn on_enter(&mut self) -> cosmic::Task<crate::pages::Message> {
|
||||
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<crate::app::Message> {
|
||||
fn connect(&mut self, conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
|
||||
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<crate::app::Message> {
|
||||
fn update_state(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
|
||||
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<crate::app::Message> {
|
|||
})
|
||||
}
|
||||
|
||||
fn update_devices(conn: zbus::Connection) -> Task<crate::app::Message> {
|
||||
fn update_devices(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
|
||||
cosmic::task::future(async move {
|
||||
let filter =
|
||||
|device_type| matches!(device_type, network_manager::devices::DeviceType::Ethernet);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// 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<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
conn: Connection,
|
||||
) -> iced_futures::Subscription<Event> {
|
||||
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<Event>) {
|
||||
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<Event>,
|
||||
) -> 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)
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// 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<String>,
|
||||
) -> zbus::Result<Vec<AccessPoint>> {
|
||||
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::<String, AccessPoint>::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<str>,
|
||||
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,
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// 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<ActiveConnection<'_>>,
|
||||
) -> zbus::Result<Vec<ActiveConnectionInfo>> {
|
||||
let mut info = Vec::<ActiveConnectionInfo>::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<Ipv4Addr>,
|
||||
},
|
||||
WiFi {
|
||||
name: String,
|
||||
ip_addresses: Vec<Ipv4Addr>,
|
||||
hw_address: String,
|
||||
state: ActiveConnectionState,
|
||||
strength: u8,
|
||||
},
|
||||
Vpn {
|
||||
name: String,
|
||||
ip_addresses: Vec<Ipv4Addr>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ActiveConnectionInfo {
|
||||
pub fn name(&self) -> String {
|
||||
match &self {
|
||||
Self::Wired { name, .. } => name.clone(),
|
||||
Self::WiFi { name, .. } => name.clone(),
|
||||
Self::Vpn { name, .. } => name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,257 +0,0 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// 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<DeviceConnection>,
|
||||
pub known_connections: Vec<KnownDeviceConnection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct DeviceConnection {
|
||||
pub path: ObjectPath<'static>,
|
||||
pub id: String,
|
||||
pub uuid: Arc<str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct KnownDeviceConnection {
|
||||
pub id: String,
|
||||
pub uuid: Arc<str>,
|
||||
}
|
||||
|
||||
pub async fn list(
|
||||
conn: &zbus::Connection,
|
||||
device_type_filter: fn(DeviceType) -> bool,
|
||||
) -> zbus::Result<Vec<DeviceInfo>> {
|
||||
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::<String>()
|
||||
.ok()?;
|
||||
|
||||
let uuid = settings["connection"]
|
||||
.get("uuid")?
|
||||
.downcast_ref::<String>()
|
||||
.ok()?;
|
||||
|
||||
Some(DeviceConnection {
|
||||
id,
|
||||
uuid: Arc::from(uuid),
|
||||
path,
|
||||
})
|
||||
}
|
||||
),)
|
||||
.filter_map(|res| async move { res })
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
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::<String>()
|
||||
.ok()?;
|
||||
|
||||
if interface_name != interface {
|
||||
return None;
|
||||
}
|
||||
|
||||
let id = connection.get("id")?.downcast_ref::<String>().ok()?;
|
||||
let uuid = connection.get("uuid")?.downcast_ref::<String>().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::<Vec<DeviceInfo>>()
|
||||
.await;
|
||||
|
||||
Ok(devices_info)
|
||||
}
|
||||
|
||||
pub fn subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
has_popup: bool,
|
||||
conn: Connection,
|
||||
) -> iced_futures::Subscription<Event> {
|
||||
struct Wrapper<I> {
|
||||
id: I,
|
||||
has_popup: bool,
|
||||
conn: Connection,
|
||||
}
|
||||
impl<I: Hash> Hash for Wrapper<I> {
|
||||
fn hash<H: hash::Hasher>(&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<Event>,
|
||||
) {
|
||||
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<Event>,
|
||||
) -> 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)
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
#[derive(Clone, PartialEq, Eq, Default, Debug, PartialOrd, Ord)]
|
||||
pub struct HwAddress {
|
||||
octets: Vec<u8>,
|
||||
}
|
||||
|
||||
impl HwAddress {
|
||||
pub fn from_str(arg: &str) -> Option<Self> {
|
||||
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<u8> = 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<String> = 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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<tokio::sync::Mutex<Option<tokio::sync::oneshot::Sender<SecureString>>>>;
|
||||
|
||||
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<secret_service::Error>),
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
fn parse_hints(hints: Vec<String>) -> Vec<SecretHint> {
|
||||
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<String>,
|
||||
previous: SecureString,
|
||||
tx: SecretSender,
|
||||
},
|
||||
CancelGetSecrets {
|
||||
uuid: String,
|
||||
name: String,
|
||||
},
|
||||
Failed(Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Request {
|
||||
SetSecrets {
|
||||
setting_name: String,
|
||||
uuid: String,
|
||||
secrets: HashMap<String, SecureString>,
|
||||
applied_tx: oneshot::Sender<()>,
|
||||
},
|
||||
GetSecrets {
|
||||
setting_name: String,
|
||||
uuid: String,
|
||||
resp_tx: oneshot::Sender<HashMap<String, SecureString>>,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn secret_agent_stream(
|
||||
identifier: impl AsRef<str>,
|
||||
rx: tokio::sync::mpsc::Receiver<Request>,
|
||||
) -> impl Stream<Item = Event> {
|
||||
iced_futures::stream::channel(
|
||||
4,
|
||||
move |mut msg_tx: futures::channel::mpsc::Sender<Event>| 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<Event>,
|
||||
mut rx: tokio::sync::mpsc::Receiver<Request>,
|
||||
) -> 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::<zbus::zvariant::Str>() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(value) = value.downcast_ref::<zbus::zvariant::Str>() else {
|
||||
continue;
|
||||
};
|
||||
// we only care about "<secret>-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<zbus::zvariant::Dict>) -> bool {
|
||||
setting.map(setting_has_always_ask).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_connection_always_ask(connection: &HashMap<String, HashMap<String, OwnedValue>>) -> 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::<String>().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::<zbus::zvariant::Dict>().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::<zbus::zvariant::Dict>().ok()),
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if has_always_ask(
|
||||
connection
|
||||
.get("802-1x")
|
||||
.and_then(|d| d.get("data"))
|
||||
.and_then(|data| data.downcast_ref::<zbus::zvariant::Dict>().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::<zbus::zvariant::Dict>().ok()),
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if has_always_ask(
|
||||
connection
|
||||
.get("802-1x")
|
||||
.and_then(|d| d.get("data"))
|
||||
.and_then(|data| data.downcast_ref::<zbus::zvariant::Dict>().ok()),
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SettingsSecretAgent {
|
||||
tx: futures::channel::mpsc::Sender<Event>,
|
||||
}
|
||||
|
||||
#[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::<String>().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<String, HashMap<String, zbus::zvariant::OwnedValue>>,
|
||||
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<String, HashMap<String, zbus::zvariant::OwnedValue>>,
|
||||
connection_path: zbus::zvariant::ObjectPath<'_>,
|
||||
setting_name: String,
|
||||
hints: Vec<String>,
|
||||
flags: u32,
|
||||
) -> HashMap<String, HashMap<String, zbus::zvariant::OwnedValue>> {
|
||||
self.get_secrets_inner(connection, connection_path, setting_name, hints, flags)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// SaveSecrets method
|
||||
async fn save_secrets(
|
||||
&self,
|
||||
connection: HashMap<String, HashMap<String, zbus::zvariant::OwnedValue>>,
|
||||
_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<String, HashMap<String, zbus::zvariant::OwnedValue>>,
|
||||
connection_path: zbus::zvariant::ObjectPath<'_>,
|
||||
setting_name: String,
|
||||
hints: Vec<String>,
|
||||
flags: u32,
|
||||
) -> Result<HashMap<String, HashMap<String, zbus::zvariant::OwnedValue>>, 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::<String>().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::<String>().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<String, HashMap<String, zbus::zvariant::OwnedValue>>,
|
||||
_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::<String>().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<String, HashMap<String, zbus::zvariant::OwnedValue>>,
|
||||
) -> 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::<String>().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::<String>().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::<String>().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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// 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<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||
id: I,
|
||||
conn: Connection,
|
||||
) -> iced_futures::Subscription<Event> {
|
||||
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<Event>) {
|
||||
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<Event>,
|
||||
) -> 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue