1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
//! Structured, redaction-safe records for fail-soft configuration drops.
use crate::Scheme;
use crate::util::redact_offending_token;
/// Why one configuration value was ignored.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RejectionKind {
/// A proxy endpoint or URL could not be parsed.
InvalidProxyEndpoint,
/// A bypass-list entry could not be parsed.
InvalidBypassPattern,
/// A named proxy scheme was not recognised.
UnknownProxyScheme,
/// The source expressed a setting this crate cannot model.
UnsupportedMapping,
}
/// Where a rejected value was read from.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RejectionSource {
/// A Windows `ProxyServer`-style token.
ProxyServer,
/// A bypass / `no_proxy` / `ProxyOverride` token.
BypassList,
/// A process environment variable.
EnvironmentVariable(String),
/// A KDE `kioslaverc` key.
Kioslaverc(String),
/// A GNOME GSettings key.
GSettings(String),
/// A SystemConfiguration proxies key, as macOS's dynamic store and iOS's
/// `CFNetworkCopySystemProxySettings()` both spell it.
SystemConfiguration(String),
/// An Android `ProxyInfo` field, read from `ConnectivityManager.getDefaultProxy()`.
ProxyInfo(String),
}
/// One fail-soft drop with a typed reason and origin.
///
/// Construction masks URL-shaped `user:password` in `input`, or withholds the token when a
/// credential fragment may remain (for example a password that contains whitespace). Derived
/// [`Debug`] and the text accessors therefore cannot expose a password even when the caller
/// supplies an unparseable credential-bearing URL.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct RejectedValue {
kind: RejectionKind,
source: RejectionSource,
redacted_input: String,
scheme: Option<Scheme>,
}
impl RejectedValue {
/// Record a rejected raw value, redacting credentials immediately.
#[must_use]
pub(crate) fn new(
kind: RejectionKind,
source: RejectionSource,
input: impl AsRef<str>,
) -> Self {
Self {
kind,
source,
redacted_input: redact_offending_token(input.as_ref()),
scheme: None,
}
}
/// Name the request scheme this drop took an answer away from.
///
/// This specifies which requests lost an answer. A slot the parser recognised names
/// its own scheme (`socksProxy` → [`Scheme::Socks`]); a key that decides every request
/// (a PAC or WPAD switch) names [`Scheme::All`]; an unrecognised token names nothing,
/// because the crate cannot say which requests it would have covered.
///
/// `None` is not a safe default. `resolve` answers an unattributed record as if the
/// value had never been configured. This is right for the unrecognised token and wrong
/// for everything else. The widest drops (PAC/WPAD switches) look most like "no single
/// scheme" and least deserve `None`.
///
/// It takes the [`Option`] rather than the [`Scheme`] because several callers hold one:
/// a helper shared between a per-scheme loop and a whole-configuration key knows which
/// it was called for, and would otherwise have to say so with a `match` around the
/// construction, which is also what hides the masking from `debug_masking`'s scanner.
#[must_use]
pub(crate) const fn for_scheme(mut self, scheme: Option<Scheme>) -> Self {
self.scheme = scheme;
self
}
/// Request scheme this drop took an answer away from, when one is known.
///
/// Under the `resolve` feature, `resolve` reports
/// [`Error::ProxyEntryUnusable`](crate::Error::ProxyEntryUnusable) rather than
/// `ProxyStep::Direct` for such a scheme, so a caller is not told "no proxy" about a
/// request the platform would have proxied. Those two names are left unlinked because
/// the feature they live behind can be off while this type is still documented.
#[must_use]
pub const fn affected_scheme(&self) -> Option<Scheme> {
self.scheme
}
/// Typed reason the value was dropped.
#[must_use]
pub const fn kind(&self) -> RejectionKind {
self.kind
}
/// Configuration origin of the dropped value.
#[must_use]
pub const fn source(&self) -> &RejectionSource {
&self.source
}
/// Redacted original input.
#[must_use]
pub fn redacted_input(&self) -> &str {
&self.redacted_input
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn construction_redacts_before_debug_or_accessors_can_observe_the_value() {
let rejected = RejectedValue::new(
RejectionKind::InvalidProxyEndpoint,
RejectionSource::EnvironmentVariable("http_proxy".to_owned()),
"http://alice:hunter2@bad host:8080",
);
assert_eq!(rejected.redacted_input(), "http://alice:***@bad host:8080");
assert!(!format!("{rejected:?}").contains("hunter2"));
}
// The two inputs leave [`redact_offending_token`] through different branches (the `//`
// one masks in place and keeps naming the proxy, the space one is withheld outright)
// and this test does not say which is which. It checks the layer: whichever branch
// processes a value, `new` has already been through it before any accessor or `Debug`
// can observe the field. Which branch each input takes is pinned one layer down, in
// `util`'s `redact_offending_token_masks_a_double_slash_inside_a_password` and
// `redact_offending_token_withholds_when_a_password_holds_a_boundary`.
#[test]
fn construction_hides_a_password_that_contains_whitespace_or_double_slash() {
for (input, secret) in [
("http://alice:aa//bb@proxy.corp:8080", "aa//bb"),
("http://alice:my pass@proxy.corp:8080", "my pass"),
] {
let rejected = RejectedValue::new(
RejectionKind::InvalidProxyEndpoint,
RejectionSource::ProxyServer,
input,
);
assert!(!rejected.redacted_input().contains(secret), "{rejected:?}");
assert!(!format!("{rejected:?}").contains(secret), "{rejected:?}");
}
}
}