Skip to main content

capmonstercloud_client/requests/
amazon_waf.rs

1use serde::ser::{Serialize, SerializeMap, Serializer};
2
3use super::{require_non_empty, CaptchaRequest};
4use crate::proxy::ProxyInfo;
5use crate::timeouts::GetResultTimeouts;
6
7/// Option 1: visible AWS WAF captcha (`jsapi.js` / `captcha.js` only).
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct AmazonVisibleCaptcha {
10    /// Value of the `apiKey` field used when rendering the captcha.
11    pub website_key: String,
12    /// Link to `jsapi.js` on the HTML page (`<Integration URL>jsapi.js`).
13    pub captcha_script: String,
14    /// Browser User-Agent (current Windows one). Only the visible-captcha option accepts it.
15    pub user_agent: Option<String>,
16}
17
18impl AmazonVisibleCaptcha {
19    pub fn new(website_key: impl Into<String>, captcha_script: impl Into<String>) -> Self {
20        Self {
21            website_key: website_key.into(),
22            captcha_script: captcha_script.into(),
23            user_agent: None,
24        }
25    }
26
27    pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
28        self.user_agent = Some(value.into());
29        self
30    }
31}
32
33/// Option 2: challenge plus captcha. The values come from `window.gokuProps`
34/// (`key`, `context`, `iv`) on the 405 page of the protected site.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct AmazonChallengeAndCaptcha {
37    /// Link to `challenge.js`.
38    pub challenge_script: String,
39    /// `window.gokuProps.key`.
40    pub website_key: String,
41    /// `window.gokuProps.context`.
42    pub context: String,
43    /// `window.gokuProps.iv`.
44    pub iv: String,
45    /// Link to `captcha.js`; may be absent if only a challenge is present.
46    pub captcha_script: Option<String>,
47}
48
49impl AmazonChallengeAndCaptcha {
50    pub fn new(
51        challenge_script: impl Into<String>,
52        website_key: impl Into<String>,
53        context: impl Into<String>,
54        iv: impl Into<String>,
55    ) -> Self {
56        Self {
57            challenge_script: challenge_script.into(),
58            website_key: website_key.into(),
59            context: context.into(),
60            iv: iv.into(),
61            captcha_script: None,
62        }
63    }
64
65    pub fn with_captcha_script(mut self, value: impl Into<String>) -> Self {
66        self.captcha_script = Some(value.into());
67        self
68    }
69}
70
71/// Option 3: invisible captcha, only `challenge.js` is loaded. `context` and `iv` are sent
72/// as empty strings, as the API requires.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct AmazonChallengeOnly {
75    /// Link to `challenge.js`.
76    pub challenge_script: String,
77}
78
79impl AmazonChallengeOnly {
80    pub fn new(challenge_script: impl Into<String>) -> Self {
81        Self {
82            challenge_script: challenge_script.into(),
83        }
84    }
85}
86
87/// The three mutually exclusive parameter sets accepted by the API for `AmazonTask`.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum AmazonWafVariant {
90    VisibleCaptcha(AmazonVisibleCaptcha),
91    ChallengeAndCaptcha(AmazonChallengeAndCaptcha),
92    ChallengeOnly(AmazonChallengeOnly),
93}
94
95impl From<AmazonVisibleCaptcha> for AmazonWafVariant {
96    fn from(v: AmazonVisibleCaptcha) -> Self {
97        Self::VisibleCaptcha(v)
98    }
99}
100
101impl From<AmazonChallengeAndCaptcha> for AmazonWafVariant {
102    fn from(v: AmazonChallengeAndCaptcha) -> Self {
103        Self::ChallengeAndCaptcha(v)
104    }
105}
106
107impl From<AmazonChallengeOnly> for AmazonWafVariant {
108    fn from(v: AmazonChallengeOnly) -> Self {
109        Self::ChallengeOnly(v)
110    }
111}
112
113impl Serialize for AmazonWafVariant {
114    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
115        let mut map = serializer.serialize_map(None)?;
116        match self {
117            AmazonWafVariant::VisibleCaptcha(v) => {
118                map.serialize_entry("websiteKey", &v.website_key)?;
119                map.serialize_entry("captchaScript", &v.captcha_script)?;
120                if let Some(ua) = &v.user_agent {
121                    map.serialize_entry("userAgent", ua)?;
122                }
123            }
124            AmazonWafVariant::ChallengeAndCaptcha(v) => {
125                map.serialize_entry("challengeScript", &v.challenge_script)?;
126                map.serialize_entry("websiteKey", &v.website_key)?;
127                map.serialize_entry("context", &v.context)?;
128                map.serialize_entry("iv", &v.iv)?;
129                if let Some(script) = &v.captcha_script {
130                    map.serialize_entry("captchaScript", script)?;
131                }
132            }
133            AmazonWafVariant::ChallengeOnly(v) => {
134                map.serialize_entry("challengeScript", &v.challenge_script)?;
135                map.serialize_entry("context", "")?;
136                map.serialize_entry("iv", "")?;
137            }
138        }
139        map.end()
140    }
141}
142
143/// Amazon AWS WAF captcha and challenge (`AmazonTask`).
144///
145/// Build it with [`AmazonWafRequest::new`] and one of [`AmazonVisibleCaptcha`],
146/// [`AmazonChallengeAndCaptcha`], [`AmazonChallengeOnly`]; the type system makes it impossible
147/// to mix parameters of different options.
148#[derive(Debug, Clone, serde::Serialize)]
149pub struct AmazonWafRequest {
150    /// Address of the main page where the captcha is solved.
151    #[serde(rename = "websiteURL")]
152    pub website_url: String,
153    /// Which of the three parameter sets is used.
154    #[serde(flatten)]
155    pub variant: AmazonWafVariant,
156    #[serde(flatten, skip_serializing_if = "Option::is_none")]
157    pub proxy: Option<ProxyInfo>,
158    /// `false` (default): the response has `captcha_voucher` and `existing_token`.
159    /// `true`: the response has the `aws-waf-token` cookie.
160    #[serde(rename = "cookieSolution", skip_serializing_if = "Option::is_none")]
161    pub cookie_solution: Option<bool>,
162}
163
164impl AmazonWafRequest {
165    pub fn new(website_url: impl Into<String>, variant: impl Into<AmazonWafVariant>) -> Self {
166        Self {
167            website_url: website_url.into(),
168            variant: variant.into(),
169            proxy: None,
170            cookie_solution: None,
171        }
172    }
173
174    pub fn with_proxy(mut self, proxy: ProxyInfo) -> Self {
175        self.proxy = Some(proxy);
176        self
177    }
178
179    pub fn with_cookie_solution(mut self, value: bool) -> Self {
180        self.cookie_solution = Some(value);
181        self
182    }
183}
184
185impl CaptchaRequest for AmazonWafRequest {
186    const TASK_TYPE: &'static str = "AmazonTask";
187
188    fn timeouts(&self) -> GetResultTimeouts {
189        GetResultTimeouts::secs(1.0, 10.0, 3.0, 180.0)
190    }
191
192    fn validate(&self) -> crate::error::Result<()> {
193        require_non_empty("websiteURL", &self.website_url)?;
194        match &self.variant {
195            AmazonWafVariant::VisibleCaptcha(v) => {
196                require_non_empty("websiteKey", &v.website_key)?;
197                require_non_empty("captchaScript", &v.captcha_script)?;
198            }
199            AmazonWafVariant::ChallengeAndCaptcha(v) => {
200                require_non_empty("challengeScript", &v.challenge_script)?;
201                require_non_empty("websiteKey", &v.website_key)?;
202                require_non_empty("context", &v.context)?;
203                require_non_empty("iv", &v.iv)?;
204            }
205            // `context` and `iv` are sent as "" by design for this variant.
206            AmazonWafVariant::ChallengeOnly(v) => {
207                require_non_empty("challengeScript", &v.challenge_script)?;
208            }
209        }
210        Ok(())
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::proxy::ProxyType;
218    use serde_json::json;
219
220    #[test]
221    fn option1_minimal() {
222        let task = AmazonWafRequest::new(
223            "https://example.com",
224            AmazonVisibleCaptcha::new("KEY", "https://x/jsapi.js"),
225        )
226        .to_task()
227        .unwrap();
228        assert_eq!(
229            task,
230            json!({"type": "AmazonTask", "websiteURL": "https://example.com",
231                   "websiteKey": "KEY", "captchaScript": "https://x/jsapi.js"})
232        );
233    }
234
235    #[test]
236    fn option1_full() {
237        let task = AmazonWafRequest::new(
238            "https://example.com",
239            AmazonVisibleCaptcha::new("KEY", "https://x/jsapi.js").with_user_agent("UA"),
240        )
241        .with_proxy(ProxyInfo::new(ProxyType::Http, "1.2.3.4", 80))
242        .with_cookie_solution(true)
243        .to_task()
244        .unwrap();
245        assert_eq!(
246            task,
247            json!({"type": "AmazonTask", "websiteURL": "https://example.com",
248                   "websiteKey": "KEY", "captchaScript": "https://x/jsapi.js",
249                   "userAgent": "UA", "cookieSolution": true,
250                   "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 80})
251        );
252    }
253
254    #[test]
255    fn option2_minimal_and_full() {
256        let minimal = AmazonWafRequest::new(
257            "https://example.com",
258            AmazonChallengeAndCaptcha::new("https://x/challenge.js", "K", "C", "IV"),
259        )
260        .to_task()
261        .unwrap();
262        assert_eq!(
263            minimal,
264            json!({"type": "AmazonTask", "websiteURL": "https://example.com",
265                   "challengeScript": "https://x/challenge.js", "websiteKey": "K",
266                   "context": "C", "iv": "IV"})
267        );
268
269        let full = AmazonWafRequest::new(
270            "https://example.com",
271            AmazonChallengeAndCaptcha::new("https://x/challenge.js", "K", "C", "IV")
272                .with_captcha_script("https://x/captcha.js"),
273        )
274        .with_cookie_solution(false)
275        .to_task()
276        .unwrap();
277        assert_eq!(
278            full,
279            json!({"type": "AmazonTask", "websiteURL": "https://example.com",
280                   "challengeScript": "https://x/challenge.js", "websiteKey": "K",
281                   "context": "C", "iv": "IV", "captchaScript": "https://x/captcha.js",
282                   "cookieSolution": false})
283        );
284    }
285
286    #[test]
287    fn option3_sends_empty_context_and_iv() {
288        let task = AmazonWafRequest::new(
289            "https://example.com",
290            AmazonChallengeOnly::new("https://x/challenge.js"),
291        )
292        .with_cookie_solution(true)
293        .to_task()
294        .unwrap();
295        assert_eq!(
296            task,
297            json!({"type": "AmazonTask", "websiteURL": "https://example.com",
298                   "challengeScript": "https://x/challenge.js", "context": "", "iv": "",
299                   "cookieSolution": true})
300        );
301    }
302
303    #[test]
304    fn empty_required_strings_are_rejected() {
305        let err = AmazonWafRequest::new("", AmazonChallengeOnly::new("https://x/challenge.js"))
306            .to_task()
307            .unwrap_err();
308        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteURL")));
309        let err = AmazonWafRequest::new("https://example.com", AmazonVisibleCaptcha::new("", "S"))
310            .to_task()
311            .unwrap_err();
312        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteKey")));
313        let err = AmazonWafRequest::new("https://example.com", AmazonVisibleCaptcha::new("K", ""))
314            .to_task()
315            .unwrap_err();
316        assert!(
317            matches!(err, crate::error::Error::Validation(ref m) if m.contains("captchaScript"))
318        );
319        let err = AmazonWafRequest::new(
320            "https://example.com",
321            AmazonChallengeAndCaptcha::new("", "K", "C", "IV"),
322        )
323        .to_task()
324        .unwrap_err();
325        assert!(
326            matches!(err, crate::error::Error::Validation(ref m) if m.contains("challengeScript"))
327        );
328        let err = AmazonWafRequest::new(
329            "https://example.com",
330            AmazonChallengeAndCaptcha::new("S", "", "C", "IV"),
331        )
332        .to_task()
333        .unwrap_err();
334        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteKey")));
335        let err = AmazonWafRequest::new(
336            "https://example.com",
337            AmazonChallengeAndCaptcha::new("S", "K", "", "IV"),
338        )
339        .to_task()
340        .unwrap_err();
341        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("context")));
342        let err = AmazonWafRequest::new(
343            "https://example.com",
344            AmazonChallengeAndCaptcha::new("S", "K", "C", ""),
345        )
346        .to_task()
347        .unwrap_err();
348        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("iv")));
349        let err = AmazonWafRequest::new("https://example.com", AmazonChallengeOnly::new(""))
350            .to_task()
351            .unwrap_err();
352        assert!(
353            matches!(err, crate::error::Error::Validation(ref m) if m.contains("challengeScript"))
354        );
355    }
356}