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;