scufflecloud_core/captcha/
turnstile.rs

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