scatter-proxy 0.9.0

Async request scheduler for unreliable SOCKS5 proxies — multi-path race for maximum throughput
Documentation
use std::future::Future;
use std::pin::Pin;

/// Resolves a WAF challenge on behalf of a specific proxy node.
///
/// When the scheduler receives a [`Challenge`](crate::BodyVerdict::Challenge) verdict it calls
/// `resolve` on the *same* proxy node that triggered the challenge.  A successful
/// return value (some cookie string) is stored in [`ProxyManager`](crate::proxy::ProxyManager)
/// and injected into subsequent requests via a [`RequestMutator`](crate::mutator::RequestMutator).
///
/// Return `None` to signal that the challenge cannot be solved; the scheduler
/// will mark that proxy node as `Dead`.
///
/// # Example
///
/// ```rust,ignore
/// use scatter_proxy::ChallengeResolver;
///
/// struct MyResolver;
///
/// impl ChallengeResolver for MyResolver {
///     fn resolve<'a>(
///         &'a self,
///         _proxy_url: &'a str,
///         body: &'a [u8],
///     ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Option<String>> + Send + 'a>> {
///         Box::pin(async move {
///             // parse challenge HTML and compute cookie value
///             Some("solved_cookie_value".to_string())
///         })
///     }
/// }
/// ```
pub trait ChallengeResolver: Send + Sync + 'static {
    /// Attempt to solve the WAF challenge contained in `body`.
    ///
    /// `proxy_url` — the proxy node URL that received the challenge page.
    /// `body`      — the raw response body (HTML) of the challenge page.
    ///
    /// Returns the resolved cookie *value* (not the full `key=value` header),
    /// or `None` if solving fails.
    fn resolve<'a>(
        &'a self,
        proxy_url: &'a str,
        body: &'a [u8],
    ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>>;
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;

    struct AlwaysSolves;

    impl ChallengeResolver for AlwaysSolves {
        fn resolve<'a>(
            &'a self,
            _proxy_url: &'a str,
            _body: &'a [u8],
        ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>> {
            Box::pin(async { Some("cookie123".to_string()) })
        }
    }

    struct AlwaysFails;

    impl ChallengeResolver for AlwaysFails {
        fn resolve<'a>(
            &'a self,
            _proxy_url: &'a str,
            _body: &'a [u8],
        ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'a>> {
            Box::pin(async { None })
        }
    }

    #[tokio::test]
    async fn resolver_returns_some_on_success() {
        let r = AlwaysSolves;
        let result = r
            .resolve("socks5h://1.2.3.4:1080", b"<html>challenge</html>")
            .await;
        assert_eq!(result, Some("cookie123".to_string()));
    }

    #[tokio::test]
    async fn resolver_returns_none_on_failure() {
        let r = AlwaysFails;
        let result = r
            .resolve("socks5h://1.2.3.4:1080", b"<html>hard challenge</html>")
            .await;
        assert!(result.is_none());
    }

    #[tokio::test]
    async fn resolver_can_be_used_as_trait_object() {
        let r: Arc<dyn ChallengeResolver> = Arc::new(AlwaysSolves);
        let result = r.resolve("socks5h://1.2.3.4:1080", b"body").await;
        assert!(result.is_some());
    }
}