Lines
82.54 %
Functions
37.5 %
Branches
100 %
// Copyright 2020, Collabora Ltd.
// SPDX-License-Identifier: MIT OR Apache-2.0
use std::convert::TryInto;
use reqwest::Identity;
use std::fs::File;
use std::io::Read;
use std::time::Duration;
use thiserror::Error;
use url::Url;
use crate::ddi::poll;
/// [Direct Device Integration](https://www.eclipse.org/hawkbit/apis/ddi_api/) client.
#[derive(Debug, Clone)]
pub struct Client {
base_url: Url,
client: reqwest::Client,
}
/// The method of Authorization for the client and the secret authentification token.
pub enum ClientAuthorization {
/// use a target token that is unique per target
TargetToken(String),
/// use a common gateway token for all targets
GatewayToken(String),
/// do not send an authorization header
None,
/// DDI errors
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum Error {
/// URL error
#[error("Could not parse url")]
ParseUrlError(#[from] url::ParseError),
/// Token error
#[error("Invalid token format")]
InvalidToken(#[from] reqwest::header::InvalidHeaderValue),
/// HTTP error
#[error(transparent)]
ReqwestError(#[from] reqwest::Error),
/// Error parsing sleep field from server
#[error("Failed to parse polling sleep")]
InvalidSleep,
/// IO error
#[error("IO error {0}")]
Io(#[from] std::io::Error),
/// Invalid checksum
#[cfg(feature = "hash-digest")]
#[error("Invalid Checksum")]
ChecksumError(crate::ddi::deployment_base::ChecksumType),
impl Client {
/// Create a new DDI client.
///
/// # Arguments
/// * `url`: the URL of the hawkBit server, such as `http://my-server.com:8080`
/// * `tenant`: the server tenant
/// * `controller_id`: the id of the controller
/// * `authorization`: the authorization method and secret authentification token of the controller
pub fn new(
url: &str,
tenant: &str,
controller_id: &str,
authorization: ClientAuthorization,
server_cert: Option<&str>,
client_cert: Option<&str>,
timeout: Option<Duration>,
) -> Result<Self, Error> {
let host: Url = url.parse()?;
let path = format!("{}/controller/v1/{}", tenant, controller_id);
let base_url = host.join(&path)?;
let mut client_builder = reqwest::Client::builder();
let mut headers = reqwest::header::HeaderMap::new();
match authorization {
ClientAuthorization::TargetToken(key_token) => {
headers.insert(
reqwest::header::AUTHORIZATION,
format!("TargetToken {}", &key_token).try_into()?,
);
ClientAuthorization::GatewayToken(key_token) => {
format!("GatewayToken {}", &key_token).try_into()?,
ClientAuthorization::None => {
// no authorization header needed
if let Some(cert_file) = client_cert {
let mut buf = Vec::new();
File::open(cert_file)?.read_to_end(&mut buf)?;
let identity = Identity::from_pem(&buf)?;
client_builder = client_builder.identity(identity);
// Set the server certificate if provided
if let Some(cert_file) = server_cert {
if cert_file.ends_with(".crt") {
let certs = reqwest::Certificate::from_pem_bundle(&buf)?;
for cert in certs {
client_builder = client_builder.add_root_certificate(cert);
} else {
let cert = reqwest::Certificate::from_pem(&buf)?;
client_builder = client_builder
.tls_built_in_root_certs(false)
.https_only(true);
// Add timeouts to all connections
if let Some(timeout) = timeout {
.connect_timeout(timeout)
.read_timeout(timeout);
let client = client_builder
.default_headers(headers)
.connection_verbose(true)
.build()?;
Ok(Self { base_url, client })
/// Poll the server for updates
pub async fn poll(&self) -> Result<poll::Reply, Error> {
let reply = self.client.get(self.base_url.clone()).send().await?;
reply.error_for_status_ref()?;
let reply = reply.json::<poll::ReplyInternal>().await?;
Ok(poll::Reply::new(reply, self.client.clone()))