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}