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}