leyline/core/config/
proxy.rs1use 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}