Skip to main content

codoseo_web/
turnstile.rs

1//! Cloudflare Turnstile on the audit form (cloud only, and only when keys are configured).
2//! The page loads Cloudflare's script, which adds a `cf-turnstile-response` field to the form;
3//! the server checks that token with Cloudflare before starting any crawl.
4
5use std::net::IpAddr;
6
7use serde::Deserialize;
8
9use crate::state::AppState;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Verdict {
13    /// Turnstile isn't configured, or Cloudflare confirmed the token.
14    Passed,
15    /// No token, or Cloudflare rejected it.
16    Failed,
17    /// Cloudflare couldn't be asked. The form fails closed rather than let bots through.
18    Unavailable,
19}
20
21#[derive(Deserialize)]
22struct SiteVerify {
23    success: bool,
24}
25
26pub async fn verify(state: &AppState, token: Option<&str>, ip: Option<IpAddr>) -> Verdict {
27    let Some(cfg) = &state.config.turnstile else {
28        return Verdict::Passed;
29    };
30    // Tokens are a few hundred bytes; refuse anything absurd without a round trip.
31    let Some(token) = token
32        .map(str::trim)
33        .filter(|t| !t.is_empty() && t.len() <= 2048)
34    else {
35        return Verdict::Failed;
36    };
37    let mut form = vec![
38        ("secret", cfg.secret.clone()),
39        ("response", token.to_owned()),
40    ];
41    if let Some(ip) = ip {
42        form.push(("remoteip", ip.to_string()));
43    }
44    let response = state
45        .http
46        .post(cfg.verify_url.clone())
47        .form(&form)
48        .send()
49        .await
50        .and_then(|r| r.error_for_status());
51    let body = match response {
52        Ok(r) => r.json::<SiteVerify>().await,
53        Err(e) => {
54            tracing::warn!(error = %e, "turnstile verification unavailable");
55            return Verdict::Unavailable;
56        }
57    };
58    match body {
59        Ok(v) if v.success => Verdict::Passed,
60        Ok(_) => Verdict::Failed,
61        Err(e) => {
62            tracing::warn!(error = %e, "turnstile verification returned an unreadable answer");
63            Verdict::Unavailable
64        }
65    }
66}