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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
//! Pure parsers for OS proxy string formats.
//!
//! List parsers drop malformed *elements* and keep the rest. Dropping a scheme endpoint
//! without recording it would be fail-open; bypass drops are fail-closed → `rejected`.
//!
//! # Writing a bypass list
//!
//! Every store has one, no two of them read it the same way, and the differences decide
//! which hosts go direct. What an entry means to this crate depends on the list it is
//! written in: it is the store's own reading, except where a cell says the two differ:
//!
//! | Entry | `no_proxy`, KDE `NoProxyFor` | Windows `ProxyOverride` | macOS `ExceptionsList` | GNOME `ignore-hosts` |
//! |---|---|---|---|---|
//! | separator between entries | `,` alone: a `;` leaves the two names one dead rule, and an entry still holding a space is rejected | `;`, `,` or whitespace | the array is the separator | the array is the separator |
//! | `example.com` | that host **and everything under it** | that host alone | that host alone | that host **and everything under it** |
//! | `.example.com`, `*.example.com` | the subdomains, not `example.com` itself | `*.example.com` is the subdomains; `.example.com` is **rejected**: Windows has no reading for a leading `.` (below) | the subdomains | the same rule as the bare name: the domain *and* its subdomains |
//! | a `*` anywhere else (`192.168.*`) | a glob here, which is Chromium's reading; Go and libproxy take it as literal text no host carries | a glob | syntax only as a trailing `.*`, which misses the bare host a Mac's own `name.*` still reaches; anything else is rejected | never syntax; rejected |
//! | a bare `*` | every destination direct | every destination direct | rejected: it matches no host on a Mac | rejected, same reason |
//! | `10.0.0.0/8` | the network | **rejected**: Windows has no `/` in this grammar, and one entry holding it turns the proxy off for every destination (below); write the range as `10.*` | the network | the network |
//! | `example.com:8080` | holds the entry to that port | holds it | **rejected**: macOS compares the whole entry to the host name, so a port kills it | holds it, but a portless `http://` URL is asked about with port 0 and will not meet it |
//! | `<local>`, `<-loopback>` | read | read | read | **rejected**: GLib reads neither and compares the token to a host so named |
//! | an entry ending in a dot (`example.com.`) | the dot is the DNS root and comes off; KDE: **rejected**; KIO compares the text | **rejected** | the dot comes off, as CFNetwork reads it | **rejected** |
//! | a trailing dot on the destination (`example.com.`) | shed, so `example.com` matches it; KDE: kept, so it does not | kept | shed | kept |
//! | no entry at all | `localhost`, `127.0.0.0/8`, `::1` and `*.localhost` go direct, link-local does not; KDE: nothing does | `localhost`, `loopback`, `127.0.0.1`, `::1` and link-local go direct | `localhost`, `127.0.0.1` and `::1` go direct while the dictionary has a bypass key; with none, nothing does | nothing goes direct |
//! | a space at either end | trimmed | trimmed | **kept**, and no host carries it, so the entry is rejected | trailing trimmed, leading kept and rejected |
//!
//! A rejected entry lands in
//! [`BypassRules::rejected`](crate::BypassRules::rejected) with the reason, so a rule that
//! does nothing is not mistaken for a live one.
//!
//! Windows readers differ on entries starting with `.`: WinINet refuses the whole list,
//! while WinHTTP and the registry keep it but proxy the subdomains. Write `*.name`.
//! A `/` makes the registry reading send every destination direct. Both spellings are
//! rejected here while neighboring entries stay live.
//!
//! A bare name changes meaning between the first column
//! and the next two, so `contoso.com` in `no_proxy` covers `api.contoso.com` and the same
//! text in `ProxyOverride` does not. And `<local>` / `<-loopback>` are read out of every
//! list but GNOME's so the sources share one vocabulary, while WinINet is the only resolver
//! that acts on both and KDE's KIO acts on `<local>` alone: a macOS store spelling one gets
//! a bypass from this crate and none from the machine.
//! `the_local_token_bypasses_nothing_here` in `tests/mac_exceptions_list.rs` measures that.
//! A GNOME entry spelling one is rejected, because GLib reads it as a host name.
//!
//! The last row is [`BypassRules::implicit`](crate::BypassRules::implicit): the
//! destinations each resolver sends direct when no entry names them. The Windows and macOS
//! cells are measured against WinINet and CFNetwork; the GNOME and KDE cells are read off
//! GLib's and KIO's matchers, which have no such set.
use HashMap;
use crate;
use crate;
use crate;
use crateError;
use crateProxyMode;
/// The port assumed when a Windows `ProxyServer` entry omits one.
pub const WINDOWS_DEFAULT_PORT: u16 = 80;
// Go's `httpproxy` and libproxy's `config-kde.c` split on `,` alone. KF5 KIO's `revmatch`
// (`kprotocolmanager.cpp`) and Chromium's `proxy_config_service_linux.cc` also split at a
// space, which KDE's dialog cannot write; a space-only list is one entry here, refused.
// `;` is not a separator: every supplier's reader keeps a `;` list whole as one dead rule,
// and splitting it would send direct hosts they all proxy.
const LIST_SEPARATORS: = ;
// The same, plus whitespace, for the Windows bypass list only.
//
// WinHTTP documents `lpszProxyBypass` as "one or more server names separated by
// semicolons or whitespace" (`WINHTTP_PROXY_INFO`), and Chromium reads the string
// `WinHttpGetIEProxyConfigForCurrentUser` returns with
// `base::StringTokenizer(proxy_bypass, ";, \t\n\r")`
// (`ProxyConfigServiceWin::SetFromIEConfig`). Splitting on `;` and `,` alone would turn
// `ProxyOverride = "*.corp.example intranet"` into one `Domain` pattern that no host can
// match, with nothing in [`BypassRules::rejected`] to say so.
//
// [`no_proxy`] keeps the narrower set: Go's `httpproxy`, its reference, splits on `,`
// alone. An entry that still contains whitespace there is caught by
// `HostPattern::parse`, so it is recorded rather than dead.
const WINDOWS_BYPASS_SEPARATORS: = ;
// Characters that separate entries in the Windows `ProxyServer` string.
//
// The same `WINHTTP_PROXY_INFO` page: "The proxy server list contains one or more of the
// following strings separated by semicolons or whitespace." No `,`: that character is in
// [`LIST_SEPARATORS`] because environment variables and GNOME put it there, and neither
// writes this key. Named rather than spelled inline at the split so that the reason a
// server list and a bypass list disagree about one character sits next to both.
const WINDOWS_SERVER_SEPARATORS: = ;
/// Parse Windows `ProxyServer` ([`WINHTTP_CURRENT_USER_IE_PROXY_CONFIG`](https://learn.microsoft.com/en-us/windows/win32/api/winhttp/ns-winhttp-winhttp_current_user_ie_proxy_config)).
///
/// Bare `host:port` → [`Scheme::All`]; or `http=`/`https=`/`ftp=`/`socks=`/`all=`.
/// Missing port → the port of the token's own `scheme://` when it has one, else
/// [`WINDOWS_DEFAULT_PORT`] (1080 for bare `socks=`). Empty scheme →
/// [`ProxyEntry::Disabled`]. Bad tokens skipped; recorded on [`windows_manual`]. A scheme
/// named twice keeps the last token that was not skipped, so a bad last spelling leaves the
/// earlier one standing. A `socks=` entry also fills every scheme the string left
/// unset; the last two examples below are that rule and its limit. An `all=` entry or a
/// bare `host:port` sets every scheme, so `socks=` fills none beside it (an empty `all=`
/// included); a rejected `all=` sets nothing and leaves the gaps to `socks=`.
///
/// ```
/// # use proxy_watch::{parse, ProxyScheme, Scheme};
/// let map = parse::proxy_server("http=h:8080;https=h");
/// assert_eq!(map[&Scheme::Http].endpoint().unwrap().port, 8080);
/// assert_eq!(map[&Scheme::Https].endpoint().unwrap().port, 80);
///
/// let map = parse::proxy_server("socks=127.0.0.1:1080");
/// assert_eq!(map[&Scheme::Http].endpoint().unwrap().authority(), "127.0.0.1:1080");
/// assert_eq!(
/// map[&Scheme::Http].endpoint().unwrap().scheme_hint,
/// Some(ProxyScheme::Socks4)
/// );
/// let map = parse::proxy_server("http=h1:8080;socks=127.0.0.1:1080");
/// assert_eq!(map[&Scheme::Http].endpoint().unwrap().authority(), "h1:8080");
/// ```
// [`proxy_server`] plus the redacted original text of every token it silently drops.
//
// This is what [`windows_manual`] calls instead of [`proxy_server`], so that the dropped
// tokens end up recorded on the [`ProxyMode::Manual`](crate::ProxyMode::Manual) it
// returns.
// See `proxy_server`: an explicit `all=` answer blocks the SOCKS fallback, including
// a disabled entry. A rejected `all=` leaves a gap that SOCKS fills.
// Log-only sink for a `ProxyServer` token [`proxy_server`] could not parse.
//
// `err` is only read inside the `WARN` line below, so without the `tracing` feature it
// would otherwise be flagged unused.
// Split `http=host:port` into `("http", "host:port")`. Only an `=` before the first `:`,
// `/`, `?` or `#` separates a key, so a password's or query's `=` stays in the endpoint
// text for `ProxyEndpoint::parse`, which cuts the authority at the same characters. A user
// name's `=` comes before its `:` and does separate one: `u=x:pw@h` is the unknown scheme
// `u`, and is rejected.
/// Parse a Windows `ProxyOverride` registry value into [`BypassRules`].
///
/// Like [`no_proxy`], but entries are separated by `;`, `,` or whitespace, as WinHTTP
/// documents, and **a bare name matches that name alone** rather than the domain under it:
/// `contoso.com` here does not bypass `api.contoso.com`. Write `*.contoso.com` for the
/// subdomains, which is also what Windows asks for; `.contoso.com` is rejected rather than
/// read, because no measured reader grants it. So is `contoso.com.`: the store compares a
/// trailing `.` as written, so it would match only a destination spelled with one. A CIDR
/// entry is rejected too, because
/// Windows answers a `/` with the whole list: write the range as the wildcard Windows does
/// read, `10.*`. `<local>` → [`HostPattern::Local`](crate::HostPattern::Local) and
/// `<-loopback>` (IE9+) →
/// [`HostPattern::SubtractImplicit`](crate::HostPattern::SubtractImplicit) are read by
/// [`no_proxy`] as well as by this one, as Chromium reads them. Malformed entries skipped
/// into [`BypassRules::rejected`].
///
/// An IPv4 address entry is read only as four decimal octets. WinINet and WinHTTP compare
/// an address entry with the destination as text, so `012.1.2.3` there is not `10.1.2.3` in
/// either position; such an entry is rejected, where [`no_proxy`] reads it as the address a
/// URL parser would. Limitation: every entry point reads the destination folded (a `Url` or
/// a `Host` already holds `012.1.2.3` as `10.1.2.3`, and
/// [`matches_authority`](crate::BypassRules::matches_authority) parses its text the same
/// way), so an entry `10.1.2.3` or `10.*` reports a bypass for a destination written
/// `012.1.2.3`, which Windows hands to the proxy.
///
/// The bare-name rule is measured against WinINet and WinHTTP. The reimplementations this
/// crate reads alongside, Chromium and libproxy, answer the question differently, so
/// neither could settle it. The readings are the rows of
/// `a_bare_name_in_a_windows_list_is_the_one_host` in `tests/bypass.rs`.
///
/// The implicit set is measured the same way, and is [`ImplicitBypass::WinInet`]:
/// `localhost`, `loopback`, `127.0.0.1`, `::1` and the link-local ranges. It excludes the
/// rest of `127.0.0.0/8`, `*.localhost` and IPv4-mapped spellings. WinHTTP agrees on the
/// link-local ranges and bypasses all of `127.0.0.0/8` even under `<-loopback>`. The store
/// is WinINet's, so this follows WinINet, and a WinHTTP client is therefore told "proxy"
/// for loopback it sends direct. The rows are
/// `wininet_implicit_bypass_is_the_measured_six` in `tests/bypass.rs`.
///
/// ```
/// # use proxy_watch::parse;
/// let rules = parse::proxy_override("<local>;*.contoso.com;<-loopback>");
/// assert!(rules.excludes_simple_hostnames());
/// assert!(!rules.bypass_loopback());
/// assert!(rules.matches_authority("www.contoso.com"));
///
/// // Whitespace separates too, so this is two rules and not one dead one.
/// let rules = parse::proxy_override("*.contoso.com intranet");
/// assert!(rules.matches_authority("www.contoso.com"));
/// assert!(rules.matches_authority("intranet"));
///
/// // A bare name is the host itself. The same text in `no_proxy` takes the subdomains.
/// let rules = parse::proxy_override("contoso.com");
/// assert!(rules.matches_authority("contoso.com"));
/// assert!(!rules.matches_authority("api.contoso.com"));
/// assert!(parse::no_proxy("contoso.com").matches_authority("api.contoso.com"));
///
/// // A `/` is recorded, not read. `no_proxy` takes the same text as a mask.
/// let rules = parse::proxy_override("10.0.0.0/8");
/// assert!(!rules.matches_authority("10.1.2.3"));
/// assert_eq!(rules.rejected.len(), 1);
/// assert!(parse::no_proxy("10.0.0.0/8").matches_authority("10.1.2.3"));
/// ```
/// Parse `no_proxy` into [`BypassRules`]: `*`, CIDR, domains, `:port`, and a localhost
/// bypass that stays on unless an entry clears it. Entries are separated by `,` alone, not
/// whitespace and not `;`, which is what Go's `httpproxy` does and what KDE's readers do
/// with `NoProxyFor`. Malformed → [`BypassRules::rejected`]. An entry
/// that repeats one already in [`BypassRules::patterns`] is dropped, first spelling kept,
/// so the list can be shorter than the string had entries.
///
/// Go's `httpproxy` uses the same separator, but this crate's matching follows
/// Chromium's `ProxyHostMatchingRules` where the two readers differ.
///
/// The Windows tokens `<local>` and `<-loopback>` are read here too, which Go does not do;
/// Chromium feeds the `no_proxy` environment variable through the same
/// `ProxyHostMatchingRules::ParseFromString` as any other bypass list, and that is where
/// the tokens are recognised. See [`proxy_override`], which differs in its separators and
/// in reading a bare name as the one host rather than as the domain under it.
///
/// Two further departures from Go follow from reading `*` as Chromium's glob rather than
/// Go's optional prefix. A star is matched against the destination's text, so `*.*.*.1`
/// bypasses `10.0.0.1`, where Go answers no domain entry at all against an address
/// destination (`httpproxy/proxy.go:367-369`). An entry whose body ends in a label that
/// reads as a number (`.example.123`) goes to [`BypassRules::rejected`]. This is
/// unreachable for the special schemes these lists are written about, but a
/// `custom://a.example.123/` it would have matched is proxied instead. That URL matters
/// because `resolve` sends an unknown scheme to the catch-all entry rather than declining
/// it.
///
/// An IPv4 address written other than as four decimal octets is read the URL way, as
/// Chromium's `IPAddress::AssignFromIPLiteral` reads it, in an address entry and in a CIDR
/// block whose address has four parts: `012.1.2.3` is `10.1.2.3` and `012.1.2.0/24` is
/// `10.1.2.0/24`. A block with a short-form address (`192.168.1/24`) is an error. Limitation: curl and Go read neither
/// as an address, so under them the entry matches nothing, and this answers `Direct` for a
/// destination those two proxy.
///
/// ```
/// # use proxy_watch::parse;
/// let rules = parse::no_proxy("<local>,<-loopback>");
/// assert!(rules.excludes_simple_hostnames());
/// assert!(!rules.bypass_loopback());
/// ```
// KDE's `NoProxyFor` (and the variable it names under `ProxyType = 4`): `no_proxy`'s
// separators and grammar, KIO's comparison.
pub
// Fold a list's entries into one rule set, each read in its own dialect.
//
// Takes an iterator rather than a string because not every source is one: GNOME's
// `ignore-hosts` arrives as a GSettings `as`, where the array *is* the delimiter and no
// character in an element is one.
pub
/// Assemble a [`ProxyMode::Manual`] from the two Windows registry strings.
///
/// [`proxy_server`] + [`proxy_override`]; the returned manual mode records dropped server
/// tokens. Empty server, no rejects →
/// [`ProxyMode::Direct`]; reject-only stays `Manual` so rejects are not lost.
///
/// ```
/// # use proxy_watch::parse;
/// let mode = parse::windows_manual("http=h:8080;https=h:-1", "");
/// assert_eq!(mode.rejected().unwrap()[0].redacted_input(), "https=h:-1");
/// // The well formed `http=` entry survives alongside the dropped `https=` one.
/// assert!(mode.endpoint_for(proxy_watch::Scheme::Http).is_some());
/// ```