proxy_watch/error.rs
1//! Crate-wide error type.
2
3use thiserror::Error;
4
5use crate::util::{redact_offending_token, redact_userinfo};
6use crate::{RejectedValue, Scheme};
7
8/// Errors from reading OS proxy configuration, and from routing a URL through what was read.
9///
10/// `#[non_exhaustive]`. No `#[from] io::Error` — I/O is always wrapped with context.
11/// Hand-written [`Debug`]: [`PacFetchRequired`](Error::PacFetchRequired)'s [`url::Url`]
12/// would print credentials; other payloads are masked at construction or safe.
13#[derive(Error)]
14#[non_exhaustive]
15pub enum Error {
16 /// A Windows `ProxyServer` style specification could not be parsed.
17 #[error("invalid proxy server specification {input:?}: {reason}")]
18 InvalidProxyServer {
19 /// Offending token, masked — and withheld outright when it may be a credential.
20 input: String,
21 /// Why parsing failed.
22 reason: String,
23 },
24
25 /// A single bypass / `no_proxy` / `ProxyOverride` entry could not be parsed.
26 #[error("invalid bypass pattern {input:?}: {reason}")]
27 InvalidBypassPattern {
28 /// Offending entry, masked like [`InvalidProxyServer`](Error::InvalidProxyServer)'s.
29 input: String,
30 /// Why parsing failed.
31 reason: String,
32 },
33
34 /// An `AutoConfigURL`-style setting read from the OS is malformed.
35 ///
36 /// Not the environment: `*_proxy` values go through
37 /// [`ProxyEndpoint::parse`](crate::ProxyEndpoint::parse), which answers with
38 /// [`InvalidProxyServer`](Error::InvalidProxyServer) instead.
39 #[error("invalid proxy URL {input:?}: {source}")]
40 InvalidProxyUrl {
41 /// Offending input, masked like [`InvalidProxyServer`](Error::InvalidProxyServer)'s.
42 input: String,
43 /// Underlying parse error.
44 #[source]
45 source: url::ParseError,
46 },
47
48 /// The proxy URL carried a scheme this crate does not understand.
49 ///
50 /// Recognised schemes are listed on [`ProxyScheme`](crate::ProxyScheme).
51 #[error("unsupported proxy scheme {0:?}")]
52 UnsupportedProxyScheme(String),
53
54 /// `http_proxy` under CGI: a non-empty `REQUEST_METHOD` ⇒ forged `Proxy:` header
55 /// (httpoxy, CVE-2016-5385). Refused like Go's httpproxy; not applied to
56 /// `https_proxy`/`no_proxy`, whose names no request header can produce.
57 ///
58 /// KDE's `ProxyType = 4` raises the same error on a wider rule, because there the
59 /// *file* names the variable each slot reads: any name a request header could have set
60 /// — RFC 3875 §4.1.18's `HTTP_` prefix — is refused whichever slot named it, including
61 /// the one holding the bypass list. `variable` is that name as written.
62 #[error("refusing to use {variable} value in CGI environment")]
63 CgiHttpProxy {
64 /// Variable name as set in the environment.
65 variable: String,
66 },
67
68 /// An I/O failure, annotated with the operation that caused it.
69 ///
70 /// OS error codes are carried as [`std::io::Error`], built by `from_raw_os_error`.
71 #[error("{context}")]
72 Io {
73 /// Operation in progress when the failure occurred.
74 context: String,
75 /// Underlying I/O error.
76 #[source]
77 source: std::io::Error,
78 },
79
80 /// Linux sandbox: desktop settings unreadable (Flatpak without dconf → keyfile
81 /// defaults). Portal fallback when possible; else this error — not a false Direct.
82 #[error("running inside a {sandbox} sandbox: {reason}")]
83 Sandboxed {
84 /// Detected sandbox name.
85 sandbox: String,
86 /// Why no source could be read.
87 reason: String,
88 },
89
90 /// No store to read. Off Windows/macOS/Linux there is no backend at all; on Linux it is
91 /// a reading and not a property of the build — *neither* desktop store answered, which
92 /// both desktop features being off guarantees and which a build with one of them on also
93 /// reaches whenever the schema and `kioslaverc` are both missing from the machine.
94 ///
95 /// Not the answer inside a sandbox: that route is chosen before either store is
96 /// consulted and reports [`Sandboxed`](Self::Sandboxed), including when the feature that
97 /// would carry the portal fallback is off.
98 #[error("watching the system proxy configuration is not supported on this platform")]
99 Unsupported,
100
101 /// `resolve()` needs PAC/WPAD evaluation (`resolve_with_pac` / `pac` feature).
102 /// Deliberately an error, not silent Direct.
103 ///
104 /// `resolve_with_pac` answers with it too, for
105 /// [`WpadAutoDetect`](crate::ProxyMode::WpadAutoDetect) handed no script body:
106 /// discovery is a non-goal, so the fix there is to supply the script rather than to
107 /// change entry point. `mode` is `"wpad"` either way and does not tell the two apart.
108 #[error("resolving this URL requires evaluating a proxy auto-config script ({mode})")]
109 PacNotSupported {
110 /// Auto-config mode in effect.
111 mode: &'static str,
112 },
113
114 /// The system configured a proxy for this scheme, the value could not be used, and no
115 /// other entry covers it.
116 ///
117 /// Returning `ProxyStep::Direct` here would assert something
118 /// the platform does not do: KDE expands a `[$e]` value from the session environment and
119 /// proxies the request, and this crate deliberately does not expand it. Only a scheme
120 /// with no catch-all reaches this — a live `socksProxy` covers the drop first, exactly as
121 /// Chromium's `fallback_proxies` does. Where this reports, Chromium goes direct instead:
122 /// its `ProxyList::AddProxyChain` drops a malformed entry with "Silently discard
123 /// malformed inputs", leaving `MapUrlSchemeToProxyList` to answer `nullptr`.
124 // `{scheme}` names the slot, so the second half says "this request" rather than repeating
125 // it: for a `ws`/`wss` URL the slot is `Https` or `Socks`, and "nothing else covers https"
126 // would describe a request nobody made.
127 #[error("the configured {scheme} proxy could not be used and nothing else covers this request")]
128 ProxyEntryUnusable {
129 /// Which requests lost an answer — read off the drop's own attribution, not off the
130 /// slot it was found under. In every mode this crate builds the two are the same
131 /// value, because each backend files its record against the slot it dropped, so this
132 /// is the slot, and the slot is what decides coverage rather than the request's own
133 /// scheme: a lost catch-all reports [`Scheme::All`] whatever was asked for, and a
134 /// `ws`/`wss` request reports whichever of the chain it lost. A caller who hands
135 /// [`ProxyMode::manual`](crate::ProxyMode::manual) a record attributed to one scheme
136 /// under the key of another gets the attribution back rather than the key. Nothing
137 /// normalises the pair: the record is the caller's, and the same value is what
138 /// [`ProxyMode::rejected`](crate::ProxyMode::rejected) hands back.
139 scheme: Scheme,
140 /// First drop naming that scheme; masked at construction. The rest stay reachable
141 /// through [`ProxyMode::rejected`](crate::ProxyMode::rejected) *on the mode this
142 /// error was resolved against*, which is the caller's own `config.effective`
143 /// wherever the caller supplied it. Where a resolver builds a mode of its own it is
144 /// not: `WinHttpPacResolver::resolve_config` answers a WPAD miss by re-reading the
145 /// registry into a `Manual` and resolving against that, and the mode goes out of
146 /// scope with the call while the caller still holds `WpadAutoDetect`, which has no
147 /// list. This field is what survives that.
148 rejected: RejectedValue,
149 },
150
151 /// `resolve_with_pac` + [`ProxyMode::Pac`](crate::ProxyMode::Pac) without script body.
152 /// Fetch is caller's job; `url` stored verbatim, masked only in `{}`/`{:?}`.
153 #[error(
154 "the PAC script at {} must be fetched by the caller before it can be evaluated",
155 redact_userinfo(.url.as_str())
156 )]
157 PacFetchRequired {
158 /// Script URL.
159 url: url::Url,
160 },
161
162 /// PAC run failed (syntax, throw, limits). `reason` masked/sanitized at construction.
163 #[error("PAC evaluation failed: {reason}")]
164 PacEvaluation {
165 /// Engine message (sanitized).
166 reason: String,
167 },
168
169 /// Evaluating a PAC script overran `PacPolicy::timeout`.
170 #[error("PAC evaluation exceeded its {timeout:?} budget")]
171 PacTimeout {
172 /// Budget that was exceeded.
173 timeout: std::time::Duration,
174 },
175
176 /// `FindProxyForURL` returned nothing usable (malformed candidates skipped first).
177 /// `result` masked at construction.
178 #[error("PAC script returned no usable proxy candidate: {result:?}")]
179 PacInvalidResult {
180 /// Raw return string (masked).
181 result: String,
182 },
183
184 /// `pac` on but no engine (`pac-boa` or a custom `pac::PacEvaluator`).
185 ///
186 /// Not linked: the item exists only with the `pac` feature on, and this variant does not.
187 #[error("no PAC JavaScript engine is enabled; build with the `pac-boa` feature")]
188 PacEngineUnavailable,
189}
190
191impl Error {
192 // Mask `user:password` in `input` once so Display/Debug stay safe downstream — with
193 // or without the `@` that would make it recognisable, since `input` is by definition
194 // a token that failed to parse.
195 pub(crate) fn proxy_server(input: impl AsRef<str>, reason: impl Into<String>) -> Self {
196 Error::InvalidProxyServer {
197 input: redact_offending_token(input.as_ref()),
198 reason: reason.into(),
199 }
200 }
201
202 // Same masking as [`Self::proxy_server`] (malformed bypasses can still look like userinfo).
203 pub(crate) fn bypass(input: impl AsRef<str>, reason: impl Into<String>) -> Self {
204 Error::InvalidBypassPattern {
205 input: redact_offending_token(input.as_ref()),
206 reason: reason.into(),
207 }
208 }
209
210 // Mask raw text that failed `Url::parse` (cannot re-parse to strip userinfo).
211 #[cfg_attr(
212 not(any(target_os = "linux", target_os = "macos", windows)),
213 allow(dead_code)
214 )]
215 pub(crate) fn invalid_proxy_url(input: impl AsRef<str>, source: url::ParseError) -> Self {
216 Error::InvalidProxyUrl {
217 input: redact_offending_token(input.as_ref()),
218 source,
219 }
220 }
221
222 // No `allow(dead_code)` for a target with no backend, unlike its neighbour above:
223 // `watch::ThreadGuard`'s `Drop` builds this error when a backend thread panics, and a
224 // `Drop` impl is live everywhere.
225 pub(crate) fn io(context: impl Into<String>, source: std::io::Error) -> Self {
226 Error::Io {
227 context: context.into(),
228 source,
229 }
230 }
231
232 // Mask + sanitize, in that order — see
233 // [`crate::util::redact_and_sanitize_untrusted`] (PAC return is attacker-chosen).
234 #[cfg_attr(not(feature = "pac"), allow(dead_code))]
235 pub(crate) fn pac_invalid_result(result: impl AsRef<str>) -> Self {
236 Error::PacInvalidResult {
237 result: crate::util::redact_and_sanitize_untrusted(result.as_ref()),
238 }
239 }
240
241 // Mask + sanitize engine/`throw` text. `pac/boa.rs` hands over what the engine said;
242 // `pac/winhttp.rs` hands over a sentence it wrote itself and still comes through here,
243 // so the variant is never built any other way.
244 #[cfg_attr(
245 not(any(feature = "pac-boa", all(windows, feature = "pac-windows-native"))),
246 allow(dead_code)
247 )]
248 pub(crate) fn pac_evaluation(reason: impl AsRef<str>) -> Self {
249 Error::PacEvaluation {
250 reason: crate::util::redact_and_sanitize_untrusted(reason.as_ref()),
251 }
252 }
253}
254
255impl std::fmt::Debug for Error {
256 // See enum docs: only [`Error::PacFetchRequired`]'s URL needs special casing.
257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 match self {
259 Error::InvalidProxyServer { input, reason } => f
260 .debug_struct("InvalidProxyServer")
261 .field("input", input)
262 .field("reason", reason)
263 .finish(),
264 Error::InvalidBypassPattern { input, reason } => f
265 .debug_struct("InvalidBypassPattern")
266 .field("input", input)
267 .field("reason", reason)
268 .finish(),
269 Error::InvalidProxyUrl { input, source } => f
270 .debug_struct("InvalidProxyUrl")
271 .field("input", input)
272 .field("source", source)
273 .finish(),
274 Error::UnsupportedProxyScheme(scheme) => f
275 .debug_tuple("UnsupportedProxyScheme")
276 .field(scheme)
277 .finish(),
278 Error::CgiHttpProxy { variable } => f
279 .debug_struct("CgiHttpProxy")
280 .field("variable", variable)
281 .finish(),
282 Error::Io { context, source } => f
283 .debug_struct("Io")
284 .field("context", context)
285 .field("source", source)
286 .finish(),
287 Error::Sandboxed { sandbox, reason } => f
288 .debug_struct("Sandboxed")
289 .field("sandbox", sandbox)
290 .field("reason", reason)
291 .finish(),
292 Error::Unsupported => write!(f, "Unsupported"),
293 Error::PacNotSupported { mode } => f
294 .debug_struct("PacNotSupported")
295 .field("mode", mode)
296 .finish(),
297 Error::ProxyEntryUnusable { scheme, rejected } => f
298 .debug_struct("ProxyEntryUnusable")
299 .field("scheme", scheme)
300 .field("rejected", rejected)
301 .finish(),
302 Error::PacFetchRequired { url } => f
303 .debug_struct("PacFetchRequired")
304 .field("url", &format_args!("{}", redact_userinfo(url.as_str())))
305 .finish(),
306 Error::PacEvaluation { reason } => f
307 .debug_struct("PacEvaluation")
308 .field("reason", reason)
309 .finish(),
310 Error::PacTimeout { timeout } => f
311 .debug_struct("PacTimeout")
312 .field("timeout", timeout)
313 .finish(),
314 Error::PacInvalidResult { result } => f
315 .debug_struct("PacInvalidResult")
316 .field("result", result)
317 .finish(),
318 Error::PacEngineUnavailable => write!(f, "PacEngineUnavailable"),
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 const SECRET: &str = "hunter2";
328
329 #[test]
330 fn proxy_server_masks_the_password_in_display_and_debug() {
331 let input = format!("http://alice:{SECRET}@proxy.corp:99999");
332 let error = Error::proxy_server(&input, "port out of range");
333
334 let display = error.to_string();
335 let debug = format!("{error:?}");
336 assert!(!display.contains(SECRET), "{display}");
337 assert!(!debug.contains(SECRET), "{debug}");
338 // Not vacuous: the rest of the input is still there.
339 assert!(display.contains("proxy.corp"), "{display}");
340 assert!(debug.contains("proxy.corp"), "{debug}");
341 }
342
343 #[test]
344 fn invalid_proxy_url_masks_the_password_in_display_and_debug() {
345 let input = format!("http://alice:{SECRET}@proxy.corp:99999");
346 let error = Error::invalid_proxy_url(&input, url::ParseError::EmptyHost);
347
348 let display = error.to_string();
349 let debug = format!("{error:?}");
350 assert!(!display.contains(SECRET), "{display}");
351 assert!(!debug.contains(SECRET), "{debug}");
352 assert!(display.contains("proxy.corp"), "{display}");
353 assert!(debug.contains("proxy.corp"), "{debug}");
354 }
355
356 // The payload looks like a bare scheme name, but
357 // [`ProxyEndpoint::parse`](crate::ProxyEndpoint::parse) splits on the first `"://"`
358 // *before* it looks for userinfo, so credentials written ahead of the scheme end up
359 // inside it.
360 #[test]
361 fn an_unsupported_scheme_masks_credentials_written_ahead_of_it() {
362 let input = format!("alice:{SECRET}@http://proxy.corp:8080");
363 let error = crate::ProxyEndpoint::parse(&input, 80).unwrap_err();
364
365 assert!(
366 matches!(error, Error::UnsupportedProxyScheme(_)),
367 "fixture precondition: expected an unsupported-scheme error, got {error:?}"
368 );
369 let display = error.to_string();
370 let debug = format!("{error:?}");
371 assert!(!display.contains(SECRET), "{display}");
372 assert!(!debug.contains(SECRET), "{debug}");
373 // `SafeError` only exists with the `tracing` feature, so this rendering is checked
374 // conditionally: an unconditional reference stops `cargo test
375 // --no-default-features` from compiling at all.
376 #[cfg(feature = "tracing")]
377 {
378 let traced = crate::trace::SafeError(&error).to_string();
379 assert!(!traced.contains(SECRET), "{traced}");
380 }
381 // Not vacuous: the user name and the mangled scheme still identify the mistake.
382 assert!(display.contains("alice"), "{display}");
383 }
384
385 // The `url` field stays a genuine, fetchable `url::Url`, so this variant masks at
386 // *display* time rather than at construction. Both rendering paths must still come
387 // out clean.
388 #[test]
389 fn pac_fetch_required_masks_the_password_in_display_and_debug() {
390 let url = url::Url::parse(&format!("https://alice:{SECRET}@wpad.corp/proxy.pac")).unwrap();
391 let error = Error::PacFetchRequired { url: url.clone() };
392
393 let display = error.to_string();
394 let debug = format!("{error:?}");
395 assert!(!display.contains(SECRET), "{display}");
396 assert!(!debug.contains(SECRET), "{debug}");
397 assert!(display.contains("wpad.corp"), "{display}");
398 assert!(debug.contains("wpad.corp"), "{debug}");
399
400 // The field itself is untouched: the caller still needs the real URL to fetch.
401 if let Error::PacFetchRequired { url: preserved } = &error {
402 assert_eq!(preserved, &url);
403 assert_eq!(preserved.password(), Some(SECRET));
404 } else {
405 unreachable!();
406 }
407 }
408
409 #[test]
410 fn pac_invalid_result_masks_the_password_in_display_and_debug() {
411 let error = Error::pac_invalid_result(format!("PROXY alice:{SECRET}@proxy.corp:8080"));
412
413 let display = error.to_string();
414 let debug = format!("{error:?}");
415 assert!(!display.contains(SECRET), "{display}");
416 assert!(!debug.contains(SECRET), "{debug}");
417 assert!(display.contains("proxy.corp"), "{display}");
418 assert!(debug.contains("proxy.corp"), "{debug}");
419 }
420
421 // A `FindProxyForURL` that returns an unusable string chooses that string outright,
422 // so it is at least as attacker-controlled as an engine message quoting a `throw`.
423 #[test]
424 fn pac_invalid_result_strips_control_characters_and_truncates() {
425 let error = Error::pac_invalid_result("BOGUS\nWARN a fake second log line\r\nthird");
426
427 let display = error.to_string();
428 let debug = format!("{error:?}");
429 assert!(!display.contains('\n'), "{display}");
430 assert!(!display.contains('\r'), "{display}");
431 assert!(!debug.contains('\n'), "{debug}");
432 assert!(!debug.contains('\r'), "{debug}");
433 assert!(
434 display.contains("BOGUS.WARN a fake second log line..third"),
435 "{display}"
436 );
437
438 let long = Error::pac_invalid_result("y".repeat(4096));
439 assert!(long.to_string().len() < 512, "{}", long.to_string().len());
440 assert!(long.to_string().contains('…'), "{long}");
441 }
442
443 // `Error::proxy_server`/`Error::bypass` mask only their `input` field; `reason` is
444 // trusted. `crate::util::split_host_port`'s `Err` tests that trust: on malformed
445 // input it hands back the tail of the string it was splitting, which — because
446 // `ProxyEndpoint::parse` cuts the authority off at the first `/`/`?`/`#` *before* it
447 // looks for `@` — can be a stranded password fragment rather than a port. This drives
448 // the real helper the way `endpoint.rs` does, not a made-up reason string.
449 #[test]
450 fn proxy_server_reason_does_not_leak_a_password_stranded_past_a_slash() {
451 let reason = crate::util::split_host_port(&format!("bob:{SECRET}"))
452 .expect_err("a non-numeric \"port\" must fail to parse");
453 let error = Error::proxy_server(format!("http://bob:{SECRET}/x@proxy.corp:8080"), reason);
454
455 let display = error.to_string();
456 let debug = format!("{error:?}");
457 assert!(!display.contains(SECRET), "{display}");
458 assert!(!debug.contains(SECRET), "{debug}");
459 // Not vacuous: the rest of the address is still there, in both `input` (already
460 // masked) and `reason` (now safe by construction rather than by luck).
461 assert!(display.contains("proxy.corp"), "{display}");
462 assert!(debug.contains("proxy.corp"), "{debug}");
463 }
464
465 // `HostPattern::parse` never processes `@` — a bypass entry is not supposed to be a
466 // URL — so `bypass.rs` rejects any `@`-bearing entry outright, with a reason that
467 // names the mistake instead of repeating it.
468 #[test]
469 fn bypass_reason_does_not_leak_a_password_before_an_at_sign() {
470 let error = crate::bypass::HostPattern::parse(&format!("alice:{SECRET}@proxy.corp"))
471 .expect_err("an '@'-bearing entry must be rejected, not silently misparsed");
472
473 let display = error.to_string();
474 let debug = format!("{error:?}");
475 assert!(!display.contains(SECRET), "{display}");
476 assert!(!debug.contains(SECRET), "{debug}");
477 assert!(display.contains("proxy.corp"), "{display}");
478 assert!(debug.contains("proxy.corp"), "{debug}");
479 }
480
481 // A syntax error repeats the malformed PAC URL literal the script embedded, so the
482 // engine's message can quote a `user:password@` fragment.
483 #[test]
484 fn pac_evaluation_masks_the_password_in_display_and_debug() {
485 let error = Error::pac_evaluation(format!(
486 "the PAC script failed to load: unexpected token in string literal \
487 \"http://alice:{SECRET}@proxy.corp/x.pac\""
488 ));
489
490 let display = error.to_string();
491 let debug = format!("{error:?}");
492 assert!(!display.contains(SECRET), "{display}");
493 assert!(!debug.contains(SECRET), "{debug}");
494 assert!(display.contains("proxy.corp"), "{display}");
495 assert!(debug.contains("proxy.corp"), "{debug}");
496 }
497
498 // A PAC script's own `throw` reaches `reason` verbatim via the engine's message, so a
499 // script author chooses its bytes. Newlines must not survive into `Display`/`Debug`,
500 // or a script could forge extra log lines wherever a caller prints the error.
501 #[test]
502 fn pac_evaluation_strips_control_characters_in_display_and_debug() {
503 let error = Error::pac_evaluation(
504 "FindProxyForURL failed: Error: forged\nWARN a fake second log line\r\nthird",
505 );
506
507 let display = error.to_string();
508 let debug = format!("{error:?}");
509 assert!(!display.contains('\n'), "{display}");
510 assert!(!display.contains('\r'), "{display}");
511 assert!(!debug.contains('\n'), "{debug}");
512 assert!(!debug.contains('\r'), "{debug}");
513 // Not vacuous: the message survives, control characters swapped for `.`.
514 assert!(
515 display.contains("forged.WARN a fake second log line..third"),
516 "{display}"
517 );
518 }
519
520 // A pathological script (or engine) message must not make this error unbounded, even
521 // without the `tracing` feature.
522 #[test]
523 fn pac_evaluation_truncates_a_long_reason_in_display_and_debug() {
524 let error = Error::pac_evaluation(format!("FindProxyForURL failed: {}", "y".repeat(4096)));
525
526 let display = error.to_string();
527 let debug = format!("{error:?}");
528 assert!(display.len() < 512, "{}", display.len());
529 assert!(debug.len() < 512, "{}", debug.len());
530 assert!(display.contains('…'), "{display}");
531 assert!(debug.contains('…'), "{debug}");
532 }
533
534 // One row per variant. Only [`Error::PacFetchRequired`] needs the hand-written impl at
535 // all; the other arms are what `derive(Debug)` would have written, copied out by hand
536 // because a hand-written impl cannot delegate to a derive for the rest. This test is the
537 // only thing that notices a copy drifting: `Sandboxed` printed without its `reason` — the
538 // field that says *why* the sandbox left nothing readable, and the whole content of that
539 // error — and `Unsupported` printed as `PacEngineUnavailable`, which leaves two distinct
540 // failures indistinguishable wherever a caller logs `{:?}`.
541 //
542 // Exact strings, so that a label, an order or a name cannot change unseen. Where a field
543 // has a `Debug` of its own the expectation defers to it rather than copying it out,
544 // which is the one thing this impl does not own.
545 #[test]
546 fn every_variant_debug_names_itself_and_keeps_its_fields() {
547 let rejected = RejectedValue::new(
548 crate::RejectionKind::UnsupportedMapping,
549 crate::RejectionSource::ProxyServer,
550 "socks",
551 );
552 for (error, expected) in [
553 (
554 Error::proxy_server("proxy.corp", "no port"),
555 r#"InvalidProxyServer { input: "proxy.corp", reason: "no port" }"#.to_owned(),
556 ),
557 (
558 Error::bypass("*.corp", "empty label"),
559 r#"InvalidBypassPattern { input: "*.corp", reason: "empty label" }"#.to_owned(),
560 ),
561 (
562 Error::invalid_proxy_url("not-a-url", url::ParseError::RelativeUrlWithoutBase),
563 format!(
564 r#"InvalidProxyUrl {{ input: "not-a-url", source: {:?} }}"#,
565 url::ParseError::RelativeUrlWithoutBase
566 ),
567 ),
568 (
569 Error::UnsupportedProxyScheme("gopher".to_owned()),
570 r#"UnsupportedProxyScheme("gopher")"#.to_owned(),
571 ),
572 (
573 Error::CgiHttpProxy {
574 variable: "http_proxy".to_owned(),
575 },
576 r#"CgiHttpProxy { variable: "http_proxy" }"#.to_owned(),
577 ),
578 (
579 Error::io(
580 "reading dconf",
581 std::io::Error::other("dconf exited nonzero"),
582 ),
583 format!(
584 r#"Io {{ context: "reading dconf", source: {:?} }}"#,
585 std::io::Error::other("dconf exited nonzero")
586 ),
587 ),
588 (
589 Error::Sandboxed {
590 sandbox: "flatpak".to_owned(),
591 reason: "dconf is not on the bus".to_owned(),
592 },
593 r#"Sandboxed { sandbox: "flatpak", reason: "dconf is not on the bus" }"#.to_owned(),
594 ),
595 (Error::Unsupported, "Unsupported".to_owned()),
596 (
597 Error::PacNotSupported { mode: "wpad" },
598 r#"PacNotSupported { mode: "wpad" }"#.to_owned(),
599 ),
600 (
601 Error::ProxyEntryUnusable {
602 scheme: Scheme::Https,
603 rejected: rejected.clone(),
604 },
605 format!(
606 "ProxyEntryUnusable {{ scheme: {:?}, rejected: {rejected:?} }}",
607 Scheme::Https
608 ),
609 ),
610 (
611 // No credentials, so that the row pins the rendering and not the masking —
612 // which `pac_fetch_required_masks_the_password_in_display_and_debug` owns.
613 Error::PacFetchRequired {
614 url: url::Url::parse("https://wpad.corp/proxy.pac").unwrap(),
615 },
616 "PacFetchRequired { url: https://wpad.corp/proxy.pac }".to_owned(),
617 ),
618 (
619 Error::pac_evaluation("the engine refused the script"),
620 r#"PacEvaluation { reason: "the engine refused the script" }"#.to_owned(),
621 ),
622 (
623 Error::PacTimeout {
624 timeout: std::time::Duration::from_millis(1500),
625 },
626 "PacTimeout { timeout: 1.5s }".to_owned(),
627 ),
628 (
629 Error::pac_invalid_result("BOGUS"),
630 r#"PacInvalidResult { result: "BOGUS" }"#.to_owned(),
631 ),
632 (
633 Error::PacEngineUnavailable,
634 "PacEngineUnavailable".to_owned(),
635 ),
636 ] {
637 assert_eq!(format!("{error:?}"), expected);
638 }
639 }
640}