Skip to main content

ezcapsolver/
solution.rs

1use std::collections::{BTreeMap, HashMap};
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
4use serde_json::Value;
5
6use crate::{Error, Result};
7
8/// Raw solution field from a task response.
9#[derive(Debug, Clone, PartialEq, Default)]
10pub enum Solution {
11    /// The response did not include a solution field.
12    #[default]
13    Missing,
14    /// The response included a solution value, including JSON null.
15    Value(Value),
16}
17
18impl Solution {
19    /// Returns true when the response did not include a solution field.
20    #[must_use]
21    pub const fn is_missing(&self) -> bool {
22        matches!(self, Self::Missing)
23    }
24
25    /// Returns the raw JSON value when the field was present.
26    #[must_use]
27    pub const fn as_value(&self) -> Option<&Value> {
28        match self {
29            Self::Missing => None,
30            Self::Value(value) => Some(value),
31        }
32    }
33
34    /// Consumes the wrapper and returns the raw JSON value when present.
35    #[must_use]
36    pub fn into_value(self) -> Option<Value> {
37        match self {
38            Self::Missing => None,
39            Self::Value(value) => Some(value),
40        }
41    }
42
43    /// Deserializes the raw JSON value into a caller-selected type.
44    ///
45    /// The raw value is borrowed, so only the fields `T` actually reads are
46    /// copied and the stored solution remains available afterwards.
47    pub fn deserialize<T>(&self) -> Result<T>
48    where
49        T: DeserializeOwned,
50    {
51        let value = self.as_value().ok_or_else(|| {
52            Error::UnexpectedResponse("ready task result does not contain a solution".to_owned())
53        })?;
54        // Clone only on the failure path: when decoding fails the raw value is the
55        // only thing left to diagnose it with, so it must not go down with the
56        // TaskResult.
57        T::deserialize(value).map_err(|source| Error::SolutionDecode {
58            source,
59            raw: Box::new(value.clone()),
60        })
61    }
62}
63
64impl Serialize for Solution {
65    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
66    where
67        S: Serializer,
68    {
69        match self {
70            Self::Missing => serializer.serialize_none(),
71            Self::Value(value) => value.serialize(serializer),
72        }
73    }
74}
75
76impl<'de> Deserialize<'de> for Solution {
77    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
78    where
79        D: Deserializer<'de>,
80    {
81        Value::deserialize(deserializer).map(Self::Value)
82    }
83}
84
85/// ReCaptcha token solution observed from V2 and V3 workers.
86///
87/// Only the token is guaranteed. The two header companions are emitted by some
88/// worker builds and not others, so they default to an empty string rather than
89/// failing the decode — a task that solved and was billed must not be thrown
90/// away over a field the caller may not even need.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[serde(remote = "Self")]
93pub struct RecaptchaSolution {
94    /// Token submitted to the protected website.
95    #[serde(rename = "gRecaptchaResponse")]
96    pub token: String,
97    /// Matching `Sec-CH-UA` request header, empty when the worker omits it.
98    #[serde(default)]
99    pub sec_ch_ua: String,
100    /// Matching user agent, empty when the worker omits it.
101    #[serde(default)]
102    pub user_agent: String,
103    /// Additional worker fields.
104    #[serde(flatten, skip_serializing)]
105    pub extra: BTreeMap<String, Value>,
106}
107
108/// FunCaptcha (Arkose Labs) token solution.
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110#[serde(remote = "Self")]
111pub struct FunCaptchaSolution {
112    /// Token submitted to the protected website.
113    pub token: String,
114    /// Additional worker fields.
115    #[serde(flatten, skip_serializing)]
116    pub extra: BTreeMap<String, Value>,
117}
118
119/// FunCaptcha classification result, shape not yet confirmed.
120///
121/// No reliable sample exists yet, so this declares no fields of its own —
122/// everything the worker returns lands in [`Self::extra`]. Add fields here as
123/// samples confirm them.
124#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
125#[non_exhaustive]
126#[serde(remote = "Self")]
127pub struct FunCaptchaClassificationSolution {
128    /// Every field the worker returned.
129    #[serde(flatten, skip_serializing)]
130    pub extra: BTreeMap<String, Value>,
131}
132
133/// HCaptcha classification result, shape not yet confirmed.
134///
135/// No reliable sample exists yet, so this declares no fields of its own —
136/// everything the worker returns lands in [`Self::extra`]. Add fields here as
137/// samples confirm them.
138#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
139#[non_exhaustive]
140#[serde(remote = "Self")]
141pub struct HcaptchaClassificationSolution {
142    /// Every field the worker returned.
143    #[serde(flatten, skip_serializing)]
144    pub extra: BTreeMap<String, Value>,
145}
146
147/// Cloudflare Turnstile solution.
148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
149#[serde(remote = "Self")]
150pub struct CloudflareTurnstileSolution {
151    /// Token submitted to the protected website.
152    pub token: String,
153    /// Request headers to replay together with the token.
154    #[serde(default)]
155    pub header: BTreeMap<String, String>,
156    /// Additional worker fields.
157    #[serde(flatten, skip_serializing)]
158    pub extra: BTreeMap<String, Value>,
159}
160
161/// Cloudflare five-second challenge solution.
162///
163/// Every field describes one part of the browser state the worker ended up
164/// with; replaying the headers and cookies against the protected site is what
165/// actually clears the challenge.
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167#[serde(remote = "Self", rename_all = "camelCase")]
168pub struct Cloudflare5sSolution {
169    /// Request headers to replay against the protected site.
170    #[serde(default)]
171    pub header: BTreeMap<String, String>,
172    /// Clearance cookies returned by the worker.
173    #[serde(default)]
174    pub cookies: BTreeMap<String, String>,
175    /// Browser fingerprint used by the worker, such as `chrome149`.
176    #[serde(default)]
177    pub tls_version: String,
178    /// Challenge page body, empty when the worker captured none.
179    #[serde(default)]
180    pub body: String,
181    /// Turnstile token embedded in the challenge, empty when the flow did not
182    /// produce one.
183    #[serde(default)]
184    pub s_token: String,
185    /// Additional worker fields.
186    #[serde(flatten, skip_serializing)]
187    pub extra: BTreeMap<String, Value>,
188}
189
190/// HCaptcha solution observed from current workers.
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192#[serde(remote = "Self")]
193pub struct HcaptchaSolution {
194    /// Generated HCaptcha pass identifier.
195    #[serde(rename = "generated_pass_UUID")]
196    pub generated_pass_uuid: String,
197    /// User agent associated with the result.
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub ua: Option<String>,
200    /// lang
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    pub lang: Option<String>,
203    /// Additional worker fields.
204    #[serde(flatten, skip_serializing)]
205    pub extra: BTreeMap<String, Value>,
206}
207
208/// PerimeterX solution.
209///
210/// The worker returns the clearance cookies as top-level fields, not nested
211/// under a `cookies` object. Field names carry a leading underscore on the
212/// wire; the Rust fields drop it because a leading underscore means
213/// "intentionally unused" here.
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
215#[serde(remote = "Self")]
216pub struct PerimeterXSolution {
217    /// `_px3` clearance cookie, the value that actually passes the check.
218    #[serde(rename = "_px3")]
219    pub px3: String,
220    /// `_pxvid` visitor identifier.
221    #[serde(rename = "_pxvid", default)]
222    pub pxvid: String,
223    /// `_pxde` data-enrichment cookie.
224    #[serde(rename = "_pxde", default)]
225    pub pxde: String,
226    /// Additional worker fields.
227    #[serde(flatten, skip_serializing)]
228    pub extra: BTreeMap<String, Value>,
229}
230
231/// Akamai Web solution used in multi-round flows.
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233#[serde(remote = "Self")]
234pub struct AkamaiWebSolution {
235    /// Sensor payload for the current round.
236    pub payload: String,
237    /// Encoded state passed to the next round.
238    ///
239    /// Sent back as `encodeData` in the following round. A round that ends the
240    /// flow carries none, so its absence is not a decoding failure.
241    #[serde(default)]
242    pub encodedata: String,
243    /// Additional worker fields.
244    #[serde(flatten, skip_serializing)]
245    pub extra: BTreeMap<String, Value>,
246}
247
248/// Akamai SBSD solution.
249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
250#[serde(remote = "Self")]
251pub struct AkamaiSbsdSolution {
252    /// Base64-encoded payload returned by the worker.
253    pub payload: String,
254    /// `bm_lso_time` value produced alongside the payload.
255    ///
256    /// Optional: not every response carries one.
257    #[serde(default, skip_serializing_if = "Option::is_none")]
258    pub bm_lso_time: Option<String>,
259    /// Additional worker fields.
260    #[serde(flatten, skip_serializing)]
261    pub extra: BTreeMap<String, Value>,
262}
263
264/// TLS forwarding response.
265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
266#[serde(remote = "Self")]
267pub struct TlsForwardSolution {
268    /// Worker response status.
269    #[serde(default)]
270    pub status: u16,
271    /// Upstream HTTP status code.
272    #[serde(default)]
273    pub code: u16,
274    /// Upstream response headers.
275    #[serde(default)]
276    pub headers: BTreeMap<String, Value>,
277    /// Upstream response cookies.
278    #[serde(default)]
279    pub cookies: HashMap<String, Value>,
280    /// Upstream response body.
281    #[serde(default)]
282    pub body: String,
283    /// Additional worker fields.
284    #[serde(flatten, skip_serializing)]
285    pub extra: BTreeMap<String, Value>,
286}
287
288/// ReCaptcha V2 image classification result.
289///
290/// Unknown type values are preserved for the caller to interpret. Missing
291/// fields use their defaults: an empty type, `false`, and an empty object list.
292#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
293#[serde(remote = "Self", default, rename_all = "camelCase")]
294pub struct ReClassificationSolution {
295    /// Worker result type, usually `multi` or `single`.
296    pub r#type: String,
297    /// Whether a single image contains the requested object.
298    pub has_object: bool,
299    /// Zero-based cell indexes to select.
300    pub objects: Vec<usize>,
301    /// Additional worker fields.
302    #[serde(flatten, skip_serializing)]
303    pub extra: BTreeMap<String, Value>,
304}
305
306impl ReClassificationSolution {
307    /// Returns true when the worker reported a multi-cell grid result.
308    #[must_use]
309    pub fn is_multi(&self) -> bool {
310        self.r#type == "multi"
311    }
312
313    /// Returns true when the worker reported a single-image result.
314    #[must_use]
315    pub fn is_single(&self) -> bool {
316        self.r#type == "single"
317    }
318}
319
320/// DataDome solution, returned by both challenge steps.
321///
322/// Step 1 carries the challenge URL in `url`; step 2 carries the validation
323/// instructions. Earlier workers returned step 1 as a bare string — it is an
324/// object now, so both steps decode into this one type.
325///
326/// Shared by `DataDomeTaskProxyless` and `DataDomeTagsTaskProxyless`.
327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
328#[serde(remote = "Self")]
329pub struct DataDomeSolution {
330    /// Challenge kind, such as `slider` or `interstitial`.
331    #[serde(default)]
332    pub kind: Option<String>,
333    /// Challenge URL on step 1, validation endpoint on step 2.
334    #[serde(default)]
335    pub url: Option<String>,
336    /// Optional validation request body.
337    #[serde(default)]
338    pub body: Option<String>,
339    /// Additional worker fields.
340    #[serde(flatten, skip_serializing)]
341    pub extra: BTreeMap<String, Value>,
342}
343
344/// Incapsula worker response envelope.
345#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
346#[serde(remote = "Self")]
347pub struct IncapsulaSolution {
348    /// Inner worker status code.
349    #[serde(default)]
350    pub status: Option<u16>,
351    /// JSON string posted to the Incapsula sensor endpoint.
352    ///
353    /// This is stringified JSON and is submitted verbatim; decoding it here
354    /// would change what the caller has to send.
355    pub data: String,
356    /// Additional worker fields.
357    #[serde(flatten, skip_serializing)]
358    pub extra: BTreeMap<String, Value>,
359}
360
361/// Gives a result model both halves of its serde handling.
362///
363/// Decoding keeps the derived behaviour: `extra` is flattened, so a field this
364/// release does not declare is collected instead of dropped. Encoding is the
365/// half that needs rewriting — a flattened map is written after the declared
366/// fields and would emit a colliding key a second time, which reads back as
367/// whichever copy the parser keeps. `skip_serializing` takes `extra` out of the
368/// derived encoder so the merge can put it back under the declared fields.
369///
370/// `#[serde(remote = "Self")]` keeps both derived implementations reachable as
371/// inherent functions, so neither call below recurses.
372macro_rules! impl_solution_serde {
373    ($($ty:ident),+ $(,)?) => {$(
374        impl Serialize for $ty {
375            fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
376            where
377                S: Serializer,
378            {
379                crate::wire::merge_extra(|s| <$ty>::serialize(self, s), &self.extra, serializer)
380            }
381        }
382
383        impl<'de> Deserialize<'de> for $ty {
384            fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
385            where
386                D: Deserializer<'de>,
387            {
388                <$ty>::deserialize(deserializer)
389            }
390        }
391    )+};
392}
393
394impl_solution_serde!(
395    AkamaiSbsdSolution,
396    AkamaiWebSolution,
397    Cloudflare5sSolution,
398    CloudflareTurnstileSolution,
399    DataDomeSolution,
400    FunCaptchaClassificationSolution,
401    FunCaptchaSolution,
402    HcaptchaClassificationSolution,
403    HcaptchaSolution,
404    IncapsulaSolution,
405    PerimeterXSolution,
406    ReClassificationSolution,
407    RecaptchaSolution,
408    TlsForwardSolution,
409);