1#![doc = include_str!("../README.md")]
2use connector::Connector;
3use error::{SiteVerifyErrors, TurnstileError};
4use http_body_util::{BodyExt, Full};
5use hyper::{
6 body::Bytes,
7 header::{CONTENT_TYPE, USER_AGENT},
8 Method, Request,
9};
10use hyper_util::{client::legacy::Client as HyperClient, rt::TokioExecutor};
11use secrecy::{ExposeSecret, Secret};
12use serde::{Deserialize, Serialize};
13
14mod connector;
15pub mod error;
16
17#[cfg(test)]
18mod test;
19
20pub struct TurnstileClient {
22 secret: Secret<String>,
23 http: HyperClient<Connector, Full<Bytes>>,
24}
25
26#[derive(Debug, Default, Clone, Serialize, Deserialize)]
30pub struct SiteVerifyRequest {
31 pub secret: Option<String>,
33 pub response: String,
35 #[serde(rename = "remote_ip")]
37 pub remote_ip: Option<String>,
38 #[cfg(feature = "idempotency")]
40 pub idempotency_key: Option<uuid::Uuid>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct SiteVerifyResponse {
48 pub success: bool,
50 #[serde(rename = "challenge_ts")]
52 pub timestamp: String,
53 pub hostname: String,
55 pub action: String,
57 pub cdata: String,
59}
60
61impl From<RawSiteVerifyResponse> for SiteVerifyResponse {
62 fn from(raw: RawSiteVerifyResponse) -> Self {
63 Self {
64 success: raw.success,
65 timestamp: raw.timestamp.unwrap_or_default(),
66 hostname: raw.hostname.unwrap_or_default(),
67 action: raw.action.unwrap_or_default(),
68 cdata: raw.cdata.unwrap_or_default(),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74struct RawSiteVerifyResponse {
75 success: bool,
76 #[serde(rename = "challenge_ts")]
77 timestamp: Option<String>,
78 hostname: Option<String>,
79 #[serde(rename = "error-codes")]
80 error_codes: SiteVerifyErrors,
81 action: Option<String>,
82 cdata: Option<String>,
83}
84
85const TURNSTILE_USER_AGENT: &str = concat!(
86 "cf-turnstile (",
87 env!("CARGO_PKG_HOMEPAGE"),
88 ", ",
89 env!("CARGO_PKG_VERSION"),
90 ")",
91);
92
93impl TurnstileClient {
94 pub fn new(secret: Secret<String>) -> Self {
96 let connector = connector::create();
97 let http =
98 hyper_util::client::legacy::Client::builder(TokioExecutor::new()).build(connector);
99
100 Self { http, secret }
101 }
102
103 pub async fn siteverify(
105 &self,
106 request: SiteVerifyRequest,
107 ) -> Result<SiteVerifyResponse, TurnstileError> {
108 let request = if request.secret.is_none() {
110 SiteVerifyRequest {
111 secret: Some(self.secret.expose_secret().clone()),
112 ..request
113 }
114 } else {
115 request.clone()
116 };
117
118 let body = Full::new(Bytes::from(serde_json::to_string(&request)?));
119
120 let request = Request::builder()
121 .method(Method::POST)
122 .uri("https://challenges.cloudflare.com/turnstile/v0/siteverify")
123 .header(USER_AGENT, TURNSTILE_USER_AGENT)
124 .header(CONTENT_TYPE, "application/json")
125 .body(body)
126 .expect("request builder");
127
128 let response = self.http.request(request).await?;
129
130 let body_bytes = response.collect().await?.to_bytes();
131 let body = serde_json::from_slice::<RawSiteVerifyResponse>(&body_bytes)?;
132
133 if !body.error_codes.is_empty() {
134 return Err(TurnstileError::SiteVerifyError(body.error_codes));
135 }
136
137 let transformed = SiteVerifyResponse::from(body);
138
139 Ok(transformed)
140 }
141}
142
143#[cfg(feature = "idempotency")]
145pub fn generate_indepotency_key() -> Option<uuid::Uuid> {
146 Some(uuid::Uuid::new_v4())
147}