coap_client/
lib.rs

1use std::str::FromStr;
2use std::time::Duration;
3/// Rust Async CoAP Client
4// https://github.com/ryankurte/rust-coap-client
5// Copyright 2021 ryan kurte <ryan@kurte.nz>
6use std::{
7    convert::{TryFrom, TryInto},
8    marker::PhantomData,
9};
10
11use log::{debug, error};
12use structopt::StructOpt;
13use strum_macros::{Display, EnumString, EnumVariantNames};
14
15pub use coap_lite::RequestType as Method;
16use coap_lite::{CoapRequest, MessageType, Packet, ResponseType};
17
18pub mod backend;
19pub use backend::Backend;
20
21pub const COAP_MTU: usize = 1600;
22
23/// Client connection options
24#[derive(Debug, Clone, PartialEq, StructOpt)]
25pub struct ClientOptions {
26    #[structopt(long, parse(try_from_str = humantime::parse_duration), default_value = "500ms")]
27    /// Client / Connection timeout
28    pub connect_timeout: Duration,
29
30    /// CA certificate for TLS/DTLS modes
31    #[structopt(long)]
32    pub tls_ca: Option<String>,
33
34    /// Client certificate for TLS/DTLS modes with client-auth
35    #[structopt(long)]
36    pub tls_cert: Option<String>,
37
38    /// Client key for TLS/DTLS modes with client-auth
39    #[structopt(long)]
40    pub tls_key: Option<String>,
41
42    /// Skip verifying peer certificate
43    #[structopt(long)]
44    pub tls_skip_verify: bool,
45}
46
47impl Default for ClientOptions {
48    fn default() -> Self {
49        Self {
50            connect_timeout: Duration::from_secs(2),
51            tls_ca: None,
52            tls_cert: None,
53            tls_key: None,
54            tls_skip_verify: false,
55        }
56    }
57}
58
59/// Request options, for configuring CoAP requests
60#[derive(Debug, Clone, PartialEq, StructOpt)]
61pub struct RequestOptions {
62    #[structopt(long)]
63    /// Disable message acknowlegement
64    pub non_confirmable: bool,
65    #[structopt(long, default_value = "3")]
66    /// Number of retries (for acknowleged messages)
67    pub retries: usize,
68    #[structopt(long, parse(try_from_str = humantime::parse_duration), default_value = "2s")]
69    /// Request -> response timeout
70    pub timeout: Duration,
71    #[structopt(long, parse(try_from_str = humantime::parse_duration), default_value = "500ms")]
72    /// Base period for exponential backoff
73    pub backoff: Duration,
74}
75
76impl Default for RequestOptions {
77    fn default() -> Self {
78        Self {
79            non_confirmable: false,
80            retries: 3,
81            timeout: Duration::from_secs(2),
82            backoff: Duration::from_millis(500),
83        }
84    }
85}
86
87/// Supported transports / schemes
88#[derive(Clone, PartialEq, Debug, Display, EnumString, EnumVariantNames)]
89pub enum Transport {
90    /// Basic UDP transport
91    #[strum(serialize = "udp", serialize = "coap")]
92    Udp,
93    /// Datagram TLS over UDP
94    #[strum(serialize = "dtls", serialize = "coaps")]
95    Dtls,
96    /// Basic TLS transport
97    Tcp,
98    /// TLS over TCP
99    Tls,
100}
101
102/// CoAP client errors
103// TODO: impl std::error::Error via thiserror
104#[derive(Debug, thiserror::Error)]
105pub enum Error<T: std::fmt::Debug> {
106    #[error("Transport / Backend error: {:?}", 0)]
107    Transport(T),
108    #[error("Invalid host specification")]
109    InvalidHost,
110    #[error("Invalid URL")]
111    InvalidUrl,
112    #[error("Invalid Scheme")]
113    InvalidScheme,
114}
115
116/// Options for connecting client to hosts
117#[derive(Clone, PartialEq, Debug)]
118pub struct HostOptions {
119    /// Transport / scheme for connection
120    pub scheme: Transport,
121    /// Host to connect to
122    pub host: String,
123    /// Port for connection
124    pub port: u16,
125    /// Resource path (if provided)
126    pub resource: String,
127}
128
129impl Default for HostOptions {
130    fn default() -> Self {
131        Self {
132            scheme: Transport::Udp,
133            host: "localhost".to_string(),
134            port: 5683,
135            resource: "".to_string(),
136        }
137    }
138}
139
140impl ToString for HostOptions {
141    fn to_string(&self) -> String {
142        format!("{}://{}:{}", self.scheme, self.port, self.host)
143    }
144}
145
146impl TryFrom<(&str, u16)> for HostOptions {
147    type Error = std::io::Error;
148
149    /// Convert from host and port
150    fn try_from(v: (&str, u16)) -> Result<HostOptions, Self::Error> {
151        Ok(Self {
152            host: v.0.to_string(),
153            port: v.1,
154            ..Default::default()
155        })
156    }
157}
158
159impl TryFrom<(Transport, &str, u16)> for HostOptions {
160    type Error = std::io::Error;
161
162    /// Convert from scheme, host and port
163    fn try_from(v: (Transport, &str, u16)) -> Result<HostOptions, Self::Error> {
164        Ok(Self {
165            scheme: v.0,
166            host: v.1.to_string(),
167            port: v.2,
168            ..Default::default()
169        })
170    }
171}
172
173impl TryFrom<&str> for HostOptions {
174    type Error = std::io::Error;
175
176    /// Parse from string
177    fn try_from(url: &str) -> Result<HostOptions, Self::Error> {
178        // Split URL to parameters
179        let params = match url::Url::from_str(url) {
180            Ok(v) => v,
181            Err(e) => {
182                error!("Error parsing URL '{}': {:?}", url, e);
183                return Err(std::io::Error::new(
184                    std::io::ErrorKind::Other,
185                    "Invalid Url",
186                ));
187            }
188        };
189
190        // Match transport (or default to UDP)
191        let s = params.scheme();
192        let scheme = match (s, Transport::from_str(s)) {
193            ("", _) => Transport::Udp,
194            (_, Ok(v)) => v,
195            (_, Err(_e)) => {
196                error!("Unrecognized or unsupported scheme: {}", params.scheme());
197                return Err(std::io::Error::new(
198                    std::io::ErrorKind::Other,
199                    "Invalid Scheme",
200                ));
201            }
202        };
203
204        // Match port (or derive based on scheme default)
205        let p = params.port();
206        let port = match (p, &scheme) {
207            (Some(p), _) => p,
208            (None, Transport::Udp) => 5683,
209            (None, Transport::Dtls) => 5684,
210            (None, Transport::Tcp) => 5683,
211            (None, Transport::Tls) => 5684,
212        };
213
214        Ok(HostOptions {
215            scheme,
216            host: params.host_str().unwrap_or("localhost").to_string(),
217            port,
218            resource: params.path().to_string(),
219        })
220    }
221}
222
223/// Async CoAP client, generic over Backend implementations
224pub struct Client<E, T: Backend<E>> {
225    transport: T,
226    _e: PhantomData<E>,
227}
228
229/// Tokio base CoAP client
230#[cfg(feature = "backend-tokio")]
231pub type TokioClient = Client<std::io::Error, backend::Tokio>;
232
233#[cfg(feature = "backend-tokio")]
234impl TokioClient {
235    /// Create a new client with the provided host and client options
236    pub async fn connect<H>(host: H, opts: &ClientOptions) -> Result<Self, std::io::Error>
237    where
238        H: TryInto<HostOptions>,
239        <H as TryInto<HostOptions>>::Error: std::fmt::Debug,
240    {
241        // Convert provided host options
242        let peer: HostOptions = match host.try_into() {
243            Ok(v) => v,
244            Err(e) => {
245                error!("Error parsing host options: {:?}", e);
246                return Err(std::io::Error::new(
247                    std::io::ErrorKind::Other,
248                    "Invalid host options",
249                ));
250            }
251        };
252        let connect_str = format!("{}:{}", peer.host, peer.port);
253        debug!("Using host options: {:?} (connect: {})", peer, connect_str);
254
255        // Create appropriate transport
256        let transport = match &peer.scheme {
257            Transport::Udp => backend::Tokio::new_udp(&connect_str).await?,
258            Transport::Dtls => backend::Tokio::new_dtls(&connect_str, opts).await?,
259            _ => {
260                error!("Transport '{}' not yet implemented", peer.scheme);
261                unimplemented!()
262            }
263        };
264
265        // Return client object
266        Ok(Self {
267            transport,
268            _e: PhantomData,
269        })
270    }
271
272    /// Close the provided client, ending all existing sessions
273    pub async fn close(self) -> Result<(), std::io::Error> {
274        self.transport.close().await
275    }
276}
277
278/// Mark clients as Send if the backend is
279unsafe impl<E, B: Backend<E> + Send> Send for Client<E, B> {}
280
281impl<E, T> Client<E, T>
282where
283    T: Backend<E>,
284    E: std::fmt::Debug,
285{
286    /// Perform a basic CoAP request
287    pub async fn request(
288        &mut self,
289        method: Method,
290        resource: &str,
291        data: Option<&[u8]>,
292        opts: &RequestOptions,
293    ) -> Result<Packet, Error<E>> {
294        // Build request object
295        let mut request = CoapRequest::<&str>::new();
296
297        request.message.header.message_id = rand::random();
298
299        request.set_method(method);
300        request.set_path(resource);
301
302        match !opts.non_confirmable {
303            true => request.message.header.set_type(MessageType::Confirmable),
304            false => request.message.header.set_type(MessageType::NonConfirmable),
305        }
306
307        if let Some(d) = data {
308            request.message.payload = d.to_vec();
309        }
310
311        let t = rand::random::<u32>();
312        let token = t.to_le_bytes().to_vec();
313        request.message.set_token(token);
314
315        // Send request via backing transport
316        let resp = self
317            .transport
318            .request(request.message, opts.clone())
319            .await
320            .map_err(Error::Transport)?;
321
322        // TODO: handle response error codes here...
323
324        Ok(resp)
325    }
326
327    /// Observe the provided resource
328    pub async fn observe(
329        &mut self,
330        resource: &str,
331        opts: &RequestOptions,
332    ) -> Result<<T as Backend<E>>::Observe, E> {
333        self.transport
334            .observe(resource.to_string(), opts.clone())
335            .await
336    }
337
338    /// Deregister an observation
339    pub async fn unobserve(&mut self, o: <T as Backend<E>>::Observe) -> Result<(), E> {
340        self.transport.unobserve(o).await
341    }
342
343    /// Perform a Get request from the provided resource
344    pub async fn get(
345        &mut self,
346        resource: &str,
347        opts: &RequestOptions,
348    ) -> Result<Vec<u8>, Error<E>> {
349        let resp = self.request(Method::Get, resource, None, opts).await?;
350        Ok(resp.payload)
351    }
352
353    /// Perform a Put request to the provided resource
354    pub async fn put(
355        &mut self,
356        resource: &str,
357        data: Option<&[u8]>,
358        opts: &RequestOptions,
359    ) -> Result<Vec<u8>, Error<E>> {
360        let resp = self.request(Method::Put, resource, data, opts).await?;
361        Ok(resp.payload)
362    }
363
364    /// Perform a Post request to the provided resource
365    pub async fn post(
366        &mut self,
367        resource: &str,
368        data: Option<&[u8]>,
369        opts: &RequestOptions,
370    ) -> Result<Vec<u8>, Error<E>> {
371        let resp = self.request(Method::Post, resource, data, opts).await?;
372        Ok(resp.payload)
373    }
374}
375
376fn token_as_u32(token: &[u8]) -> u32 {
377    let mut v = 0;
378
379    for i in 0..token.len() {
380        v |= (token[i] as u32) << (i * 8);
381    }
382
383    v
384}
385
386fn status_is_ok(status: ResponseType) -> bool {
387    use ResponseType::*;
388
389    match status {
390        Created | Deleted | Valid | Changed | Content | Continue => true,
391        _ => false,
392    }
393}