1use 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#[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#[derive(Debug)]
39pub enum ChallengePipelineResult {
40 NoChallenge,
42 Submission {
44 detection: ChallengeDetection,
45 submission: ChallengeSubmission,
46 },
47 Mitigation {
49 detection: ChallengeDetection,
50 plan: MitigationPlan,
51 },
52 Unsupported {
54 detection: ChallengeDetection,
55 reason: UnsupportedReason,
56 },
57 Failed {
59 detection: ChallengeDetection,
60 error: PipelineError,
61 },
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum UnsupportedReason {
67 MissingSolver(&'static str),
68 MissingDependency(&'static str),
69 UnknownChallenge,
70}
71
72#[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
105pub 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 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 pub fn set_detector(&mut self, detector: ChallengeDetector) {
136 self.detector = detector;
137 }
138
139 pub fn detector(&self) -> &ChallengeDetector {
141 &self.detector
142 }
143
144 pub fn detector_mut(&mut self) -> &mut ChallengeDetector {
146 &mut self.detector
147 }
148
149 pub fn with_javascript_v1(mut self, solver: JavascriptV1Solver) -> Self {
151 self.javascript_v1 = Some(solver);
152 self
153 }
154
155 pub fn with_javascript_v2(mut self, solver: JavascriptV2Solver) -> Self {
157 self.javascript_v2 = Some(solver);
158 self
159 }
160
161 pub fn with_managed_v3(mut self, solver: ManagedV3Solver) -> Self {
163 self.managed_v3 = Some(solver);
164 self
165 }
166
167 pub fn with_turnstile(mut self, solver: TurnstileSolver) -> Self {
169 self.turnstile = Some(solver);
170 self
171 }
172
173 pub fn with_rate_limit(mut self, handler: RateLimitHandler) -> Self {
175 self.rate_limit = Some(handler);
176 self
177 }
178
179 pub fn with_access_denied(mut self, handler: AccessDeniedHandler) -> Self {
181 self.access_denied = Some(handler);
182 self
183 }
184
185 pub fn with_bot_management(mut self, handler: BotManagementHandler) -> Self {
187 self.bot_management = Some(handler);
188 self
189 }
190
191 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 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}