From 6b7de2a83d5dfd646f243ebe3484ed155c8f1814 Mon Sep 17 00:00:00 2001 From: AJ Bagwell Date: Tue, 30 Jul 2024 14:26:50 +0100 Subject: [PATCH] Add support for unicode in SSIDs and PSKs The corresponding c code for encoding and decoding them can be found in wpa_supplicant/src/utils/common.c printf_decode escapes a SSID for returning in scan results wpa_config_parse_string reads a string from the config or the socket. It might be nicer to use the printf encoded P"I \xf0\x9f\xa6\x80 rust" style but I could not find a decent encoder. --- Cargo.toml | 1 + src/sta/mod.rs | 14 ++++++-- src/sta/types.rs | 90 +++++++++++++++++++++++++++++------------------- 3 files changed, 68 insertions(+), 37 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 937e714..964c5dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["hostapd", "wpa-supplicant", "wpa_supplicant", "wpa-cli", "wifi"] [dependencies] config = {version="0", default-features = false, features = ["ini"]} +hex = "0.4" log = { version = "0" } serde = {version = "1", features = ["derive"] } thiserror = "1" diff --git a/src/sta/mod.rs b/src/sta/mod.rs index 6cbff1f..5c2d3e5 100644 --- a/src/sta/mod.rs +++ b/src/sta/mod.rs @@ -227,9 +227,9 @@ impl WifiStation { let cmd = format!( "SET_NETWORK {id} {}", match param { - SetNetwork::Ssid(ssid) => format!("ssid \"{ssid}\""), + SetNetwork::Ssid(ssid) => format!("ssid {}", conf_escape(&ssid)), SetNetwork::Bssid(bssid) => format!("bssid \"{bssid}\""), - SetNetwork::Psk(psk) => format!("psk \"{psk}\""), + SetNetwork::Psk(psk) => format!("psk {}", conf_escape(&psk)), SetNetwork::KeyMgmt(mgmt) => format!("key_mgmt {}", mgmt), } ); @@ -306,6 +306,16 @@ impl WifiStation { } } +/// convert to wpa config format idealy a "quoted string" +/// in case of new-lines, quotes or emoji fall back to hex encoding the whole thing +fn conf_escape(raw: &str) -> String { + if raw.bytes().all(|b| b.is_ascii_graphic() && b != b'"') { + format!("\"{raw}\"") + } else { + hex::encode(raw) + } +} + struct SelectRequest { response: oneshot::Sender>, timeout: tokio::task::JoinHandle<()>, diff --git a/src/sta/types.rs b/src/sta/types.rs index c55cdfe..b9a06ba 100644 --- a/src/sta/types.rs +++ b/src/sta/types.rs @@ -16,43 +16,63 @@ pub struct ScanResult { } impl ScanResult { + fn from_line(line: &str) -> Option { + let (mac, rest) = line.split_once('\t')?; + let (frequency, rest) = rest.split_once('\t')?; + let (signal, rest) = rest.split_once('\t')?; + let signal = isize::from_str(signal).ok()?; + let (flags, escaped_name) = rest.split_once('\t')?; + let mut bytes = escaped_name.as_bytes().iter().copied(); + let mut name = vec![]; + // undo "printf_encode" + loop { + name.push(match bytes.next() { + Some(b'\\') => match bytes.next()? { + b'n' => b'\n', + b'r' => b'\r', + b't' => b'\t', + b'e' => b'\x1b', + b'x' => { + let hex = [bytes.next()?, bytes.next()?]; + u8::from_str_radix(std::str::from_utf8(&hex).ok()?, 16).ok()? + } + c => c, + }, + Some(c) => c, + None => break, + }) + } + let name = String::from_utf8(name).ok()?; + Some(ScanResult { + mac: mac.to_string(), + frequency: frequency.to_string(), + signal, + flags: flags.to_string(), + name, + }) + } + + // Overide to allow tabs in the raw string to avoid double escaping everything + #[allow(clippy::tabs_in_doc_comments)] + /// Parses lines from a scan result + ///``` + ///use wifi_ctrl::sta::ScanResult; + ///let results = ScanResult::vec_from_str(r#"bssid / frequency / signal level / flags / ssid + ///00:5f:67:90:da:64 2417 -35 [WPA-PSK-CCMP][WPA2-PSK-CCMP][ESS] TP-Link DA64 + ///e0:91:f5:7d:11:c0 2462 -33 [WPA2-PSK-CCMP][WPS][ESS] ¯\\_(\xe3\x83\x84)_/¯ + ///"#).unwrap(); + ///assert_eq!(results[0].mac, "00:5f:67:90:da:64"); + ///assert_eq!(results[0].name, "TP-Link DA64"); + ///assert_eq!(results[1].signal, -33); + ///assert_eq!(results[1].name, r#"¯\_(ツ)_/¯"#); + ///``` pub fn vec_from_str(response: &str) -> Result> { let mut results = Vec::new(); - let split = response.split('\n').skip(1); - for line in split { - let mut line_split = line.split_whitespace(); - if let (Some(mac), Some(frequency), Some(signal), Some(flags)) = ( - line_split.next(), - line_split.next(), - line_split.next(), - line_split.next(), - ) { - let mut name: Option = None; - for text in line_split { - match &mut name { - Some(started) => { - started.push(' '); - started.push_str(text); - } - None => { - name = Some(text.to_string()); - } - } - } - if let Some(name) = name { - if let Ok(signal) = isize::from_str(signal) { - let scan_result = ScanResult { - mac: mac.to_string(), - frequency: frequency.to_string(), - signal, - flags: flags.to_string(), - name, - }; - results.push(scan_result); - } else { - warn!("Invalid string for signal: {signal}"); - } - } + for line in response.lines().skip(1) { + if let Some(scan_result) = ScanResult::from_line(line) { + results.push(scan_result); + } else { + warn!("Invalid result from scan: {line}"); } } Ok(results)