1use 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#[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 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 }
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 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 .await?
122 .error_for_status2()
123 .await
124 }
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 .await?
140 .error_for_status2()
141 .await
142 }
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 .await?
158 .error_for_status2()
159 .await
160 .map(|_| ())
161 }
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 }
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 .await?
199 .error_for_status3()
200 .await
201 }
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}