1use std::str::FromStr;
2use std::time::Duration;
3use 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#[derive(Debug, Clone, PartialEq, StructOpt)]
25pub struct ClientOptions {
26 #[structopt(long, parse(try_from_str = humantime::parse_duration), default_value = "500ms")]
27 pub connect_timeout: Duration,
29
30 #[structopt(long)]
32 pub tls_ca: Option<String>,
33
34 #[structopt(long)]
36 pub tls_cert: Option<String>,
37
38 #[structopt(long)]
40 pub tls_key: Option<String>,
41
42 #[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#[derive(Debug, Clone, PartialEq, StructOpt)]
61pub struct RequestOptions {
62 #[structopt(long)]
63 pub non_confirmable: bool,
65 #[structopt(long, default_value = "3")]
66 pub retries: usize,
68 #[structopt(long, parse(try_from_str = humantime::parse_duration), default_value = "2s")]
69 pub timeout: Duration,
71 #[structopt(long, parse(try_from_str = humantime::parse_duration), default_value = "500ms")]
72 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#[derive(Clone, PartialEq, Debug, Display, EnumString, EnumVariantNames)]
89pub enum Transport {
90 #[strum(serialize = "udp", serialize = "coap")]
92 Udp,
93 #[strum(serialize = "dtls", serialize = "coaps")]
95 Dtls,
96 Tcp,
98 Tls,
100}
101
102#[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#[derive(Clone, PartialEq, Debug)]
118pub struct HostOptions {
119 pub scheme: Transport,
121 pub host: String,
123 pub port: u16,
125 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 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 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 fn try_from(url: &str) -> Result<HostOptions, Self::Error> {
178 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 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 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
223pub struct Client<E, T: Backend<E>> {
225 transport: T,
226 _e: PhantomData<E>,
227}
228
229#[cfg(feature = "backend-tokio")]
231pub type TokioClient = Client<std::io::Error, backend::Tokio>;
232
233#[cfg(feature = "backend-tokio")]
234impl TokioClient {
235 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 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 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 Ok(Self {
267 transport,
268 _e: PhantomData,
269 })
270 }
271
272 pub async fn close(self) -> Result<(), std::io::Error> {
274 self.transport.close().await
275 }
276}
277
278unsafe 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 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 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 let resp = self
317 .transport
318 .request(request.message, opts.clone())
319 .await
320 .map_err(Error::Transport)?;
321
322 Ok(resp)
325 }
326
327 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 pub async fn unobserve(&mut self, o: <T as Backend<E>>::Observe) -> Result<(), E> {
340 self.transport.unobserve(o).await
341 }
342
343 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 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 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}