scufflecloud_core/captcha/
turnstile.rs1use 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 #[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}