Skip to main content

leyline/core/config/
proxy.rs

1use crate::core::Kind;
2use crate::util::redact;
3
4use super::host::{normalize_host, pattern_matches};
5
6#[derive(Clone, PartialEq, Eq, Hash)]
7pub struct ProxyUrl(String);
8
9impl std::fmt::Debug for ProxyUrl {
10    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11        f.debug_tuple("ProxyUrl").field(&redact(&self.0)).finish()
12    }
13}
14
15impl ProxyUrl {
16    pub fn parse(raw: impl AsRef<str>) -> crate::core::Result<Self> {
17        let raw = raw.as_ref().trim();
18        let parsed = Self::parse_inner(raw)?;
19        match parsed.scheme() {
20            "http" | "https" | "socks5" | "socks5h" => {}
21            other => {
22                return Err(crate::core::Error::new(Kind::Config).with_message(format!(
23                    "unsupported proxy scheme {other:?}; expected http, https, socks5, or socks5h"
24                )));
25            }
26        }
27        if parsed.host_str().is_none() {
28            return Err(
29                crate::core::Error::new(Kind::Config).with_message("proxy URL must include a host")
30            );
31        }
32        Ok(Self(raw.to_string()))
33    }
34
35    pub(crate) fn configured(raw: &str) -> Self {
36        Self(raw.to_owned())
37    }
38
39    fn parse_inner(raw: &str) -> crate::core::Result<url::Url> {
40        url::Url::parse(raw).map_err(|e| {
41            crate::core::Error::new(Kind::Config).with_message(format!("invalid proxy URL: {e}"))
42        })
43    }
44
45    #[cfg(test)]
46    pub(crate) fn as_str(&self) -> &str {
47        &self.0
48    }
49}
50
51impl std::fmt::Display for ProxyUrl {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.write_str(&redact(&self.0))
54    }
55}
56
57impl From<ProxyUrl> for String {
58    fn from(value: ProxyUrl) -> Self {
59        value.0
60    }
61}
62
63#[derive(Clone)]
64#[non_exhaustive]
65pub struct ProxyConfig {
66    pub(super) rules: Vec<ProxyRule>,
67    pub(super) no_proxy: NoProxy,
68    pub(super) no_proxy_explicit: bool,
69    pub(super) use_env: bool,
70    pub(super) from_env: bool,
71    pub(super) rejected: Option<RejectedProxy>,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub(super) struct RejectedProxy {
76    variable: &'static str,
77}
78
79impl RejectedProxy {
80    fn error(self, kind: Kind) -> crate::core::Error {
81        crate::core::Error::new(kind).with_message(format!(
82            "the {} environment variable is not a valid proxy URL (expected an http, https, \
83             socks5, or socks5h URL with a host); fix or unset it, or set a proxy with \
84             `SessionBuilder::proxy`",
85            self.variable
86        ))
87    }
88}
89
90impl std::fmt::Debug for ProxyConfig {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("ProxyConfig")
93            .field("rules", &self.rules)
94            .field("no_proxy", &self.no_proxy)
95            .field("no_proxy_explicit", &self.no_proxy_explicit)
96            .field("use_env", &self.use_env)
97            .field("from_env", &self.from_env)
98            .field("rejected", &self.rejected)
99            .finish()
100    }
101}
102
103impl Default for ProxyConfig {
104    fn default() -> Self {
105        Self {
106            rules: Vec::new(),
107            no_proxy: NoProxy::from_env().unwrap_or_default(),
108            no_proxy_explicit: false,
109            use_env: true,
110            from_env: false,
111            rejected: None,
112        }
113    }
114}
115
116impl ProxyConfig {
117    pub fn new() -> Self {
118        Self::default()
119    }
120
121    pub fn rule(mut self, rule: ProxyRule) -> Self {
122        self.rules.push(rule);
123        self
124    }
125
126    pub fn no_proxy(mut self, no_proxy: NoProxy) -> Self {
127        self.no_proxy = no_proxy;
128        self.no_proxy_explicit = true;
129        self
130    }
131
132    pub fn env(mut self, on: bool) -> Self {
133        self.use_env = on;
134        self
135    }
136
137    pub(crate) fn set_default_proxy(mut self, proxy_url: impl Into<String>) -> Self {
138        self.rules.retain(|r| r.scheme != ProxyRuleScheme::All);
139        self.rules.push(ProxyRule::all(proxy_url));
140        self.from_env = false;
141        self
142    }
143
144    pub(crate) fn set_from_env(mut self) -> Self {
145        self.from_env = true;
146        self
147    }
148
149    pub(crate) fn reject_env(mut self, variable: &'static str) -> Self {
150        self.rejected = Some(RejectedProxy { variable });
151        self
152    }
153
154    pub(crate) fn rejects(&self) -> bool {
155        self.rejected.is_some()
156    }
157
158    pub(crate) fn rejection(&self, kind: Kind) -> Option<crate::core::Error> {
159        self.rejected.map(|rejected| rejected.error(kind))
160    }
161
162    pub(crate) fn rules(&self) -> &[ProxyRule] {
163        &self.rules
164    }
165
166    pub(crate) fn primary(&self) -> Option<&str> {
167        self.rules
168            .iter()
169            .find(|r| r.scheme == ProxyRuleScheme::All)
170            .or_else(|| self.rules.first())
171            .map(|r| r.url.as_str())
172    }
173
174    #[cfg(feature = "http3")]
175    pub(crate) fn proxies_every_url(&self) -> bool {
176        let bypass =
177            (self.no_proxy_explicit || self.from_env) && !self.no_proxy.patterns.is_empty();
178        !bypass && self.rules.iter().any(|r| r.scheme == ProxyRuleScheme::All)
179    }
180
181    pub(crate) fn uses_env(&self) -> bool {
182        self.use_env
183    }
184
185    pub(crate) fn proxy_for(&self, url: &url::Url) -> crate::core::Result<Option<&str>> {
186        if let Some(rejected) = self.rejected {
187            return Err(rejected.error(Kind::Proxy));
188        }
189        let host = url.host_str().unwrap_or("");
190        let Some(winner) = self
191            .rules
192            .iter()
193            .find(|rule| rule.matches(url.scheme()))
194            .map(|rule| rule.url.as_str())
195        else {
196            return Ok(None);
197        };
198        if (self.no_proxy_explicit || self.from_env) && self.no_proxy.matches(host) {
199            return Ok(None);
200        }
201        Ok(Some(winner))
202    }
203}
204
205impl From<&str> for ProxyConfig {
206    fn from(proxy_url: &str) -> Self {
207        Self::new().set_default_proxy(proxy_url)
208    }
209}
210
211impl From<&String> for ProxyConfig {
212    fn from(proxy_url: &String) -> Self {
213        Self::new().set_default_proxy(proxy_url.as_str())
214    }
215}
216
217impl From<String> for ProxyConfig {
218    fn from(proxy_url: String) -> Self {
219        Self::new().set_default_proxy(proxy_url)
220    }
221}
222
223impl From<ProxyUrl> for ProxyConfig {
224    fn from(proxy_url: ProxyUrl) -> Self {
225        Self::new().set_default_proxy(proxy_url)
226    }
227}
228
229#[derive(Clone, PartialEq, Eq)]
230#[non_exhaustive]
231pub struct ProxyRule {
232    scheme: ProxyRuleScheme,
233    url: String,
234}
235
236impl std::fmt::Debug for ProxyRule {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        f.debug_struct("ProxyRule")
239            .field("scheme", &self.scheme)
240            .field("url", &redact(&self.url))
241            .finish()
242    }
243}
244
245impl ProxyRule {
246    pub fn all(proxy_url: impl Into<String>) -> Self {
247        Self::for_scheme(ProxyRuleScheme::All, proxy_url)
248    }
249
250    pub fn http(proxy_url: impl Into<String>) -> Self {
251        Self::for_scheme(ProxyRuleScheme::Http, proxy_url)
252    }
253
254    pub fn https(proxy_url: impl Into<String>) -> Self {
255        Self::for_scheme(ProxyRuleScheme::Https, proxy_url)
256    }
257
258    fn for_scheme(scheme: ProxyRuleScheme, proxy_url: impl Into<String>) -> Self {
259        Self {
260            scheme,
261            url: proxy_url.into(),
262        }
263    }
264
265    fn matches(&self, scheme: &str) -> bool {
266        self.scheme == ProxyRuleScheme::All
267            || ProxyRuleScheme::for_url_scheme(scheme) == Some(self.scheme)
268    }
269
270    pub(crate) fn url(&self) -> &str {
271        &self.url
272    }
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
276enum ProxyRuleScheme {
277    All,
278    Http,
279    Https,
280}
281
282impl ProxyRuleScheme {
283    fn for_url_scheme(scheme: &str) -> Option<Self> {
284        match scheme {
285            "http" | "ws" => Some(Self::Http),
286            "https" | "wss" => Some(Self::Https),
287            _ => None,
288        }
289    }
290}
291
292#[derive(Debug, Clone, Default, PartialEq, Eq)]
293#[non_exhaustive]
294pub struct NoProxy {
295    patterns: Vec<String>,
296}
297
298impl NoProxy {
299    pub(crate) fn from_string(raw: &str) -> Option<Self> {
300        let patterns: Vec<String> = raw
301            .split(',')
302            .map(str::trim)
303            .filter(|s| !s.is_empty())
304            .map(ToOwned::to_owned)
305            .collect();
306        (!patterns.is_empty()).then_some(Self { patterns })
307    }
308
309    pub(crate) fn from_env() -> Option<Self> {
310        std::env::var("NO_PROXY")
311            .ok()
312            .or_else(|| std::env::var("no_proxy").ok())
313            .and_then(|raw| Self::from_string(&raw))
314    }
315
316    pub fn new<I, S>(patterns: I) -> Self
317    where
318        I: IntoIterator<Item = S>,
319        S: Into<String>,
320    {
321        Self {
322            patterns: patterns.into_iter().map(Into::into).collect(),
323        }
324    }
325
326    pub(crate) fn matches(&self, host: &str) -> bool {
327        let host = normalize_host(host);
328        self.patterns.iter().any(|raw| pattern_matches(&host, raw))
329    }
330}