Skip to main content

asimov_http/proxy/
reqwest.rs

1// This is free and unencumbered software released into the public domain.
2
3//! Reqwest-specific installation of a resolved single-origin policy.
4
5use super::{OriginProxy, system::Configuration};
6use alloc::{boxed::Box, sync::Arc, vec::Vec};
7use reqwest::{ClientBuilder, NoProxy, Proxy};
8
9impl OriginProxy {
10    /// Installs this origin's proxy policy on a reqwest builder (`reqwest`).
11    ///
12    /// Replaces existing proxies, disables automatic proxy discovery and
13    /// redirects, and prefers system-resolved IPv4 for SOCKS4. TLS, service
14    /// credentials, and timeouts remain the caller's settings. Apply this after
15    /// any other proxy/redirect settings and then build the client.
16    ///
17    /// **The resulting raw reqwest client must only send requests to
18    /// [`Self::origin`].** Use [`Self::check_origin`] before dispatching dynamic
19    /// URLs. Requests to other origins and manual redirects need separately
20    /// resolved clients; this adapter does not enforce request URL restrictions.
21    ///
22    /// ```no_run
23    /// # async fn example() -> Result<(), Box<dyn core::error::Error>> {
24    /// use asimov_http::proxy::{OriginProxy, ProxyOptions};
25    /// let url = reqwest::Url::parse("https://example.com/resource")?;
26    /// let proxy = OriginProxy::resolve(&url, &ProxyOptions::default())?;
27    /// let client = proxy.configure_reqwest(reqwest::Client::builder())?.build()?;
28    /// proxy.check_origin(&url)?;
29    /// let response = client.get(url).send().await?;
30    /// # Ok(())
31    /// # }
32    /// ```
33    ///
34    /// # Errors
35    ///
36    /// Returns reqwest's error if the selected endpoint cannot configure a proxy.
37    pub fn configure_reqwest(
38        &self,
39        builder: ClientBuilder,
40    ) -> Result<ClientBuilder, reqwest::Error> {
41        apply(builder, &self.configuration)
42    }
43}
44
45pub(super) fn apply(
46    builder: ClientBuilder,
47    configuration: &Configuration,
48) -> Result<ClientBuilder, reqwest::Error> {
49    // Reqwest must not rediscover settings through unified system-proxy features.
50    let builder = builder
51        .no_proxy()
52        .redirect(reqwest::redirect::Policy::none());
53    if super::bypasses_all(&configuration.no_proxy) {
54        return Ok(builder);
55    }
56    Ok(match &configuration.proxy {
57        Some(proxy) => {
58            let builder = if proxy.scheme() == "socks4" {
59                builder.dns_resolver(Arc::new(Socks4Resolver))
60            } else {
61                builder
62            };
63            builder.proxy(
64                Proxy::all(proxy.clone())?.no_proxy(NoProxy::from_string(&configuration.no_proxy)),
65            )
66        },
67        None => builder,
68    })
69}
70
71/// Reqwest selects only the first locally resolved SOCKS destination. Prefer
72/// IPv4 for SOCKS4, while retaining IPv6 for the proxy itself. Keep system DNS.
73struct Socks4Resolver;
74
75impl reqwest::dns::Resolve for Socks4Resolver {
76    fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
77        Box::pin(async move {
78            use std::net::ToSocketAddrs;
79            let mut addresses = tokio::task::spawn_blocking(move || {
80                (name.as_str(), 0)
81                    .to_socket_addrs()
82                    .map(|addresses| addresses.collect::<Vec<_>>())
83            })
84            .await??;
85            addresses.sort_by_key(|address| address.is_ipv6());
86            Ok(Box::new(addresses.into_iter()) as reqwest::dns::Addrs)
87        })
88    }
89}
90
91#[cfg(test)]
92mod tests;