1
// Copyright 2020, Collabora Ltd.
2
// SPDX-License-Identifier: MIT OR Apache-2.0
3

            
4
use std::convert::TryInto;
5

            
6
use reqwest::Identity;
7
use std::fs::File;
8
use std::io::Read;
9
use std::time::Duration;
10
use thiserror::Error;
11
use url::Url;
12

            
13
use crate::ddi::poll;
14

            
15
/// [Direct Device Integration](https://www.eclipse.org/hawkbit/apis/ddi_api/) client.
16
#[derive(Debug, Clone)]
17
pub struct Client {
18
    base_url: Url,
19
    client: reqwest::Client,
20
}
21

            
22
/// The method of Authorization for the client and the secret authentification token.
23
#[derive(Debug, Clone)]
24
pub enum ClientAuthorization {
25
    /// use a target token that is unique per target
26
    TargetToken(String),
27
    /// use a common gateway token for all targets
28
    GatewayToken(String),
29
    /// do not send an authorization header
30
    None,
31
}
32

            
33
/// DDI errors
34
#[non_exhaustive]
35
#[derive(Error, Debug)]
36
pub enum Error {
37
    /// URL error
38
    #[error("Could not parse url")]
39
    ParseUrlError(#[from] url::ParseError),
40
    /// Token error
41
    #[error("Invalid token format")]
42
    InvalidToken(#[from] reqwest::header::InvalidHeaderValue),
43
    /// HTTP error
44
    #[error(transparent)]
45
    ReqwestError(#[from] reqwest::Error),
46
    /// Error parsing sleep field from server
47
    #[error("Failed to parse polling sleep")]
48
    InvalidSleep,
49
    /// IO error
50
    #[error("IO error {0}")]
51
    Io(#[from] std::io::Error),
52
    /// Invalid checksum
53
    #[cfg(feature = "hash-digest")]
54
    #[error("Invalid Checksum")]
55
    ChecksumError(crate::ddi::deployment_base::ChecksumType),
56
}
57

            
58
impl Client {
59
    /// Create a new DDI client.
60
    ///
61
    /// # Arguments
62
    /// * `url`: the URL of the hawkBit server, such as `http://my-server.com:8080`
63
    /// * `tenant`: the server tenant
64
    /// * `controller_id`: the id of the controller
65
    /// * `authorization`: the authorization method and secret authentification token of the controller
66
88
    pub fn new(
67
88
        url: &str,
68
88
        tenant: &str,
69
88
        controller_id: &str,
70
88
        authorization: ClientAuthorization,
71
88
        server_cert: Option<&str>,
72
88
        client_cert: Option<&str>,
73
88
        timeout: Option<Duration>,
74
88
    ) -> Result<Self, Error> {
75
88
        let host: Url = url.parse()?;
76
88
        let path = format!("{}/controller/v1/{}", tenant, controller_id);
77
88
        let base_url = host.join(&path)?;
78

            
79
88
        let mut client_builder = reqwest::Client::builder();
80

            
81
88
        let mut headers = reqwest::header::HeaderMap::new();
82
88
        match authorization {
83
64
            ClientAuthorization::TargetToken(key_token) => {
84
64
                headers.insert(
85
64
                    reqwest::header::AUTHORIZATION,
86
64
                    format!("TargetToken {}", &key_token).try_into()?,
87
                );
88
            }
89
12
            ClientAuthorization::GatewayToken(key_token) => {
90
12
                headers.insert(
91
12
                    reqwest::header::AUTHORIZATION,
92
12
                    format!("GatewayToken {}", &key_token).try_into()?,
93
                );
94
            }
95
12
            ClientAuthorization::None => {
96
12
                // no authorization header needed
97
12
            }
98
        }
99

            
100
88
        if let Some(cert_file) = client_cert {
101
            let mut buf = Vec::new();
102
            File::open(cert_file)?.read_to_end(&mut buf)?;
103
            let identity = Identity::from_pem(&buf)?;
104
            client_builder = client_builder.identity(identity);
105
88
        }
106

            
107
        // Set the server certificate if provided
108
88
        if let Some(cert_file) = server_cert {
109
4
            let mut buf = Vec::new();
110
4
            File::open(cert_file)?.read_to_end(&mut buf)?;
111

            
112
4
            if cert_file.ends_with(".crt") {
113
                let certs = reqwest::Certificate::from_pem_bundle(&buf)?;
114
                for cert in certs {
115
                    client_builder = client_builder.add_root_certificate(cert);
116
                }
117
            } else {
118
4
                let cert = reqwest::Certificate::from_pem(&buf)?;
119
4
                client_builder = client_builder.add_root_certificate(cert);
120
            }
121

            
122
4
            client_builder = client_builder
123
4
                .tls_built_in_root_certs(false)
124
4
                .https_only(true);
125
84
        }
126

            
127
        // Add timeouts to all connections
128
88
        if let Some(timeout) = timeout {
129
            client_builder = client_builder
130
                .connect_timeout(timeout)
131
                .read_timeout(timeout);
132
88
        }
133

            
134
88
        let client = client_builder
135
88
            .default_headers(headers)
136
88
            .connection_verbose(true)
137
88
            .build()?;
138
88
        Ok(Self { base_url, client })
139
88
    }
140

            
141
    /// Poll the server for updates
142
115
    pub async fn poll(&self) -> Result<poll::Reply, Error> {
143
46
        let reply = self.client.get(self.base_url.clone()).send().await?;
144
44
        reply.error_for_status_ref()?;
145

            
146
36
        let reply = reply.json::<poll::ReplyInternal>().await?;
147
36
        Ok(poll::Reply::new(reply, self.client.clone()))
148
46
    }
149
}