use std::num::NonZeroU32;
use governor::{
Quota,
RateLimiter,
clock::DefaultClock,
middleware::NoOpMiddleware,
state::{InMemoryState, NotKeyed},
};
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
pub use racal::reqwest::{ApiClient, ApiError};
use racal::{FromApiState, Queryable};
use reqwest::{Client, RequestBuilder, Response, header::HeaderMap};
use serde::de::DeserializeOwned;
use crate::{
model::{
LoginResponse,
LoginResponseOrCurrentUser,
SecondFactorVerificationStatus,
},
query::{Authenticating, Authentication, VerifySecondFactor},
};
type NormalRateLimiter =
RateLimiter<NotKeyed, InMemoryState, DefaultClock, NoOpMiddleware>;
#[must_use]
fn http_rate_limiter() -> NormalRateLimiter {
RateLimiter::direct(
Quota::per_minute(NonZeroU32::try_from(12).unwrap())
.allow_burst(NonZeroU32::try_from(5).unwrap()),
)
}
pub struct UnauthenticatedVRC {
user_agent: String,
http: Client,
rate_limiter: NormalRateLimiter,
auth: Authenticating,
}
#[async_trait::async_trait]
impl racal::reqwest::ApiClient<Authenticating> for UnauthenticatedVRC {
fn state(&self) -> &Authenticating { &self.auth }
fn client(&self) -> &reqwest::Client { &self.http }
async fn before_request(
&self, req: RequestBuilder,
) -> Result<RequestBuilder, racal::reqwest::ApiError> {
self.rate_limiter.until_ready().await;
Ok(req)
}
}
pub struct AuthenticatedVRC {
user_agent: String,
http: Client,
rate_limiter: NormalRateLimiter,
auth: Authentication,
}
#[async_trait::async_trait]
impl racal::reqwest::ApiClient<Authentication> for AuthenticatedVRC {
fn state(&self) -> &Authentication { &self.auth }
fn client(&self) -> &reqwest::Client { &self.http }
async fn before_request(
&self, req: RequestBuilder,
) -> Result<RequestBuilder, racal::reqwest::ApiError> {
self.rate_limiter.until_ready().await;
Ok(req)
}
async fn handle_response<ReturnType, FromState, QueryableType>(
&self, queryable: QueryableType, response: Response,
) -> Result<ReturnType, ApiError>
where
ReturnType: DeserializeOwned,
FromState: FromApiState<Authentication>,
QueryableType: Queryable<FromState, ReturnType> + Send + Sync,
{
let response = response.error_for_status()?;
let val = response.bytes().await?;
Ok(queryable.deserialize(&val)?)
}
}
impl AuthenticatedVRC {
fn http_client(
user_agent: &str, auth: &Authentication,
) -> Result<Client, ApiError> {
use serde::ser::Error;
let builder = Client::builder();
let mut headers = HeaderMap::new();
headers
.insert(reqwest::header::ACCEPT, "application/json".parse().unwrap());
headers.insert(
reqwest::header::CONTENT_TYPE,
"application/json".parse().unwrap(),
);
let mut cookie =
"apiKey=".to_owned() + crate::API_KEY + "; auth=" + &auth.token;
if let Some(second_factor) = &auth.second_factor_token {
cookie.push_str("; twoFactorAuth=");
cookie.push_str(second_factor);
}
headers.insert(
reqwest::header::COOKIE,
cookie.parse().map_err(|_| {
serde_json::Error::custom("Couldn't turn auth into a cookie header")
})?,
);
Ok(builder.user_agent(user_agent).default_headers(headers).build()?)
}
pub fn downgrade(
self, auth: impl Into<Authenticating>,
) -> Result<UnauthenticatedVRC, ApiError> {
let auth = auth.into();
Ok(UnauthenticatedVRC {
http: UnauthenticatedVRC::http_client(&self.user_agent, &auth)?,
rate_limiter: self.rate_limiter,
user_agent: self.user_agent,
auth,
})
}
pub fn new(
user_agent: String, auth: impl Into<Authentication>,
) -> Result<Self, ApiError> {
let auth = auth.into();
Ok(Self {
http: Self::http_client(&user_agent, &auth)?,
rate_limiter: http_rate_limiter(),
user_agent,
auth,
})
}
pub fn recreate(
self, auth: impl Into<Authentication>,
) -> Result<Self, ApiError> {
let auth = auth.into();
Ok(Self {
http: Self::http_client(&self.user_agent, &auth)?,
rate_limiter: http_rate_limiter(),
user_agent: self.user_agent,
auth,
})
}
pub async fn verify_second_factor(
&self, second_factor: VerifySecondFactor,
) -> Result<(SecondFactorVerificationStatus, String), ApiError> {
use serde::ser::Error;
let request = Self::build_request(
self.client(),
Authentication::from_state(self.state()),
&second_factor,
)?;
let request = self.before_request(request).await?;
let response = request.send().await?;
let auth: String = extract_cookie(response.headers(), "twoFactorAuth=")
.ok_or_else(|| {
serde_json::Error::custom("twoFactorAuth cookie is missing")
})?;
let resp = self.handle_response::<SecondFactorVerificationStatus, Authentication, crate::query::VerifySecondFactor>(second_factor, response).await?;
Ok((resp, auth))
}
pub fn change_second_factor(
self, second_factor_token: impl Into<Option<String>>,
) -> Result<Self, ApiError> {
let mut auth = self.auth;
auth.second_factor_token = second_factor_token.into();
Ok(Self {
http: Self::http_client(&self.user_agent, &auth)?,
rate_limiter: http_rate_limiter(),
user_agent: self.user_agent,
auth,
})
}
}
impl UnauthenticatedVRC {
fn http_client(
user_agent: &str, auth: &Authenticating,
) -> Result<Client, ApiError> {
use base64::Engine as _;
use serde::ser::Error;
let builder = Client::builder();
let mut headers = HeaderMap::new();
headers
.insert(reqwest::header::ACCEPT, "application/json".parse().unwrap());
headers.insert(
reqwest::header::CONTENT_TYPE,
"application/json".parse().unwrap(),
);
headers.insert(
reqwest::header::COOKIE,
format!("apiKey={}", crate::API_KEY).parse().map_err(|_| {
serde_json::Error::custom("Couldn't turn auth into a cookie header")
})?,
);
let auth = "Basic ".to_owned()
+ &base64::engine::general_purpose::URL_SAFE.encode(
utf8_percent_encode(&auth.username, NON_ALPHANUMERIC).to_string()
+ ":" + &utf8_percent_encode(&auth.password, NON_ALPHANUMERIC)
.to_string(),
);
headers.insert(
reqwest::header::AUTHORIZATION,
auth.parse().map_err(|_| {
serde_json::Error::custom("Couldn't turn username into a header")
})?,
);
Ok(builder.user_agent(user_agent).default_headers(headers).build()?)
}
pub fn upgrade(
self, auth: impl Into<Authentication> + Send,
) -> Result<AuthenticatedVRC, ApiError> {
let auth = auth.into();
Ok(AuthenticatedVRC {
http: AuthenticatedVRC::http_client(&self.user_agent, &auth)?,
rate_limiter: self.rate_limiter,
user_agent: self.user_agent,
auth,
})
}
pub fn new(
user_agent: String, auth: impl Into<Authenticating>,
) -> Result<Self, ApiError> {
let auth = auth.into();
Ok(Self {
http: Self::http_client(&user_agent, &auth)?,
rate_limiter: http_rate_limiter(),
user_agent,
auth,
})
}
pub fn recreate(
self, auth: impl Into<Authenticating>,
) -> Result<Self, ApiError> {
let auth = auth.into();
Ok(Self {
http: Self::http_client(&self.user_agent, &auth)?,
rate_limiter: http_rate_limiter(),
user_agent: self.user_agent,
auth,
})
}
pub async fn login(&self) -> Result<(LoginResponse, String), ApiError> {
use serde::ser::Error;
let queryable = crate::query::GetCurrentUser;
let request = Self::build_request(
self.client(),
Authenticating::from_state(self.state()),
&queryable,
)?;
let request = self.before_request(request).await?;
let response = request.send().await?;
let auth: String = extract_cookie(response.headers(), "auth=")
.ok_or_else(|| serde_json::Error::custom("auth cookie is missing"))?;
let resp: LoginResponseOrCurrentUser =
self.handle_response(queryable, response).await?;
let resp = match resp {
LoginResponseOrCurrentUser::Login(login_resp) => login_resp,
LoginResponseOrCurrentUser::User(_) => {
return Err(
serde_json::Error::custom("Expected login response and not user")
.into(),
);
}
};
Ok((resp, auth))
}
}
fn extract_cookie(headers: &HeaderMap, first_term: &str) -> Option<String> {
headers
.iter()
.filter(|(name, _)| {
name.as_str() == "Set-Cookie" || name.as_str() == "set-cookie"
})
.filter_map(|(_, val)| val.to_str().ok())
.find_map(|value| {
value
.split_terminator(';')
.find_map(|value| value.trim_start().strip_prefix(first_term))
.map(std::borrow::ToOwned::to_owned)
})
}