Skip to main content

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}