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}