huawei_dongle_api/
client.rs1use crate::{
26 api,
27 config::Config,
28 error::{Error, Result},
29 models::common::check_for_api_error,
30 retry::RetryStrategy,
31 session::SessionManager,
32};
33use reqwest::{Client as HttpClient, ClientBuilder, Response};
34use tracing::{debug, trace};
35use url::Url;
36
37#[derive(Debug)]
65pub struct Client {
66 http_client: HttpClient,
67 config: Config,
68 session: SessionManager,
69 retry_strategy: RetryStrategy,
70}
71
72impl Client {
73 pub fn new(config: Config) -> Result<Self> {
93 let http_client = ClientBuilder::new()
94 .cookie_store(true)
95 .timeout(config.timeout)
96 .user_agent(&config.user_agent)
97 .build()?;
98
99 let session = SessionManager::new(http_client.clone(), config.base_url.clone());
100
101 let retry_strategy = RetryStrategy {
102 max_attempts: config.max_retries,
103 initial_delay: config.retry_delay,
104 max_delay: config.max_retry_delay,
105 ..Default::default()
106 };
107
108 Ok(Self {
109 http_client,
110 config,
111 session,
112 retry_strategy,
113 })
114 }
115
116 pub fn with_default_config() -> Result<Self> {
118 Self::new(Config::default())
119 }
120
121 pub fn for_url<S: AsRef<str>>(url: S) -> Result<Self> {
123 let config = Config::for_url(url)?;
124 Self::new(config)
125 }
126
127 pub fn device(&self) -> api::device::DeviceApi {
128 api::device::DeviceApi::new(self)
129 }
130
131 pub fn monitoring(&self) -> api::monitoring::MonitoringApi {
132 api::monitoring::MonitoringApi::new(self)
133 }
134
135 pub fn network(&self) -> api::network::NetworkApi {
136 api::network::NetworkApi::new(self)
137 }
138
139 pub fn sms(&self) -> api::sms::SmsApi {
140 api::sms::SmsApi::new(self)
141 }
142
143 pub fn dhcp(&self) -> api::dhcp::DhcpApi {
144 api::dhcp::DhcpApi::new(self)
145 }
146
147 pub fn auth(&self) -> api::auth::AuthApi {
148 api::auth::AuthApi::new(self)
149 }
150
151 pub(crate) fn session(&self) -> &SessionManager {
152 &self.session
153 }
154
155 pub(crate) async fn get(&self, path: &str) -> Result<Response> {
156 let url = self.build_url(path)?;
157 trace!("GET {}", url);
158
159 self.retry_strategy
160 .execute(|| async {
161 let response = self.http_client.get(url.clone()).send().await?;
162 self.check_response_status(&response).await?;
163 self.session
164 .update_token_from_headers(response.headers())
165 .await;
166 Ok(response)
167 })
168 .await
169 }
170
171 pub(crate) async fn get_authenticated(&self, path: &str) -> Result<Response> {
172 let url = self.build_url(path)?;
173 trace!("GET {} (authenticated)", url);
174
175 let result = self.get_authenticated_internal(&url).await;
176 match &result {
177 Err(Error::CsrfTokenInvalid) | Err(Error::SessionTokenInvalid) => {
178 debug!("CSRF/Session error detected, refreshing token and retrying");
179 self.session.refresh_csrf_token().await?;
180 self.get_authenticated_internal(&url).await
181 }
182 _ => result,
183 }
184 }
185
186 async fn get_authenticated_internal(&self, url: &Url) -> Result<Response> {
188 self.retry_strategy
189 .execute(|| async {
190 let csrf_token = self.session.get_csrf_token().await?;
191
192 let response = self
193 .http_client
194 .get(url.clone())
195 .header("X-Requested-With", "XMLHttpRequest")
196 .header("__RequestVerificationToken", &csrf_token)
197 .send()
198 .await?;
199
200 self.check_response_status(&response).await?;
201 self.session
202 .update_token_from_headers(response.headers())
203 .await;
204 Ok(response)
205 })
206 .await
207 }
208
209 pub(crate) async fn post_xml(&self, path: &str, xml_body: &str) -> Result<Response> {
210 let url = self.build_url(path)?;
211 trace!("POST {} with XML body", url);
212
213 let result = self.post_xml_internal(&url, xml_body).await;
214 match &result {
215 Err(Error::CsrfTokenInvalid) | Err(Error::SessionTokenInvalid) => {
216 debug!("CSRF/Session error detected, refreshing token and retrying");
217 self.session.refresh_csrf_token().await?;
218 self.post_xml_internal(&url, xml_body).await
219 }
220 _ => result,
221 }
222 }
223
224 async fn post_xml_internal(&self, url: &Url, xml_body: &str) -> Result<Response> {
226 self.retry_strategy
227 .execute(|| async {
228 let csrf_token = self.session.get_csrf_token().await?;
229
230 let response = self
231 .http_client
232 .post(url.clone())
233 .header(
234 "Content-Type",
235 "application/x-www-form-urlencoded; charset=UTF-8",
236 )
237 .header("X-Requested-With", "XMLHttpRequest")
238 .header("__RequestVerificationToken", &csrf_token)
239 .body(xml_body.to_string())
240 .send()
241 .await?;
242
243 self.check_response_status(&response).await?;
244 self.session
245 .update_token_from_headers(response.headers())
246 .await;
247 Ok(response)
248 })
249 .await
250 }
251
252 fn build_url(&self, path: &str) -> Result<Url> {
253 let path = if path.starts_with('/') {
254 path.to_string()
255 } else {
256 format!("/{path}")
257 };
258
259 Ok(self.config.base_url.join(&path)?)
260 }
261
262 async fn check_response_status(&self, response: &Response) -> Result<()> {
264 let status = response.status();
265
266 if status.is_success() {
267 return Ok(());
268 }
269
270 if status == 401 || status == 403 {
271 debug!("Authentication error, invalidating session");
272 self.session.invalidate_session().await;
273 return Err(Error::authentication(format!(
274 "Authentication failed: HTTP {status}"
275 )));
276 }
277
278 if status.is_client_error() {
279 return Err(Error::api(
280 status.as_u16() as i32,
281 format!("Client error: HTTP {status}"),
282 ));
283 }
284
285 if status.is_server_error() {
286 return Err(Error::api(
287 status.as_u16() as i32,
288 format!("Server error: HTTP {status}"),
289 ));
290 }
291
292 Err(Error::api(
293 status.as_u16() as i32,
294 format!("Unexpected status: HTTP {status}"),
295 ))
296 }
297
298 pub(crate) async fn check_xml_for_errors(&self, xml_text: &str) -> Result<()> {
300 if let Some(api_error) = check_for_api_error(xml_text) {
301 debug!(
302 "API error detected: {} - {}",
303 api_error.code,
304 api_error.error_message()
305 );
306
307 if api_error.is_csrf_error() || api_error.is_session_error() {
308 debug!("Session/CSRF error, invalidating session");
309 self.session.invalidate_session().await;
310 }
311
312 return Err(Error::api(
313 api_error.code.as_int(),
314 api_error.error_message(),
315 ));
316 }
317 Ok(())
318 }
319
320 pub(crate) async fn post_xml_with_retry<F, T>(
322 &self,
323 path: &str,
324 xml_body: &str,
325 parse_fn: F,
326 ) -> Result<T>
327 where
328 F: Fn(&str) -> Result<T>,
329 {
330 let response = self.post_xml(path, xml_body).await?;
331 let text = response.text().await?;
332
333 match self.check_xml_for_errors(&text).await {
334 Ok(()) => parse_fn(&text),
335 Err(Error::CsrfTokenInvalid) | Err(Error::SessionTokenInvalid) => {
336 debug!("CSRF/Session error in response, refreshing token and retrying");
337 self.session.refresh_csrf_token().await?;
338
339 let response = self.post_xml(path, xml_body).await?;
340 let text = response.text().await?;
341 self.check_xml_for_errors(&text).await?;
342 parse_fn(&text)
343 }
344 Err(e) => Err(e),
345 }
346 }
347
348 pub(crate) async fn get_authenticated_with_retry<F, T>(
350 &self,
351 path: &str,
352 parse_fn: F,
353 ) -> Result<T>
354 where
355 F: Fn(&str) -> Result<T>,
356 {
357 let response = self.get_authenticated(path).await?;
358 let text = response.text().await?;
359
360 match self.check_xml_for_errors(&text).await {
361 Ok(()) => parse_fn(&text),
362 Err(Error::CsrfTokenInvalid) | Err(Error::SessionTokenInvalid) => {
363 debug!("CSRF/Session error in response, refreshing token and retrying");
364 self.session.refresh_csrf_token().await?;
365
366 let response = self.get_authenticated(path).await?;
367 let text = response.text().await?;
368 self.check_xml_for_errors(&text).await?;
369 parse_fn(&text)
370 }
371 Err(e) => Err(e),
372 }
373 }
374
375 pub fn base_url(&self) -> &Url {
376 &self.config.base_url
377 }
378
379 pub fn config(&self) -> &Config {
380 &self.config
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn test_client_creation() {
390 let config = Config::default();
391 let client = Client::new(config);
392 assert!(client.is_ok());
393 }
394
395 #[test]
396 fn test_client_for_url() {
397 let client = Client::for_url("http://192.168.62.1");
398 assert!(client.is_ok());
399
400 let client = client.unwrap();
401 assert_eq!(client.base_url().as_str(), "http://192.168.62.1/");
402 }
403
404 #[test]
405 fn test_build_url() {
406 let client = Client::for_url("http://192.168.8.1").unwrap();
407
408 let url = client.build_url("/api/device/information").unwrap();
409 assert_eq!(url.as_str(), "http://192.168.8.1/api/device/information");
410
411 let url = client.build_url("api/device/information").unwrap();
412 assert_eq!(url.as_str(), "http://192.168.8.1/api/device/information");
413 }
414}