scufflecloud_core/captcha/
turnstile.rs

1use std::net::IpAddr;
2use std::sync::Arc;
3
4use ext_traits::DisplayExt;
5use tonic_types::{ErrorDetails, StatusExt};
6
7const TURNSTILE_VERIFY_URL: &str = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
8
9#[derive(Debug, serde_derive::Serialize)]
10struct TurnstileSiteVerifyPayload {
11    pub secret: String,
12    pub response: String,
13    pub remoteip: Option<String>,
14}
15
16#[derive(Debug, serde_derive::Deserialize)]
17struct TurnstileSiteVerifyResponse {
18    pub success: bool,
19    // pub chanllenge_ts: chrono::DateTime<chrono::Utc>,
20    // pub hostname: String,
21    #[serde(rename = "error-codes")]
22    pub error_codes: Vec<String>,
23}
24
25#[derive(Debug, thiserror::Error)]
26pub(crate) enum TrunstileVerifyError {
27    #[error("request to verify server failed: {0}")]
28    HttpRequest(#[from] reqwest::Error),
29    #[error("turnstile error code: {0}")]
30    TurnstileError(String),
31    #[error("missing error code in turnstile response")]
32    MissingErrorCode,
33}
34
35pub(crate) async fn verify<G: core_traits::Global>(
36    global: &Arc<G>,
37    remote_ip: IpAddr,
38    token: &str,
39) -> Result<(), TrunstileVerifyError> {
40    let payload = TurnstileSiteVerifyPayload {
41        secret: global.turnstile_secret_key().to_string(),
42        response: token.to_string(),
43        remoteip: Some(remote_ip.to_string()),
44    };
45
46    let res = global
47        .external_http_client()
48        .post(TURNSTILE_VERIFY_URL)
49        .json(&payload)
50        .send()
51        .await;
52
53    if res.is_err() {
54        tracing::warn!("failed to send turnstile verify request: {:?}", res);
55    }
56
57    let res: TurnstileSiteVerifyResponse = res?.json().await?;
58
59    if !res.success {
60        let Some(error_code) = res.error_codes.into_iter().next() else {
61            return Err(TrunstileVerifyError::MissingErrorCode);
62        };
63        return Err(TrunstileVerifyError::TurnstileError(error_code));
64    }
65
66    Ok(())
67}
68
69pub(crate) async fn verify_in_tonic<G: core_traits::Global>(
70    global: &Arc<G>,
71    remote_ip: IpAddr,
72    token: &str,
73) -> Result<(), tonic::Status> {
74    match verify(global, remote_ip, token).await {
75        Ok(_) => Ok(()),
76        Err(TrunstileVerifyError::TurnstileError(e)) => Err(tonic::Status::with_error_details(
77            tonic::Code::Unauthenticated,
78            TrunstileVerifyError::TurnstileError(e).to_string(),
79            ErrorDetails::new(),
80        )),
81        Err(e) => Err(e.into_tonic_internal_err("failed to verify turnstile token")),
82    }
83}