netmaker/
client.rs

1//
2// Copyright (c) 2021 RepliXio Ltd. All rights reserved.
3// Use is subject to license terms.
4//
5
6use std::cell::{Ref, RefCell};
7use std::fmt;
8use std::time;
9
10use serde::{de, ser};
11use serde_json as json;
12
13use crate::api;
14use crate::error::{self, NetMakerFault};
15
16mod network;
17mod node;
18
19/// NetMaker client object. Controls all the interaction with NetMaker server
20#[derive(Clone, Debug)]
21pub struct NetMaker {
22    base: String,
23    username: String,
24    password: String,
25    token: RefCell<Option<String>>,
26    user_agent: String,
27    connect_timeout: time::Duration,
28}
29
30impl NetMaker {
31    /// Creates new client object with specified `url`. Uses `username` and `password`
32    /// authentication issuing authentication token. Expiring token is refreshed automatically
33    /// as needed.
34    pub fn new(url: &str, username: &str, password: &str) -> Self {
35        let base = url.to_string();
36        let username = username.to_string();
37        let password = password.to_string();
38        let token = RefCell::new(None);
39        let user_agent = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
40        let connect_timeout = time::Duration::from_secs(5);
41
42        Self {
43            base,
44            username,
45            password,
46            token,
47            user_agent,
48            connect_timeout,
49        }
50    }
51
52    async fn refresh_token(&self) -> Result<(), error::NetMakerFault> {
53        if self.token_expired() {
54            let token = self
55                .authenticate(&self.username, &self.password)
56                .await
57                .map_err(NetMakerFault::Unauthenticated)?
58                .authtoken;
59            self.token.replace(Some(token));
60        }
61        Ok(())
62    }
63
64    fn token_expired(&self) -> bool {
65        if let Some(token) = self.token().as_deref() {
66            let token: Result<jwt::Token<jwt::Header, jwt::Claims, _>, _> =
67                jwt::Token::parse_unverified(token);
68            if let Ok(token) = token {
69                if let Some(exp) = token.claims().registered.expiration {
70                    if let Ok(duration) = time::SystemTime::now().duration_since(time::UNIX_EPOCH) {
71                        return duration.as_secs() > exp - 30;
72                    }
73                }
74            }
75        }
76        true
77    }
78
79    pub fn token(&self) -> Ref<'_, Option<String>> {
80        self.token.borrow()
81        // Ref::map(token, Option::as_ref)
82    }
83
84    async fn authenticate(
85        &self,
86        username: &str,
87        password: &str,
88    ) -> Result<api::SuccessfulUserLoginResponse, api::ErrorResponse> {
89        let username = username.to_string();
90        let password = password.to_string();
91        let body = api::UserAuthParams { username, password };
92        let text = self.post("/api/users/adm/authenticate", body).await?;
93        let response =
94            json::from_str::<api::SuccessResponse<api::SuccessfulUserLoginResponse>>(&text)?;
95        Ok(response.response)
96    }
97
98    // fn inspect<T>(&self, output: &Output<T>)
99    // where
100    //     T: fmt::Debug,
101    // {
102    //     log::debug!("Output {:?}", output);
103    // }
104
105    pub fn url(&self, path: impl fmt::Display) -> String {
106        format!("{}{}", self.base, path)
107    }
108
109    async fn delete<P>(&self, path: P) -> Result<String, api::ErrorResponse>
110    where
111        P: fmt::Display,
112    {
113        let url = self.url(path);
114        self.client()
115            .await?
116            .delete(url)
117            .optionally_bearer_auth(self.token().as_deref())
118            .inspect()
119            .send()
120            // .retry()
121            .await?
122            .error_for_status2()
123            .await
124        // .inspect(|output| self.inspect(output))
125    }
126
127    async fn get<P>(&self, path: P) -> Result<String, api::ErrorResponse>
128    where
129        P: fmt::Display,
130    {
131        let url = self.url(path);
132        self.client()
133            .await?
134            .get(url)
135            .optionally_bearer_auth(self.token().as_deref())
136            .inspect()
137            .send()
138            // .retry()
139            .await?
140            .error_for_status2()
141            .await
142        // .inspect(|output| self.inspect(output))
143    }
144
145    async fn _head<P>(&self, path: P) -> Result<(), api::ErrorResponse>
146    where
147        P: fmt::Display,
148    {
149        let url = self.url(path);
150        self.client()
151            .await?
152            .head(url)
153            .optionally_bearer_auth(self.token().as_deref())
154            .inspect()
155            .send()
156            // .retry()
157            .await?
158            .error_for_status2()
159            .await
160            .map(|_| ())
161        // .inspect(|output| self.inspect(output))
162    }
163
164    async fn post<P, B, T>(&self, path: P, body: B) -> Result<String, api::ErrorResponse>
165    where
166        P: fmt::Display,
167        B: Into<Option<T>>,
168        T: ser::Serialize,
169    {
170        let body = body.into();
171        let url = self.url(path);
172        self.client()
173            .await?
174            .post(url)
175            .optionally_bearer_auth(self.token().as_deref())
176            .inspect()
177            .optionally_json(body.as_ref())
178            .send()
179            .await?
180            .error_for_status2()
181            .await
182        // .inspect(|output| self.inspect(output))
183    }
184
185    async fn _put<P, T>(&self, path: P) -> Result<T, api::ErrorResponse>
186    where
187        P: fmt::Display,
188        T: de::DeserializeOwned + ser::Serialize + fmt::Debug,
189    {
190        let url = self.url(path);
191        self.client()
192            .await?
193            .put(url)
194            .optionally_bearer_auth(self.token().as_deref())
195            .inspect()
196            .send()
197            // .retry()
198            .await?
199            .error_for_status3()
200            .await
201        // .inspect(|output| self.inspect(output))
202    }
203
204    async fn client(&self) -> reqwest::Result<reqwest::Client> {
205        reqwest::Client::builder()
206            .user_agent(&self.user_agent)
207            .connect_timeout(self.connect_timeout)
208            .build()
209    }
210}
211
212#[async_trait::async_trait]
213trait ResponseExt: Sized {
214    async fn status_and_bytes(self) -> reqwest::Result<(reqwest::StatusCode, bytes::Bytes)>;
215
216    async fn status_and_text(self) -> reqwest::Result<(reqwest::StatusCode, String)>;
217
218    async fn error_for_status2(self) -> Result<String, api::ErrorResponse> {
219        let (status, text) = self.status_and_text().await?;
220
221        if status.is_success() {
222            Ok(text)
223        } else {
224            Err(api::ErrorResponse::from_split_response(status, text))
225        }
226    }
227
228    async fn error_for_status3<T>(self) -> Result<T, api::ErrorResponse>
229    where
230        T: de::DeserializeOwned,
231    {
232        let (code, text) = self.status_and_text().await?;
233
234        println!("ES3:\n{}", text);
235        if let Ok(success) = json::from_str(&text) {
236            return Ok(success);
237        }
238
239        if let Ok(error) = json::from_str::<api::ErrorResponse>(&text) {
240            return Err(error);
241        }
242
243        Err(api::ErrorResponse::from_split_response(code, text))
244    }
245}
246
247#[async_trait::async_trait]
248impl ResponseExt for reqwest::Response {
249    async fn status_and_bytes(self) -> reqwest::Result<(reqwest::StatusCode, bytes::Bytes)> {
250        let status = self.status();
251        let bytes = self.bytes().await?;
252        Ok((status, bytes))
253    }
254
255    async fn status_and_text(self) -> reqwest::Result<(reqwest::StatusCode, String)> {
256        let status = self.status();
257        let text = self.text().await?;
258        Ok((status, text))
259    }
260}
261
262trait Inspector {
263    fn inspect(self) -> Self;
264}
265
266impl Inspector for reqwest::RequestBuilder {
267    fn inspect(self) -> Self {
268        if let Some(request) = self.try_clone().and_then(|builder| builder.build().ok()) {
269            log::trace!("{} {}", request.method(), request.url());
270
271            request.headers().iter().for_each(|(header, value)| {
272                if header == reqwest::header::AUTHORIZATION {
273                    log::trace!("{}: {}", header, "[REDACTED]");
274                } else {
275                    log::trace!("{}: {}", header, String::from_utf8_lossy(value.as_bytes()))
276                }
277            });
278        }
279
280        self
281    }
282}
283
284trait Optionally {
285    fn optionally_bearer_auth(self, token: Option<&str>) -> Self;
286    fn optionally_json<T>(self, body: Option<&T>) -> Self
287    where
288        T: ser::Serialize;
289
290    fn optionally<T, F>(self, option: Option<T>, f: F) -> Self
291    where
292        F: FnOnce(Self, T) -> Self,
293        Self: Sized,
294    {
295        if let Some(option) = option {
296            f(self, option)
297        } else {
298            self
299        }
300    }
301}
302
303impl Optionally for reqwest::RequestBuilder {
304    fn optionally_bearer_auth(self, token: Option<&str>) -> Self {
305        self.optionally(token, Self::bearer_auth)
306    }
307
308    fn optionally_json<T>(self, body: Option<&T>) -> Self
309    where
310        T: ser::Serialize,
311    {
312        self.optionally(body, Self::json)
313    }
314}