shared/
captcha.rs

1use compact_str::ToCompactString;
2use std::sync::{Arc, LazyLock};
3
4static CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
5    reqwest::Client::builder()
6        .user_agent(format!("github.com/calagopus/panel {}", crate::VERSION))
7        .build()
8        .expect("Failed to create HTTP client")
9});
10
11pub struct Captcha {
12    settings: Arc<super::settings::Settings>,
13}
14
15impl Captcha {
16    pub fn new(settings: Arc<super::settings::Settings>) -> Self {
17        Self { settings }
18    }
19
20    pub async fn verify(
21        &self,
22        ip: crate::GetIp,
23        captcha: Option<String>,
24    ) -> Result<(), compact_str::CompactString> {
25        let settings = self
26            .settings
27            .get()
28            .await
29            .map_err(|e| e.to_compact_string())?;
30
31        let captcha = match captcha {
32            Some(c) => c,
33            None => {
34                if matches!(
35                    settings.captcha_provider,
36                    super::settings::CaptchaProvider::None
37                ) {
38                    return Ok(());
39                } else {
40                    return Err("captcha: required".into());
41                }
42            }
43        };
44
45        match &settings.captcha_provider {
46            super::settings::CaptchaProvider::None => Ok(()),
47            super::settings::CaptchaProvider::Turnstile { secret_key, .. } => {
48                let response = CLIENT
49                    .post("https://challenges.cloudflare.com/turnstile/v0/siteverify")
50                    .json(&serde_json::json!({
51                        "secret": secret_key,
52                        "response": captcha,
53                        "remoteip": ip.to_string(),
54                    }))
55                    .send()
56                    .await
57                    .map_err(|e| e.to_compact_string())?;
58
59                if response.status().is_success() {
60                    let body: serde_json::Value =
61                        response.json().await.map_err(|e| e.to_compact_string())?;
62                    if let Some(success) = body.get("success")
63                        && success.as_bool().unwrap_or(false)
64                    {
65                        return Ok(());
66                    }
67                }
68
69                Err("captcha: verification failed".into())
70            }
71            super::settings::CaptchaProvider::Recaptcha { v3, secret_key, .. } => {
72                let response = CLIENT
73                    .post("https://www.google.com/recaptcha/api/siteverify")
74                    .form(&[
75                        ("secret", secret_key.as_str()),
76                        ("response", captcha.as_str()),
77                        ("remoteip", ip.to_string().as_str()),
78                    ])
79                    .send()
80                    .await
81                    .map_err(|e| e.to_compact_string())?;
82
83                if response.status().is_success() {
84                    let body: serde_json::Value =
85                        response.json().await.map_err(|e| e.to_compact_string())?;
86                    if let Some(success) = body.get("success")
87                        && success.as_bool().unwrap_or(false)
88                    {
89                        if *v3 {
90                            if let Some(score) = body.get("score")
91                                && score.as_f64().unwrap_or(0.0) >= 0.5
92                            {
93                                return Ok(());
94                            }
95                        } else {
96                            return Ok(());
97                        }
98                    }
99                }
100
101                Err("captcha: verification failed".into())
102            }
103            super::settings::CaptchaProvider::Hcaptcha {
104                secret_key,
105                site_key,
106            } => {
107                let response = CLIENT
108                    .post("https://hcaptcha.com/siteverify")
109                    .form(&[
110                        ("secret", secret_key.as_str()),
111                        ("sitekey", site_key.as_str()),
112                        ("response", captcha.as_str()),
113                        ("remoteip", ip.to_string().as_str()),
114                    ])
115                    .send()
116                    .await
117                    .map_err(|e| e.to_compact_string())?;
118
119                if response.status().is_success() {
120                    let body: serde_json::Value =
121                        response.json().await.map_err(|e| e.to_compact_string())?;
122                    if let Some(success) = body.get("success")
123                        && success.as_bool().unwrap_or(false)
124                    {
125                        return Ok(());
126                    }
127                }
128
129                Err("captcha: verification failed".into())
130            }
131            super::settings::CaptchaProvider::FriendlyCaptcha { api_key, site_key } => {
132                let response = CLIENT
133                    .post("https://global.frcapi.com/api/v2/captcha/siteverify")
134                    .header("X-API-Key", api_key.as_str())
135                    .json(&serde_json::json!({
136                        "sitekey": site_key.as_str(),
137                        "response": captcha.as_str(),
138                    }))
139                    .send()
140                    .await
141                    .map_err(|e| e.to_compact_string())?;
142
143                if response.status().is_success() {
144                    let body: serde_json::Value =
145                        response.json().await.map_err(|e| e.to_compact_string())?;
146                    if let Some(success) = body.get("success")
147                        && success.as_bool().unwrap_or(false)
148                    {
149                        return Ok(());
150                    }
151                }
152
153                Err("captcha: verification failed".into())
154            }
155        }
156    }
157}