feat(wifi): share public network with QR code
This commit is contained in:
parent
ae3fdd5cf9
commit
b9dfcde7b1
5 changed files with 214 additions and 2 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -3606,6 +3606,7 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ouroboros",
|
"ouroboros",
|
||||||
|
"qrcode",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
|
|
@ -6287,6 +6288,12 @@ dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "qrcode"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "166f136dfdb199f98186f3649cf7a0536534a61417a1a30221b492b4fb60ce3f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-error"
|
name = "quick-error"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ cosmic-randr = { git = "https://github.com/pop-os/cosmic-randr" }
|
||||||
tokio = { version = "1.48.0", features = ["macros"] }
|
tokio = { version = "1.48.0", features = ["macros"] }
|
||||||
|
|
||||||
[workspace.dependencies.libcosmic]
|
[workspace.dependencies.libcosmic]
|
||||||
features = ["dbus-config", "multi-window", "winit", "tokio"]
|
features = ["dbus-config", "multi-window", "winit", "tokio", "qr_code"]
|
||||||
git = "https://github.com/pop-os/libcosmic"
|
git = "https://github.com/pop-os/libcosmic"
|
||||||
|
|
||||||
[workspace.dependencies.cosmic-config]
|
[workspace.dependencies.cosmic-config]
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use std::{
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
Apply, Element, Task,
|
Apply, Element, Task,
|
||||||
|
app::ContextDrawer,
|
||||||
iced::{Alignment, Length},
|
iced::{Alignment, Length},
|
||||||
iced_core::text::Wrapping,
|
iced_core::text::Wrapping,
|
||||||
iced_widget::focus_next,
|
iced_widget::focus_next,
|
||||||
|
|
@ -54,6 +55,8 @@ pub enum Message {
|
||||||
PasswordRequest(network_manager::SSID),
|
PasswordRequest(network_manager::SSID),
|
||||||
/// Update the password from the dialog
|
/// Update the password from the dialog
|
||||||
PasswordUpdate(SecureString),
|
PasswordUpdate(SecureString),
|
||||||
|
/// Request QR code drawer for sharing WiFi credentials
|
||||||
|
QRCodeRequest(network_manager::SSID),
|
||||||
/// Selects a device to display connections from
|
/// Selects a device to display connections from
|
||||||
SelectDevice(Arc<network_manager::devices::DeviceInfo>),
|
SelectDevice(Arc<network_manager::devices::DeviceInfo>),
|
||||||
/// Opens settings page for the access point.
|
/// Opens settings page for the access point.
|
||||||
|
|
@ -84,7 +87,7 @@ impl From<Message> for crate::pages::Message {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
enum WiFiDialog {
|
enum WiFiDialog {
|
||||||
Forget(network_manager::SSID),
|
Forget(network_manager::SSID),
|
||||||
Password {
|
Password {
|
||||||
|
|
@ -96,6 +99,14 @@ enum WiFiDialog {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// QR code sharing context drawer state
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
struct QRCodeDrawer {
|
||||||
|
ssid: network_manager::SSID,
|
||||||
|
password: Option<String>,
|
||||||
|
security_type: NetworkType,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
entity: page::Entity,
|
entity: page::Entity,
|
||||||
|
|
@ -111,6 +122,10 @@ pub struct Page {
|
||||||
withheld_devices: Option<Vec<network_manager::devices::DeviceInfo>>,
|
withheld_devices: Option<Vec<network_manager::devices::DeviceInfo>>,
|
||||||
/// Withhold state update if the view more popup is shown.
|
/// Withhold state update if the view more popup is shown.
|
||||||
withheld_state: Option<NetworkManagerState>,
|
withheld_state: Option<NetworkManagerState>,
|
||||||
|
/// QR code data for WiFi sharing drawer
|
||||||
|
qr_code_data: Option<widget::qr_code::Data>,
|
||||||
|
/// QR code context drawer state
|
||||||
|
qr_drawer: Option<QRCodeDrawer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -208,6 +223,50 @@ impl page::Page<crate::pages::Message> for Page {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn context_drawer(&self) -> Option<ContextDrawer<'_, crate::pages::Message>> {
|
||||||
|
let drawer = self.qr_drawer.as_ref()?;
|
||||||
|
|
||||||
|
let theme = cosmic::theme::active();
|
||||||
|
let spacing = &theme.cosmic().spacing;
|
||||||
|
|
||||||
|
let qr_section = if let Some(ref qr_data) = self.qr_code_data {
|
||||||
|
widget::container(widget::qr_code(qr_data).cell_size(5)).center_x(Length::Fill)
|
||||||
|
} else {
|
||||||
|
widget::container(widget::text::body(fl!("qr-code-unavailable")))
|
||||||
|
};
|
||||||
|
|
||||||
|
let description = widget::text::body(fl!("scan-to-connect-description"))
|
||||||
|
.apply(widget::container)
|
||||||
|
.center_x(Length::Fill);
|
||||||
|
|
||||||
|
let mut info_items = widget::list_column();
|
||||||
|
|
||||||
|
info_items = info_items.add(widget::settings::item(
|
||||||
|
fl!("network-name"),
|
||||||
|
drawer.ssid.as_ref(),
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some(ref pass) = drawer.password {
|
||||||
|
info_items = info_items.add(widget::settings::item(fl!("password"), pass.as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = column::column()
|
||||||
|
.spacing(spacing.space_s)
|
||||||
|
.push(qr_section)
|
||||||
|
.push(description)
|
||||||
|
.push(info_items);
|
||||||
|
|
||||||
|
Some(
|
||||||
|
cosmic::app::context_drawer(
|
||||||
|
content
|
||||||
|
.apply(Element::from)
|
||||||
|
.map(crate::pages::Message::WiFi),
|
||||||
|
crate::pages::Message::CloseContextDrawer,
|
||||||
|
)
|
||||||
|
.title(fl!("share")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn header_view(&self) -> Option<cosmic::Element<'_, crate::pages::Message>> {
|
fn header_view(&self) -> Option<cosmic::Element<'_, crate::pages::Message>> {
|
||||||
Some(
|
Some(
|
||||||
widget::button::standard(fl!("add-network"))
|
widget::button::standard(fl!("add-network"))
|
||||||
|
|
@ -253,6 +312,12 @@ impl page::Page<crate::pages::Message> for Page {
|
||||||
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_context_drawer_close(&mut self) -> Task<crate::pages::Message> {
|
||||||
|
self.qr_drawer = None;
|
||||||
|
self.qr_code_data = None;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
|
|
@ -354,6 +419,41 @@ impl Page {
|
||||||
return update_devices(conn);
|
return update_devices(conn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::NetworkManager(network_manager::Event::WiFiCredentials {
|
||||||
|
ssid,
|
||||||
|
password,
|
||||||
|
security_type,
|
||||||
|
}) => {
|
||||||
|
// Generate QR code data in WiFi format: WIFI:T:<type>;S:<ssid>;P:<password>;;
|
||||||
|
// Special characters must be escaped according to WiFi QR code spec
|
||||||
|
let escaped_ssid = escape_wifi_qr_string(ssid.as_ref());
|
||||||
|
let qr_string = if let Some(ref pass) = password {
|
||||||
|
let security = match security_type {
|
||||||
|
NetworkType::PSK => "WPA",
|
||||||
|
NetworkType::EAP => "WPA",
|
||||||
|
NetworkType::Open => "",
|
||||||
|
};
|
||||||
|
let escaped_password = escape_wifi_qr_string(pass);
|
||||||
|
format!(
|
||||||
|
"WIFI:T:{};S:{};P:{};;",
|
||||||
|
security, escaped_ssid, escaped_password
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("WIFI:T:;S:{};;", escaped_ssid)
|
||||||
|
};
|
||||||
|
|
||||||
|
self.qr_code_data = widget::qr_code::Data::new(qr_string).ok();
|
||||||
|
|
||||||
|
self.qr_drawer = Some(QRCodeDrawer {
|
||||||
|
ssid,
|
||||||
|
password,
|
||||||
|
security_type,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open the context drawer
|
||||||
|
return cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity));
|
||||||
|
}
|
||||||
|
|
||||||
Message::AddNetwork => {
|
Message::AddNetwork => {
|
||||||
tokio::task::spawn(super::nm_add_wifi());
|
tokio::task::spawn(super::nm_add_wifi());
|
||||||
}
|
}
|
||||||
|
|
@ -455,6 +555,15 @@ impl Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::QRCodeRequest(ssid) => {
|
||||||
|
self.view_more_popup = None;
|
||||||
|
if let Some(nm) = self.nm_state.as_mut() {
|
||||||
|
_ = nm
|
||||||
|
.sender
|
||||||
|
.unbounded_send(network_manager::Request::GetWiFiCredentials(ssid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Message::ViewMore(ssid) => {
|
Message::ViewMore(ssid) => {
|
||||||
self.view_more_popup = ssid;
|
self.view_more_popup = ssid;
|
||||||
if self.view_more_popup.is_none() {
|
if self.view_more_popup.is_none() {
|
||||||
|
|
@ -606,6 +715,17 @@ impl Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Escapes special characters in WiFi QR code strings according to the spec
|
||||||
|
/// Special characters that need escaping: \ ; , : "
|
||||||
|
/// https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
|
||||||
|
fn escape_wifi_qr_string(s: &str) -> String {
|
||||||
|
s.replace('\\', "\\\\")
|
||||||
|
.replace(';', "\\;")
|
||||||
|
.replace(',', "\\,")
|
||||||
|
.replace(':', "\\:")
|
||||||
|
.replace('"', "\\\"")
|
||||||
|
}
|
||||||
|
|
||||||
fn devices_view() -> Section<crate::pages::Message> {
|
fn devices_view() -> Section<crate::pages::Message> {
|
||||||
crate::slab!(descriptions {
|
crate::slab!(descriptions {
|
||||||
airplane_mode_txt = fl!("airplane-on");
|
airplane_mode_txt = fl!("airplane-on");
|
||||||
|
|
@ -617,6 +737,7 @@ fn devices_view() -> Section<crate::pages::Message> {
|
||||||
known_networks_txt = fl!("known-networks");
|
known_networks_txt = fl!("known-networks");
|
||||||
no_networks_txt = fl!("no-networks");
|
no_networks_txt = fl!("no-networks");
|
||||||
settings_txt = fl!("settings");
|
settings_txt = fl!("settings");
|
||||||
|
share_txt = fl!("share");
|
||||||
visible_networks_txt = fl!("visible-networks");
|
visible_networks_txt = fl!("visible-networks");
|
||||||
wifi_txt = fl!("wifi");
|
wifi_txt = fl!("wifi");
|
||||||
});
|
});
|
||||||
|
|
@ -742,6 +863,12 @@ fn devices_view() -> Section<crate::pages::Message> {
|
||||||
Message::Settings(network.ssid.clone()),
|
Message::Settings(network.ssid.clone()),
|
||||||
§ion.descriptions[settings_txt],
|
§ion.descriptions[settings_txt],
|
||||||
))
|
))
|
||||||
|
.push_maybe(is_known.then(|| {
|
||||||
|
popup_button(
|
||||||
|
Message::QRCodeRequest(network.ssid.clone()),
|
||||||
|
§ion.descriptions[share_txt],
|
||||||
|
)
|
||||||
|
}))
|
||||||
.push_maybe(is_known.then(|| {
|
.push_maybe(is_known.then(|| {
|
||||||
popup_button(
|
popup_button(
|
||||||
Message::ForgetRequest(network.ssid.clone()),
|
Message::ForgetRequest(network.ssid.clone()),
|
||||||
|
|
|
||||||
4
i18n/en/cosmic_settings.ftl
vendored
4
i18n/en/cosmic_settings.ftl
vendored
|
|
@ -27,12 +27,16 @@ disconnect = Disconnect
|
||||||
forget = Forget
|
forget = Forget
|
||||||
known-networks = Known networks
|
known-networks = Known networks
|
||||||
network-and-wireless = Network & wireless
|
network-and-wireless = Network & wireless
|
||||||
|
network-name = Network Name
|
||||||
no-networks = No networks have been found.
|
no-networks = No networks have been found.
|
||||||
no-vpn = No VPN connections available.
|
no-vpn = No VPN connections available.
|
||||||
password = Password
|
password = Password
|
||||||
password-confirm = Confirm password
|
password-confirm = Confirm password
|
||||||
|
qr-code-unavailable = QR code not available
|
||||||
remove = Remove
|
remove = Remove
|
||||||
|
scan-to-connect-description = Scan the QR code to connect to this network.
|
||||||
settings = Settings
|
settings = Settings
|
||||||
|
share = Share network
|
||||||
username = Username
|
username = Username
|
||||||
visible-networks = Visible networks
|
visible-networks = Visible networks
|
||||||
identity = Identity
|
identity = Identity
|
||||||
|
|
|
||||||
|
|
@ -451,6 +451,73 @@ async fn start_listening(
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some(Request::GetWiFiCredentials(ssid)) => {
|
||||||
|
let s = match NetworkManagerSettings::new(&conn).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(why) => {
|
||||||
|
tracing::error!(?why, "error getting network manager settings");
|
||||||
|
return State::Waiting(conn, rx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine network type - default to PSK for encrypted networks
|
||||||
|
let mut security_type = NetworkType::PSK;
|
||||||
|
|
||||||
|
let known_conns = s.list_connections().await.unwrap_or_default();
|
||||||
|
for c in known_conns {
|
||||||
|
let settings = c.get_settings().await.ok().unwrap_or_default();
|
||||||
|
let settings_parsed = Settings::new(settings.clone());
|
||||||
|
|
||||||
|
if let Some(saved_ssid) = settings_parsed
|
||||||
|
.wifi
|
||||||
|
.clone()
|
||||||
|
.and_then(|w| w.ssid)
|
||||||
|
.and_then(|s| String::from_utf8(s).ok())
|
||||||
|
{
|
||||||
|
if saved_ssid == ssid.as_ref() {
|
||||||
|
let password = c
|
||||||
|
.get_secrets("802-11-wireless-security")
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|secrets| {
|
||||||
|
// Look for PSK password
|
||||||
|
secrets
|
||||||
|
.get("802-11-wireless-security")
|
||||||
|
.and_then(|sec| sec.get("psk"))
|
||||||
|
.and_then(|v| {
|
||||||
|
v.downcast_ref::<zbus::zvariant::Str>().ok()
|
||||||
|
})
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.or_else(|| {
|
||||||
|
// Fallback to WEP key
|
||||||
|
secrets
|
||||||
|
.get("802-11-wireless-security")
|
||||||
|
.and_then(|sec| sec.get("wep-key0"))
|
||||||
|
.and_then(|v| {
|
||||||
|
v.downcast_ref::<zbus::zvariant::Str>().ok()
|
||||||
|
})
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no password found, might be open network
|
||||||
|
if password.is_none() {
|
||||||
|
security_type = NetworkType::Open;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = output
|
||||||
|
.send(Event::WiFiCredentials {
|
||||||
|
ssid: ssid.clone(),
|
||||||
|
password,
|
||||||
|
security_type,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
return State::Finished;
|
return State::Finished;
|
||||||
}
|
}
|
||||||
|
|
@ -540,6 +607,8 @@ pub enum Request {
|
||||||
password: SecureString,
|
password: SecureString,
|
||||||
hw_address: HwAddress,
|
hw_address: HwAddress,
|
||||||
},
|
},
|
||||||
|
/// Get WiFi credentials for a known access point.
|
||||||
|
GetWiFiCredentials(SSID),
|
||||||
/// Signal to reload the service.
|
/// Signal to reload the service.
|
||||||
Reload,
|
Reload,
|
||||||
/// Remove a connection profile.
|
/// Remove a connection profile.
|
||||||
|
|
@ -568,6 +637,11 @@ pub enum Event {
|
||||||
WiFiEnabled(bool),
|
WiFiEnabled(bool),
|
||||||
WirelessAccessPoints,
|
WirelessAccessPoints,
|
||||||
ActiveConns,
|
ActiveConns,
|
||||||
|
WiFiCredentials {
|
||||||
|
ssid: SSID,
|
||||||
|
password: Option<String>,
|
||||||
|
security_type: NetworkType,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue