Skip to main content

capmonstercloud_client/requests/
tspd.rs

1use serde::Serialize;
2
3use super::{require_non_empty, CaptchaRequest};
4use crate::error::{Error, Result};
5use crate::proxy::ProxyInfo;
6use crate::timeouts::GetResultTimeouts;
7
8/// TSPD challenge data. Values are dynamic: extract them right before creating the task.
9#[derive(Debug, Clone, Serialize)]
10pub struct TspdMetadata {
11    /// Cookies obtained on the TSPD challenge page, e.g. `"TS386a400d029=0826...; ...;"`.
12    #[serde(rename = "tspdCookie")]
13    pub tspd_cookie: String,
14    /// The entire TSPD page encoded in base64.
15    #[serde(rename = "htmlPageBase64")]
16    pub html_page_base64: String,
17}
18
19impl TspdMetadata {
20    pub fn new(tspd_cookie: impl Into<String>, html_page_base64: impl Into<String>) -> Self {
21        Self {
22            tspd_cookie: tspd_cookie.into(),
23            html_page_base64: html_page_base64.into(),
24        }
25    }
26}
27
28/// TSPD (`CustomTask`, class `tspd`). Your own proxy is required.
29#[derive(Debug, Clone, Serialize)]
30pub struct TspdCustomTaskRequest {
31    /// URL of the page where the TSPD challenge is located.
32    #[serde(rename = "websiteURL")]
33    pub website_url: String,
34    pub metadata: TspdMetadata,
35    /// Required proxy. The proxy login must not contain the characters `;`, `|`, `)`.
36    #[serde(flatten)]
37    pub proxy: ProxyInfo,
38    /// Browser User-Agent (current Windows UA); should match the one used when the cookie and
39    /// the page were obtained.
40    #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
41    pub user_agent: Option<String>,
42}
43
44impl TspdCustomTaskRequest {
45    pub fn new(website_url: impl Into<String>, metadata: TspdMetadata, proxy: ProxyInfo) -> Self {
46        Self {
47            website_url: website_url.into(),
48            metadata,
49            proxy,
50            user_agent: None,
51        }
52    }
53
54    pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
55        self.user_agent = Some(value.into());
56        self
57    }
58}
59
60impl CaptchaRequest for TspdCustomTaskRequest {
61    const TASK_TYPE: &'static str = "CustomTask";
62    const CLASS: Option<&'static str> = Some("tspd");
63
64    fn timeouts(&self) -> GetResultTimeouts {
65        GetResultTimeouts::secs(1.0, 0.0, 1.0, 60.0)
66    }
67
68    fn validate(&self) -> Result<()> {
69        require_non_empty("websiteURL", &self.website_url)?;
70        require_non_empty("metadata.tspdCookie", &self.metadata.tspd_cookie)?;
71        require_non_empty("metadata.htmlPageBase64", &self.metadata.html_page_base64)?;
72        if let Some(login) = &self.proxy.proxy_login {
73            if login.contains([';', '|', ')']) {
74                return Err(Error::Validation(
75                    "proxy login must not contain the characters ';', '|', ')' for TSPD".into(),
76                ));
77            }
78        }
79        Ok(())
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::proxy::ProxyType;
87    use serde_json::json;
88
89    fn proxy() -> ProxyInfo {
90        ProxyInfo::new(ProxyType::Http, "1.2.3.4", 8080).with_auth("u", "p")
91    }
92
93    #[test]
94    fn minimal_task() {
95        let task = TspdCustomTaskRequest::new(
96            "https://example.com",
97            TspdMetadata::new("TS1=a", "PGh0bWw+"),
98            proxy(),
99        )
100        .to_task()
101        .unwrap();
102        assert_eq!(
103            task,
104            json!({
105                "type": "CustomTask", "class": "tspd", "websiteURL": "https://example.com",
106                "metadata": {"tspdCookie": "TS1=a", "htmlPageBase64": "PGh0bWw+"},
107                "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080,
108                "proxyLogin": "u", "proxyPassword": "p"
109            })
110        );
111    }
112
113    #[test]
114    fn full_task() {
115        let task =
116            TspdCustomTaskRequest::new("https://example.com", TspdMetadata::new("C", "H"), proxy())
117                .with_user_agent("UA")
118                .to_task()
119                .unwrap();
120        assert_eq!(task["userAgent"], "UA");
121        assert_eq!(
122            task["metadata"],
123            json!({"tspdCookie": "C", "htmlPageBase64": "H"})
124        );
125    }
126
127    #[test]
128    fn rejects_forbidden_login_chars() {
129        for bad in ["a;b", "a|b", "a)b"] {
130            let p = ProxyInfo::new(ProxyType::Http, "1.2.3.4", 8080).with_auth(bad, "p");
131            let req = TspdCustomTaskRequest::new("https://e.com", TspdMetadata::new("C", "H"), p);
132            assert!(matches!(req.to_task(), Err(Error::Validation(_))), "{bad}");
133        }
134    }
135
136    #[test]
137    fn empty_required_strings_are_rejected() {
138        for (url, cookie, html, field) in [
139            ("", "C", "H", "websiteURL"),
140            ("https://example.com", "", "H", "metadata.tspdCookie"),
141            ("https://example.com", "C", "", "metadata.htmlPageBase64"),
142        ] {
143            let err = TspdCustomTaskRequest::new(url, TspdMetadata::new(cookie, html), proxy())
144                .to_task()
145                .unwrap_err();
146            assert!(matches!(err, Error::Validation(ref m) if m.contains(field)));
147        }
148    }
149}