Skip to main content

capmonstercloud_client/requests/
geetest.rs

1use serde::ser::{Serialize, SerializeMap, Serializer};
2use serde_json::Value;
3
4use super::{require_non_empty, CaptchaRequest};
5use crate::proxy::ProxyInfo;
6use crate::timeouts::GetResultTimeouts;
7
8/// GeeTest version together with the parameters that only exist for that version.
9///
10/// Serialized (flattened into the task) as `"version": 3, "challenge": ...` or
11/// `"version": 4, "initParameters": ...`.
12#[derive(Debug, Clone, PartialEq)]
13pub enum GeetestVersion {
14    /// GeeTest v3. `challenge` is a dynamic key: obtain a fresh one before creating each task
15    /// and before the captcha is initialized on the page (afterwards it becomes invalid).
16    V3 { challenge: String },
17    /// GeeTest v4 (`gt` is the `captcha_id`). `init_parameters` are extra v4 parameters used
18    /// together with `riskType`, e.g. `{"riskType": "slide"}`.
19    V4 { init_parameters: Option<Value> },
20}
21
22impl Serialize for GeetestVersion {
23    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
24        match self {
25            GeetestVersion::V3 { challenge } => {
26                let mut map = serializer.serialize_map(Some(2))?;
27                map.serialize_entry("version", &3)?;
28                map.serialize_entry("challenge", challenge)?;
29                map.end()
30            }
31            GeetestVersion::V4 { init_parameters } => {
32                let mut map = serializer.serialize_map(None)?;
33                map.serialize_entry("version", &4)?;
34                if let Some(params) = init_parameters {
35                    map.serialize_entry("initParameters", params)?;
36                }
37                map.end()
38            }
39        }
40    }
41}
42
43/// GeeTest captcha (`GeeTestTask`), versions 3 and 4.
44#[derive(Debug, Clone, serde::Serialize)]
45pub struct GeetestRequest {
46    /// URL of the page where the captcha is initialized (for v3 usually the `Referer` of the
47    /// `gettype.php` request, which may differ from the URL in the address bar).
48    #[serde(rename = "websiteURL")]
49    pub website_url: String,
50    /// The `gt` key of the domain (for v4: the `captcha_id`).
51    pub gt: String,
52    /// Version-specific parameters.
53    #[serde(flatten)]
54    pub version: GeetestVersion,
55    #[serde(flatten, skip_serializing_if = "Option::is_none")]
56    pub proxy: Option<ProxyInfo>,
57    /// GeeTest API server subdomain (must differ from `api.geetest.com`). May be required
58    /// for some sites.
59    #[serde(
60        rename = "geetestApiServerSubdomain",
61        skip_serializing_if = "Option::is_none"
62    )]
63    pub geetest_api_server_subdomain: Option<String>,
64    /// Path to the captcha script used to display it on the page, **sent as a JSON string**.
65    /// May be required for some sites.
66    #[serde(rename = "geetestGetLib", skip_serializing_if = "Option::is_none")]
67    pub geetest_get_lib: Option<String>,
68    /// Browser User-Agent (use a current Windows one).
69    #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
70    pub user_agent: Option<String>,
71}
72
73impl GeetestRequest {
74    fn with_version(
75        website_url: impl Into<String>,
76        gt: impl Into<String>,
77        version: GeetestVersion,
78    ) -> Self {
79        Self {
80            website_url: website_url.into(),
81            gt: gt.into(),
82            version,
83            proxy: None,
84            geetest_api_server_subdomain: None,
85            geetest_get_lib: None,
86            user_agent: None,
87        }
88    }
89
90    /// GeeTest v3 request; `challenge` is required by the API for this version.
91    pub fn new_v3(
92        website_url: impl Into<String>,
93        gt: impl Into<String>,
94        challenge: impl Into<String>,
95    ) -> Self {
96        Self::with_version(
97            website_url,
98            gt,
99            GeetestVersion::V3 {
100                challenge: challenge.into(),
101            },
102        )
103    }
104
105    /// GeeTest v4 request without `initParameters`.
106    pub fn new_v4(website_url: impl Into<String>, gt: impl Into<String>) -> Self {
107        Self::with_version(
108            website_url,
109            gt,
110            GeetestVersion::V4 {
111                init_parameters: None,
112            },
113        )
114    }
115
116    /// GeeTest v4 request with `initParameters` (e.g. `json!({"riskType": "slide"})`).
117    pub fn new_v4_with_init_parameters(
118        website_url: impl Into<String>,
119        gt: impl Into<String>,
120        init_parameters: Value,
121    ) -> Self {
122        Self::with_version(
123            website_url,
124            gt,
125            GeetestVersion::V4 {
126                init_parameters: Some(init_parameters),
127            },
128        )
129    }
130
131    pub fn with_proxy(mut self, proxy: ProxyInfo) -> Self {
132        self.proxy = Some(proxy);
133        self
134    }
135
136    pub fn with_geetest_api_server_subdomain(mut self, value: impl Into<String>) -> Self {
137        self.geetest_api_server_subdomain = Some(value.into());
138        self
139    }
140
141    pub fn with_geetest_get_lib(mut self, value: impl Into<String>) -> Self {
142        self.geetest_get_lib = Some(value.into());
143        self
144    }
145
146    pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
147        self.user_agent = Some(value.into());
148        self
149    }
150}
151
152impl CaptchaRequest for GeetestRequest {
153    const TASK_TYPE: &'static str = "GeeTestTask";
154
155    fn timeouts(&self) -> GetResultTimeouts {
156        GetResultTimeouts::secs(1.0, 0.0, 1.0, 80.0)
157    }
158
159    fn validate(&self) -> crate::error::Result<()> {
160        require_non_empty("websiteURL", &self.website_url)?;
161        require_non_empty("gt", &self.gt)?;
162        if let GeetestVersion::V3 { challenge } = &self.version {
163            require_non_empty("challenge", challenge)?;
164        }
165        Ok(())
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::proxy::ProxyType;
173    use serde_json::json;
174
175    #[test]
176    fn v3_minimal() {
177        let task = GeetestRequest::new_v3("https://example.com", "GT", "CH")
178            .to_task()
179            .unwrap();
180        assert_eq!(
181            task,
182            json!({"type": "GeeTestTask", "websiteURL": "https://example.com", "gt": "GT",
183                   "version": 3, "challenge": "CH"})
184        );
185    }
186
187    #[test]
188    fn v3_full() {
189        let task = GeetestRequest::new_v3("https://example.com", "GT", "CH")
190            .with_proxy(ProxyInfo::new(ProxyType::Http, "1.2.3.4", 80).with_auth("u", "p"))
191            .with_geetest_api_server_subdomain("api-na.geetest.com")
192            .with_geetest_get_lib("{\"static_servers\":[\"x\"]}")
193            .with_user_agent("UA")
194            .to_task()
195            .unwrap();
196        assert_eq!(
197            task,
198            json!({"type": "GeeTestTask", "websiteURL": "https://example.com", "gt": "GT",
199                   "version": 3, "challenge": "CH",
200                   "geetestApiServerSubdomain": "api-na.geetest.com",
201                   "geetestGetLib": "{\"static_servers\":[\"x\"]}", "userAgent": "UA",
202                   "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 80,
203                   "proxyLogin": "u", "proxyPassword": "p"})
204        );
205    }
206
207    #[test]
208    fn v4_minimal_has_no_challenge() {
209        let task = GeetestRequest::new_v4("https://example.com", "CAPTCHA_ID")
210            .to_task()
211            .unwrap();
212        assert_eq!(
213            task,
214            json!({"type": "GeeTestTask", "websiteURL": "https://example.com",
215                   "gt": "CAPTCHA_ID", "version": 4})
216        );
217    }
218
219    #[test]
220    fn v4_full() {
221        let task = GeetestRequest::new_v4_with_init_parameters(
222            "https://example.com",
223            "CAPTCHA_ID",
224            json!({"riskType": "slide"}),
225        )
226        .with_user_agent("UA")
227        .to_task()
228        .unwrap();
229        assert_eq!(
230            task,
231            json!({"type": "GeeTestTask", "websiteURL": "https://example.com",
232                   "gt": "CAPTCHA_ID", "version": 4,
233                   "initParameters": {"riskType": "slide"}, "userAgent": "UA"})
234        );
235    }
236
237    #[test]
238    fn empty_required_strings_are_rejected() {
239        let err = GeetestRequest::new_v3("", "GT", "CH")
240            .to_task()
241            .unwrap_err();
242        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteURL")));
243        let err = GeetestRequest::new_v3("https://example.com", "", "CH")
244            .to_task()
245            .unwrap_err();
246        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("gt")));
247        let err = GeetestRequest::new_v3("https://example.com", "GT", "")
248            .to_task()
249            .unwrap_err();
250        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("challenge")));
251        let err = GeetestRequest::new_v4("https://example.com", "")
252            .to_task()
253            .unwrap_err();
254        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("gt")));
255    }
256}