use const_format::concatcp;
use log::debug;
use reqwest::{header, Response, StatusCode};
use thiserror::Error;
use tokio::time::{sleep, Duration};
pub mod account;
pub mod domain;
pub mod rrset;
pub mod token;
pub const API_URL: &str = "https://desec.io/api/v1";
pub const USERAGENT: &str = concatcp!(
"desec-api-client/",
env!("CARGO_PKG_VERSION"),
" (unoffical deSEC API client written in Rust)"
);
#[derive(Error, Debug)]
pub enum Error {
#[error("An error occurred during the request")]
Reqwest(reqwest::Error),
#[error("You hit a rate limit and need to wait {0} seconds. Additional Info: {1}")]
RateLimited(u64, String),
#[error("You hit a rate limit and need to wait. Additional Info: {0}")]
RateLimitedWithoutRetry(String),
#[error("The maximum count of retries has been reached")]
RateLimitedMaxRetriesReached,
#[error("The requested resource does not exist or you are not the owner")]
NotFound,
#[error("The given credentials are not valid")]
Forbidden,
#[error("API returned status code {0} with message '{1}'")]
ApiError(u16, String),
#[error("API returned undocumented status code {0} with message '{1}'")]
UnexpectedStatusCode(u16, String),
#[error("API returned an invalid response. error: {0}, body: {1}")]
InvalidAPIResponse(String, String),
#[error("An error occurred while serializing a JSON value: {0}")]
Serialize(String),
#[error("Failed to create HTTP client: {0}")]
ReqwestClientBuilder(String),
#[error("Request is unauthorized: {0}")]
Unauthorized(String),
#[error("Client has not been logged in, so you cannot logout")]
CannotLogout,
}
#[derive(Debug, Clone)]
pub struct Client {
client: reqwest::Client,
retry: bool,
max_wait_retry: u64,
max_retries: usize,
logged_in: bool,
}
impl Client {
fn get_client(token: Option<String>, logged_in: Option<bool>) -> Result<Self, Error> {
let mut client = reqwest::ClientBuilder::new().user_agent(USERAGENT);
if let Some(token) = token {
let mut headers = header::HeaderMap::new();
headers.insert(
"Authorization",
header::HeaderValue::from_str(format!("Token {}", token.as_str()).as_str())
.unwrap(),
);
client = client.default_headers(headers);
}
let client = client
.build()
.map_err(|error| Error::ReqwestClientBuilder(error.to_string()))?;
Ok(Client {
client,
retry: true,
max_wait_retry: 60,
max_retries: 3,
logged_in: logged_in.unwrap_or_default(),
})
}
pub fn new(token: String) -> Result<Self, Error> {
let mut headers = header::HeaderMap::new();
headers.insert(
"Authorization",
header::HeaderValue::from_str(format!("Token {}", token.as_str()).as_str()).unwrap(),
);
Client::get_client(Some(token), None)
}
pub async fn new_from_credentials(email: &str, password: &str) -> Result<Self, Error> {
let login = account::login(email, password).await?;
Client::get_client(Some(login.token), Some(true))
}
fn new_unauth() -> Result<Self, Error> {
Client::get_client(None, None)
}
pub async fn logout(self) -> Result<(), Error> {
if !self.logged_in {
return Err(Error::CannotLogout);
}
let response = self.post("/auth/logout/", None).await?;
match response.status() {
StatusCode::NO_CONTENT => Ok(()),
_ => Err(Error::UnexpectedStatusCode(
response.status().into(),
response.text().await.unwrap_or_default(),
)),
}
}
pub fn set_retry(&mut self, retry: bool) {
self.retry = retry;
}
pub fn get_retry(&self) -> &bool {
&self.retry
}
pub fn set_max_wait_retry(&mut self, max_wait_retry: u64) {
self.max_wait_retry = max_wait_retry;
}
pub fn get_max_wait_retry(&self) -> &u64 {
&self.max_wait_retry
}
pub fn set_max_retries(&mut self, max_retries: usize) {
self.max_retries = max_retries;
}
pub fn get_max_retries(&self) -> &usize {
&self.max_retries
}
async fn process_request(&self, request: reqwest::Request) -> Result<Response, Error> {
let mut retries: usize = 0;
loop {
if retries > self.max_retries {
debug!("Giving up after {} retries", self.max_retries);
return Err(Error::RateLimitedMaxRetriesReached);
}
let result = self
.client
.execute(
request
.try_clone()
.expect("this request should always be clonable"),
)
.await;
match result {
Ok(response) => match response.status() {
StatusCode::OK
| StatusCode::CREATED
| StatusCode::NO_CONTENT
| StatusCode::ACCEPTED => return Ok(response),
StatusCode::TOO_MANY_REQUESTS => {
let ttw =
parse_time_to_wait(response, self.max_wait_retry, self.retry).await?;
debug!("Request has been throttled, we wait {} seconds", ttw);
sleep(Duration::from_secs(ttw)).await;
retries += 1;
}
StatusCode::UNAUTHORIZED => {
return Err(Error::Unauthorized(
response.text().await.unwrap_or_default(),
))
}
StatusCode::FORBIDDEN => return Err(Error::Forbidden),
StatusCode::BAD_REQUEST => {
return Err(Error::ApiError(
response.status().as_u16(),
response.text().await.unwrap_or_default(),
))
}
StatusCode::NOT_FOUND => return Err(Error::NotFound),
_ => {
return Err(Error::UnexpectedStatusCode(
response.status().into(),
response.text().await.unwrap_or_default(),
))
}
},
Err(error) => return Err(Error::Reqwest(error)),
}
}
}
async fn get(&self, endpoint: &str) -> Result<Response, Error> {
let request = self
.client
.get(format!("{}{}", API_URL, endpoint))
.build()
.map_err(Error::Reqwest)?;
self.process_request(request).await
}
async fn post(&self, endpoint: &str, body: Option<String>) -> Result<Response, Error> {
let request = self
.client
.post(format!("{}{}", API_URL, endpoint).as_str())
.header("Content-Type", "application/json")
.body(body.unwrap_or_default()) .build()
.map_err(Error::Reqwest)?;
self.process_request(request).await
}
async fn patch(&self, endpoint: &str, body: String) -> Result<Response, Error> {
let request = self
.client
.patch(format!("{}{}", API_URL, endpoint).as_str())
.header("Content-Type", "application/json")
.body(body)
.build()
.map_err(Error::Reqwest)?;
self.process_request(request).await
}
async fn delete(&self, endpoint: &str) -> Result<Response, Error> {
let request = self
.client
.delete(format!("{}{}", API_URL, endpoint).as_str())
.build()
.map_err(Error::Reqwest)?;
self.process_request(request).await
}
}
async fn parse_time_to_wait(
response: Response,
max_wait_retry: u64,
should_retry: bool,
) -> Result<u64, Error> {
let time_to_wait = match response.headers().get("retry-after") {
Some(header) => match header.to_str() {
Ok(header) => header.parse().map_err(|_| {
Error::RateLimitedWithoutRetry(format!(
"Request was throttled and cannot parse retry after {:?}",
header
))
})?,
Err(_) => return Err(Error::RateLimitedWithoutRetry(
"Request got throttled with retry-after header containing non-visible ASCII chars"
.to_string(),
)),
},
None => {
return Err(Error::RateLimitedWithoutRetry(
"Request got throttled without retry-after header".to_string(),
))
}
};
if !should_retry {
let msg = String::from("Request has been throttled, but retries are disabled");
debug!("{}", msg);
return Err(Error::RateLimited(
time_to_wait,
response.text().await.unwrap_or(msg),
));
}
if time_to_wait > max_wait_retry {
let msg = format!(
"Wait time for retry {} exceeds max accepted wait time per retry {}",
time_to_wait, max_wait_retry
);
debug!("{}", msg);
return Err(Error::RateLimited(time_to_wait, msg));
}
Ok(time_to_wait)
}