use crate::{
api,
config::Config,
error::{Error, Result},
models::common::check_for_api_error,
retry::RetryStrategy,
session::SessionManager,
};
use reqwest::{Client as HttpClient, ClientBuilder, Response};
use tracing::{debug, trace};
use url::Url;
#[derive(Debug)]
pub struct Client {
http_client: HttpClient,
config: Config,
session: SessionManager,
retry_strategy: RetryStrategy,
}
impl Client {
pub fn new(config: Config) -> Result<Self> {
let http_client = ClientBuilder::new()
.cookie_store(true)
.timeout(config.timeout)
.user_agent(&config.user_agent)
.build()?;
let session = SessionManager::new(http_client.clone(), config.base_url.clone());
let retry_strategy = RetryStrategy {
max_attempts: config.max_retries,
initial_delay: config.retry_delay,
max_delay: config.max_retry_delay,
..Default::default()
};
Ok(Self {
http_client,
config,
session,
retry_strategy,
})
}
pub fn with_default_config() -> Result<Self> {
Self::new(Config::default())
}
pub fn for_url<S: AsRef<str>>(url: S) -> Result<Self> {
let config = Config::for_url(url)?;
Self::new(config)
}
pub fn device(&self) -> api::device::DeviceApi {
api::device::DeviceApi::new(self)
}
pub fn monitoring(&self) -> api::monitoring::MonitoringApi {
api::monitoring::MonitoringApi::new(self)
}
pub fn network(&self) -> api::network::NetworkApi {
api::network::NetworkApi::new(self)
}
pub fn sms(&self) -> api::sms::SmsApi {
api::sms::SmsApi::new(self)
}
pub fn dhcp(&self) -> api::dhcp::DhcpApi {
api::dhcp::DhcpApi::new(self)
}
pub fn auth(&self) -> api::auth::AuthApi {
api::auth::AuthApi::new(self)
}
pub(crate) fn session(&self) -> &SessionManager {
&self.session
}
pub(crate) async fn get(&self, path: &str) -> Result<Response> {
let url = self.build_url(path)?;
trace!("GET {}", url);
self.retry_strategy
.execute(|| async {
let response = self.http_client.get(url.clone()).send().await?;
self.check_response_status(&response).await?;
self.session.update_token_from_headers(response.headers()).await;
Ok(response)
})
.await
}
pub(crate) async fn get_authenticated(&self, path: &str) -> Result<Response> {
let url = self.build_url(path)?;
trace!("GET {} (authenticated)", url);
let result = self.get_authenticated_internal(&url).await;
match &result {
Err(Error::CsrfTokenInvalid) | Err(Error::SessionTokenInvalid) => {
debug!("CSRF/Session error detected, refreshing token and retrying");
self.session.refresh_csrf_token().await?;
self.get_authenticated_internal(&url).await
}
_ => result
}
}
async fn get_authenticated_internal(&self, url: &Url) -> Result<Response> {
self.retry_strategy
.execute(|| async {
let csrf_token = self.session.get_csrf_token().await?;
let response = self
.http_client
.get(url.clone())
.header("X-Requested-With", "XMLHttpRequest")
.header("__RequestVerificationToken", &csrf_token)
.send()
.await?;
self.check_response_status(&response).await?;
self.session.update_token_from_headers(response.headers()).await;
Ok(response)
})
.await
}
pub(crate) async fn post_xml(&self, path: &str, xml_body: &str) -> Result<Response> {
let url = self.build_url(path)?;
trace!("POST {} with XML body", url);
let result = self.post_xml_internal(&url, xml_body).await;
match &result {
Err(Error::CsrfTokenInvalid) | Err(Error::SessionTokenInvalid) => {
debug!("CSRF/Session error detected, refreshing token and retrying");
self.session.refresh_csrf_token().await?;
self.post_xml_internal(&url, xml_body).await
}
_ => result
}
}
async fn post_xml_internal(&self, url: &Url, xml_body: &str) -> Result<Response> {
self.retry_strategy
.execute(|| async {
let csrf_token = self.session.get_csrf_token().await?;
let response = self
.http_client
.post(url.clone())
.header(
"Content-Type",
"application/x-www-form-urlencoded; charset=UTF-8",
)
.header("X-Requested-With", "XMLHttpRequest")
.header("__RequestVerificationToken", &csrf_token)
.body(xml_body.to_string())
.send()
.await?;
self.check_response_status(&response).await?;
self.session.update_token_from_headers(response.headers()).await;
Ok(response)
})
.await
}
fn build_url(&self, path: &str) -> Result<Url> {
let path = if path.starts_with('/') {
path.to_string()
} else {
format!("/{}", path)
};
Ok(self.config.base_url.join(&path)?)
}
async fn check_response_status(&self, response: &Response) -> Result<()> {
let status = response.status();
if status.is_success() {
return Ok(());
}
if status == 401 || status == 403 {
debug!("Authentication error, invalidating session");
self.session.invalidate_session().await;
return Err(Error::authentication(format!(
"Authentication failed: HTTP {}",
status
)));
}
if status.is_client_error() {
return Err(Error::api(
status.as_u16() as i32,
format!("Client error: HTTP {}", status),
));
}
if status.is_server_error() {
return Err(Error::api(
status.as_u16() as i32,
format!("Server error: HTTP {}", status),
));
}
Err(Error::api(
status.as_u16() as i32,
format!("Unexpected status: HTTP {}", status),
))
}
pub(crate) async fn check_xml_for_errors(&self, xml_text: &str) -> Result<()> {
if let Some(api_error) = check_for_api_error(xml_text) {
debug!("API error detected: {} - {}", api_error.code, api_error.error_message());
if api_error.is_csrf_error() || api_error.is_session_error() {
debug!("Session/CSRF error, invalidating session");
self.session.invalidate_session().await;
}
return Err(Error::api(
api_error.code.as_int(),
api_error.error_message(),
));
}
Ok(())
}
pub(crate) async fn post_xml_with_retry<F, T>(&self, path: &str, xml_body: &str, parse_fn: F) -> Result<T>
where
F: Fn(&str) -> Result<T>,
{
let response = self.post_xml(path, xml_body).await?;
let text = response.text().await?;
match self.check_xml_for_errors(&text).await {
Ok(()) => parse_fn(&text),
Err(Error::CsrfTokenInvalid) | Err(Error::SessionTokenInvalid) => {
debug!("CSRF/Session error in response, refreshing token and retrying");
self.session.refresh_csrf_token().await?;
let response = self.post_xml(path, xml_body).await?;
let text = response.text().await?;
self.check_xml_for_errors(&text).await?;
parse_fn(&text)
}
Err(e) => Err(e),
}
}
pub(crate) async fn get_authenticated_with_retry<F, T>(&self, path: &str, parse_fn: F) -> Result<T>
where
F: Fn(&str) -> Result<T>,
{
let response = self.get_authenticated(path).await?;
let text = response.text().await?;
match self.check_xml_for_errors(&text).await {
Ok(()) => parse_fn(&text),
Err(Error::CsrfTokenInvalid) | Err(Error::SessionTokenInvalid) => {
debug!("CSRF/Session error in response, refreshing token and retrying");
self.session.refresh_csrf_token().await?;
let response = self.get_authenticated(path).await?;
let text = response.text().await?;
self.check_xml_for_errors(&text).await?;
parse_fn(&text)
}
Err(e) => Err(e),
}
}
pub fn base_url(&self) -> &Url {
&self.config.base_url
}
pub fn config(&self) -> &Config {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_creation() {
let config = Config::default();
let client = Client::new(config);
assert!(client.is_ok());
}
#[test]
fn test_client_for_url() {
let client = Client::for_url("http://192.168.62.1");
assert!(client.is_ok());
let client = client.unwrap();
assert_eq!(client.base_url().as_str(), "http://192.168.62.1/");
}
#[test]
fn test_build_url() {
let client = Client::for_url("http://192.168.8.1").unwrap();
let url = client.build_url("/api/device/information").unwrap();
assert_eq!(url.as_str(), "http://192.168.8.1/api/device/information");
let url = client.build_url("api/device/information").unwrap();
assert_eq!(url.as_str(), "http://192.168.8.1/api/device/information");
}
}