Skip to main content

capmonstercloud_client/requests/
alibaba.rs

1use serde::Serialize;
2
3use super::{require_non_empty, CaptchaRequest};
4use crate::error::Result;
5use crate::proxy::ProxyInfo;
6use crate::timeouts::GetResultTimeouts;
7
8/// Alibaba captcha parameters. Only `scene_id` and `prefix` are always required; set the other
9/// ones only if the target website uses them.
10#[derive(Debug, Clone, Serialize)]
11pub struct AlibabaMetadata {
12    /// Captcha scenario identifier, e.g. `"1ww7426c"`.
13    #[serde(rename = "sceneId")]
14    pub scene_id: String,
15    /// Captcha initialization parameter: the subdomain of the request URL that loads the task,
16    /// e.g. `dlw3kug` for `https://dlw3kug.captcha-open.example.aliyuncs.com/`.
17    pub prefix: String,
18    /// Unique user/session identifier on the website side.
19    #[serde(rename = "userId", skip_serializing_if = "Option::is_none")]
20    pub user_id: Option<String>,
21    /// Additional (secondary) user identifier.
22    #[serde(rename = "userUserId", skip_serializing_if = "Option::is_none")]
23    pub user_user_id: Option<String>,
24    /// Version/type of the verification mechanism, e.g. `"1.0"`.
25    #[serde(rename = "verifyType", skip_serializing_if = "Option::is_none")]
26    pub verify_type: Option<String>,
27    /// Server region, e.g. `"sgp"`. Affects the token type (`cn` gives `WEB#`, `sgp` gives `SG_WEB#`).
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub region: Option<String>,
30    /// Verification ID of the current captcha session.
31    #[serde(rename = "UserCertifyId", skip_serializing_if = "Option::is_none")]
32    pub user_certify_id: Option<String>,
33    /// Link to the captcha JS library used by the website (may change on every page render).
34    #[serde(rename = "apiGetLib", skip_serializing_if = "Option::is_none")]
35    pub api_get_lib: Option<String>,
36    /// Full URL (with query) of the verification page, for websites where Alibaba returns a
37    /// separate `/punish` URL.
38    #[serde(rename = "punishUrl", skip_serializing_if = "Option::is_none")]
39    pub punish_url: Option<String>,
40    /// Return cookies together with the solution (use `true` for Alibaba WAF sites).
41    #[serde(rename = "cookieRequired", skip_serializing_if = "Option::is_none")]
42    pub cookie_required: Option<bool>,
43}
44
45impl AlibabaMetadata {
46    pub fn new(scene_id: impl Into<String>, prefix: impl Into<String>) -> Self {
47        Self {
48            scene_id: scene_id.into(),
49            prefix: prefix.into(),
50            user_id: None,
51            user_user_id: None,
52            verify_type: None,
53            region: None,
54            user_certify_id: None,
55            api_get_lib: None,
56            punish_url: None,
57            cookie_required: None,
58        }
59    }
60
61    pub fn with_user_id(mut self, value: impl Into<String>) -> Self {
62        self.user_id = Some(value.into());
63        self
64    }
65
66    pub fn with_user_user_id(mut self, value: impl Into<String>) -> Self {
67        self.user_user_id = Some(value.into());
68        self
69    }
70
71    pub fn with_verify_type(mut self, value: impl Into<String>) -> Self {
72        self.verify_type = Some(value.into());
73        self
74    }
75
76    pub fn with_region(mut self, value: impl Into<String>) -> Self {
77        self.region = Some(value.into());
78        self
79    }
80
81    pub fn with_user_certify_id(mut self, value: impl Into<String>) -> Self {
82        self.user_certify_id = Some(value.into());
83        self
84    }
85
86    pub fn with_api_get_lib(mut self, value: impl Into<String>) -> Self {
87        self.api_get_lib = Some(value.into());
88        self
89    }
90
91    pub fn with_punish_url(mut self, value: impl Into<String>) -> Self {
92        self.punish_url = Some(value.into());
93        self
94    }
95
96    pub fn with_cookie_required(mut self, value: bool) -> Self {
97        self.cookie_required = Some(value);
98        self
99    }
100}
101
102/// Alibaba CAPTCHA / WAF (`CustomTask`, class `alibaba`).
103#[derive(Debug, Clone, Serialize)]
104pub struct AlibabaCustomTaskRequest {
105    /// Full URL of the page with the captcha.
106    #[serde(rename = "websiteURL")]
107    pub website_url: String,
108    pub metadata: AlibabaMetadata,
109    #[serde(flatten, skip_serializing_if = "Option::is_none")]
110    pub proxy: Option<ProxyInfo>,
111    /// Browser User-Agent (current Windows UA supported by CapMonster Cloud).
112    #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
113    pub user_agent: Option<String>,
114}
115
116impl AlibabaCustomTaskRequest {
117    pub fn new(website_url: impl Into<String>, metadata: AlibabaMetadata) -> Self {
118        Self {
119            website_url: website_url.into(),
120            metadata,
121            proxy: None,
122            user_agent: None,
123        }
124    }
125
126    pub fn with_proxy(mut self, proxy: ProxyInfo) -> Self {
127        self.proxy = Some(proxy);
128        self
129    }
130
131    pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
132        self.user_agent = Some(value.into());
133        self
134    }
135}
136
137impl CaptchaRequest for AlibabaCustomTaskRequest {
138    const TASK_TYPE: &'static str = "CustomTask";
139    const CLASS: Option<&'static str> = Some("alibaba");
140
141    fn timeouts(&self) -> GetResultTimeouts {
142        GetResultTimeouts::secs(1.0, 0.0, 3.0, 120.0)
143    }
144
145    fn validate(&self) -> Result<()> {
146        require_non_empty("websiteURL", &self.website_url)?;
147        require_non_empty("metadata.sceneId", &self.metadata.scene_id)?;
148        require_non_empty("metadata.prefix", &self.metadata.prefix)?;
149        Ok(())
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::proxy::ProxyType;
157    use serde_json::json;
158
159    #[test]
160    fn minimal_task() {
161        let task = AlibabaCustomTaskRequest::new(
162            "https://example.com",
163            AlibabaMetadata::new("1ww7426c", "dlw3kug"),
164        )
165        .to_task()
166        .unwrap();
167        assert_eq!(
168            task,
169            json!({
170                "type": "CustomTask", "class": "alibaba", "websiteURL": "https://example.com",
171                "metadata": {"sceneId": "1ww7426c", "prefix": "dlw3kug"}
172            })
173        );
174    }
175
176    #[test]
177    fn full_task() {
178        let meta = AlibabaMetadata::new("S", "P")
179            .with_user_id("U")
180            .with_user_user_id("UU")
181            .with_verify_type("1.0")
182            .with_region("sgp")
183            .with_user_certify_id("CID")
184            .with_api_get_lib("https://x/AliyunCaptcha.js")
185            .with_punish_url("https://x/punish?a=b")
186            .with_cookie_required(true);
187        let task = AlibabaCustomTaskRequest::new("https://example.com", meta)
188            .with_proxy(ProxyInfo::new(ProxyType::Http, "1.2.3.4", 80))
189            .with_user_agent("UA")
190            .to_task()
191            .unwrap();
192        assert_eq!(
193            task,
194            json!({
195                "type": "CustomTask", "class": "alibaba", "websiteURL": "https://example.com",
196                "metadata": {
197                    "sceneId": "S", "prefix": "P", "userId": "U", "userUserId": "UU",
198                    "verifyType": "1.0", "region": "sgp", "UserCertifyId": "CID",
199                    "apiGetLib": "https://x/AliyunCaptcha.js", "punishUrl": "https://x/punish?a=b",
200                    "cookieRequired": true
201                },
202                "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 80, "userAgent": "UA"
203            })
204        );
205    }
206
207    #[test]
208    fn empty_required_strings_are_rejected() {
209        let err = AlibabaCustomTaskRequest::new("", AlibabaMetadata::new("S", "P"))
210            .to_task()
211            .unwrap_err();
212        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteURL")));
213        let err =
214            AlibabaCustomTaskRequest::new("https://example.com", AlibabaMetadata::new("", "P"))
215                .to_task()
216                .unwrap_err();
217        assert!(
218            matches!(err, crate::error::Error::Validation(ref m) if m.contains("metadata.sceneId"))
219        );
220        let err =
221            AlibabaCustomTaskRequest::new("https://example.com", AlibabaMetadata::new("S", ""))
222                .to_task()
223                .unwrap_err();
224        assert!(
225            matches!(err, crate::error::Error::Validation(ref m) if m.contains("metadata.prefix"))
226        );
227    }
228}