#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::string::String;
#[cfg(feature = "std")]
use std::net::IpAddr;
#[cfg(feature = "std")]
use std::pin::Pin;
pub use crate::error::DuckityError;
#[cfg(feature = "std")]
use crate::schemas::{ChallengeResponse, ValidateRequest, ValidateResponse};
pub mod core;
mod error;
#[cfg(feature = "std")]
mod schemas;
#[cfg(feature = "std")]
static HOSTED_BASE_URL: &str = "https://api.duckity.com/d1";
#[cfg(feature = "std")]
pub fn solve(protection_profile_id: impl Into<String>) -> ChallengeGetter {
ChallengeGetter {
base_url: HOSTED_BASE_URL.to_string(),
protection_profile_id: protection_profile_id.into(),
}
}
#[cfg(feature = "std")]
pub struct ChallengeGetter {
base_url: String,
protection_profile_id: String,
}
#[cfg(feature = "std")]
impl ChallengeGetter {
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
pub async fn send(self) -> Result<String, DuckityError> {
let client = reqwest::Client::new();
let url = {
let mut url = self.base_url;
url.push_str(&format!("/challenges/{}/issue", self.protection_profile_id));
url
};
let request = client.post(url);
let response = request.send().await?;
let response: ChallengeResponse = response.json().await?;
Ok(response.challenge)
}
pub async fn send_and_solve(self) -> Result<String, DuckityError> {
let challenge = self.send().await?;
tokio::task::spawn_blocking(move || {
let decoded = core::decode(&challenge)?;
let solution = core::solve(&decoded);
let encoded = core::encode(&challenge, &solution)?;
Ok(encoded)
})
.await
.unwrap()
}
}
#[cfg(feature = "std")]
impl IntoFuture for ChallengeGetter {
type Output = Result<String, DuckityError>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move { self.send_and_solve().await })
}
}
#[cfg(feature = "std")]
pub struct SolutionValidator {
protection_profile_id: String,
application_secret: String,
ip: IpAddr,
base_url: String,
solution: String,
}
#[cfg(feature = "std")]
impl SolutionValidator {
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
async fn send(self) -> Result<bool, DuckityError> {
let client = reqwest::Client::new();
let url = {
let mut url = self.base_url;
url.push_str(&format!(
"/challenges/{}/validate",
self.protection_profile_id
));
url
};
let request = client
.post(url)
.json(&ValidateRequest {
solution: self.solution,
ip: self.ip,
})
.bearer_auth(self.application_secret);
let response = request.send().await?;
let response: ValidateResponse = response.json().await?;
Ok(response.is_valid)
}
}
#[cfg(feature = "std")]
impl IntoFuture for SolutionValidator {
type Output = Result<bool, DuckityError>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move { self.send().await })
}
}
#[cfg(feature = "std")]
pub fn validate(
solution: impl Into<String>,
ip: impl Into<IpAddr>,
application_secret: impl Into<String>,
protection_profile_id: impl Into<String>,
) -> SolutionValidator {
SolutionValidator {
protection_profile_id: protection_profile_id.into(),
application_secret: application_secret.into(),
ip: ip.into(),
base_url: HOSTED_BASE_URL.to_string(),
solution: solution.into(),
}
}