scatter_proxy/challenge.rs
1use std::future::Future;
2use std::pin::Pin;
3
4/// Resolves a WAF challenge on behalf of a specific proxy node.
5///
6/// When the scheduler receives a [`Challenge`](crate::BodyVerdict::Challenge) verdict it calls
7/// `resolve` on the *same* proxy node that triggered the challenge. A successful
8/// return value (some cookie string) is stored in [`ProxyManager`](crate::proxy::ProxyManager)
9/// and injected into subsequent requests via a [`RequestMutator`](crate::mutator::RequestMutator).
10///
11/// Return `None` to signal that the challenge cannot be solved; the scheduler
12/// will mark that proxy node as `Dead`.
13///
14/// # Example
15///
16/// ```rust,ignore
17/// use scatter_proxy::ChallengeResolver;
18///
19/// struct MyResolver;
20///
21/// impl ChallengeResolver for MyResolver {
22/// fn resolve<'a>(
23/// &'a self,
24/// _proxy_url: &'a str,
25/// body: &'a [u8],
26/// ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Option<String>> + Send + 'a>> {
27/// Box::pin(async move {
28/// // parse challenge HTML and compute cookie value
29/// Some("solved_cookie_value".to_string())
30/// })
31/// }
32/// }
33/// ```
34pub trait ChallengeResolver: Send + Sync + 'static {
35 /// Attempt to solve the WAF challenge contained in `body`.
36 ///
37 /// `proxy_url` — the proxy node URL that received the challenge page.
38 /// `body` — the raw response body (HTML) of the challenge page.
39 ///
40 /// Returns the resolved cookie *value* (not the full `key=value` header),
41 /// or `None` if solving fails.
42 fn resolve<'a>(
43 &'a self,
44 proxy_url: &'a str,
45 body: &'a [u8],
46 ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>>;
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use std::sync::Arc;
53
54 struct AlwaysSolves;
55
56 impl ChallengeResolver for AlwaysSolves {
57 fn resolve<'a>(
58 &'a self,
59 _proxy_url: &'a str,
60 _body: &'a [u8],
61 ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>> {
62 Box::pin(async { Some("cookie123".to_string()) })
63 }
64 }
65
66 struct AlwaysFails;
67
68 impl ChallengeResolver for AlwaysFails {
69 fn resolve<'a>(
70 &'a self,
71 _proxy_url: &'a str,
72 _body: &'a [u8],
73 ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>> {
74 Box::pin(async { None })
75 }
76 }
77
78 #[tokio::test]
79 async fn resolver_returns_some_on_success() {
80 let r = AlwaysSolves;
81 let result = r
82 .resolve("socks5h://1.2.3.4:1080", b"<html>challenge</html>")
83 .await;
84 assert_eq!(result, Some("cookie123".to_string()));
85 }
86
87 #[tokio::test]
88 async fn resolver_returns_none_on_failure() {
89 let r = AlwaysFails;
90 let result = r
91 .resolve("socks5h://1.2.3.4:1080", b"<html>hard challenge</html>")
92 .await;
93 assert!(result.is_none());
94 }
95
96 #[tokio::test]
97 async fn resolver_can_be_used_as_trait_object() {
98 let r: Arc<dyn ChallengeResolver> = Arc::new(AlwaysSolves);
99 let result = r.resolve("socks5h://1.2.3.4:1080", b"body").await;
100 assert!(result.is_some());
101 }
102}