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",
|
||||
"once_cell",
|
||||
"ouroboros",
|
||||
"qrcode",
|
||||
"rustc-hash 2.1.1",
|
||||
"thiserror 1.0.69",
|
||||
"unicode-segmentation",
|
||||
|
|
@ -6287,6 +6288,12 @@ dependencies = [
|
|||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qrcode"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "166f136dfdb199f98186f3649cf7a0536534a61417a1a30221b492b4fb60ce3f"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
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"] }
|
||||
|
||||
[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"
|
||||
|
||||
[workspace.dependencies.cosmic-config]
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use std::{
|
|||
use anyhow::Context;
|
||||
use cosmic::{
|
||||
Apply, Element, Task,
|
||||
app::ContextDrawer,
|
||||
iced::{Alignment, Length},
|
||||
iced_core::text::Wrapping,
|
||||
iced_widget::focus_next,
|
||||
|
|
@ -54,6 +55,8 @@ pub enum Message {
|
|||
PasswordRequest(network_manager::SSID),
|
||||
/// Update the password from the dialog
|
||||
PasswordUpdate(SecureString),
|
||||
/// Request QR code drawer for sharing WiFi credentials
|
||||
QRCodeRequest(network_manager::SSID),
|
||||
/// Selects a device to display connections from
|
||||
SelectDevice(Arc<network_manager::devices::DeviceInfo>),
|
||||
/// 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 {
|
||||
Forget(network_manager::SSID),
|
||||
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)]
|
||||
pub struct Page {
|
||||
entity: page::Entity,
|
||||
|
|
@ -111,6 +122,10 @@ pub struct Page {
|
|||
withheld_devices: Option<Vec<network_manager::devices::DeviceInfo>>,
|
||||
/// Withhold state update if the view more popup is shown.
|
||||
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)]
|
||||
|
|
@ -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>> {
|
||||
Some(
|
||||
widget::button::standard(fl!("add-network"))
|
||||
|
|
@ -253,6 +312,12 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
|
||||
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 {
|
||||
|
|
@ -354,6 +419,41 @@ impl Page {
|
|||
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 => {
|
||||
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) => {
|
||||
self.view_more_popup = ssid;
|
||||
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> {
|
||||
crate::slab!(descriptions {
|
||||
airplane_mode_txt = fl!("airplane-on");
|
||||
|
|
@ -617,6 +737,7 @@ fn devices_view() -> Section<crate::pages::Message> {
|
|||
known_networks_txt = fl!("known-networks");
|
||||
no_networks_txt = fl!("no-networks");
|
||||
settings_txt = fl!("settings");
|
||||
share_txt = fl!("share");
|
||||
visible_networks_txt = fl!("visible-networks");
|
||||
wifi_txt = fl!("wifi");
|
||||
});
|
||||
|
|
@ -742,6 +863,12 @@ fn devices_view() -> Section<crate::pages::Message> {
|
|||
Message::Settings(network.ssid.clone()),
|
||||
§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(|| {
|
||||
popup_button(
|
||||
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
|
||||
known-networks = Known networks
|
||||
network-and-wireless = Network & wireless
|
||||
network-name = Network Name
|
||||
no-networks = No networks have been found.
|
||||
no-vpn = No VPN connections available.
|
||||
password = Password
|
||||
password-confirm = Confirm password
|
||||
qr-code-unavailable = QR code not available
|
||||
remove = Remove
|
||||
scan-to-connect-description = Scan the QR code to connect to this network.
|
||||
settings = Settings
|
||||
share = Share network
|
||||
username = Username
|
||||
visible-networks = Visible networks
|
||||
identity = Identity
|
||||
|
|
|
|||
|
|
@ -451,6 +451,73 @@ async fn start_listening(
|
|||
.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 => {
|
||||
return State::Finished;
|
||||
}
|
||||
|
|
@ -540,6 +607,8 @@ pub enum Request {
|
|||
password: SecureString,
|
||||
hw_address: HwAddress,
|
||||
},
|
||||
/// Get WiFi credentials for a known access point.
|
||||
GetWiFiCredentials(SSID),
|
||||
/// Signal to reload the service.
|
||||
Reload,
|
||||
/// Remove a connection profile.
|
||||
|
|
@ -568,6 +637,11 @@ pub enum Event {
|
|||
WiFiEnabled(bool),
|
||||
WirelessAccessPoints,
|
||||
ActiveConns,
|
||||
WiFiCredentials {
|
||||
ssid: SSID,
|
||||
password: Option<String>,
|
||||
security_type: NetworkType,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue