Skip to main content

asimov_http/proxy/
hyper.rs

1// This is free and unencumbered software released into the public domain.
2
3use super::{Error, OriginProxy, bypasses_all};
4use hyper_util::client::proxy::matcher::Matcher;
5use url::Url;
6
7impl OriginProxy {
8    /// Returns this origin's effective endpoint for a Hyper connector (`hyper`).
9    ///
10    /// Applies NO_PROXY using the same matcher as reqwest, including the shared
11    /// wildcard fix for IP literals. Native exclusions were resolved earlier.
12    /// This does not perform backend environment/system discovery. The returned
13    /// URL retains credentials; use it only for [`Self::origin`]. The caller
14    /// owns CONNECT/SOCKS support, TLS, timeouts, and connection pooling.
15    ///
16    /// # Errors
17    ///
18    /// Returns [`Error::InvalidOrigin`] if the origin cannot be represented as
19    /// an HTTP URI for matching.
20    pub fn proxy_for_hyper(&self) -> Result<Option<&Url>, Error> {
21        let Some(proxy) = self.proxy_url() else {
22            return Ok(None);
23        };
24        if bypasses_all(self.no_proxy()) {
25            return Ok(None);
26        }
27        let uri = self
28            .origin()
29            .ascii_serialization()
30            .parse::<http::Uri>()
31            .map_err(|_| Error::InvalidOrigin("origin cannot be represented as an HTTP URI"))?;
32        // Use only the matcher's bypass parser. The sentinel is never connected
33        // to; endpoint selection and credential validation belong to adapters.
34        let matcher = Matcher::builder()
35            .all("http://proxy.invalid")
36            .no(self.no_proxy())
37            .build();
38        Ok(matcher.intercept(&uri).is_some().then_some(proxy))
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use crate::proxy::{ProxyMode, ProxyOptions};
46
47    #[test]
48    fn bypass_rules_apply_before_exposing_an_endpoint() {
49        for (target, bypass, direct) in [
50            ("https://example.com/", ".EXAMPLE.COM", true),
51            ("https://api.example.com/", "example.com", true),
52            ("https://notexample.com/", "example.com", false),
53            ("https://127.0.0.1/", "127.0.0.0/8", true),
54            ("https://[::1]/", "::1", true),
55            ("https://[2001:db8::1]/", "2001:db8::/32", true),
56            ("https://127.0.0.1/", " * ", true),
57            ("https://[::1]/", "*", true),
58            ("https://localhost/", "127.0.0.0/8", false),
59            ("https://example.com:8443/", "example.com:8443", false),
60            ("https://example.com/", "", false),
61        ] {
62            for endpoint in ["http://user:pass@proxy.invalid", "socks5h://proxy.invalid"] {
63                let target = Url::parse(target).unwrap();
64                let policy = OriginProxy::resolve(
65                    &target,
66                    &ProxyOptions {
67                        mode: ProxyMode::Explicit(endpoint.into()),
68                        no_proxy: Some(bypass.into()),
69                    },
70                )
71                .unwrap();
72                assert_eq!(
73                    policy.proxy_for_hyper().unwrap().is_none(),
74                    direct,
75                    "{target}, {bypass}"
76                );
77                if !direct {
78                    assert_eq!(policy.proxy_for_hyper().unwrap(), policy.proxy_url());
79                }
80            }
81        }
82    }
83}