Skip to main content

scatter_proxy/
mutator.rs

1/// Mutates a cloned request just before it is dispatched through a specific proxy.
2///
3/// The scheduler calls `mutate` after `try_clone()` and before `client.execute()`,
4/// giving implementations a chance to inject per-proxy request headers such as
5/// cookies obtained from a prior WAF challenge solve.
6///
7/// # Example
8///
9/// ```rust,ignore
10/// use scatter_proxy::RequestMutator;
11/// use std::sync::Arc;
12///
13/// struct CookieMutator {
14///     proxy_manager: Arc<scatter_proxy::ProxyManager>,
15/// }
16///
17/// impl RequestMutator for CookieMutator {
18///     fn mutate(&self, proxy_url: &str, mut req: reqwest::Request) -> reqwest::Request {
19///         if let Some(cookie_val) = self.proxy_manager.get_cookie(proxy_url) {
20///             let header_val = format!("acw_sc__v2={cookie_val}");
21///             if let Ok(v) = header_val.parse() {
22///                 req.headers_mut().insert(reqwest::header::COOKIE, v);
23///             }
24///         }
25///         req
26///     }
27/// }
28/// ```
29pub trait RequestMutator: Send + Sync + 'static {
30    /// Inject per-proxy modifications into `req` before it is sent.
31    ///
32    /// `proxy_url` — the proxy node URL that will execute the request.
33    /// `req`       — an already `try_clone()`d request ready for execution.
34    ///
35    /// Return the (possibly modified) request.
36    fn mutate(&self, proxy_url: &str, req: reqwest::Request) -> reqwest::Request;
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42    use std::sync::Arc;
43
44    struct NoOpMutator;
45
46    impl RequestMutator for NoOpMutator {
47        fn mutate(&self, _proxy_url: &str, req: reqwest::Request) -> reqwest::Request {
48            req
49        }
50    }
51
52    struct HeaderInjectMutator {
53        key: &'static str,
54        value: &'static str,
55    }
56
57    impl RequestMutator for HeaderInjectMutator {
58        fn mutate(&self, _proxy_url: &str, mut req: reqwest::Request) -> reqwest::Request {
59            if let (Ok(name), Ok(val)) = (
60                reqwest::header::HeaderName::from_bytes(self.key.as_bytes()),
61                self.value.parse::<reqwest::header::HeaderValue>(),
62            ) {
63                req.headers_mut().insert(name, val);
64            }
65            req
66        }
67    }
68
69    fn build_request() -> reqwest::Request {
70        reqwest::Client::new()
71            .get("http://example.com/")
72            .build()
73            .unwrap()
74    }
75
76    #[test]
77    fn no_op_mutator_leaves_request_unchanged() {
78        let m = NoOpMutator;
79        let req = build_request();
80        let mutated = m.mutate("socks5h://1.2.3.4:1080", req);
81        assert_eq!(mutated.url().as_str(), "http://example.com/");
82    }
83
84    #[test]
85    fn header_inject_mutator_adds_header() {
86        let m = HeaderInjectMutator {
87            key: "x-proxy-node",
88            value: "node-42",
89        };
90        let req = build_request();
91        let mutated = m.mutate("socks5h://1.2.3.4:1080", req);
92        assert_eq!(
93            mutated
94                .headers()
95                .get("x-proxy-node")
96                .and_then(|v| v.to_str().ok()),
97            Some("node-42"),
98        );
99    }
100
101    #[test]
102    fn mutator_can_be_used_as_trait_object() {
103        let m: Arc<dyn RequestMutator> = Arc::new(NoOpMutator);
104        let req = build_request();
105        let _mutated = m.mutate("socks5h://1.2.3.4:1080", req);
106    }
107
108    #[test]
109    fn different_proxy_urls_reach_mutator() {
110        struct UrlCapture(std::sync::Mutex<String>);
111        impl RequestMutator for UrlCapture {
112            fn mutate(&self, proxy_url: &str, req: reqwest::Request) -> reqwest::Request {
113                *self.0.lock().unwrap() = proxy_url.to_string();
114                req
115            }
116        }
117
118        let m = UrlCapture(std::sync::Mutex::new(String::new()));
119        let req = build_request();
120        m.mutate("socks5h://9.9.9.9:1080", req);
121        assert_eq!(*m.0.lock().unwrap(), "socks5h://9.9.9.9:1080");
122    }
123}