Skip to content

Commit

Permalink
Support Unicode SSID in status responses
Browse files Browse the repository at this point in the history
This replaces the INI config parser from the config crate with a
simplified parser that only support k=v pairs, but does proper printf
unsecaping for non printable/on ASCII characters.
  • Loading branch information
aj-bagwell committed Oct 16, 2024
1 parent 40b8eae commit 94ff56e
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 71 deletions.
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ readme = "README.md"
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"] }
Expand Down
68 changes: 44 additions & 24 deletions src/ap/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,10 @@ pub struct Status {

impl Status {
pub fn from_response(response: &str) -> Result<Status> {
use config::{Config, File, FileFormat};
let config = Config::builder()
.add_source(File::from_str(response, FileFormat::Ini))
.build()
.map_err(|e| error::Error::ParsingWifiStatus {
e,
s: response.into(),
})?;

Ok(config.try_deserialize::<Status>().unwrap())
crate::config::deserialize_str(response).map_err(|e| error::Error::ParsingWifiStatus {
e,
s: response.into(),
})
}
}

Expand All @@ -58,37 +52,63 @@ pub struct Config {
pub ssid: String,
#[serde(deserialize_with = "deserialize_enabled_bool")]
pub wps_state: bool,
#[serde(deserialize_with = "deserialize_i32")]
pub wpa: i32,
pub ket_mgmt: String,
pub key_mgmt: String,
pub group_cipher: String,
pub rsn_pairwise_cipher: String,
pub wpa_pairwise_cipher: String,
}

impl Config {
/// Decode from the response sent from the supplicant
/// ```
/// # use wifi_ctrl::ap::Config;
/// let resp = r#"
///bssid=e0:91:f5:7d:11:c0
///ssid=\xc2\xaf\\_(\xe3\x83\x84)_/\xc2\xaf
///wps_state=enabled
///wpa=12
///group_cipher=CCMP
///key_mgmt=WPA2-PSK
///wpa_state=COMPLETED
///rsn_pairwise_cipher=foo
///wpa_pairwise_cipher=bar
///"#;
/// let config = Config::from_response(resp).unwrap();
/// assert_eq!(config.wps_state, true);
/// assert_eq!(config.wpa, 12);
/// assert_eq!(config.ssid, r#"¯\_(ツ)_/¯"#);
/// ```
pub fn from_response(response: &str) -> Result<Config> {
use config::{File, FileFormat};
let config = config::Config::builder()
.add_source(File::from_str(response, FileFormat::Ini))
.build()
.map_err(|e| error::Error::ParsingWifiConfig {
e,
s: response.into(),
})?;

Ok(config.try_deserialize::<Config>().unwrap())
crate::config::deserialize_str(response).map_err(|e| error::Error::ParsingWifiConfig {
e,
s: response.into(),
})
}
}

fn deserialize_enabled_bool<'de, D>(deserializer: D) -> std::result::Result<bool, D::Error>
where
D: de::Deserializer<'de>,
{
let s: &str = de::Deserialize::deserialize(deserializer)?;
let s: String = de::Deserialize::deserialize(deserializer)?;

match s {
match s.as_str() {
"enabled" => Ok(true),
"disabled" => Ok(false),
_ => Err(de::Error::unknown_variant(s, &["enabled", "disabled"])),
_ => Err(de::Error::unknown_variant(&s, &["enabled", "disabled"])),
}
}

fn deserialize_i32<'de, D>(deserializer: D) -> std::result::Result<i32, D::Error>
where
D: de::Deserializer<'de>,
{
let s: String = de::Deserialize::deserialize(deserializer)?;

match s.parse() {
Ok(n) => Ok(n),
_ => Err(de::Error::custom("invalid int")),
}
}
76 changes: 76 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::collections::HashMap;
use std::fmt::Display;

use serde::de::value::MapDeserializer;
use serde::Deserialize;
use thiserror::Error;

#[derive(Debug, Error, PartialEq, Eq, Clone)]
pub enum ConfigError {
#[error("Missing '=' delimiter in config line")]
MissingDelimterEqual,
#[error("escape code is not made up of valid hex code")]
InvalidEscape,
#[error("escape code is incomplete")]
IncompleteEscape,
#[error("escaped value is not valid uft8 after unescaping")]
NonUtf8Escape,
#[error("Value could not be decoded")]
SerdeError(String),
}

impl serde::de::Error for ConfigError {
fn custom<T>(msg: T) -> Self
where
T: Display,
{
Self::SerdeError(msg.to_string())
}
}

pub(crate) fn to_map(response: &str) -> Result<HashMap<&str, String>, ConfigError> {
let mut map = HashMap::new();
for line in response.trim().lines() {
let (k, v) = line
.split_once('=')
.ok_or(ConfigError::MissingDelimterEqual)?;
map.insert(k, unprintf(v)?);
}
Ok(map)
}

pub(crate) fn deserialize_str<'de, T: Deserialize<'de>>(response: &str) -> Result<T, ConfigError> {
let map = to_map(response)?;
Ok(T::deserialize(MapDeserializer::new(map.into_iter()))?)
}

pub(crate) fn unprintf(escaped: &str) -> std::result::Result<String, ConfigError> {
let mut bytes = escaped.as_bytes().iter().copied();
let mut unescaped = vec![];
// undo "printf_encode"
loop {
unescaped.push(match bytes.next() {
Some(b'\\') => match bytes.next().ok_or(ConfigError::IncompleteEscape)? {
b'n' => b'\n',
b'r' => b'\r',
b't' => b'\t',
b'e' => b'\x1b',
b'x' => {
let hex = [
bytes.next().ok_or(ConfigError::IncompleteEscape)?,
bytes.next().ok_or(ConfigError::IncompleteEscape)?,
];
u8::from_str_radix(
std::str::from_utf8(&hex).or(Err(ConfigError::InvalidEscape))?,
16,
)
.or(Err(ConfigError::InvalidEscape))?
}
c => c,
},
Some(c) => c,
None => break,
})
}
String::from_utf8(unescaped).or(Err(ConfigError::NonUtf8Escape))
}
15 changes: 2 additions & 13 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use super::*;
use broadcast::error::SendError;
use config::ConfigError;
use thiserror::Error;

#[derive(Error, Debug)]
Expand Down Expand Up @@ -51,11 +50,11 @@ impl Clone for Error {
Self::Io(err) => Self::Io(clone_io(err)),
Self::StartupAborted => Self::StartupAborted,
Self::ParsingWifiStatus { e, s } => Self::ParsingWifiStatus {
e: clone_config_err(e),
e: e.clone(),
s: s.clone(),
},
Self::ParsingWifiConfig { e, s } => Self::ParsingWifiConfig {
e: clone_config_err(e),
e: e.clone(),
s: s.clone(),
},
Self::UnexpectedWifiApRepsonse(s) => Self::UnexpectedWifiApRepsonse(s.clone()),
Expand Down Expand Up @@ -90,13 +89,3 @@ fn clone_io(err: &std::io::Error) -> std::io::Error {
std::io::Error::new(err.kind(), err.to_string())
}
}

fn clone_config_err(err: &config::ConfigError) -> config::ConfigError {
match err {
ConfigError::Frozen => ConfigError::Frozen,
ConfigError::NotFound(path) => ConfigError::NotFound(path.clone()),
ConfigError::PathParse(err) => ConfigError::PathParse(err.clone()),
ConfigError::Message(message) => ConfigError::Message(message.clone()),
e => ConfigError::Message(e.to_string()),
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub mod error;
/// WiFi Station (network client) runtime and types
pub mod sta;

pub(crate) mod config;
pub(crate) mod socket_handle;

use socket_handle::SocketHandle;
Expand Down
47 changes: 14 additions & 33 deletions src/sta/types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::{error, warn, Result, SocketHandle};
use super::{config, config::unprintf, error, warn, Result, SocketHandle};

use serde::Serialize;
use std::collections::HashMap;
use std::fmt::Display;
Expand All @@ -22,27 +23,7 @@ impl ScanResult {
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()?;
let name = unprintf(escaped_name).ok()?;
Some(ScanResult {
mac: mac.to_string(),
frequency: frequency.to_string(),
Expand All @@ -60,7 +41,7 @@ impl 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);
Expand Down Expand Up @@ -102,11 +83,15 @@ impl NetworkResult {
.request(&format!("GET_NETWORK {network_id} ssid"))
.await?
.trim_matches('\"');
let ssid = unprintf(ssid).map_err(|e| error::Error::ParsingWifiStatus {
e,
s: ssid.to_string(),
})?;
if let Ok(network_id) = usize::from_str(network_id) {
if let Some(flags) = line_split.last() {
results.push(NetworkResult {
flags: flags.into(),
ssid: ssid.into(),
ssid: ssid,
network_id,
})
}
Expand All @@ -123,15 +108,11 @@ impl NetworkResult {
pub type Status = HashMap<String, String>;

pub(crate) fn parse_status(response: &str) -> Result<Status> {
use config::{Config, File, FileFormat};
let config = Config::builder()
.add_source(File::from_str(response, FileFormat::Ini))
.build()
.map_err(|e| error::Error::ParsingWifiStatus {
e,
s: response.into(),
})?;
Ok(config.try_deserialize::<HashMap<String, String>>().unwrap())
let map = config::to_map(response).map_err(|e| error::Error::ParsingWifiStatus {
e,
s: response.into(),
})?;
Ok(map.into_iter().map(|(k, v)| (k.to_owned(), v)).collect())
}

#[derive(Debug)]
Expand Down

0 comments on commit 94ff56e

Please sign in to comment.