#![doc = include_str!("../README.md")]
use connector::Connector;
use error::{SiteVerifyErrors, TurnstileError};
use http_body_util::{BodyExt, Full};
use hyper::{
body::Bytes,
header::{CONTENT_TYPE, USER_AGENT},
Method, Request,
};
use hyper_util::{client::legacy::Client as HyperClient, rt::TokioExecutor};
use secrecy::{ExposeSecret, Secret};
use serde::{Deserialize, Serialize};
mod connector;
pub mod error;
#[cfg(test)]
mod test;
pub struct TurnstileClient {
secret: Secret<String>,
http: HyperClient<Connector, Full<Bytes>>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct SiteVerifyRequest {
pub secret: Option<String>,
pub response: String,
#[serde(rename = "remote_ip")]
pub remote_ip: Option<String>,
#[cfg(feature = "idempotency")]
pub idempotency_key: Option<uuid::Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SiteVerifyResponse {
pub success: bool,
#[serde(rename = "challenge_ts")]
pub timestamp: String,
pub hostname: String,
pub action: String,
pub cdata: String,
}
impl From<RawSiteVerifyResponse> for SiteVerifyResponse {
fn from(raw: RawSiteVerifyResponse) -> Self {
Self {
success: raw.success,
timestamp: raw.timestamp.unwrap_or_default(),
hostname: raw.hostname.unwrap_or_default(),
action: raw.action.unwrap_or_default(),
cdata: raw.cdata.unwrap_or_default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RawSiteVerifyResponse {
success: bool,
#[serde(rename = "challenge_ts")]
timestamp: Option<String>,
hostname: Option<String>,
#[serde(rename = "error-codes")]
error_codes: SiteVerifyErrors,
action: Option<String>,
cdata: Option<String>,
}
const TURNSTILE_USER_AGENT: &str = concat!(
"cf-turnstile (",
env!("CARGO_PKG_HOMEPAGE"),
", ",
env!("CARGO_PKG_VERSION"),
")",
);
impl TurnstileClient {
pub fn new(secret: Secret<String>) -> Self {
let connector = connector::create();
let http =
hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(connector);
Self { http, secret }
}
pub async fn siteverify(
&self,
request: SiteVerifyRequest,
) -> Result<SiteVerifyResponse, TurnstileError> {
let request = if request.secret.is_none() {
SiteVerifyRequest {
secret: Some(self.secret.expose_secret().clone()),
..request
}
} else {
request.clone()
};
let body = Full::new(Bytes::from(serde_json::to_string(&request)?));
let request = Request::builder()
.method(Method::POST)
.uri("https://challenges.cloudflare.com/turnstile/v0/siteverify")
.header(USER_AGENT, TURNSTILE_USER_AGENT)
.header(CONTENT_TYPE, "application/json")
.body(body)
.expect("request builder");
let response = self.http.request(request).await?;
let body_bytes = response.collect().await?.to_bytes();
let body = serde_json::from_slice::<RawSiteVerifyResponse>(&body_bytes)?;
if !body.error_codes.is_empty() {
return Err(TurnstileError::SiteVerifyError(body.error_codes));
}
let transformed = SiteVerifyResponse::from(body);
Ok(transformed)
}
}
#[cfg(feature = "idempotency")]
pub fn generate_indepotency_key() -> Option<uuid::Uuid> {
Some(uuid::Uuid::new_v4())
}