cloudscraper_rs/challenges/
pipeline.rs

1//! Challenge orchestration pipeline.
2//!
3//! Brings together the detector and the individual solver/mitigation handlers in
4//! a single entry point. The pipeline analyses a [`ChallengeResponse`] to figure
5//! out which solver should run and returns the next action the caller should
6//! perform (submit a payload, apply a mitigation plan, or declare the response
7//! unsupported).
8
9use std::fmt;
10
11use thiserror::Error;
12
13use crate::challenges::core::{ChallengeResponse, ChallengeSubmission};
14use crate::challenges::detectors::{ChallengeDetection, ChallengeDetector, ChallengeType};
15use crate::challenges::solvers::{
16    FailureRecorder, FingerprintManager, MitigationPlan, TlsProfileManager,
17    access_denied::{AccessDeniedError, AccessDeniedHandler, ProxyPool},
18    bot_management::{BotManagementError, BotManagementHandler},
19    javascript_v1::{JavascriptV1Error, JavascriptV1Solver},
20    javascript_v2::{JavascriptV2Error, JavascriptV2Solver},
21    managed_v3::{ManagedV3Error, ManagedV3Solver},
22    rate_limit::{RateLimitError, RateLimitHandler},
23    turnstile::{TurnstileError, TurnstileSolver},
24};
25
26/// Operational context passed to the pipeline when mitigation handlers need to
27/// mutate shared services (proxy pool, TLS manager, fingerprint generator…).
28#[derive(Default)]
29pub struct PipelineContext<'a> {
30    pub proxy_pool: Option<&'a mut dyn ProxyPool>,
31    pub current_proxy: Option<&'a str>,
32    pub failure_recorder: Option<&'a dyn FailureRecorder>,
33    pub fingerprint_manager: Option<&'a mut dyn FingerprintManager>,
34    pub tls_manager: Option<&'a mut dyn TlsProfileManager>,
35}
36
37/// High level result returned by the pipeline after analysing a response.
38#[derive(Debug)]
39pub enum ChallengePipelineResult {
40    /// The response does not look like a Cloudflare challenge.
41    NoChallenge,
42    /// A solver produced a submission payload that should be posted back to Cloudflare.
43    Submission {
44        detection: ChallengeDetection,
45        submission: ChallengeSubmission,
46    },
47    /// A mitigation-only handler provided a retry/back-off plan.
48    Mitigation {
49        detection: ChallengeDetection,
50        plan: MitigationPlan,
51    },
52    /// The pipeline detected a challenge but lacks the required solver or dependency.
53    Unsupported {
54        detection: ChallengeDetection,
55        reason: UnsupportedReason,
56    },
57    /// An available solver failed while processing the challenge.
58    Failed {
59        detection: ChallengeDetection,
60        error: PipelineError,
61    },
62}
63
64/// Reason why the pipeline could not act on a detected challenge.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum UnsupportedReason {
67    MissingSolver(&'static str),
68    MissingDependency(&'static str),
69    UnknownChallenge,
70}
71
72/// Wrapper around individual solver error types.
73#[derive(Debug, Error)]
74pub enum PipelineError {
75    #[error("javascript v1 solver error: {0}")]
76    JavascriptV1(#[from] JavascriptV1Error),
77    #[error("javascript v2 solver error: {0}")]
78    JavascriptV2(#[from] JavascriptV2Error),
79    #[error("managed v3 solver error: {0}")]
80    ManagedV3(#[from] ManagedV3Error),
81    #[error("turnstile solver error: {0}")]
82    Turnstile(#[from] TurnstileError),
83    #[error("rate limit handler error: {0}")]
84    RateLimit(#[from] RateLimitError),
85    #[error("access denied handler error: {0}")]
86    AccessDenied(#[from] AccessDeniedError),
87    #[error("bot management handler error: {0}")]
88    BotManagement(#[from] BotManagementError),
89}
90
91impl fmt::Display for UnsupportedReason {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        match self {
94            UnsupportedReason::MissingSolver(name) => {
95                write!(f, "required solver '{name}' is not configured")
96            }
97            UnsupportedReason::MissingDependency(name) => {
98                write!(f, "missing required dependency: {name}")
99            }
100            UnsupportedReason::UnknownChallenge => write!(f, "unrecognised challenge"),
101        }
102    }
103}
104
105// Display is provided by the thiserror derive.
106
107/// Coordinates challenge detection and solver selection.
108pub struct ChallengePipeline {
109    detector: ChallengeDetector,
110    javascript_v1: Option<JavascriptV1Solver>,
111    javascript_v2: Option<JavascriptV2Solver>,
112    managed_v3: Option<ManagedV3Solver>,
113    turnstile: Option<TurnstileSolver>,
114    rate_limit: Option<RateLimitHandler>,
115    access_denied: Option<AccessDeniedHandler>,
116    bot_management: Option<BotManagementHandler>,
117}
118
119impl ChallengePipeline {
120    /// Create a pipeline with the provided detector and no solvers configured.
121    pub fn new(detector: ChallengeDetector) -> Self {
122        Self {
123            detector,
124            javascript_v1: None,
125            javascript_v2: None,
126            managed_v3: None,
127            turnstile: None,
128            rate_limit: None,
129            access_denied: None,
130            bot_management: None,
131        }
132    }
133
134    /// Replace the underlying detector.
135    pub fn set_detector(&mut self, detector: ChallengeDetector) {
136        self.detector = detector;
137    }
138
139    /// Borrow the detector immutably.
140    pub fn detector(&self) -> &ChallengeDetector {
141        &self.detector
142    }
143
144    /// Borrow the detector mutably (e.g. to read metrics or adaptive patterns).
145    pub fn detector_mut(&mut self) -> &mut ChallengeDetector {
146        &mut self.detector
147    }
148
149    /// Attach the JavaScript v1 solver.
150    pub fn with_javascript_v1(mut self, solver: JavascriptV1Solver) -> Self {
151        self.javascript_v1 = Some(solver);
152        self
153    }
154
155    /// Attach the JavaScript v2 solver.
156    pub fn with_javascript_v2(mut self, solver: JavascriptV2Solver) -> Self {
157        self.javascript_v2 = Some(solver);
158        self
159    }
160
161    /// Attach the Managed Challenge v3 solver.
162    pub fn with_managed_v3(mut self, solver: ManagedV3Solver) -> Self {
163        self.managed_v3 = Some(solver);
164        self
165    }
166
167    /// Attach the Turnstile solver.
168    pub fn with_turnstile(mut self, solver: TurnstileSolver) -> Self {
169        self.turnstile = Some(solver);
170        self
171    }
172
173    /// Attach the rate limit mitigation handler.
174    pub fn with_rate_limit(mut self, handler: RateLimitHandler) -> Self {
175        self.rate_limit = Some(handler);
176        self
177    }
178
179    /// Attach the access denied mitigation handler.
180    pub fn with_access_denied(mut self, handler: AccessDeniedHandler) -> Self {
181        self.access_denied = Some(handler);
182        self
183    }
184
185    /// Attach the bot management mitigation handler.
186    pub fn with_bot_management(mut self, handler: BotManagementHandler) -> Self {
187        self.bot_management = Some(handler);
188        self
189    }
190
191    /// Evaluate a response and decide which solver should handle it.
192    pub async fn evaluate<'a>(
193        &'a mut self,
194        response: &ChallengeResponse<'_>,
195        context: PipelineContext<'a>,
196    ) -> ChallengePipelineResult {
197        let Some(detection) = self.detector.detect(response) else {
198            return ChallengePipelineResult::NoChallenge;
199        };
200
201        let PipelineContext {
202            proxy_pool,
203            current_proxy,
204            failure_recorder,
205            fingerprint_manager,
206            tls_manager,
207        } = context;
208
209        let detection_for_branch = detection.clone();
210
211        match detection.challenge_type {
212            ChallengeType::JavaScriptV1 => {
213                let Some(solver) = self.javascript_v1.as_ref() else {
214                    return unsupported(
215                        detection_for_branch,
216                        UnsupportedReason::MissingSolver("javascript_v1"),
217                    );
218                };
219                match solver.solve(response) {
220                    Ok(submission) => ChallengePipelineResult::Submission {
221                        detection: detection_for_branch,
222                        submission,
223                    },
224                    Err(err) => ChallengePipelineResult::Failed {
225                        detection: detection_for_branch,
226                        error: PipelineError::JavascriptV1(err),
227                    },
228                }
229            }
230            ChallengeType::JavaScriptV2 => {
231                let Some(solver) = self.javascript_v2.as_ref() else {
232                    return unsupported(
233                        detection_for_branch,
234                        UnsupportedReason::MissingSolver("javascript_v2"),
235                    );
236                };
237
238                let result = if JavascriptV2Solver::is_captcha_challenge(response) {
239                    solver.solve_with_captcha(response).await
240                } else {
241                    solver.solve(response)
242                };
243
244                match result {
245                    Ok(submission) => ChallengePipelineResult::Submission {
246                        detection: detection_for_branch,
247                        submission,
248                    },
249                    Err(JavascriptV2Error::CaptchaProviderMissing) => unsupported(
250                        detection_for_branch,
251                        UnsupportedReason::MissingDependency("captcha_provider"),
252                    ),
253                    Err(err) => ChallengePipelineResult::Failed {
254                        detection: detection_for_branch,
255                        error: PipelineError::JavascriptV2(err),
256                    },
257                }
258            }
259            ChallengeType::ManagedV3 => {
260                let Some(solver) = self.managed_v3.as_ref() else {
261                    return unsupported(
262                        detection_for_branch,
263                        UnsupportedReason::MissingSolver("managed_v3"),
264                    );
265                };
266                match solver.solve(response) {
267                    Ok(submission) => ChallengePipelineResult::Submission {
268                        detection: detection_for_branch,
269                        submission,
270                    },
271                    Err(err) => ChallengePipelineResult::Failed {
272                        detection: detection_for_branch,
273                        error: PipelineError::ManagedV3(err),
274                    },
275                }
276            }
277            ChallengeType::Turnstile => {
278                let Some(solver) = self.turnstile.as_ref() else {
279                    return unsupported(
280                        detection_for_branch,
281                        UnsupportedReason::MissingSolver("turnstile"),
282                    );
283                };
284                match solver.solve(response).await {
285                    Ok(submission) => ChallengePipelineResult::Submission {
286                        detection: detection_for_branch,
287                        submission,
288                    },
289                    Err(TurnstileError::CaptchaProviderMissing) => unsupported(
290                        detection_for_branch,
291                        UnsupportedReason::MissingDependency("captcha_provider"),
292                    ),
293                    Err(err) => ChallengePipelineResult::Failed {
294                        detection: detection_for_branch,
295                        error: PipelineError::Turnstile(err),
296                    },
297                }
298            }
299            ChallengeType::RateLimit => {
300                let Some(handler) = self.rate_limit.as_ref() else {
301                    return unsupported(
302                        detection_for_branch,
303                        UnsupportedReason::MissingSolver("rate_limit"),
304                    );
305                };
306                match handler.plan(response, failure_recorder) {
307                    Ok(plan) => ChallengePipelineResult::Mitigation {
308                        detection: detection_for_branch,
309                        plan,
310                    },
311                    Err(err) => ChallengePipelineResult::Failed {
312                        detection: detection_for_branch,
313                        error: PipelineError::RateLimit(err),
314                    },
315                }
316            }
317            ChallengeType::AccessDenied => {
318                let Some(handler) = self.access_denied.as_ref() else {
319                    return unsupported(
320                        detection_for_branch,
321                        UnsupportedReason::MissingSolver("access_denied"),
322                    );
323                };
324                match handler.plan(response, proxy_pool, current_proxy) {
325                    Ok(plan) => ChallengePipelineResult::Mitigation {
326                        detection: detection_for_branch,
327                        plan,
328                    },
329                    Err(err) => ChallengePipelineResult::Failed {
330                        detection: detection_for_branch,
331                        error: PipelineError::AccessDenied(err),
332                    },
333                }
334            }
335            ChallengeType::BotManagement => {
336                let Some(handler) = self.bot_management.as_ref() else {
337                    return unsupported(
338                        detection_for_branch,
339                        UnsupportedReason::MissingSolver("bot_management"),
340                    );
341                };
342                match handler.plan(response, fingerprint_manager, tls_manager, failure_recorder) {
343                    Ok(plan) => ChallengePipelineResult::Mitigation {
344                        detection: detection_for_branch,
345                        plan,
346                    },
347                    Err(err) => ChallengePipelineResult::Failed {
348                        detection: detection_for_branch,
349                        error: PipelineError::BotManagement(err),
350                    },
351                }
352            }
353            ChallengeType::Unknown => {
354                unsupported(detection_for_branch, UnsupportedReason::UnknownChallenge)
355            }
356        }
357    }
358
359    /// Feed the detector with challenge outcome data for adaptive scoring.
360    pub fn record_outcome(&mut self, pattern_id: &str, success: bool) {
361        self.detector.learn_from_outcome(pattern_id, success);
362    }
363}
364
365impl Default for ChallengePipeline {
366    fn default() -> Self {
367        Self::new(ChallengeDetector::new())
368    }
369}
370
371fn unsupported(
372    detection: ChallengeDetection,
373    reason: UnsupportedReason,
374) -> ChallengePipelineResult {
375    ChallengePipelineResult::Unsupported { detection, reason }
376}