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