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
12
    pub fn new(opts: ClientOptions) -> Self {
109
12
        Client {
110
12
            client: ReqwestClient::new(),
111
12
            auth_base: opts.auth_base,
112
12
            coremgr_base: opts.coremgr_base,
113
12
            client_id: opts.client_id,
114
12
            client_secret: opts.client_secret,
115
12
            access_token: Arc::new(Mutex::new(None)),
116
12
        }
117
12
    }
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
13
    pub async fn request(
125
13
        &mut self,
126
13
        method: Method,
127
13
        api_path: &str,
128
13
        body: Option<Bytes>,
129
13
    ) -> Result<(StatusCode, Bytes), Error> {
130
13
        let url = format!("{}{}", self.coremgr_base, api_path);
131
13
        let mut retry = 1;
132
        loop {
133
            let token;
134
14
            {
135
14
                let mutex = self.access_token.lock().unwrap();
136
14
                token = (*mutex).clone();
137
14
            }
138
14
            let token = match token {
139
12
                None => self.auth_token().await?,
140
2
                Some(token) => token,
141
            };
142
10
            let mut builder = self.client.request(method.clone(), url.as_str());
143
10
            builder = builder.bearer_auth(token);
144
10
            if let Some(body) = body.as_ref() {
145
3
                builder = builder.header(header::CONTENT_TYPE, "application/json");
146
3
                builder = builder.body(body.clone());
147
7
            }
148
10
            let req = match builder.build() {
149
1
                Err(e) => return Err(Error::Std(Box::new(e))),
150
9
                Ok(req) => req,
151
            };
152
9
            let resp = match self.client.execute(req).await {
153
1
                Err(e) => return Err(Error::Std(Box::new(e))),
154
8
                Ok(resp) => resp,
155
8
            };
156
8
            let status = resp.status();
157
8
            let body = match resp.bytes().await {
158
                Err(e) => return Err(Error::Std(Box::new(e))),
159
8
                Ok(body) => body,
160
8
            };
161
8
            if status != StatusCode::UNAUTHORIZED || retry <= 0 {
162
7
                return Ok((status, body));
163
1
            }
164
1
            retry -= 1;
165
1
            {
166
1
                let mut mutex = self.access_token.lock().unwrap();
167
1
                *mutex = None;
168
1
            }
169
        }
170
13
    }
171

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

            
205
8
        Ok(tokens.access_token)
206
12
    }
207
}