use std::convert::{TryFrom, TryInto};
use std::str::FromStr;
use std::time::Duration;
use log::{debug, error};
use structopt::StructOpt;
use strum_macros::{Display, EnumString, EnumVariantNames};
pub use coap_lite::RequestType as Method;
use coap_lite::{CoapRequest, MessageType, Packet};
pub mod backend;
pub use backend::Backend;
pub const COAP_MTU: usize = 1600;
#[derive(Debug, Clone, PartialEq, StructOpt)]
pub struct ClientOptions {
#[structopt(long, parse(try_from_str = humantime::parse_duration), default_value = "500ms")]
pub connect_timeout: Duration,
#[structopt(long)]
pub tls_ca: Option<String>,
#[structopt(long)]
pub tls_cert: Option<String>,
#[structopt(long)]
pub tls_key: Option<String>,
#[structopt(long)]
pub tls_skip_verify: bool,
}
impl Default for ClientOptions {
fn default() -> Self {
Self {
connect_timeout: Duration::from_secs(2),
tls_ca: None,
tls_cert: None,
tls_key: None,
tls_skip_verify: false,
}
}
}
#[derive(Debug, Clone, PartialEq, StructOpt)]
pub struct RequestOptions {
#[structopt(long)]
non_confirmable: bool,
#[structopt(long, default_value = "3")]
retries: usize,
#[structopt(long, parse(try_from_str = humantime::parse_duration), default_value = "2s")]
timeout: Duration,
#[structopt(long, parse(try_from_str = humantime::parse_duration), default_value = "500ms")]
backoff: Duration,
}
impl Default for RequestOptions {
fn default() -> Self {
Self {
non_confirmable: false,
retries: 3,
timeout: Duration::from_secs(2),
backoff: Duration::from_millis(500),
}
}
}
#[derive(Clone, PartialEq, Debug, Display, EnumString, EnumVariantNames)]
pub enum Transport {
#[strum(serialize = "udp", serialize = "coap")]
Udp,
#[strum(serialize = "dtls", serialize = "coaps")]
Dtls,
Tcp,
Tls,
}
#[derive(Debug, thiserror::Error)]
pub enum Error<T: std::fmt::Debug> {
#[error("Transport / Backend error: {:?}", 0)]
Transport(T),
#[error("Invalid host specification")]
InvalidHost,
#[error("Invalid URL")]
InvalidUrl,
#[error("Invalid Scheme")]
InvalidScheme,
}
#[derive(Clone, PartialEq, Debug)]
pub struct HostOptions {
pub scheme: Transport,
pub host: String,
pub port: u16,
pub resource: String,
}
impl Default for HostOptions {
fn default() -> Self {
Self {
scheme: Transport::Udp,
host: "localhost".to_string(),
port: 5683,
resource: "".to_string(),
}
}
}
impl ToString for HostOptions {
fn to_string(&self) -> String {
format!("{}://{}:{}", self.scheme, self.port, self.host)
}
}
impl TryFrom<(&str, u16)> for HostOptions {
type Error = std::io::Error;
fn try_from(v: (&str, u16)) -> Result<HostOptions, Self::Error> {
Ok(Self {
host: v.0.to_string(),
port: v.1,
..Default::default()
})
}
}
impl TryFrom<(Transport, &str, u16)> for HostOptions {
type Error = std::io::Error;
fn try_from(v: (Transport, &str, u16)) -> Result<HostOptions, Self::Error> {
Ok(Self {
scheme: v.0,
host: v.1.to_string(),
port: v.2,
..Default::default()
})
}
}
impl TryFrom<&str> for HostOptions {
type Error = std::io::Error;
fn try_from(url: &str) -> Result<HostOptions, Self::Error> {
let params = match url::Url::from_str(url) {
Ok(v) => v,
Err(e) => {
error!("Error parsing URL: {:?}", e);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Invalid Url",
));
}
};
let s = params.scheme();
let scheme = match (s, Transport::from_str(s)) {
("", _) => Transport::Udp,
(_, Ok(v)) => v,
(_, Err(_e)) => {
error!("Unrecognized or unsupported scheme: {}", params.scheme());
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Invalid Scheme",
));
}
};
let p = params.port();
let port = match (p, &scheme) {
(Some(p), _) => p,
(None, Transport::Udp) => 5683,
(None, Transport::Dtls) => 5684,
(None, Transport::Tcp) => 5683,
(None, Transport::Tls) => 5684,
};
Ok(HostOptions {
scheme,
host: params.host_str().unwrap_or("localhost").to_string(),
port,
resource: params.path().to_string(),
})
}
}
pub struct Client<T: Backend> {
message_id: u16,
transport: T,
}
#[cfg(feature = "backend-tokio")]
pub type TokioClient = Client<backend::Tokio>;
#[cfg(feature = "backend-tokio")]
impl TokioClient {
pub async fn connect<H>(host: H, opts: &ClientOptions) -> Result<Self, std::io::Error>
where
H: TryInto<HostOptions>,
<H as TryInto<HostOptions>>::Error: std::fmt::Debug,
{
let peer: HostOptions = match host.try_into() {
Ok(v) => v,
Err(e) => {
error!("Error parsing host options: {:?}", e);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Invalid host options",
));
}
};
let connect_str = format!("{}:{}", peer.host, peer.port);
debug!("Using host options: {:?} (connect: {})", peer, connect_str);
let transport = match &peer.scheme {
Transport::Udp => backend::Tokio::new_udp(&connect_str).await?,
Transport::Dtls => backend::Tokio::new_dtls(&connect_str, opts).await?,
_ => {
error!("Transport '{}' not yet implemented", peer.scheme);
unimplemented!()
}
};
Ok(Self {
message_id: rand::random(),
transport,
})
}
pub async fn close(self) -> Result<(), std::io::Error> {
self.transport.close().await
}
}
impl<T, E> Client<T>
where
T: Backend<Error = E>,
E: std::fmt::Debug,
{
pub async fn request(
&mut self,
method: Method,
resource: &str,
data: Option<&[u8]>,
opts: &RequestOptions,
) -> Result<Packet, Error<E>> {
let mut request = CoapRequest::<&str>::new();
request.message.header.message_id = self.message_id;
self.message_id += 1;
request.set_method(method);
request.set_path(resource);
match !opts.non_confirmable {
true => request.message.header.set_type(MessageType::Confirmable),
false => request.message.header.set_type(MessageType::NonConfirmable),
}
if let Some(d) = data {
request.message.payload = d.to_vec();
}
let t = rand::random::<u32>();
let token = t.to_be_bytes().to_vec();
request.message.set_token(token);
let resp = self
.transport
.request(request.message, opts.clone())
.await
.map_err(Error::Transport)?;
Ok(resp)
}
pub async fn get(
&mut self,
resource: &str,
opts: &RequestOptions,
) -> Result<Vec<u8>, Error<E>> {
let resp = self.request(Method::Get, resource, None, opts).await?;
Ok(resp.payload)
}
pub async fn put(
&mut self,
resource: &str,
data: Option<&[u8]>,
opts: &RequestOptions,
) -> Result<Vec<u8>, Error<E>> {
let resp = self.request(Method::Put, resource, data, opts).await?;
Ok(resp.payload)
}
pub async fn post(
&mut self,
resource: &str,
data: Option<&[u8]>,
opts: &RequestOptions,
) -> Result<Vec<u8>, Error<E>> {
let resp = self.request(Method::Post, resource, data, opts).await?;
Ok(resp.payload)
}
}
fn token_as_u32(token: &[u8]) -> u32 {
let mut v = 0;
for i in 0..token.len() {
v |= (token[i] as u32) << (i * 8);
}
v
}