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::Cap {
48                api_url,
49                site_key,
50                secret_key,
51            } => {
52                let response = CLIENT
53                    .post(format!(
54                        "{}/{site_key}/siteverify",
55                        api_url.trim_end_matches('/')
56                    ))
57                    .json(&serde_json::json!({
58                        "secret": secret_key,
59                        "response": captcha,
60                    }))
61                    .send()
62                    .await
63                    .map_err(|err| {
64                        tracing::error!("captcha: cap verification request failed: {:?}", err);
65                        err.to_compact_string()
66                    })?;
67
68                if response.status().is_success() {
69                    let body: serde_json::Value = response.json().await.map_err(|err| {
70                        tracing::error!(
71                            "captcha: cap verification response parsing failed: {:?}",
72                            err
73                        );
74                        err.to_compact_string()
75                    })?;
76                    if let Some(success) = body.get("success")
77                        && success.as_bool().unwrap_or(false)
78                    {
79                        return Ok(());
80                    }
81                }
82
83                Err("captcha: verification failed".into())
84            }
85            super::settings::CaptchaProvider::Turnstile { secret_key, .. } => {
86                let response = CLIENT
87                    .post("https://challenges.cloudflare.com/turnstile/v0/siteverify")
88                    .json(&serde_json::json!({
89                        "secret": secret_key,
90                        "response": captcha,
91                        "remoteip": ip.to_string(),
92                    }))
93                    .send()
94                    .await
95                    .map_err(|err| {
96                        tracing::error!(
97                            "captcha: turnstile verification request failed: {:?}",
98                            err
99                        );
100                        err.to_compact_string()
101                    })?;
102
103                if response.status().is_success() {
104                    let body: serde_json::Value = response.json().await.map_err(|err| {
105                        tracing::error!(
106                            "captcha: turnstile verification response parsing failed: {:?}",
107                            err
108                        );
109                        err.to_compact_string()
110                    })?;
111                    if let Some(success) = body.get("success")
112                        && success.as_bool().unwrap_or(false)
113                    {
114                        return Ok(());
115                    }
116                }
117
118                Err("captcha: verification failed".into())
119            }
120            super::settings::CaptchaProvider::Recaptcha {
121                v3,
122                threshold,
123                secret_key,
124                ..
125            } => {
126                let response = CLIENT
127                    .post("https://www.google.com/recaptcha/api/siteverify")
128                    .form(&[
129                        ("secret", secret_key.as_str()),
130                        ("response", captcha.as_str()),
131                        ("remoteip", ip.to_string().as_str()),
132                    ])
133                    .send()
134                    .await
135                    .map_err(|err| {
136                        tracing::error!(
137                            "captcha: recaptcha verification request failed: {:?}",
138                            err
139                        );
140                        err.to_compact_string()
141                    })?;
142
143                if response.status().is_success() {
144                    let body: serde_json::Value = response.json().await.map_err(|err| {
145                        tracing::error!(
146                            "captcha: recaptcha verification response parsing failed: {:?}",
147                            err
148                        );
149                        err.to_compact_string()
150                    })?;
151                    if let Some(success) = body.get("success")
152                        && success.as_bool().unwrap_or(false)
153                    {
154                        if *v3 {
155                            if let Some(score) =
156                                body.get("score").and_then(serde_json::Value::as_f64)
157                                && score >= *threshold
158                            {
159                                return Ok(());
160                            }
161                        } else {
162                            return Ok(());
163                        }
164                    }
165                }
166
167                Err("captcha: verification failed".into())
168            }
169            super::settings::CaptchaProvider::Hcaptcha {
170                secret_key,
171                site_key,
172            } => {
173                let response = CLIENT
174                    .post("https://hcaptcha.com/siteverify")
175                    .form(&[
176                        ("secret", secret_key.as_str()),
177                        ("sitekey", site_key.as_str()),
178                        ("response", captcha.as_str()),
179                        ("remoteip", ip.to_string().as_str()),
180                    ])
181                    .send()
182                    .await
183                    .map_err(|err| {
184                        tracing::error!("captcha: hcaptcha verification request failed: {:?}", err);
185                        err.to_compact_string()
186                    })?;
187
188                if response.status().is_success() {
189                    let body: serde_json::Value = response.json().await.map_err(|err| {
190                        tracing::error!(
191                            "captcha: hcaptcha verification response parsing failed: {:?}",
192                            err
193                        );
194                        err.to_compact_string()
195                    })?;
196                    if let Some(success) = body.get("success")
197                        && success.as_bool().unwrap_or(false)
198                    {
199                        return Ok(());
200                    }
201                }
202
203                Err("captcha: verification failed".into())
204            }
205            super::settings::CaptchaProvider::FriendlyCaptcha { api_key, site_key } => {
206                let response = CLIENT
207                    .post("https://global.frcapi.com/api/v2/captcha/siteverify")
208                    .header("X-API-Key", api_key.as_str())
209                    .json(&serde_json::json!({
210                        "sitekey": site_key.as_str(),
211                        "response": captcha.as_str(),
212                    }))
213                    .send()
214                    .await
215                    .map_err(|err| {
216                        tracing::error!(
217                            "captcha: friendlycaptcha verification request failed: {:?}",
218                            err
219                        );
220                        err.to_compact_string()
221                    })?;
222
223                if response.status().is_success() {
224                    let body: serde_json::Value = response.json().await.map_err(|err| {
225                        tracing::error!(
226                            "captcha: friendlycaptcha verification response parsing failed: {:?}",
227                            err
228                        );
229                        err.to_compact_string()
230                    })?;
231                    if let Some(success) = body.get("success")
232                        && success.as_bool().unwrap_or(false)
233                    {
234                        return Ok(());
235                    }
236                }
237
238                Err("captcha: verification failed".into())
239            }
240        }
241    }
242}