Skip to main content

capmonstercloud_client/requests/
recaptcha_v2_enterprise.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/// Additional reCAPTCHA Enterprise parameters (`enterprisePayload` object).
9///
10/// Documented key: `s`, an additional parameter that may be passed to
11/// `grecaptcha.enterprise.render` together with the `sitekey`.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
13pub struct EnterprisePayload {
14    /// Value of the `s` parameter of `grecaptcha.enterprise.render`.
15    pub s: String,
16}
17
18impl EnterprisePayload {
19    pub fn new(s: impl Into<String>) -> Self {
20        Self { s: s.into() }
21    }
22}
23
24/// reCAPTCHA v2 Enterprise (`RecaptchaV2EnterpriseTask`).
25///
26/// Without `proxy` the task is solved from CapMonster's own IPs; a proxy is only needed when the
27/// website does not accept the token.
28#[derive(Debug, Clone, Serialize)]
29pub struct RecaptchaV2EnterpriseRequest {
30    /// Address of a webpage with Google reCAPTCHA Enterprise.
31    #[serde(rename = "websiteURL")]
32    pub website_url: String,
33    /// The reCAPTCHA site key on the target page.
34    #[serde(rename = "websiteKey")]
35    pub website_key: String,
36    /// Proxy used by the worker (optional).
37    #[serde(flatten, skip_serializing_if = "Option::is_none")]
38    pub proxy: Option<ProxyInfo>,
39    /// Additional reCAPTCHA Enterprise parameters.
40    #[serde(rename = "enterprisePayload", skip_serializing_if = "Option::is_none")]
41    pub enterprise_payload: Option<EnterprisePayload>,
42    /// Domain to load reCAPTCHA Enterprise from, e.g. `www.google.com` or `www.recaptcha.net`.
43    #[serde(rename = "apiDomain", skip_serializing_if = "Option::is_none")]
44    pub api_domain: Option<String>,
45    /// Value of the `action` parameter, if it differs from the default `verify`.
46    #[serde(rename = "pageAction", skip_serializing_if = "Option::is_none")]
47    pub page_action: Option<String>,
48    /// Browser User-Agent (use the value supported by CapMonster Cloud).
49    #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
50    pub user_agent: Option<String>,
51    /// Cookies as `"name1=value1; name2=value2"`.
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub cookies: Option<String>,
54}
55
56impl RecaptchaV2EnterpriseRequest {
57    pub fn new(website_url: impl Into<String>, website_key: impl Into<String>) -> Self {
58        Self {
59            website_url: website_url.into(),
60            website_key: website_key.into(),
61            proxy: None,
62            enterprise_payload: None,
63            api_domain: None,
64            page_action: None,
65            user_agent: None,
66            cookies: None,
67        }
68    }
69
70    pub fn with_proxy(mut self, proxy: ProxyInfo) -> Self {
71        self.proxy = Some(proxy);
72        self
73    }
74
75    pub fn with_enterprise_payload(mut self, value: EnterprisePayload) -> Self {
76        self.enterprise_payload = Some(value);
77        self
78    }
79
80    /// Shortcut for `with_enterprise_payload(EnterprisePayload::new(s))`.
81    pub fn with_enterprise_s(self, s: impl Into<String>) -> Self {
82        self.with_enterprise_payload(EnterprisePayload::new(s))
83    }
84
85    pub fn with_api_domain(mut self, value: impl Into<String>) -> Self {
86        self.api_domain = Some(value.into());
87        self
88    }
89
90    pub fn with_page_action(mut self, value: impl Into<String>) -> Self {
91        self.page_action = Some(value.into());
92        self
93    }
94
95    pub fn with_user_agent(mut self, value: impl Into<String>) -> Self {
96        self.user_agent = Some(value.into());
97        self
98    }
99
100    pub fn with_cookies(mut self, value: impl Into<String>) -> Self {
101        self.cookies = Some(value.into());
102        self
103    }
104}
105
106impl CaptchaRequest for RecaptchaV2EnterpriseRequest {
107    const TASK_TYPE: &'static str = "RecaptchaV2EnterpriseTask";
108
109    fn timeouts(&self) -> GetResultTimeouts {
110        GetResultTimeouts::secs(1.0, 10.0, 3.0, 180.0)
111    }
112
113    fn validate(&self) -> Result<()> {
114        require_non_empty("websiteURL", &self.website_url)?;
115        require_non_empty("websiteKey", &self.website_key)?;
116        if let Some(payload) = &self.enterprise_payload {
117            require_non_empty("enterprisePayload.s", &payload.s)?;
118        }
119        Ok(())
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::proxy::ProxyType;
127    use serde_json::json;
128
129    #[test]
130    fn minimal_task() {
131        let task = RecaptchaV2EnterpriseRequest::new("https://example.com", "KEY")
132            .to_task()
133            .unwrap();
134        assert_eq!(
135            task,
136            json!({"type": "RecaptchaV2EnterpriseTask", "websiteURL": "https://example.com", "websiteKey": "KEY"})
137        );
138    }
139
140    #[test]
141    fn full_task() {
142        let task = RecaptchaV2EnterpriseRequest::new("https://example.com", "KEY")
143            .with_proxy(ProxyInfo::new(ProxyType::Http, "1.2.3.4", 8080).with_auth("u", "p"))
144            .with_enterprise_s("SVAL")
145            .with_api_domain("www.recaptcha.net")
146            .with_page_action("login")
147            .with_user_agent("UA")
148            .with_cookies("a=b")
149            .to_task()
150            .unwrap();
151        assert_eq!(
152            task,
153            json!({
154                "type": "RecaptchaV2EnterpriseTask", "websiteURL": "https://example.com",
155                "websiteKey": "KEY",
156                "proxyType": "http", "proxyAddress": "1.2.3.4", "proxyPort": 8080,
157                "proxyLogin": "u", "proxyPassword": "p",
158                "enterprisePayload": {"s": "SVAL"}, "apiDomain": "www.recaptcha.net",
159                "pageAction": "login", "userAgent": "UA", "cookies": "a=b"
160            })
161        );
162    }
163
164    #[test]
165    fn empty_required_strings_are_rejected() {
166        let err = RecaptchaV2EnterpriseRequest::new("", "KEY")
167            .to_task()
168            .unwrap_err();
169        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteURL")));
170        let err = RecaptchaV2EnterpriseRequest::new("https://example.com", "")
171            .to_task()
172            .unwrap_err();
173        assert!(matches!(err, crate::error::Error::Validation(ref m) if m.contains("websiteKey")));
174        let err = RecaptchaV2EnterpriseRequest::new("https://example.com", "KEY")
175            .with_enterprise_s("")
176            .to_task()
177            .unwrap_err();
178        assert!(
179            matches!(err, crate::error::Error::Validation(ref m) if m.contains("enterprisePayload.s"))
180        );
181    }
182}