Skip to main content

asimov_http/proxy/
system.rs

1// This is free and unencumbered software released into the public domain.
2
3use super::{Error, ProxyMode, ProxyOptions, environment_proxy, parse_endpoint};
4use alloc::string::String;
5use core::fmt;
6use url::{Origin, Url};
7
8#[cfg(target_os = "macos")]
9mod macos;
10#[cfg(any(windows, target_os = "linux", test))]
11mod native;
12#[cfg(windows)]
13mod windows;
14
15/// A proxy policy resolved for exactly one HTTP(S) origin (`system-proxy`).
16///
17/// Resolution snapshots the process environment and supported native settings.
18/// Uppercase variables, including empty values, precede lowercase variants.
19/// Per-scheme environment proxies precede native settings, then `ALL_PROXY`.
20/// Nonempty environment overrides with unusable URLs skip native discovery and
21/// fall back to a usable `ALL_PROXY`, or direct access. Explicit options are
22/// validated instead. Presence of `REQUEST_METHOD` disables automatic selection.
23///
24/// Native discovery reads global macOS HTTP/HTTPS/SOCKS settings, Windows static
25/// Internet Settings (including protocol maps and per-machine policy), and
26/// GNOME/KDE desktop settings. Native bypass grammars remain distinct from
27/// `NO_PROXY`. Empty local bypass overrides suppress native exclusions too.
28/// macOS/Linux native SOCKS5 settings use proxy-side DNS. PAC/WPAD, saved
29/// passwords, and per-interface/VPN settings are not resolved; portal-only
30/// sources are rejected.
31///
32/// Native bypass decisions are evaluated for this origin. **Do not reuse the
33/// resulting client for other origins or follow redirects automatically.** Use
34/// [`Self::check_origin`] before dispatch if URLs are supplied dynamically. For
35/// another origin, resolve a separate policy and configure a separate client.
36/// The `reqwest` feature adds `configure_reqwest`; it disables redirects but
37/// cannot restrict arbitrary requests made through a raw reqwest client.
38///
39/// This type does not configure service credentials, TLS trust, timeouts, or
40/// request/response semantics. Debug output omits proxy URLs and credentials.
41#[derive(Clone)]
42pub struct OriginProxy {
43    origin: Origin,
44    pub(super) configuration: Configuration,
45}
46
47impl OriginProxy {
48    /// Resolves proxy settings before constructing a client or accessing keys.
49    ///
50    /// # Errors
51    ///
52    /// Rejects non-HTTP(S) targets with [`Error::InvalidOrigin`]. Invalid
53    /// explicit proxy URLs fail even when bypassed. Native read/configuration
54    /// failures and unsupported resolver sources retain their proxy error.
55    pub fn resolve(url: &Url, options: &ProxyOptions) -> Result<Self, Error> {
56        if !matches!(url.scheme(), "http" | "https") || url.host_str().is_none() {
57            return Err(Error::InvalidOrigin("expected an HTTP(S) URL with a host"));
58        }
59        let configuration = resolve_options(
60            options,
61            || Environment::capture(url),
62            |env| {
63                #[cfg(target_os = "macos")]
64                return macos::read(url, env);
65                #[cfg(windows)]
66                return windows::read(url, env);
67                #[cfg(target_os = "linux")]
68                return native::read_linux(url, env);
69                #[cfg(not(any(target_os = "macos", windows, target_os = "linux")))]
70                {
71                    let _ = env;
72                    Ok(Route::Automatic)
73                }
74            },
75        )?;
76        Ok(Self {
77            origin: url.origin(),
78            configuration,
79        })
80    }
81
82    /// The bound scheme, host, and effective port, without paths or credentials.
83    pub fn origin(&self) -> &Origin {
84        &self.origin
85    }
86
87    /// The selected endpoint before applying [`Self::no_proxy`] rules.
88    ///
89    /// `None` selects direct access. `Some` is a candidate: adapters must still
90    /// apply the NO_PROXY list, including `*` for IP literals. Native exclusions
91    /// have already been evaluated for this origin. The URL may contain proxy
92    /// credentials; do not log it without redaction.
93    pub fn proxy_url(&self) -> Option<&Url> {
94        self.configuration.proxy.as_ref()
95    }
96
97    /// Environment-syntax bypass rules to apply to [`Self::proxy_url`].
98    ///
99    /// This is the effective inherited or explicitly replaced `NO_PROXY` list;
100    /// native exclusion syntax is never copied into it. The reqwest adapter
101    /// applies it automatically. Other adapters must preserve its semantics.
102    pub fn no_proxy(&self) -> &str {
103        &self.configuration.no_proxy
104    }
105
106    /// Checks a destination before dispatch through an origin-scoped client.
107    ///
108    /// # Errors
109    ///
110    /// Returns [`Error::InvalidOrigin`] if scheme, host, or effective port differs.
111    pub fn check_origin(&self, url: &Url) -> Result<(), Error> {
112        if url.origin() != self.origin {
113            return Err(Error::InvalidOrigin(
114                "request must use the configured origin",
115            ));
116        }
117        Ok(())
118    }
119}
120
121impl fmt::Debug for OriginProxy {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        f.debug_struct("OriginProxy")
124            .field("origin", &self.origin)
125            .finish_non_exhaustive()
126    }
127}
128
129#[derive(Default)]
130struct Environment {
131    proxy: String,
132    all_proxy: String,
133    no_proxy: String,
134    no_proxy_override: bool,
135    cgi: bool,
136}
137
138impl Environment {
139    fn capture(url: &Url) -> Self {
140        fn first(upper: &str, lower: &str) -> String {
141            std::env::var(upper)
142                .or_else(|_| std::env::var(lower))
143                .unwrap_or_default()
144        }
145        Self {
146            proxy: match url.scheme() {
147                "https" => first("HTTPS_PROXY", "https_proxy"),
148                _ => first("HTTP_PROXY", "http_proxy"),
149            },
150            all_proxy: first("ALL_PROXY", "all_proxy"),
151            no_proxy: first("NO_PROXY", "no_proxy"),
152            no_proxy_override: false,
153            cgi: std::env::var_os("REQUEST_METHOD").is_some(),
154        }
155    }
156
157    fn use_native_bypass(&self) -> bool {
158        !self.no_proxy_override && self.no_proxy.is_empty()
159    }
160}
161
162#[derive(Debug, PartialEq)]
163enum Route {
164    Automatic,
165    Direct,
166    Native(Url),
167}
168
169#[derive(Clone)]
170pub(super) struct Configuration {
171    pub(super) proxy: Option<Url>,
172    pub(super) no_proxy: String,
173}
174
175fn resolve_options(
176    options: &ProxyOptions,
177    capture: impl FnOnce() -> Environment,
178    native: impl FnOnce(&Environment) -> Result<Route, Error>,
179) -> Result<Configuration, Error> {
180    match &options.mode {
181        ProxyMode::Direct => Ok(Configuration {
182            proxy: None,
183            no_proxy: String::new(),
184        }),
185        ProxyMode::Explicit(input) => {
186            let proxy = parse_endpoint(input)?;
187            let no_proxy = options
188                .no_proxy
189                .clone()
190                .unwrap_or_else(|| capture().no_proxy);
191            Ok(Configuration {
192                proxy: Some(proxy),
193                no_proxy,
194            })
195        },
196        ProxyMode::Automatic => {
197            let mut env = capture();
198            if let Some(no_proxy) = &options.no_proxy {
199                env.no_proxy.clone_from(no_proxy);
200                env.no_proxy_override = true;
201            }
202            resolve_environment(&env, || native(&env))
203        },
204    }
205}
206
207fn select(
208    env: &Environment,
209    native: impl FnOnce() -> Result<Route, Error>,
210) -> Result<Route, Error> {
211    if env.cgi || super::bypasses_all(&env.no_proxy) {
212        Ok(Route::Direct)
213    } else if !env.proxy.is_empty() {
214        // Preserve the legacy ALL_PROXY fallback for unusable environment URLs.
215        Ok(Route::Automatic)
216    } else {
217        native()
218    }
219}
220
221fn resolve_environment(
222    env: &Environment,
223    native: impl FnOnce() -> Result<Route, Error>,
224) -> Result<Configuration, Error> {
225    let proxy = match select(env, native)? {
226        Route::Automatic => {
227            environment_proxy(&env.proxy).or_else(|| environment_proxy(&env.all_proxy))
228        },
229        Route::Direct => None,
230        Route::Native(proxy) => Some(proxy),
231    };
232    Ok(Configuration {
233        proxy,
234        no_proxy: env.no_proxy.clone(),
235    })
236}
237
238#[cfg(test)]
239mod tests;