1use serde::ser::{Serialize, SerializeMap, Serializer};
2
3use super::{require_non_empty, CaptchaRequest};
4use crate::proxy::ProxyInfo;
5use crate::timeouts::GetResultTimeouts;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct AmazonVisibleCaptcha {
10 pub website_key: String,
12 pub captcha_script: String,
14 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#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct AmazonChallengeAndCaptcha {
37 pub challenge_script: String,
39 pub website_key: String,
41 pub context: String,
43 pub iv: String,
45 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#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct AmazonChallengeOnly {
75 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#[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#[derive(Debug, Clone, serde::Serialize)]
149pub struct AmazonWafRequest {
150 #[serde(rename = "websiteURL")]
152 pub website_url: String,
153 #[serde(flatten)]
155 pub variant: AmazonWafVariant,
156 #[serde(flatten, skip_serializing_if = "Option::is_none")]
157 pub proxy: Option<ProxyInfo>,
158 #[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 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}