1use serde::{Serialize, Serializer};
2
3use super::{require_non_empty, CaptchaRequest};
4use crate::error::Result;
5use crate::proxy::ProxyInfo;
6use crate::timeouts::GetResultTimeouts;
7
8#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
11pub struct TurnstileStandard {
12 #[serde(flatten, skip_serializing_if = "Option::is_none")]
13 pub proxy: Option<ProxyInfo>,
14 #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
16 pub user_agent: Option<String>,
17 #[serde(rename = "pageAction", skip_serializing_if = "Option::is_none")]
19 pub page_action: Option<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub data: Option<String>,
23}
24
25impl TurnstileStandard {
26 pub fn new() -> Self {
27 Self::default()
28 }
29
30 pub fn with_proxy(mut self, proxy: ProxyInfo) -> Self {
31 self.proxy = Some(proxy);
32 self
33 }
34
35 pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
36 self.user_agent = Some(value.into());
37 self
38 }
39
40 pub fn with_page_action(mut self, value: impl Into<String>) -> Self {
41 self.page_action = Some(value.into());
42 self
43 }
44
45 pub fn with_data(mut self, value: impl Into<String>) -> Self {
46 self.data = Some(value.into());
47 self
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55pub struct TurnstileChallengeToken {
56 #[serde(rename = "pageAction")]
58 pub page_action: String,
59 #[serde(rename = "userAgent")]
61 pub user_agent: String,
62 pub data: String,
64 #[serde(rename = "pageData")]
66 pub page_data: String,
67 #[serde(flatten, skip_serializing_if = "Option::is_none")]
68 pub proxy: Option<ProxyInfo>,
69 #[serde(rename = "apiJsUrl", skip_serializing_if = "Option::is_none")]
71 pub api_js_url: Option<String>,
72}
73
74impl TurnstileChallengeToken {
75 pub fn new(
76 page_action: impl Into<String>,
77 user_agent: impl Into<String>,
78 data: impl Into<String>,
79 page_data: impl Into<String>,
80 ) -> Self {
81 Self {
82 page_action: page_action.into(),
83 user_agent: user_agent.into(),
84 data: data.into(),
85 page_data: page_data.into(),
86 proxy: None,
87 api_js_url: None,
88 }
89 }
90
91 pub fn with_proxy(mut self, proxy: ProxyInfo) -> Self {
92 self.proxy = Some(proxy);
93 self
94 }
95
96 pub fn with_api_js_url(mut self, value: impl Into<String>) -> Self {
97 self.api_js_url = Some(value.into());
98 self
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
105pub struct TurnstileChallengeCookie {
106 #[serde(rename = "htmlPageBase64")]
108 pub html_page_base64: String,
109 #[serde(rename = "userAgent")]
111 pub user_agent: String,
112 #[serde(flatten)]
113 pub proxy: ProxyInfo,
114}
115
116impl TurnstileChallengeCookie {
117 pub fn new(
118 html_page_base64: impl Into<String>,
119 user_agent: impl Into<String>,
120 proxy: ProxyInfo,
121 ) -> Self {
122 Self {
123 html_page_base64: html_page_base64.into(),
124 user_agent: user_agent.into(),
125 proxy,
126 }
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
133pub struct TurnstileWaitRoom {
134 #[serde(rename = "htmlPageBase64")]
136 pub html_page_base64: String,
137 #[serde(rename = "userAgent")]
139 pub user_agent: String,
140 #[serde(flatten)]
141 pub proxy: ProxyInfo,
142}
143
144impl TurnstileWaitRoom {
145 pub fn new(
146 html_page_base64: impl Into<String>,
147 user_agent: impl Into<String>,
148 proxy: ProxyInfo,
149 ) -> Self {
150 Self {
151 html_page_base64: html_page_base64.into(),
152 user_agent: user_agent.into(),
153 proxy,
154 }
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum TurnstileMode {
161 Standard(TurnstileStandard),
162 ChallengeToken(TurnstileChallengeToken),
163 ChallengeCookie(TurnstileChallengeCookie),
164 WaitRoom(TurnstileWaitRoom),
165}
166
167macro_rules! impl_from_mode {
168 ($($ty:ident => $variant:ident),*) => {$(
169 impl From<$ty> for TurnstileMode {
170 fn from(v: $ty) -> Self {
171 Self::$variant(v)
172 }
173 }
174 )*};
175}
176impl_from_mode!(
177 TurnstileStandard => Standard,
178 TurnstileChallengeToken => ChallengeToken,
179 TurnstileChallengeCookie => ChallengeCookie,
180 TurnstileWaitRoom => WaitRoom
181);
182
183#[derive(Serialize)]
184struct Tagged<'a, T: Serialize> {
185 #[serde(rename = "cloudflareTaskType")]
186 cloudflare_task_type: &'static str,
187 #[serde(flatten)]
188 inner: &'a T,
189}
190
191impl Serialize for TurnstileMode {
192 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
193 match self {
194 TurnstileMode::Standard(v) => v.serialize(serializer),
195 TurnstileMode::ChallengeToken(v) => Tagged {
196 cloudflare_task_type: "token",
197 inner: v,
198 }
199 .serialize(serializer),
200 TurnstileMode::ChallengeCookie(v) => Tagged {
201 cloudflare_task_type: "cf_clearance",
202 inner: v,
203 }
204 .serialize(serializer),
205 TurnstileMode::WaitRoom(v) => Tagged {
206 cloudflare_task_type: "wait_room",
207 inner: v,
208 }
209 .serialize(serializer),
210 }
211 }
212}
213
214#[derive(Debug, Clone, Serialize)]
221pub struct TurnstileRequest {
222 #[serde(rename = "websiteURL")]
224 pub website_url: String,
225 #[serde(rename = "websiteKey")]
228 pub website_key: String,
229 #[serde(flatten)]
230 pub mode: TurnstileMode,
231}
232
233impl TurnstileRequest {
234 pub fn new(
235 website_url: impl Into<String>,
236 website_key: impl Into<String>,
237 mode: impl Into<TurnstileMode>,
238 ) -> Self {
239 Self {
240 website_url: website_url.into(),
241 website_key: website_key.into(),
242 mode: mode.into(),
243 }
244 }
245
246 pub fn standard(website_url: impl Into<String>, website_key: impl Into<String>) -> Self {
248 Self::new(website_url, website_key, TurnstileStandard::new())
249 }
250}
251
252impl CaptchaRequest for TurnstileRequest {
253 const TASK_TYPE: &'static str = "TurnstileTask";
254
255 fn timeouts(&self) -> GetResultTimeouts {
256 GetResultTimeouts::secs(1.0, 0.0, 1.0, 80.0)
257 }
258
259 fn validate(&self) -> Result<()> {
260 require_non_empty("websiteURL", &self.website_url)?;
261 require_non_empty("websiteKey", &self.website_key)?;
262 match &self.mode {
263 TurnstileMode::Standard(_) => {}
264 TurnstileMode::ChallengeToken(v) => {
265 require_non_empty("pageAction", &v.page_action)?;
266 require_non_empty("userAgent", &v.user_agent)?;
267 require_non_empty("data", &v.data)?;
268 require_non_empty("pageData", &v.page_data)?;
269 }
270 TurnstileMode::ChallengeCookie(v) => {
271 require_non_empty("htmlPageBase64", &v.html_page_base64)?;
272 require_non_empty("userAgent", &v.user_agent)?;
273 }
274 TurnstileMode::WaitRoom(v) => {
275 require_non_empty("htmlPageBase64", &v.html_page_base64)?;
276 require_non_empty("userAgent", &v.user_agent)?;
277 }
278 }
279 Ok(())
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use crate::error::Error;
287 use crate::proxy::ProxyType;
288 use serde_json::json;
289
290 fn proxy() -> ProxyInfo {
291 ProxyInfo::new(ProxyType::Http, "1.2.3.4", 8080).with_auth("u", "p")
292 }
293
294 #[test]
295 fn standard_minimal() {
296 let task = TurnstileRequest::standard("https://example.com", "0x4A")
297 .to_task()
298 .unwrap();
299 assert_eq!(
300 task,
301 json!({"type": "TurnstileTask", "websiteURL": "https://example.com",
302 "websiteKey": "0x4A"})
303 );
304 }
305
306 #[test]
307 fn standard_full() {
308 let task = TurnstileRequest::new(
309 "https://example.com",
310 "0x4A",
311 TurnstileStandard::new()
312 .with_proxy(proxy())
313 .with_user_agent("UA")
314 .with_page_action("login")
315 .with_data("cdata"),
316 )
317 .to_task()
318 .unwrap();
319 assert_eq!(
320 task,
321 json!({"type": "TurnstileTask", "websiteURL": "https://example.com",
322 "websiteKey": "0x4A", "userAgent": "UA", "pageAction": "login",
323 "data": "cdata", "proxyType": "http", "proxyAddress": "1.2.3.4",
324 "proxyPort": 8080, "proxyLogin": "u", "proxyPassword": "p"})
325 );
326 }
327
328 #[test]
329 fn challenge_token_minimal_and_full() {
330 let minimal = TurnstileRequest::new(
331 "https://example.com",
332 "0x4A",
333 TurnstileChallengeToken::new("managed", "UA", "cdata", "pdata"),
334 )
335 .to_task()
336 .unwrap();
337 assert_eq!(
338 minimal,
339 json!({"type": "TurnstileTask", "websiteURL": "https://example.com",
340 "websiteKey": "0x4A", "cloudflareTaskType": "token",
341 "pageAction": "managed", "userAgent": "UA", "data": "cdata",
342 "pageData": "pdata"})
343 );
344
345 let full = TurnstileRequest::new(
346 "https://example.com",
347 "0x4A",
348 TurnstileChallengeToken::new("managed", "UA", "cdata", "pdata")
349 .with_proxy(proxy())
350 .with_api_js_url("https://challenges.cloudflare.com/api.js"),
351 )
352 .to_task()
353 .unwrap();
354 assert_eq!(
355 full,
356 json!({"type": "TurnstileTask", "websiteURL": "https://example.com",
357 "websiteKey": "0x4A", "cloudflareTaskType": "token",
358 "pageAction": "managed", "userAgent": "UA", "data": "cdata",
359 "pageData": "pdata", "apiJsUrl": "https://challenges.cloudflare.com/api.js",
360 "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080,
361 "proxyLogin": "u", "proxyPassword": "p"})
362 );
363 }
364
365 #[test]
366 fn cf_clearance_task() {
367 let task = TurnstileRequest::new(
368 "https://example.com",
369 "xxxxxx",
370 TurnstileChallengeCookie::new("PGh0bWw+", "UA", proxy()),
371 )
372 .to_task()
373 .unwrap();
374 assert_eq!(
375 task,
376 json!({"type": "TurnstileTask", "websiteURL": "https://example.com",
377 "websiteKey": "xxxxxx", "cloudflareTaskType": "cf_clearance",
378 "htmlPageBase64": "PGh0bWw+", "userAgent": "UA",
379 "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080,
380 "proxyLogin": "u", "proxyPassword": "p"})
381 );
382 }
383
384 #[test]
385 fn wait_room_task() {
386 let task = TurnstileRequest::new(
387 "https://example.com",
388 "xxxxxx",
389 TurnstileWaitRoom::new("PGh0bWw+", "UA", proxy()),
390 )
391 .to_task()
392 .unwrap();
393 assert_eq!(
394 task,
395 json!({"type": "TurnstileTask", "websiteURL": "https://example.com",
396 "websiteKey": "xxxxxx", "cloudflareTaskType": "wait_room",
397 "htmlPageBase64": "PGh0bWw+", "userAgent": "UA",
398 "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080,
399 "proxyLogin": "u", "proxyPassword": "p"})
400 );
401 }
402
403 #[test]
404 fn empty_html_is_rejected() {
405 let err = TurnstileRequest::new(
406 "https://example.com",
407 "xxxxxx",
408 TurnstileWaitRoom::new("", "UA", proxy()),
409 )
410 .to_task()
411 .unwrap_err();
412 assert!(matches!(err, Error::Validation(_)));
413 }
414
415 #[test]
416 fn empty_required_strings_are_rejected() {
417 let token = |a: &str, u: &str, d: &str, p: &str| {
418 TurnstileRequest::new(
419 "https://example.com",
420 "0x4A",
421 TurnstileChallengeToken::new(a, u, d, p),
422 )
423 };
424 for (req, field) in [
425 (TurnstileRequest::standard("", "0x4A"), "websiteURL"),
426 (
427 TurnstileRequest::standard("https://example.com", ""),
428 "websiteKey",
429 ),
430 (token("", "UA", "d", "p"), "pageAction"),
431 (token("a", "", "d", "p"), "userAgent"),
432 (token("a", "UA", "", "p"), "data"),
433 (token("a", "UA", "d", ""), "pageData"),
434 ] {
435 let err = req.to_task().unwrap_err();
436 assert!(
437 matches!(err, Error::Validation(ref m) if m.contains(field)),
438 "{field}"
439 );
440 }
441 let err = TurnstileRequest::new(
442 "https://example.com",
443 "0x4A",
444 TurnstileChallengeCookie::new("PGh0bWw+", "", proxy()),
445 )
446 .to_task()
447 .unwrap_err();
448 assert!(matches!(err, Error::Validation(ref m) if m.contains("userAgent")));
449 }
450}