wpactrl/wpactrl.rs
1#![deny(missing_docs)]
2use super::Result;
3use log::warn;
4use std::collections::VecDeque;
5use std::os::unix::io::{AsRawFd, RawFd};
6use std::os::unix::net::UnixDatagram;
7use std::path::{Path, PathBuf};
8use std::time::Duration;
9
10use crate::error::Error;
11
12const BUF_SIZE: usize = 10_240;
13const PATH_DEFAULT_CLIENT: &str = "/tmp";
14const PATH_DEFAULT_SERVER: &str = "/var/run/wpa_supplicant/wlan0";
15
16/// Builder object used to construct a [`Client`] session
17#[derive(Default)]
18pub struct ClientBuilder {
19 cli_path: Option<PathBuf>,
20 ctrl_path: Option<PathBuf>,
21}
22
23impl ClientBuilder {
24 /// A path-like object for this application's UNIX domain socket
25 ///
26 /// # Examples
27 ///
28 /// ```
29 /// use wpactrl::Client;
30 /// let wpa = Client::builder()
31 /// .cli_path("/tmp")
32 /// .open()
33 /// .unwrap();
34 /// ```
35 #[must_use]
36 pub fn cli_path<I, P>(mut self, cli_path: I) -> Self
37 where
38 I: Into<Option<P>>,
39 P: AsRef<Path> + Sized,
40 PathBuf: From<P>,
41 {
42 self.cli_path = cli_path.into().map(PathBuf::from);
43 self
44 }
45
46 /// A path-like object for the `wpa_supplicant` / `hostapd` UNIX domain sockets
47 ///
48 /// # Examples
49 ///
50 /// ```
51 /// use wpactrl::Client;
52 /// let wpa = Client::builder()
53 /// .ctrl_path("/var/run/wpa_supplicant/wlan0")
54 /// .open()
55 /// .unwrap();
56 /// ```
57 #[must_use]
58 pub fn ctrl_path<I, P>(mut self, ctrl_path: I) -> Self
59 where
60 I: Into<Option<P>>,
61 P: AsRef<Path> + Sized,
62 PathBuf: From<P>,
63 {
64 self.ctrl_path = ctrl_path.into().map(PathBuf::from);
65 self
66 }
67
68 /// Open a control interface to `wpa_supplicant` / `hostapd`.
69 ///
70 /// # Examples
71 ///
72 /// ```
73 /// use wpactrl::Client;
74 /// let wpa = Client::builder().open().unwrap();
75 /// ```
76 /// # Errors
77 ///
78 /// * [[`Error::Io`]] - Low-level I/O error
79 pub fn open(self) -> Result<Client> {
80 let mut counter = 0;
81 loop {
82 counter += 1;
83 let bind_filename = format!("wpa_ctrl_{}-{}", std::process::id(), counter);
84 let bind_filepath = self
85 .cli_path
86 .as_deref()
87 .unwrap_or_else(|| Path::new(PATH_DEFAULT_CLIENT))
88 .join(bind_filename);
89 match UnixDatagram::bind(&bind_filepath) {
90 Ok(socket) => {
91 socket.connect(self.ctrl_path.unwrap_or_else(|| PATH_DEFAULT_SERVER.into()))?;
92 socket.set_nonblocking(true)?;
93 return Ok(Client(ClientInternal {
94 buffer: [0; BUF_SIZE],
95 handle: socket,
96 filepath: bind_filepath,
97 }));
98 }
99 Err(ref e) if counter < 2 && e.kind() == std::io::ErrorKind::AddrInUse => {
100 std::fs::remove_file(bind_filepath)?;
101 continue;
102 }
103 Err(e) => return Err(e.into()),
104 };
105 }
106 }
107}
108
109struct ClientInternal {
110 buffer: [u8; BUF_SIZE],
111 handle: UnixDatagram,
112 filepath: PathBuf,
113}
114
115fn select(fd: RawFd, duration: Duration) -> Result<bool> {
116 let r = unsafe {
117 let mut raw_fd_set = {
118 let mut raw_fd_set = std::mem::MaybeUninit::<libc::fd_set>::uninit();
119 libc::FD_ZERO(raw_fd_set.as_mut_ptr());
120 raw_fd_set.assume_init()
121 };
122 libc::FD_SET(fd, &mut raw_fd_set);
123 libc::select(
124 fd + 1,
125 &mut raw_fd_set,
126 std::ptr::null_mut(),
127 std::ptr::null_mut(),
128 &mut libc::timeval {
129 tv_sec: duration.as_secs().try_into().unwrap(),
130 tv_usec: duration.subsec_micros().try_into().unwrap(),
131 },
132 )
133 };
134
135 if r >= 0 {
136 Ok(r > 0)
137 } else {
138 Err(Error::Wait)
139 }
140}
141
142impl ClientInternal {
143 /// Check if any messages are available
144 pub fn pending(&mut self) -> Result<bool> {
145 select(self.handle.as_raw_fd(), Duration::from_secs(0))
146 }
147
148 /// Receive a message
149 pub fn recv(&mut self) -> Result<Option<String>> {
150 if self.pending()? {
151 let buf_len = self.handle.recv(&mut self.buffer)?;
152 std::str::from_utf8(&self.buffer[0..buf_len])
153 .map(|s| Some(s.to_owned()))
154 .map_err(std::convert::Into::into)
155 } else {
156 Ok(None)
157 }
158 }
159
160 /// Send a command to `wpa_supplicant` / `hostapd`.
161 fn request<F: FnMut(&str)>(&mut self, cmd: &str, mut cb: F) -> Result<String> {
162 self.handle.send(cmd.as_bytes())?;
163 loop {
164 select(self.handle.as_raw_fd(), Duration::from_secs(10))?;
165 match self.handle.recv(&mut self.buffer) {
166 Ok(len) => {
167 let s = std::str::from_utf8(&self.buffer[0..len])?;
168 if s.starts_with('<') {
169 cb(s);
170 } else {
171 return Ok(s.to_owned());
172 }
173 }
174 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
175 Err(e) => return Err(e.into()),
176 }
177 }
178 }
179}
180
181impl Drop for ClientInternal {
182 fn drop(&mut self) {
183 if let Err(e) = std::fs::remove_file(&self.filepath) {
184 warn!("Unable to unlink {:?}", e);
185 }
186 }
187}
188
189/// A connection to `wpa_supplicant` / `hostapd`
190pub struct Client(ClientInternal);
191
192impl Client {
193 /// Creates a builder for a `wpa_supplicant` / `hostapd` connection
194 ///
195 /// # Examples
196 ///
197 /// ```
198 /// let wpa = wpactrl::Client::builder().open().unwrap();
199 /// ```
200 #[must_use]
201 pub fn builder() -> ClientBuilder {
202 ClientBuilder::default()
203 }
204
205 /// Register as an event monitor for control interface messages
206 ///
207 /// # Examples
208 ///
209 /// ```
210 /// let mut wpa = wpactrl::Client::builder().open().unwrap();
211 /// let wpa_attached = wpa.attach().unwrap();
212 /// ```
213 ///
214 /// # Errors
215 ///
216 /// * [`Error::Attach`] - Unexpected (non-OK) response
217 /// * [`Error::Io`] - Low-level I/O error
218 /// * [`Error::Utf8ToStr`] - Corrupted message or message with non-UTF8 characters
219 /// * [`Error::Wait`] - Failed to wait on underlying Unix socket
220 pub fn attach(mut self) -> Result<ClientAttached> {
221 // FIXME: None closure would be better
222 if self.0.request("ATTACH", |_: &str| ())? == "OK\n" {
223 Ok(ClientAttached(self.0, VecDeque::new()))
224 } else {
225 Err(Error::Attach)
226 }
227 }
228
229 /// Send a command to `wpa_supplicant` / `hostapd`.
230 ///
231 /// Commands are generally identical to those used in `wpa_cli`,
232 /// except all uppercase (eg `LIST_NETWORKS`, `SCAN`, etc)
233 ///
234 /// # Examples
235 ///
236 /// ```
237 /// let mut wpa = wpactrl::Client::builder().open().unwrap();
238 /// assert_eq!(wpa.request("PING").unwrap(), "PONG\n");
239 /// ```
240 ///
241 /// # Errors
242 ///
243 /// * [`Error::Io`] - Low-level I/O error
244 /// * [`Error::Utf8ToStr`] - Corrupted message or message with non-UTF8 characters
245 /// * [`Error::Wait`] - Failed to wait on underlying Unix socket
246 pub fn request(&mut self, cmd: &str) -> Result<String> {
247 self.0.request(cmd, |_: &str| ())
248 }
249}
250
251/// A connection to `wpa_supplicant` / `hostapd` that receives status messages
252pub struct ClientAttached(ClientInternal, VecDeque<String>);
253
254impl ClientAttached {
255 /// Stop listening for and discard any remaining control interface messages
256 ///
257 /// # Examples
258 ///
259 /// ```
260 /// let mut wpa = wpactrl::Client::builder().open().unwrap().attach().unwrap();
261 /// wpa.detach().unwrap();
262 /// ```
263 ///
264 /// # Errors
265 ///
266 /// * [`Error::Detach`] - Unexpected (non-OK) response
267 /// * [`Error::Io`] - Low-level I/O error
268 /// * [`Error::Utf8ToStr`] - Corrupted message or message with non-UTF8 characters
269 /// * [`Error::Wait`] - Failed to wait on underlying Unix socket
270 pub fn detach(mut self) -> Result<Client> {
271 if self.0.request("DETACH", |_: &str| ())? == "OK\n" {
272 Ok(Client(self.0))
273 } else {
274 Err(Error::Detach)
275 }
276 }
277
278 /// Receive the next control interface message.
279 ///
280 /// Note that multiple control interface messages can be pending;
281 /// call this function repeatedly until it returns None to get all of them.
282 ///
283 /// # Examples
284 ///
285 /// ```
286 /// let mut wpa = wpactrl::Client::builder().open().unwrap().attach().unwrap();
287 /// assert_eq!(wpa.recv().unwrap(), None);
288 /// ```
289 ///
290 /// # Errors
291 ///
292 /// * [`Error::Io`] - Low-level I/O error
293 /// * [`Error::Utf8ToStr`] - Corrupted message or message with non-UTF8 characters
294 /// * [`Error::Wait`] - Failed to wait on underlying Unix socket
295 pub fn recv(&mut self) -> Result<Option<String>> {
296 if let Some(s) = self.1.pop_back() {
297 Ok(Some(s))
298 } else {
299 self.0.recv()
300 }
301 }
302
303 /// Send a command to `wpa_supplicant` / `hostapd`.
304 ///
305 /// Commands are generally identical to those used in `wpa_cli`,
306 /// except all uppercase (eg `LIST_NETWORKS`, `SCAN`, etc)
307 ///
308 /// Control interface messages will be buffered as the command
309 /// runs, and will be returned on the next call to recv.
310 ///
311 /// # Examples
312 ///
313 /// ```
314 /// let mut wpa = wpactrl::Client::builder().open().unwrap();
315 /// assert_eq!(wpa.request("PING").unwrap(), "PONG\n");
316 /// ```
317 ///
318 /// # Errors
319 ///
320 /// * [`Error::Io`] - Low-level I/O error
321 /// * [`Error::Utf8ToStr`] - Corrupted message or message with non-UTF8 characters
322 /// * [`Error::Wait`] - Failed to wait on underlying Unix socket
323 pub fn request(&mut self, cmd: &str) -> Result<String> {
324 let mut messages = VecDeque::new();
325 let r = self.0.request(cmd, |s: &str| messages.push_front(s.into()));
326 self.1.extend(messages);
327 r
328 }
329}
330
331#[cfg(test)]
332mod test {
333 use serial_test::serial;
334 use super::*;
335
336 fn wpa_ctrl() -> Client {
337 Client::builder().open().unwrap()
338 }
339
340 #[test]
341 #[serial]
342 fn attach() {
343 wpa_ctrl()
344 .attach()
345 .unwrap()
346 .detach()
347 .unwrap()
348 .attach()
349 .unwrap()
350 .detach()
351 .unwrap();
352 }
353
354 #[test]
355 #[serial]
356 fn detach() {
357 let wpa = wpa_ctrl().attach().unwrap();
358 wpa.detach().unwrap();
359 }
360
361 #[test]
362 #[serial]
363 fn builder() {
364 wpa_ctrl();
365 }
366
367 #[test]
368 #[serial]
369 fn request() {
370 let mut wpa = wpa_ctrl();
371 assert_eq!(wpa.request("PING").unwrap(), "PONG\n");
372 let mut wpa_attached = wpa.attach().unwrap();
373 // FIXME: This may not trigger the callback
374 assert_eq!(wpa_attached.request("PING").unwrap(), "PONG\n");
375 }
376
377 #[test]
378 #[serial]
379 fn recv() {
380 let mut wpa = wpa_ctrl().attach().unwrap();
381 assert_eq!(wpa.recv().unwrap(), None);
382 assert_eq!(wpa.request("SCAN").unwrap(), "OK\n");
383 loop {
384 match wpa.recv().unwrap() {
385 Some(s) => {
386 assert_eq!(&s[3..], "CTRL-EVENT-SCAN-STARTED ");
387 break;
388 }
389 None => std::thread::sleep(std::time::Duration::from_millis(10)),
390 }
391 }
392 wpa.detach().unwrap();
393 }
394}