Skip to main content

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(|err| {
58                        tracing::error!(
59                            "captcha: turnstile verification request failed: {:?}",
60                            err
61                        );
62                        err.to_compact_string()
63                    })?;
64
65                if response.status().is_success() {
66                    let body: serde_json::Value = response.json().await.map_err(|err| {
67                        tracing::error!(
68                            "captcha: turnstile verification response parsing failed: {:?}",
69                            err
70                        );
71                        err.to_compact_string()
72                    })?;
73                    if let Some(success) = body.get("success")
74                        && success.as_bool().unwrap_or(false)
75                    {
76                        return Ok(());
77                    }
78                }
79
80                Err("captcha: verification failed".into())
81            }
82            super::settings::CaptchaProvider::Recaptcha { v3, secret_key, .. } => {
83                let response = CLIENT
84                    .post("https://www.google.com/recaptcha/api/siteverify")
85                    .form(&[
86                        ("secret", secret_key.as_str()),
87                        ("response", captcha.as_str()),
88                        ("remoteip", ip.to_string().as_str()),
89                    ])
90                    .send()
91                    .await
92                    .map_err(|err| {
93                        tracing::error!(
94                            "captcha: recaptcha verification request failed: {:?}",
95                            err
96                        );
97                        err.to_compact_string()
98                    })?;
99
100                if response.status().is_success() {
101                    let body: serde_json::Value = response.json().await.map_err(|err| {
102                        tracing::error!(
103                            "captcha: recaptcha verification response parsing failed: {:?}",
104                            err
105                        );
106                        err.to_compact_string()
107                    })?;
108                    if let Some(success) = body.get("success")
109                        && success.as_bool().unwrap_or(false)
110                    {
111                        if *v3 {
112                            if let Some(score) = body.get("score")
113                                && score.as_f64().unwrap_or(0.0) >= 0.5
114                            {
115                                return Ok(());
116                            }
117                        } else {
118                            return Ok(());
119                        }
120                    }
121                }
122
123                Err("captcha: verification failed".into())
124            }
125            super::settings::CaptchaProvider::Hcaptcha {
126                secret_key,
127                site_key,
128            } => {
129                let response = CLIENT
130                    .post("https://hcaptcha.com/siteverify")
131                    .form(&[
132                        ("secret", secret_key.as_str()),
133                        ("sitekey", site_key.as_str()),
134                        ("response", captcha.as_str()),
135                        ("remoteip", ip.to_string().as_str()),
136                    ])
137                    .send()
138                    .await
139                    .map_err(|err| {
140                        tracing::error!("captcha: hcaptcha verification request failed: {:?}", err);
141                        err.to_compact_string()
142                    })?;
143
144                if response.status().is_success() {
145                    let body: serde_json::Value = response.json().await.map_err(|err| {
146                        tracing::error!(
147                            "captcha: hcaptcha verification response parsing failed: {:?}",
148                            err
149                        );
150                        err.to_compact_string()
151                    })?;
152                    if let Some(success) = body.get("success")
153                        && success.as_bool().unwrap_or(false)
154                    {
155                        return Ok(());
156                    }
157                }
158
159                Err("captcha: verification failed".into())
160            }
161            super::settings::CaptchaProvider::FriendlyCaptcha { api_key, site_key } => {
162                let response = CLIENT
163                    .post("https://global.frcapi.com/api/v2/captcha/siteverify")
164                    .header("X-API-Key", api_key.as_str())
165                    .json(&serde_json::json!({
166                        "sitekey": site_key.as_str(),
167                        "response": captcha.as_str(),
168                    }))
169                    .send()
170                    .await
171                    .map_err(|err| {
172                        tracing::error!(
173                            "captcha: friendlycaptcha verification request failed: {:?}",
174                            err
175                        );
176                        err.to_compact_string()
177                    })?;
178
179                if response.status().is_success() {
180                    let body: serde_json::Value = response.json().await.map_err(|err| {
181                        tracing::error!(
182                            "captcha: friendlycaptcha verification response parsing failed: {:?}",
183                            err
184                        );
185                        err.to_compact_string()
186                    })?;
187                    if let Some(success) = body.get("success")
188                        && success.as_bool().unwrap_or(false)
189                    {
190                        return Ok(());
191                    }
192                }
193
194                Err("captcha: verification failed".into())
195            }
196        }
197    }
198}