1
//! A wrapped HTTP client that is used for Sylvia-IoT **coremgr** APIs with the following features:
2
//! - Use `client_credentials` grant type to get access token.
3
//!     - It is **REQUIRED** to register **private** clients (with secret).
4
//!     - It is **RECOMMENDED** to use the **service** role for clients, not to use **admin**,
5
//!       **manager** or **user** roles.
6
//! - Refresh token automatically to integrate network servers and application servers (or adapters)
7
//!   conviniently because they do not need to do multiple operations for one API request.
8
//!
9
//! Here is an example to create a client to access an API:
10
//!
11
//! ```rust
12
//! use reqwest::Method;
13
//! use sylvia_iot_sdk::api::http::{Client, ClientOptions};
14
//!
15
//! async fn main() {
16
//!     let opts = ClientOptions {
17
//!         auth_base: "http://localhost:1080/auth".to_string(),
18
//!         coremgr_base: "http://localhost:1080/coremgr".to_string(),
19
//!         client_id: "ADAPTER_CLIENT_ID".to_string(),
20
//!         client_secret: "ADAPTER_CLIENT_SECRET".to_string(),
21
//!     };
22
//!     let mut client = Client::new(opts);
23
//!     let url = "/api/v1/user";
24
//!     match client.request(Method::GET, url, None).await {
25
//!         Err(e) => {
26
//!             // Handle error.
27
//!             // Native and OAuth2 errors must be handled in this arm.
28
//!         },
29
//!         Ok((status_code, body)) => {
30
//!             // Handle response.
31
//!             // All status code except 401 must be handled in this arm.
32
//!         },
33
//!     }
34
//! }
35
//! ```
36
use std::{
37
    error::Error as StdError,
38
    sync::{Arc, Mutex},
39
};
40

            
41
use bytes::Bytes;
42
use reqwest::{header, Client as ReqwestClient, Method, StatusCode};
43
use serde::Deserialize;
44

            
45
/// The HTTP client to request Sylvia-IoT APIs. With this client, you do not need to handle 401
46
/// refresh token flow.
47
#[derive(Clone)]
48
pub struct Client {
49
    /// The underlying HTTP client instance.
50
    client: ReqwestClient,
51
    /// `sylvia-iot-auth` base path.
52
    auth_base: String,
53
    /// `sylvia-iot-coremgr` base path.
54
    coremgr_base: String,
55
    /// Client ID.
56
    client_id: String,
57
    /// Client secret.
58
    client_secret: String,
59
    /// The access token.
60
    access_token: Arc<Mutex<Option<String>>>,
61
}
62

            
63
/// Options of the HTTP client [`Client`] that contains OAuth2 information.
64
pub struct ClientOptions {
65
    /// `sylvia-iot-auth` base path with scheme. For example `http://localhost:1080/auth`
66
    pub auth_base: String,
67
    /// `sylvia-iot-coremgr` base path with scheme. For example `http://localhost:1080/coremgr`
68
    pub coremgr_base: String,
69
    /// Client ID.
70
    pub client_id: String,
71
    /// Client secret.
72
    pub client_secret: String,
73
}
74

            
75
#[derive(Debug)]
76
pub enum Error {
77
    Std(Box<dyn StdError>),
78
    Oauth2(Oauth2Error),
79
    Sylvia(ApiError),
80
}
81

            
82
/// The OAuth2 error response.
83
#[derive(Debug, Deserialize)]
84
pub struct Oauth2Error {
85
    /// Error code.
86
    pub error: String,
87
    /// Detail message.
88
    pub error_message: Option<String>,
89
}
90

            
91
/// The Sylvia-IoT API error response.
92
#[derive(Debug, Deserialize)]
93
pub struct ApiError {
94
    /// Error code.
95
    pub code: String,
96
    /// Detail message.
97
    pub message: Option<String>,
98
}
99

            
100
/// Response from OAuth2 token API.
101
#[derive(Deserialize)]
102
struct Oauth2TokenRes {
103
    access_token: String,
104
}
105

            
106
impl Client {
107
    /// Create an instance.
108
24
    pub fn new(opts: ClientOptions) -> Self {
109
24
        Client {
110
24
            client: ReqwestClient::new(),
111
24
            auth_base: opts.auth_base,
112
24
            coremgr_base: opts.coremgr_base,
113
24
            client_id: opts.client_id,
114
24
            client_secret: opts.client_secret,
115
24
            access_token: Arc::new(Mutex::new(None)),
116
24
        }
117
24
    }
118

            
119
    /// Execute a Sylvia-IoT API request.
120
    /// - `api_path` is the relative path (of the coremgr base) the API with query string.
121
    ///   For example: `/api/v1/user/list?contains=word`, the client will do a request with
122
    ///   `http://coremgr-host/coremgr/api/v1/user/list?contains=word` URL.
123
    /// - `body` **MUST** be JSON format.
124
26
    pub async fn request(
125
26
        &mut self,
126
26
        method: Method,
127
26
        api_path: &str,
128
26
        body: Option<Bytes>,
129
26
    ) -> Result<(StatusCode, Bytes), Error> {
130
26
        let url = format!("{}{}", self.coremgr_base, api_path);
131
26
        let mut retry = 1;
132
        loop {
133
            let token;
134
28
            {
135
28
                let mutex = self.access_token.lock().unwrap();
136
28
                token = (*mutex).clone();
137
28
            }
138
28
            let token = match token {
139
24
                None => self.auth_token().await?,
140
4
                Some(token) => token,
141
            };
142
20
            let mut builder = self.client.request(method.clone(), url.as_str());
143
20
            builder = builder.bearer_auth(token);
144
20
            if let Some(body) = body.as_ref() {
145
6
                builder = builder.header(header::CONTENT_TYPE, "application/json");
146
6
                builder = builder.body(body.clone());
147
14
            }
148
20
            let req = match builder.build() {
149
2
                Err(e) => return Err(Error::Std(Box::new(e))),
150
18
                Ok(req) => req,
151
            };
152
18
            let resp = match self.client.execute(req).await {
153
2
                Err(e) => return Err(Error::Std(Box::new(e))),
154
16
                Ok(resp) => resp,
155
16
            };
156
16
            let status = resp.status();
157
16
            let body = match resp.bytes().await {
158
                Err(e) => return Err(Error::Std(Box::new(e))),
159
16
                Ok(body) => body,
160
16
            };
161
16
            if status != StatusCode::UNAUTHORIZED || retry <= 0 {
162
14
                return Ok((status, body));
163
2
            }
164
2
            retry -= 1;
165
2
            {
166
2
                let mut mutex = self.access_token.lock().unwrap();
167
2
                *mutex = None;
168
2
            }
169
        }
170
26
    }
171

            
172
    /// To authorize the client and get access token/refresh token.
173
24
    async fn auth_token(&mut self) -> Result<String, Error> {
174
24
        let url = format!("{}/oauth2/token", self.auth_base.as_str());
175
24
        let body = [("grant_type", "client_credentials")];
176
24
        let req = match self
177
24
            .client
178
24
            .request(Method::POST, url)
179
24
            .basic_auth(self.client_id.as_str(), Some(self.client_secret.as_str()))
180
24
            .form(&body)
181
24
            .build()
182
        {
183
2
            Err(e) => return Err(Error::Std(Box::new(e))),
184
22
            Ok(req) => req,
185
        };
186
22
        let resp = match self.client.execute(req).await {
187
2
            Err(e) => return Err(Error::Std(Box::new(e))),
188
20
            Ok(resp) => resp,
189
20
        };
190
20
        if resp.status() != StatusCode::OK {
191
4
            match resp.json::<Oauth2Error>().await {
192
                Err(e) => return Err(Error::Std(Box::new(e))),
193
4
                Ok(body) => return Err(Error::Oauth2(body)),
194
            }
195
16
        }
196
16
        let tokens = match resp.json::<Oauth2TokenRes>().await {
197
            Err(e) => return Err(Error::Std(Box::new(e))),
198
16
            Ok(tokens) => tokens,
199
16
        };
200
16
        {
201
16
            let mut mutex = self.access_token.lock().unwrap();
202
16
            *mutex = Some(tokens.access_token.clone());
203
16
        }
204
16

            
205
16
        Ok(tokens.access_token)
206
24
    }
207
}