Skip to main content

huawei_dongle_api/
client.rs

1//! HTTP client for Huawei Dongle API
2//!
3//! The [`Client`] is the main entry point for interacting with Huawei LTE devices.
4//! It manages HTTP connections, session state, and provides access to all API endpoints.
5//!
6//! # Examples
7//!
8//! ```no_run
9//! use huawei_dongle_api::{Client, Config};
10//!
11//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
12//! // Create client with default settings (http://192.168.8.1)
13//! let client = Client::new(Config::default())?;
14//!
15//! // Or create with custom URL
16//! let client = Client::for_url("http://192.168.1.1")?;
17//!
18//! // Access API endpoints through the client
19//! let device_info = client.device().information().await?;
20//! let sms_count = client.sms().count().await?;
21//! # Ok(())
22//! # }
23//! ```
24
25use 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/// Main client for interacting with Huawei LTE dongles.
38///
39/// The client handles:
40/// - HTTP connection pooling
41/// - Session management and CSRF tokens
42/// - Automatic retry on transient failures
43/// - Error recovery and session refresh
44///
45/// # Thread Safety
46///
47/// The client is thread-safe and can be shared across multiple tasks using `Arc`:
48///
49/// ```no_run
50/// use std::sync::Arc;
51/// use huawei_dongle_api::{Client, Config};
52///
53/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
54/// let client = Arc::new(Client::new(Config::default())?);
55///
56/// // Clone the Arc for use in multiple tasks
57/// let client2 = client.clone();
58/// tokio::spawn(async move {
59///     let status = client2.monitoring().status().await;
60/// });
61/// # Ok(())
62/// # }
63/// ```
64#[derive(Debug)]
65pub struct Client {
66    http_client: HttpClient,
67    config: Config,
68    session: SessionManager,
69    retry_strategy: RetryStrategy,
70}
71
72impl Client {
73    /// Create a new client with the given configuration.
74    ///
75    /// # Arguments
76    ///
77    /// * `config` - Configuration for the client
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if the HTTP client cannot be created.
82    ///
83    /// # Example
84    ///
85    /// ```no_run
86    /// use huawei_dongle_api::{Client, Config};
87    ///
88    /// let config = Config::default();
89    /// let client = Client::new(config)?;
90    /// # Ok::<(), Box<dyn std::error::Error>>(())
91    /// ```
92    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    /// Create a client with default configuration
117    pub fn with_default_config() -> Result<Self> {
118        Self::new(Config::default())
119    }
120
121    /// Create a client for a specific URL
122    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    /// Internal GET implementation
187    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    /// Internal POST implementation
225    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    /// Check response status and handle common error cases
263    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    /// Check XML response for API errors and handle them appropriately
299    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    /// Execute a POST request with automatic CSRF token refresh on failure
321    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    /// Execute a GET request with automatic CSRF token refresh on failure
349    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}