proxy_watch/pac/winhttp.rs
1//! `pac-windows-native`: PAC/WPAD via WinHTTP (`cfg(windows)` + feature; no-op elsewhere).
2//!
3//! Not a [`PacEvaluator`](super::PacEvaluator): WinHTTP takes *where* the script lives
4//! (`WINHTTP_AUTOPROXY_OPTIONS`), not a JS body — separate API. Sole route that
5//! downloads/discovers for you (`winhttp.dll`); [`ProxyMode::PacInline`] stays `pac-boa`.
6//!
7//! Async `WinHttpGetProxyForUrlEx` is blocked on a manual-reset [`Event`] with a
8//! **bounded timeout** (cancel by closing the resolver; callback keeps its `Arc` alive).
9
10use std::collections::HashSet;
11use std::ffi::c_void;
12use std::fmt;
13use std::net::Ipv6Addr;
14use std::ptr;
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU32, Ordering};
17use std::time::Duration;
18
19use url::{Host, Url};
20
21use windows::Win32::Foundation::{ERROR_IO_PENDING, ERROR_SUCCESS, WAIT_OBJECT_0};
22use windows::Win32::Networking::WinHttp::{
23 ERROR_WINHTTP_AUTODETECTION_FAILED, ERROR_WINHTTP_BAD_AUTO_PROXY_SCRIPT,
24 ERROR_WINHTTP_INTERNAL_ERROR, WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_ASYNC_RESULT,
25 WINHTTP_AUTO_DETECT_TYPE_DHCP, WINHTTP_AUTO_DETECT_TYPE_DNS_A, WINHTTP_AUTOPROXY_AUTO_DETECT,
26 WINHTTP_AUTOPROXY_CONFIG_URL, WINHTTP_AUTOPROXY_OPTIONS,
27 WINHTTP_CALLBACK_FLAG_GETPROXYFORURL_COMPLETE, WINHTTP_CALLBACK_FLAG_REQUEST_ERROR,
28 WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE, WINHTTP_CALLBACK_STATUS_REQUEST_ERROR,
29 WINHTTP_FLAG_ASYNC, WINHTTP_INTERNET_SCHEME_HTTP, WINHTTP_INTERNET_SCHEME_HTTPS,
30 WINHTTP_INTERNET_SCHEME_SOCKS, WINHTTP_PROXY_RESULT, WINHTTP_PROXY_RESULT_ENTRY,
31 WinHttpCloseHandle, WinHttpCreateProxyResolver, WinHttpFreeProxyResult,
32 WinHttpGetProxyForUrlEx, WinHttpGetProxyResult, WinHttpOpen, WinHttpSetStatusCallback,
33 WinHttpSetTimeouts,
34};
35use windows::Win32::System::Threading::{SetEvent, WaitForSingleObject};
36use windows::core::PCWSTR;
37
38use crate::config::{ProxyConfig, ProxyConfigSource};
39use crate::endpoint::{ProxyEndpoint, ProxyScheme};
40use crate::error::Error;
41use crate::mode::ProxyMode;
42use crate::resolve::ProxyStep;
43use crate::sys::win::ffi::{Event, wide, wide_ptr_to_string};
44
45/// Default native resolution budget (5 s) — same as [`DEFAULT_PAC_TIMEOUT`](super::DEFAULT_PAC_TIMEOUT),
46/// but also covers WPAD discovery and script download.
47pub const DEFAULT_WINHTTP_PAC_TIMEOUT: Duration = Duration::from_secs(5);
48
49// The user agent WinHTTP reports while downloading a PAC script.
50const USER_AGENT: &str = "proxy-watch";
51
52/// Where WinHTTP looks for the PAC script (`WINHTTP_AUTOPROXY_OPTIONS` — not a body).
53#[derive(Clone, PartialEq, Eq)]
54#[non_exhaustive]
55pub enum WinHttpPacSource {
56 /// WPAD: DHCP option 252, then `wpad.<domain>`. Miss → not an error
57 /// ([`WinHttpPacResolver::resolve`]).
58 AutoDetect,
59 /// Explicit PAC URL ([`ProxyMode::Pac`]).
60 Url(Url),
61 /// WPAD first, then URL — Windows "auto-detect + script" both ticked.
62 /// [`ProxyMode`] is single-valued; [`WinHttpPacResolver::resolve_config`] rebuilds
63 /// this for [`ProxyMode::WpadAutoDetect`] by re-reading the registry.
64 AutoDetectThenUrl(Url),
65}
66
67// Masked like [`ProxyMode::Pac`]'s `Debug`: an `AutoConfigURL` read off a real machine
68// can carry `user:password@`, and this type is where that URL lands on its way into
69// WinHTTP. Deriving `Debug` would print what [`ProxyMode`] took care not to.
70impl fmt::Debug for WinHttpPacSource {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 match self {
73 Self::AutoDetect => f.write_str("AutoDetect"),
74 Self::Url(url) => f
75 .debug_tuple("Url")
76 .field(&format_args!(
77 "{}",
78 crate::util::redact_userinfo(url.as_str())
79 ))
80 .finish(),
81 Self::AutoDetectThenUrl(url) => f
82 .debug_tuple("AutoDetectThenUrl")
83 .field(&format_args!(
84 "{}",
85 crate::util::redact_userinfo(url.as_str())
86 ))
87 .finish(),
88 }
89 }
90}
91
92impl WinHttpPacSource {
93 /// [`ProxyMode::Pac`] → [`Url`](Self::Url), [`WpadAutoDetect`](ProxyMode::WpadAutoDetect)
94 /// → [`AutoDetect`](Self::AutoDetect); [`PacInline`](ProxyMode::PacInline) → `None`.
95 ///
96 /// ```
97 /// # #[cfg(all(windows, feature = "pac-windows-native"))] {
98 /// use proxy_watch::pac::WinHttpPacSource;
99 /// use proxy_watch::{ProxyMode, Url};
100 ///
101 /// let mode = ProxyMode::pac(Url::parse("http://wpad.corp/proxy.pac").unwrap());
102 /// assert!(matches!(WinHttpPacSource::from_mode(&mode), Some(WinHttpPacSource::Url(_))));
103 /// assert_eq!(
104 /// WinHttpPacSource::from_mode(&ProxyMode::WpadAutoDetect),
105 /// Some(WinHttpPacSource::AutoDetect)
106 /// );
107 /// assert_eq!(WinHttpPacSource::from_mode(&ProxyMode::Direct), None);
108 /// # }
109 /// ```
110 #[must_use]
111 pub fn from_mode(mode: &ProxyMode) -> Option<Self> {
112 match mode {
113 ProxyMode::Pac { url, .. } => Some(Self::Url(url.clone())),
114 ProxyMode::WpadAutoDetect => Some(Self::AutoDetect),
115 _ => None,
116 }
117 }
118
119 // The `dwFlags` value for `WINHTTP_AUTOPROXY_OPTIONS`.
120 fn flags(&self) -> u32 {
121 match self {
122 Self::AutoDetect => WINHTTP_AUTOPROXY_AUTO_DETECT,
123 Self::Url(_) => WINHTTP_AUTOPROXY_CONFIG_URL,
124 Self::AutoDetectThenUrl(_) => {
125 WINHTTP_AUTOPROXY_AUTO_DETECT | WINHTTP_AUTOPROXY_CONFIG_URL
126 }
127 }
128 }
129
130 fn url(&self) -> Option<&Url> {
131 match self {
132 Self::AutoDetect => None,
133 Self::Url(url) | Self::AutoDetectThenUrl(url) => Some(url),
134 }
135 }
136}
137
138// Whether a status code is the "WPAD found nothing" answer *for this request*.
139//
140// `ERROR_WINHTTP_AUTODETECTION_FAILED` can only mean a discovery miss when discovery was
141// asked for. A [`WinHttpPacSource::Url`] request names one place to look and has nothing
142// to discover, so the same code has to stay an error there: degrading it to `[Direct]`
143// would turn a failed PAC fetch into "no proxy" and silently bypass the proxy an
144// administrator configured. Measured Windows runs put a URL-side failure on
145// `ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT` instead, so no call has ever been seen to
146// take this path — the guard makes the contract stated on `resolve_raw` load-bearing
147// rather than repairing an observed answer.
148fn is_wpad_miss(flags: u32, status: u32) -> bool {
149 status == ERROR_WINHTTP_AUTODETECTION_FAILED && flags & WINHTTP_AUTOPROXY_AUTO_DETECT != 0
150}
151
152// Convert the configured resolution budget into the timeout for the
153// `WaitForSingleObject` in [`WinHttpPacResolver::resolve_raw`].
154//
155// The clamp lands at `u32::MAX - 1`, not `u32::MAX`: the latter *is* `INFINITE`, so
156// saturating there would turn the bounded wait `resolve_raw` promises into an unbounded
157// one and [`Error::PacTimeout`] could never come from it. Only a `timeout` past roughly
158// fifty days reaches the saturation at all. Same shape as `poll_wait_millis` and
159// `debounce_wait_millis` in `src/sys/win/notify.rs`, for the same reason.
160fn wait_millis(timeout: Duration) -> u32 {
161 u32::try_from(timeout.as_millis())
162 .unwrap_or(u32::MAX)
163 .min(u32::MAX - 1)
164}
165
166// Convert the same budget into the four timeouts the WinHTTP session itself is given.
167//
168// Floored at 1, not 0, for the mirror of the reason [`wait_millis`] clamps below
169// `INFINITE`: `WinHttpSetTimeouts` documents "A value of 0 or -1 sets a time-out to wait
170// infinitely", so a budget shorter than a millisecond would truncate onto exactly the
171// meaning [`WinHttpPacResolver::with_timeout`] refuses to let a caller ask for, and would
172// leave the session's name resolution, connect, send and receive unbounded for the life of
173// the resolver. Saturating at `i32::MAX` is safe at the other end: only a negative other
174// than -1 is rejected by the API.
175fn session_timeout_millis(timeout: Duration) -> i32 {
176 i32::try_from(timeout.as_millis())
177 .unwrap_or(i32::MAX)
178 .max(1)
179}
180
181/// PAC/WPAD via `WinHttpGetProxyForUrlEx`. Reuse one session; the cache it does *not*
182/// bound is described on [`resolve`](Self::resolve).
183///
184/// [`Pac`](ProxyMode::Pac) / [`WpadAutoDetect`](ProxyMode::WpadAutoDetect): native fetch.
185/// [`PacInline`](ProxyMode::PacInline): `pac-boa`. Direct/Manual: [`resolve_config`](Self::resolve_config).
186/// [`resolve_config`](Self::resolve_config) answers a hostless URL Direct, except
187/// [`PacInline`](ProxyMode::PacInline): that one is [`Error::PacNotSupported`] whether or not
188/// the URL has a host. [`resolve`](Self::resolve) does not short-circuit it — see its doc.
189/// **[`PacPolicy`](super::PacPolicy) does not apply** — real DNS/local IP.
190#[derive(Debug)]
191pub struct WinHttpPacResolver {
192 session: Session,
193 timeout: Duration,
194}
195
196impl WinHttpPacResolver {
197 /// Open a session with the default [`DEFAULT_WINHTTP_PAC_TIMEOUT`] budget.
198 ///
199 /// # Errors
200 ///
201 /// [`Error::Io`] when `WinHttpOpen`, `WinHttpSetTimeouts` or
202 /// `WinHttpSetStatusCallback` fails.
203 pub fn new() -> Result<Self, Error> {
204 Self::with_timeout(DEFAULT_WINHTTP_PAC_TIMEOUT)
205 }
206
207 /// Open a session; zero `timeout` → [`Error::PacTimeout`] (not "unlimited").
208 ///
209 /// # Errors
210 ///
211 /// [`Error::Io`] (`WinHttpOpen` / `SetTimeouts` / `SetStatusCallback`) or
212 /// [`Error::PacTimeout`] when `timeout` is zero.
213 pub fn with_timeout(timeout: Duration) -> Result<Self, Error> {
214 if timeout.is_zero() {
215 return Err(Error::PacTimeout { timeout });
216 }
217 let session = Session::open(timeout)?;
218 Ok(Self { session, timeout })
219 }
220
221 /// The per-resolution budget.
222 #[must_use]
223 pub fn timeout(&self) -> Duration {
224 self.timeout
225 }
226
227 /// Resolve `url` via `source`. Full `FindProxyForURL` chain → [`ProxyStep`]s
228 /// (HTTP/HTTPS/SOCKS**4**; `fProxy == FALSE` → Direct; FTP skipped; repeats collapse,
229 /// as in [`parse_find_proxy_result`](super::parse_find_proxy_result), so the chain can
230 /// be shorter than the entry count WinHTTP reported).
231 ///
232 /// `ERROR_WINHTTP_AUTODETECTION_FAILED` → `[Direct]`, but only from a `source` that
233 /// asked for auto-detect. Download / script failure — and that same code from a
234 /// URL-only source, where there was nothing to discover — stays an error.
235 ///
236 /// A hostless URL (`mailto:`, `data:`) is resolved rather than short-circuited: this is
237 /// the engine door, the native counterpart of [`pac::evaluate`](super::evaluate), and
238 /// `FindProxyForURL` sees it with an empty host. Answering Direct without asking belongs
239 /// to [`resolve_config`](Self::resolve_config), which was handed a whole configuration to
240 /// decide from rather than a `source` the caller had already chosen.
241 ///
242 /// Asking twice can be answered once. What WinHTTP caches is the autoproxy URL and the
243 /// script — a repeat resolution re-runs that script rather than fetching it again, and
244 /// a WPAD miss is remembered for as long as the session lives. The call that would
245 /// bypass the cache is deliberately not made: it hands the user's credentials to
246 /// whatever WPAD found. Nothing here flushes it, and neither does dropping the
247 /// resolver: `WinHttpGetProxyForUrlEx` "always executes out-of-process", and with that
248 /// service active Microsoft's AutoProxy Cache topic puts the cached URL and script
249 /// "available to the whole computer" — typically until the machine's IP address
250 /// changes, which no caller controls. Unlike [`ProxyConfig`] the result carries no
251 /// capture time to say which fetch it came from.
252 ///
253 /// A [`PacTimeout`](Error::PacTimeout) cancels the resolution without finishing it: the
254 /// abandoned operation goes on draining behind this session, and a retry issued at once
255 /// can spend its own budget waiting behind that rather than on the script. Widen the
256 /// timeout or wait before retrying.
257 ///
258 /// # Errors
259 ///
260 /// [`Error::PacTimeout`], [`Error::PacEvaluation`]
261 /// (`ERROR_WINHTTP_BAD_AUTO_PROXY_SCRIPT`), [`Error::PacInvalidResult`],
262 /// [`Error::Io`] (incl. undownloadable PAC URL).
263 pub fn resolve(&self, url: &Url, source: &WinHttpPacSource) -> Result<Vec<ProxyStep>, Error> {
264 match self.resolve_raw(url, source)? {
265 WpadOutcome::Resolved(steps) => Ok(steps),
266 // See this method's own doc: a genuine WPAD miss is not an error.
267 WpadOutcome::AutoDetectionFailed => Ok(vec![ProxyStep::Direct]),
268 }
269 }
270
271 // Distinguishes genuine AUTODETECTION_FAILED from a script's own DIRECT (fallback needs that).
272 //
273 // Not an error, synchronously either: `WinHttpGetProxyForUrlEx` need not go asynchronous
274 // to fail detection. WinHTTP remembers a WPAD miss for the life of the session, so a
275 // second call on one session can hand back `ERROR_WINHTTP_AUTODETECTION_FAILED` from the
276 // call itself, with no callback. The status means the same thing on both paths and is
277 // mapped the same way; only the callback context reference is reclaimed differently.
278 fn resolve_raw(&self, url: &Url, source: &WinHttpPacSource) -> Result<WpadOutcome, Error> {
279 let resolver = Resolver::create(&self.session)?;
280
281 // Both buffers must outlive the call; `options` only borrows them.
282 //
283 // The *config* URL is not touched — that is the address this crate asks WinHTTP to
284 // fetch, not something a script gets to read.
285 let url_w = wide(&query_url(url));
286 let config_url_w = source.url().map(|url| wide(url.as_str()));
287
288 let options = WINHTTP_AUTOPROXY_OPTIONS {
289 dwFlags: source.flags(),
290 dwAutoDetectFlags: if source.flags() & WINHTTP_AUTOPROXY_AUTO_DETECT == 0 {
291 0
292 } else {
293 WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A
294 },
295 lpszAutoConfigUrl: PCWSTR(config_url_w.as_ref().map_or(ptr::null(), Vec::as_ptr)),
296 lpvReserved: ptr::null_mut(),
297 dwReserved: 0,
298 // WinHTTP's "AutoProxy Cache" topic gives two steps: call with FALSE, and on
299 // `ERROR_WINHTTP_LOGIN_FAILURE` call again with TRUE. Only step 1 is taken
300 // here. TRUE is also what stops WinHTTP caching the autoproxy URL and script
301 // at all under the out-of-process service the `Ex` form always uses, but
302 // performance is not why step 2 is skipped: it hands the user's domain
303 // credentials to whatever WPAD pointed at, so a PAC file behind an
304 // NTLM/Negotiate challenge is left unfetched in a proxy *detection* library.
305 fAutoLogonIfChallenged: false.into(),
306 };
307
308 let pending = Arc::new(Pending::new()?);
309 // One strong reference is handed to WinHTTP as the callback context. It is
310 // reclaimed by the callback, or here when no callback will run.
311 let context = Arc::into_raw(Arc::clone(&pending));
312
313 // SAFETY: `resolver` is a live proxy resolver handle, `url_w` and `config_url_w`
314 // are NUL terminated UTF-16 buffers that outlive the call, `options` borrows only
315 // those buffers, and `context` is a pointer from `Arc::into_raw` whose referent
316 // outlives every callback because the callback owns a strong reference to it.
317 let status = unsafe {
318 WinHttpGetProxyForUrlEx(
319 resolver.0,
320 PCWSTR(url_w.as_ptr()),
321 &raw const options,
322 Some(context as usize),
323 )
324 };
325
326 if status != ERROR_IO_PENDING.0 {
327 // The call did not go asynchronous, so `status_callback` will never run for
328 // it and nobody else owns the reference we just handed out. Microsoft's docs
329 // do not state this rule explicitly, and this branch is rarely exercised in
330 // practice (see the WPAD-miss discussion on this method's doc comment) — the
331 // guarantee rests on the general WinHTTP contract that a synchronous return
332 // never also delivers an asynchronous notification for the same call.
333 // SAFETY: `context` came from `Arc::into_raw` above and has not been consumed.
334 unsafe { drop(Arc::from_raw(context)) };
335 // Mapped the same way as the async branch below; see "Not an error,
336 // synchronously either" on this method's doc comment.
337 if is_wpad_miss(options.dwFlags, status) {
338 return Ok(WpadOutcome::AutoDetectionFailed);
339 }
340 if status != ERROR_SUCCESS.0 {
341 return Err(winhttp_error("WinHttpGetProxyForUrlEx", status));
342 }
343 } else {
344 // SAFETY: the event handle is owned by `pending`, which is alive here.
345 let waited =
346 unsafe { WaitForSingleObject(pending.done.raw(), wait_millis(self.timeout)) };
347 if waited != WAIT_OBJECT_0 {
348 // Closing the handle is the documented way to cancel a pending WinHTTP
349 // operation — `WinHttpGetProxyForUrlEx` lists
350 // `ERROR_WINHTTP_OPERATION_CANCELLED` as "usually because the handle on
351 // which the request was operating was closed before the operation
352 // completed". The callback still fires, and the reference it owns keeps
353 // `pending` alive until it does.
354 //
355 // Explicit, and before the return, because otherwise the order is the
356 // wrong way round. Locals drop in reverse declaration order and `resolver`
357 // is declared first, so falling out of here would free `url_w`,
358 // `config_url_w` and `options` while the operation was still in flight and
359 // close the handle only afterwards. Microsoft states no lifetime rule for
360 // this function's arguments — checked against the reference page, which
361 // annotates both pointers `[in]` and says nothing more, while the
362 // comparable case that does have a rule states it (`WinHttpSendRequest`'s
363 // `lpOptional`). "This function always executes out-of-process" argues the
364 // same way, since arguments that cross a process boundary have to be
365 // copied to get there. Neither is a guarantee: the in-process stub may
366 // return `ERROR_IO_PENDING` first and marshal from a worker afterwards.
367 // Cancelling first costs one line and means not needing the answer.
368 drop(resolver);
369 return Err(Error::PacTimeout {
370 timeout: self.timeout,
371 });
372 }
373 let code = pending.status.load(Ordering::Acquire);
374 if is_wpad_miss(options.dwFlags, code) {
375 return Ok(WpadOutcome::AutoDetectionFailed);
376 }
377 if code != ERROR_SUCCESS.0 {
378 return Err(winhttp_error("resolving the proxy for a URL", code));
379 }
380 }
381
382 let mut raw = WINHTTP_PROXY_RESULT::default();
383 // SAFETY: `resolver` is still open and `raw` is a valid, writable out-parameter.
384 // On success WinHTTP allocates the entry array, which `ProxyResult` frees.
385 let status = unsafe { WinHttpGetProxyResult(resolver.0, &raw mut raw) };
386 if status != ERROR_SUCCESS.0 {
387 return Err(winhttp_error("WinHttpGetProxyResult", status));
388 }
389 ProxyResult(raw).to_steps().map(WpadOutcome::Resolved)
390 }
391
392 /// Like [`resolve_with_pac`](crate::resolve_with_pac) but WinHTTP fetches the script.
393 /// Direct/Manual → [`resolve`](crate::resolve()); hostless → Direct except
394 /// [`PacInline`](ProxyMode::PacInline) → [`Error::PacNotSupported`].
395 ///
396 /// [`WpadAutoDetect`](ProxyMode::WpadAutoDetect) may re-read the live registry for
397 /// PAC URL / static fallback — not a pure function of `config` alone.
398 ///
399 /// # Errors
400 ///
401 /// [`Error::PacNotSupported`] for `PacInline`, plus [`resolve`](Self::resolve)'s errors —
402 /// and, for the Direct/Manual arm this hands over, [`resolve`](crate::resolve())'s, which
403 /// is where [`Error::ProxyEntryUnusable`] comes from.
404 pub fn resolve_config(&self, config: &ProxyConfig, url: &Url) -> Result<Vec<ProxyStep>, Error> {
405 let mode = &config.effective;
406 // Asked through `has_request_host`, not `Url::host`: the Direct/Manual arm below
407 // routes on the former, and the two disagree about a URL whose host was emptied.
408 let has_host = crate::endpoint::has_request_host(url);
409 match mode {
410 ProxyMode::Direct | ProxyMode::Manual { .. } => crate::resolve::resolve(config, url),
411 ProxyMode::PacInline { .. } => Err(Error::PacNotSupported { mode: "pac-inline" }),
412 ProxyMode::WpadAutoDetect if has_host => self.resolve_wpad_with_fallback(config, url),
413 ProxyMode::WpadAutoDetect => Ok(vec![ProxyStep::Direct]),
414 _ => match WinHttpPacSource::from_mode(mode) {
415 Some(source) if has_host => self.resolve(url, &source),
416 Some(_) => Ok(vec![ProxyStep::Direct]),
417 // `ProxyMode` is `#[non_exhaustive]`; a variant added later that this
418 // engine has no answer for must say so rather than guess.
419 None => Err(Error::PacNotSupported { mode: "unknown" }),
420 },
421 }
422 }
423
424 // Resolve [`ProxyMode::WpadAutoDetect`] without confirming `Direct` when a PAC URL
425 // or a static proxy is configured beneath auto-detect.
426 fn resolve_wpad_with_fallback(
427 &self,
428 config: &ProxyConfig,
429 url: &Url,
430 ) -> Result<Vec<ProxyStep>, Error> {
431 // `wpad_fallback` re-reads the per-user Windows store, so it may be called only when
432 // that store is what produced `effective` — not merely when something did. For a
433 // config this crate built the two always coincide: `in_precedence_order` puts
434 // `Registry` first and `from_ordered_sources` takes `effective` from the first entry.
435 // But `ProxyConfig::new` exists so a caller can resolve precedence itself, and
436 // nothing stops it handing over a snapshot whose effective `WpadAutoDetect` came from
437 // `GSettings`, `Kioslaverc` or either macOS scope — every one of which produces that
438 // mode, and none of which is a Windows registry. Re-reading on their behalf answers a
439 // question about another machine with this one's proxy. Matching on the mode alone
440 // was not enough either: the first entry carrying it may be a store this arm cannot
441 // read, and reading the one it can while calling it that entry's fallback is the same
442 // wrong answer wearing the right source. So the entry has to be `Registry` *and*
443 // carry the effective mode; anything else gets no fallback rather than an invented
444 // one, and WPAD is then all it asked for.
445 let (pac_url, beneath) =
446 if config.source(ProxyConfigSource::Registry) == Some(&config.effective) {
447 crate::sys::win::wpad_fallback()?
448 } else {
449 (None, ProxyMode::Direct)
450 };
451
452 let source = match &pac_url {
453 Some(pac_url) => WinHttpPacSource::AutoDetectThenUrl(pac_url.clone()),
454 // Nothing usable configured as an `AutoConfigURL`: probe WPAD alone.
455 None => WinHttpPacSource::AutoDetect,
456 };
457
458 match self.resolve_raw(url, &source)? {
459 WpadOutcome::Resolved(steps) => Ok(steps),
460 // Neither WPAD discovery nor the `AutoConfigURL` above produced a script that
461 // could be run, so the answer is whatever was configured beneath both — which
462 // is a static proxy far more often than it is nothing. Returning the failure
463 // instead throws that away whenever a PAC URL sits between the two. See
464 // `wpad_fallback_beneath`.
465 WpadOutcome::AutoDetectionFailed => match beneath {
466 ProxyMode::Manual { .. } => {
467 // This mode is built here and dropped here, so any record it carries
468 // beyond the one an `Error::ProxyEntryUnusable` clones is lost with it —
469 // the caller still holds `WpadAutoDetect`. That error's `rejected` doc
470 // names this arm as the exception to its reachability sentence; give it
471 // somewhere else to go and the doc goes with it.
472 let manual = ProxyConfig::from_source(ProxyConfigSource::Registry, beneath);
473 crate::resolve::resolve(&manual, url)
474 }
475 // Nothing configured beneath auto-detect at all: a WPAD miss really does
476 // mean direct here, the same answer `resolve` itself would give.
477 _ => Ok(vec![ProxyStep::Direct]),
478 },
479 }
480 }
481}
482
483// The outcome [`WinHttpPacResolver::resolve_raw`] reports, distinguishing a genuine
484// `ERROR_WINHTTP_AUTODETECTION_FAILED` from every other terminal result — what
485// [`WinHttpPacResolver::resolve_wpad_with_fallback`] needs and
486// [`WinHttpPacResolver::resolve`] does not.
487#[derive(Debug)]
488enum WpadOutcome {
489 // WinHTTP produced an ordinary result: a script ran (however it answered) or a URL
490 // resolved via `WINHTTP_AUTOPROXY_CONFIG_URL`.
491 Resolved(Vec<ProxyStep>),
492 // `ERROR_WINHTTP_AUTODETECTION_FAILED`: no DHCP option 252, no `wpad.<domain>`
493 // record, or the discovered script was unusable. Not an error, but also not a
494 // script's own answer — the distinction this variant exists to carry.
495 AutoDetectionFailed,
496}
497
498// `context` must be text this crate wrote, and every call site passes a literal. The
499// requirement is the exit below, not the one above it: [`Error::pac_evaluation`] runs its
500// argument through the masking constructor, so a `context` that one day interpolated a URL
501// would still come out masked there — [`Error::io`] stores what it is handed. Written here
502// rather than at either exit because the exit that needs the rule is the one with nothing
503// in it to notice the rule being broken.
504fn winhttp_error(context: &str, code: u32) -> Error {
505 if code == ERROR_WINHTTP_BAD_AUTO_PROXY_SCRIPT {
506 // Through the masking constructor, for the reason `ProxyResult::steps` gives when
507 // it builds `PacInvalidResult` out of text this crate wrote: the variant promises
508 // in its own documentation that `reason` is masked at construction, and a promise
509 // that holds only because no `context` has yet interpolated a URL is one edit away
510 // from being false.
511 return Error::pac_evaluation(format!(
512 "{context}: the PAC script could not be executed by WinHTTP"
513 ));
514 }
515 Error::io(
516 format!("{context} (WinHTTP status {code:#010x})"),
517 std::io::Error::from_raw_os_error(code as i32),
518 )
519}
520
521// The shared state of one in-flight `WinHttpGetProxyForUrlEx` call.
522#[derive(Debug)]
523struct Pending {
524 // Manual-reset: the waiter must be able to observe a completion that happened
525 // before it reached `WaitForSingleObject`.
526 done: Event,
527 // The WinHTTP status code the callback saw, `ERROR_SUCCESS` on completion.
528 status: AtomicU32,
529}
530
531// SAFETY: the only thing in `Pending` that is not already `Sync` is the event handle,
532// which `Event` models as a raw `HANDLE`. `SetEvent` (from the WinHTTP callback thread)
533// and `WaitForSingleObject` (from the caller) are documented as safe to call concurrently
534// on the same event object, and neither side mutates the wrapper itself. Sharing is the
535// entire point: the `Arc` is what lets the callback outlive a timed-out caller.
536unsafe impl Sync for Pending {}
537
538impl Pending {
539 fn new() -> Result<Self, Error> {
540 Ok(Self {
541 done: Event::new(true, "creating the PAC resolution completion event")?,
542 status: AtomicU32::new(ERROR_SUCCESS.0),
543 })
544 }
545
546 // Record the outcome and wake the waiter.
547 //
548 // The discarded `SetEvent` result is the same invariant the `SAFETY` note below states,
549 // read from the other side: the documented failure is an invalid handle, and the handle
550 // is live for as long as `self` is. Discarded rather than propagated because there is
551 // nobody here to propagate to — this runs on WinHTTP's callback thread, reached through
552 // a raw `extern "system"` function, and the caller that wants the answer is blocked in
553 // `WaitForSingleObject` inside `resolve_raw`.
554 //
555 // What it would cost if the invariant were ever broken is worth naming, because the
556 // failure does not surface as itself: the waiter would sleep out its whole budget and
557 // return [`Error::PacTimeout`] for a resolution that had already finished — the status
558 // stored on the line above, and the proxy WinHTTP found, both discarded with it. That is
559 // a wrong error rather than a silent one, so it is visible; it just points at the clock
560 // instead of at the handle.
561 fn finish(&self, code: u32) {
562 self.status.store(code, Ordering::Release);
563 // SAFETY: `self.done` owns a live event handle for as long as `self` exists, and
564 // the caller of this function holds a strong reference to `self`.
565 unsafe {
566 let _ = SetEvent(self.done.raw());
567 }
568 }
569}
570
571// The session-wide status callback.
572unsafe extern "system" fn status_callback(
573 _handle: *mut c_void,
574 context: usize,
575 status: u32,
576 info: *mut c_void,
577 _info_len: u32,
578) {
579 let code = match status {
580 WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE => ERROR_SUCCESS.0,
581 WINHTTP_CALLBACK_STATUS_REQUEST_ERROR => {
582 if info.is_null() {
583 // Should not happen. Report something rather than claim success, but not
584 // `ERROR_WINHTTP_AUTODETECTION_FAILED`: `resolve_raw` maps that onto a
585 // non-error "go Direct", which would hide a genuine config-URL failure.
586 ERROR_WINHTTP_INTERNAL_ERROR
587 } else {
588 // SAFETY: for `WINHTTP_CALLBACK_STATUS_REQUEST_ERROR`, WinHTTP documents
589 // `lpvStatusInformation` as a pointer to a `WINHTTP_ASYNC_RESULT` that is
590 // valid for the duration of the callback.
591 unsafe { (*info.cast::<WINHTTP_ASYNC_RESULT>()).dwError }
592 }
593 }
594 // Returning here must not consume the context reference, or a later real
595 // notification would use freed memory.
596 _ => return,
597 };
598 if context == 0 {
599 return;
600 }
601 // SAFETY: `context` is the pointer produced by `Arc::into_raw` in
602 // `WinHttpPacResolver::resolve_raw`, which transferred one strong reference to this
603 // callback. Reclaiming it twice would be a double free, so what rules that out is worth
604 // naming: `WinHttpGetProxyForUrlEx`'s Remarks pair the two statuses this function acts
605 // on with the two outcomes of one pended call — "Once a callback of status
606 // WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE is returned, the application can call
607 // WinHttpGetProxyResult", and "If the call fails after returning ERROR_IO_PENDING then a
608 // callback of WINHTTP_CALLBACK_STATUS_REQUEST_ERROR will be issued". One pended call has
609 // one outcome, so the reference is reclaimed once and the allocation is still live.
610 // *Terminal* is the reading of that pairing rather than a sentence Microsoft writes —
611 // the same standing as the synchronous-return rule in `resolve_raw` — which is why the
612 // arm above returns without consuming the context for every other status, and why
613 // `Session::new` registers only these two flags.
614 let pending: Arc<Pending> = unsafe { Arc::from_raw(context as *const Pending) };
615 pending.finish(code);
616}
617
618// An owned WinHTTP session handle (`WinHttpOpen`), closed on drop.
619#[derive(Debug)]
620struct Session(*mut c_void);
621
622// SAFETY: an `HINTERNET` is process-wide state behind an opaque integer; it is `!Send` and
623// `!Sync` here only because it is modelled as a raw pointer, not because anything about the
624// handle is thread-affine.
625//
626// The load-bearing half is what Microsoft states, and it is less than the `Send`/`Sync`
627// above would suggest. Neither `WinHttpOpen`'s Remarks nor `WinHttpCreateProxyResolver`
628// (which has no Remarks at all) says a handle may be used from any thread; the reference
629// pages were checked for it rather than assumed. What "HINTERNET Handles in WinHTTP" does
630// state is one rule, and it is a caution rather than a guarantee: "These HINTERNET handles
631// cannot be closed while an API call using the handle is in progress. To avoid a race
632// condition, applications should protect the handle and prevent it from being closed for as
633// long as the API call is in progress."
634//
635// That rule is the one this impl has to answer, and ownership answers it without a lock.
636// The session handle is closed by `Session::drop` alone, `resolve` borrows the owner as
637// `&self`, and no `&self` method can run while the value is being dropped — through an
638// `Arc` no differently, since the last reference is what drops it. So the close cannot race
639// an in-progress call, for the same reason a `&mut` cannot coexist with a `&`. Resolver
640// handles never reach a second thread at all: each is a local of `resolve_raw`.
641//
642// Concurrent use from several threads is then inference, held openly as such. It rests on
643// the shape of the API rather than on a sentence: `WinHttpCreateProxyResolver` exists to
644// mint a per-operation child from one long-lived session, `WinHttpOpen`'s Remarks call a
645// single session "normally sufficient" for an application, and `WinHttpGetProxyForUrlEx`
646// "always executes out-of-process" and so is not resolving anything in the caller's own
647// address space. If that inference is ever wrong, the fix is a mutex around `Session`, not
648// a different lifetime.
649unsafe impl Send for Session {}
650// SAFETY: as above.
651unsafe impl Sync for Session {}
652
653impl Session {
654 // Open an asynchronous session that reaches the network without a proxy.
655 fn open(timeout: Duration) -> Result<Self, Error> {
656 let agent = wide(USER_AGENT);
657 // SAFETY: `agent` is a NUL terminated UTF-16 buffer that outlives the call; the
658 // two proxy arguments are unused with `WINHTTP_ACCESS_TYPE_NO_PROXY`.
659 let handle = unsafe {
660 WinHttpOpen(
661 PCWSTR(agent.as_ptr()),
662 WINHTTP_ACCESS_TYPE_NO_PROXY,
663 PCWSTR::null(),
664 PCWSTR::null(),
665 WINHTTP_FLAG_ASYNC,
666 )
667 };
668 if handle.is_null() {
669 return Err(last_error("WinHttpOpen"));
670 }
671 let session = Self(handle);
672
673 let millis = session_timeout_millis(timeout);
674 // Failure here *is* fatal, deliberately: `millis` cannot be out of range, so the
675 // only remaining cause is a session handle that is not what `WinHttpOpen` just
676 // said it was, and every later call on it would fail too.
677 //
678 // SAFETY: `session` owns a live session handle.
679 unsafe { WinHttpSetTimeouts(session.0, millis, millis, millis, millis) }
680 .map_err(|e| crate::sys::win::ffi::hresult_error("WinHttpSetTimeouts", e))?;
681
682 // SAFETY: `session` owns a live session handle and `status_callback` has the
683 // signature WinHTTP requires. Resolver handles created from this session inherit
684 // the callback.
685 let previous = unsafe {
686 WinHttpSetStatusCallback(
687 session.0,
688 Some(status_callback),
689 WINHTTP_CALLBACK_FLAG_GETPROXYFORURL_COMPLETE | WINHTTP_CALLBACK_FLAG_REQUEST_ERROR,
690 0,
691 )
692 };
693 // `WINHTTP_INVALID_STATUS_CALLBACK` is `(WINHTTP_STATUS_CALLBACK)-1`, which the
694 // `windows` crate surfaces as a `Some` holding an unusable function pointer.
695 if previous.is_some_and(|callback| callback as usize == usize::MAX) {
696 return Err(last_error("WinHttpSetStatusCallback"));
697 }
698 Ok(session)
699 }
700}
701
702impl Drop for Session {
703 fn drop(&mut self) {
704 // SAFETY: `self.0` came from `WinHttpOpen`, is owned solely by `self`, and every
705 // resolver handle derived from it is closed before the resolver struct is dropped.
706 unsafe {
707 let _ = WinHttpCloseHandle(self.0);
708 }
709 }
710}
711
712// An owned proxy resolver handle (`WinHttpCreateProxyResolver`), closed on drop.
713#[derive(Debug)]
714struct Resolver(*mut c_void);
715
716impl Resolver {
717 fn create(session: &Session) -> Result<Self, Error> {
718 let mut handle: *mut c_void = ptr::null_mut();
719 // SAFETY: `session` owns a live session handle and `handle` is a valid, writable
720 // out-parameter.
721 let status = unsafe { WinHttpCreateProxyResolver(session.0, &raw mut handle) };
722 if status != ERROR_SUCCESS.0 {
723 return Err(winhttp_error("WinHttpCreateProxyResolver", status));
724 }
725 Ok(Self(handle))
726 }
727}
728
729impl Drop for Resolver {
730 fn drop(&mut self) {
731 // SAFETY: `self.0` came from `WinHttpCreateProxyResolver` and is owned solely by
732 // `self`. Closing it while an operation is still pending is the documented way to
733 // cancel that operation; WinHTTP defers the actual release until its own callback
734 // has run.
735 unsafe {
736 let _ = WinHttpCloseHandle(self.0);
737 }
738 }
739}
740
741// A `WINHTTP_PROXY_RESULT` whose entry array is released on drop.
742struct ProxyResult(WINHTTP_PROXY_RESULT);
743
744impl ProxyResult {
745 // Convert the entry array into an ordered fallback chain.
746 fn to_steps(&self) -> Result<Vec<ProxyStep>, Error> {
747 let count = self.0.cEntries as usize;
748 let entries = if count == 0 || self.0.pEntries.is_null() {
749 &[][..]
750 } else {
751 // SAFETY: on success WinHTTP guarantees `pEntries` points at `cEntries`
752 // initialised entries, owned by `self` until `WinHttpFreeProxyResult`.
753 unsafe { std::slice::from_raw_parts(self.0.pEntries.cast_const(), count) }
754 };
755
756 // Sized from the array that is actually there, not from `cEntries`. The two agree
757 // whenever WinHTTP keeps its promise, and the guard above is already written for the
758 // case where it does not — reserving on the count would be believing it again, two
759 // lines after refusing to. `cEntries` is a `u32`, so believing it once means an
760 // allocation the process aborts on rather than the error this returns.
761 let mut steps: Vec<ProxyStep> = Vec::with_capacity(entries.len());
762 let mut seen = HashSet::with_capacity(entries.len());
763 for entry in entries {
764 // The membership test is the one
765 // [`parse_find_proxy_result`](super::result::parse_find_proxy_result) makes, and
766 // its reasoning carries over unchanged: a chain is a list of things to try in
767 // order, and a proxy that just failed is no more alive the second time it is
768 // named. Kept identical on purpose — the same PAC script reaches this path on
769 // Windows and that one everywhere else, so a chain that collapsed differently
770 // depending on which engine ran it would be a difference the script's author
771 // never asked for.
772 //
773 // The cost is the same one too, and written on the same public doc: `resolve`
774 // returns fewer steps than WinHTTP reported entries, so `len()` is not
775 // `cEntries`. Nothing downstream counts them — the chain is walked in order —
776 // but a caller comparing against the native API would see the gap.
777 //
778 // The membership test is a set for the same reason it is one over there. The
779 // entries are WinHTTP's reading of the same remote script, so `cEntries` is as
780 // much the script's number here as the candidate count is there; the quadratic on
781 // that path is not a property of the parser it shows up in. Not observed here,
782 // because reaching this line needs WinHTTP to return the entries itself.
783 //
784 // SAFETY: the entry belongs to the array borrowed above and its `pwszProxy`
785 // member is either null or a NUL terminated UTF-16 string owned by it.
786 if let Some(step) = unsafe { entry_to_step(entry) }
787 && seen.insert(step.clone())
788 {
789 steps.push(step);
790 }
791 }
792 if steps.is_empty() {
793 // This crate wrote the message itself, but it still goes through the masking
794 // constructor so `PacInvalidResult` is never built any other way.
795 return Err(Error::pac_invalid_result(format!(
796 "WinHTTP returned {count} proxy entries, none of them usable"
797 )));
798 }
799 Ok(steps)
800 }
801}
802
803impl Drop for ProxyResult {
804 fn drop(&mut self) {
805 // SAFETY: the structure was filled in by `WinHttpGetProxyResult` and is owned
806 // solely by `self`, so its entry array is released exactly once.
807 unsafe {
808 WinHttpFreeProxyResult(&raw mut self.0);
809 }
810 }
811}
812
813// One `WINHTTP_PROXY_RESULT_ENTRY`, or `None` when it names no usable transport.
814//
815// # Safety
816//
817// `entry.pwszProxy` must be null or a NUL terminated UTF-16 string valid for the call.
818unsafe fn entry_to_step(entry: &WINHTTP_PROXY_RESULT_ENTRY) -> Option<ProxyStep> {
819 if !entry.fProxy.as_bool() {
820 // `fBypass` distinguishes "the script said DIRECT" from "this destination is on
821 // the bypass list", which is a provenance detail `ProxyStep` does not model:
822 // either way the connection is made without a proxy.
823 return Some(ProxyStep::Direct);
824 }
825
826 let scheme = match entry.ProxyScheme.0 {
827 s if s == WINHTTP_INTERNET_SCHEME_HTTP.0 => ProxyScheme::Http,
828 s if s == WINHTTP_INTERNET_SCHEME_HTTPS.0 => ProxyScheme::Https,
829 // WinHTTP has one SOCKS constant and PAC's bare `SOCKS` keyword is the Netscape
830 // original, i.e. SOCKS4 — the same reading as `parse_find_proxy_result`.
831 s if s == WINHTTP_INTERNET_SCHEME_SOCKS.0 => ProxyScheme::Socks4,
832 // `WINHTTP_INTERNET_SCHEME_FTP`, or anything a later Windows adds: not a proxy
833 // transport this crate models. Skipped like a junk PAC candidate rather than
834 // failing the whole chain.
835 _ => return None,
836 };
837
838 // SAFETY: forwarded from this function's own contract.
839 let host_text = unsafe { wide_ptr_to_string(entry.pwszProxy.0) }?;
840 let host = parse_proxy_host(host_text.trim())?;
841 // No call has been seen to take the zero branch: measured against a served script that
842 // returns `PROXY 203.0.113.7` and one that returns `SOCKS 203.0.113.7`, WinHTTP fills
843 // the port in itself, and with the same numbers `default_port` would have given. Kept
844 // anyway, and named here so that the measurement is not mistaken for a reason to drop
845 // it: `ProxyPort` is a `u16` with no documented default, and port 0 is an endpoint that
846 // connects nowhere. The guard costs one comparison; removing it bets the whole native
847 // path on an undocumented WinHTTP habit.
848 let port = if entry.ProxyPort == 0 {
849 scheme.default_port()
850 } else {
851 entry.ProxyPort
852 };
853 let endpoint = ProxyEndpoint::new(host, port).with_scheme_hint(scheme);
854 // Same table as `parse_find_proxy_result`'s, and neither caller keeps its own `_` arm
855 // over it: separate arms drift apart, and a `ProxyScheme` variant neither can produce
856 // today then becomes a SOCKS4 step here and a SOCKS5 one there.
857 Some(ProxyStep::from_endpoint(endpoint))
858}
859
860// The destination URL as `WinHttpGetProxyForUrlEx` is called with it.
861//
862// Sanitized for the same reason the boa engine sanitizes: WinHTTP hands this string to
863// `FindProxyForURL`, whose script is a WPAD-discovered or plain-HTTP-delivered artefact.
864//
865// A WebSocket destination also has its scheme mapped, because WinHTTP's resolver does not
866// accept one. Chromium measured that on Windows 10 build 16299 and still maps before every
867// call — `ChangeWebSocketSchemeToHttpScheme`, applied in `ProxyResolverWinHttp::GetProxyForURL`
868// in `net/proxy_resolution/win/proxy_resolver_winhttp.cc`, whose comment adds that the
869// documented meaning of `ERROR_WINHTTP_UNRECOGNIZED_SCHEME` implies the same. Without it a
870// `ws:`/`wss:` request gets an error from a machine that has a perfectly good PAC answer for
871// it. The map is only on the string the native call sees; the `boa` engine still shows the
872// script the scheme the caller asked about, which is also where Chromium draws the line —
873// its own evaluator does not rewrite.
874//
875// Matched on the scheme rather than on a `"ws"` prefix: `wsx:` is a URL `Url::parse` accepts
876// and no relation. Once matched, dropping those two characters and writing `http` back is the
877// whole mapping, the surviving `s` of `wss` landing where WHATWG's own pairing puts it.
878// Sanitizing first changes nothing — `sanitize_url` keeps the path and query for `ws` exactly
879// as it does for `http`, and strips them for `wss` exactly as it does for `https`.
880fn query_url(url: &Url) -> String {
881 let sanitized = crate::pac::sanitize_url(url);
882 match url.scheme() {
883 "ws" | "wss" => format!("http{}", &sanitized.as_str()[2..]),
884 _ => sanitized.into(),
885 }
886}
887
888// Parse the host of a proxy entry.
889//
890// WinHTTP hands back the address exactly as the script wrote it, so an IPv6 literal may
891// arrive bare (`::1`) rather than bracketed; [`Host::parse`] only accepts the bracketed
892// form, so the bare one is tried first.
893fn parse_proxy_host(text: &str) -> Option<Host> {
894 if text.is_empty() {
895 return None;
896 }
897 if let Ok(address) = text.parse::<Ipv6Addr>() {
898 return Some(Host::Ipv6(address));
899 }
900 // No empty-domain arm below it: WHATWG host parsing makes an empty host a failure and
901 // `url` implements it there, so every input that percent-decodes and IDNA-maps to
902 // nothing arrives as `Err(ParseError::EmptyHost)` — `url-2.5.8` `src/host.rs:111`
903 // returns it before the `Ok(Host::Domain(..))` at `:119`, the only one `Host::parse`
904 // reaches. The file's other one (`:162`) is `parse_opaque_cow`'s, where an empty host is
905 // legal and unguarded — but nothing here calls `Host::parse_opaque`. An arm for the empty
906 // domain would be a rule no input could reach, and reads as a guard while being none.
907 Host::parse(text).ok()
908}
909
910fn last_error(context: &str) -> Error {
911 Error::io(context.to_owned(), std::io::Error::last_os_error())
912}
913
914#[cfg(test)]
915mod tests {
916 use super::*;
917 use windows::Win32::Networking::WinHttp::WINHTTP_INTERNET_SCHEME;
918
919 // The variant name is most of what this rendering carries: `AutoDetectThenUrl` says WPAD
920 // is tried ahead of the URL, which is the ordering the type exists to hold. These rows
921 // are the only thing holding it: printed as `Url`, the same value reads as a machine
922 // configured with a plain `AutoConfigURL` and no auto-detect at all.
923 //
924 // No credentials in the URL, so these rows pin the framing rather than the masking,
925 // which `debug_masking`'s registry owns for this type.
926 #[test]
927 fn every_pac_source_debug_names_itself_and_keeps_its_url() {
928 let url = Url::parse("https://wpad.corp/proxy.pac").unwrap();
929 for (source, expected) in [
930 (WinHttpPacSource::AutoDetect, "AutoDetect"),
931 (
932 WinHttpPacSource::Url(url.clone()),
933 "Url(https://wpad.corp/proxy.pac)",
934 ),
935 (
936 WinHttpPacSource::AutoDetectThenUrl(url.clone()),
937 "AutoDetectThenUrl(https://wpad.corp/proxy.pac)",
938 ),
939 ] {
940 assert_eq!(format!("{source:?}"), expected);
941 }
942 }
943
944 // [`INFINITE`] is never a legitimate answer for the resolution wait: saturating onto
945 // its bit pattern would drop the budget `with_timeout` accepted and leave the call
946 // blocked until WinHTTP's own timeouts fire instead, so `Error::PacTimeout` could
947 // never come from `resolve_raw`.
948 #[test]
949 fn an_overlong_timeout_is_clamped_below_infinite() {
950 use windows::Win32::System::Threading::INFINITE;
951
952 for timeout in [
953 Duration::MAX,
954 // Fits `u32` but lands exactly on `INFINITE`'s own bit pattern.
955 Duration::from_millis(u64::from(u32::MAX)),
956 Duration::from_millis(u64::from(u32::MAX) + 1),
957 ] {
958 let millis = wait_millis(timeout);
959 assert_eq!(millis, u32::MAX - 1, "{timeout:?}");
960 assert!(millis < INFINITE, "{timeout:?}");
961 }
962 }
963
964 // The clamp must not disturb the budgets anyone actually configures, including the
965 // default this module ships.
966 #[test]
967 fn an_ordinary_timeout_converts_to_milliseconds() {
968 assert_eq!(wait_millis(DEFAULT_WINHTTP_PAC_TIMEOUT), 5_000);
969 assert_eq!(wait_millis(Duration::from_millis(250)), 250);
970 assert_eq!(wait_millis(Duration::ZERO), 0);
971 }
972
973 // The other end of the same budget. `WinHttpSetTimeouts` reads 0 as "wait infinitely",
974 // so truncating a sub-millisecond budget to 0 would hand the session the one meaning
975 // `with_timeout` refuses to let a caller ask for — and would leave it there for every
976 // later resolution, since the session's timeouts are set once.
977 #[test]
978 fn a_sub_millisecond_budget_never_reaches_winhttp_as_infinite() {
979 assert_eq!(session_timeout_millis(Duration::from_micros(500)), 1);
980 assert_eq!(session_timeout_millis(Duration::from_nanos(1)), 1);
981 }
982
983 // The floor must not disturb the budgets anyone actually configures, and the other end
984 // must saturate rather than wrap into the negative range the API rejects outright.
985 #[test]
986 fn an_ordinary_budget_reaches_winhttp_unchanged() {
987 assert_eq!(
988 session_timeout_millis(DEFAULT_WINHTTP_PAC_TIMEOUT),
989 5_000_i32
990 );
991 assert_eq!(session_timeout_millis(Duration::from_millis(250)), 250_i32);
992 assert_eq!(session_timeout_millis(Duration::MAX), i32::MAX);
993 }
994
995 #[test]
996 fn sources_come_from_the_auto_config_modes_only() {
997 let url = Url::parse("http://wpad.corp/proxy.pac").unwrap();
998 assert_eq!(
999 WinHttpPacSource::from_mode(&ProxyMode::pac(url.clone())),
1000 Some(WinHttpPacSource::Url(url))
1001 );
1002 assert_eq!(
1003 WinHttpPacSource::from_mode(&ProxyMode::WpadAutoDetect),
1004 Some(WinHttpPacSource::AutoDetect)
1005 );
1006 assert_eq!(WinHttpPacSource::from_mode(&ProxyMode::Direct), None);
1007 assert_eq!(
1008 WinHttpPacSource::from_mode(&ProxyMode::pac_inline("body".to_owned())),
1009 None
1010 );
1011 // The fifth variant. `resolve_config` is not what it protects — that one returns
1012 // `Direct | Manual` to `resolve()` before reaching here, so the engine's own path
1013 // is safe whatever this answers. What it protects is the published contract: this
1014 // is a `pub fn`, and a caller that derives a source itself and hands it to
1015 // `resolve` would take a `Manual` machine to WPAD discovery, and this assertion is
1016 // the only thing in the tree that would say so.
1017 assert_eq!(
1018 WinHttpPacSource::from_mode(&ProxyMode::manual(
1019 std::collections::HashMap::new(),
1020 crate::BypassRules::new()
1021 )),
1022 None
1023 );
1024 }
1025
1026 #[test]
1027 fn autoproxy_flags_match_the_variant() {
1028 let url = Url::parse("http://wpad.corp/proxy.pac").unwrap();
1029 assert_eq!(
1030 WinHttpPacSource::AutoDetect.flags(),
1031 WINHTTP_AUTOPROXY_AUTO_DETECT
1032 );
1033 assert_eq!(
1034 WinHttpPacSource::Url(url.clone()).flags(),
1035 WINHTTP_AUTOPROXY_CONFIG_URL
1036 );
1037 assert_eq!(
1038 WinHttpPacSource::AutoDetectThenUrl(url.clone()).flags(),
1039 WINHTTP_AUTOPROXY_AUTO_DETECT | WINHTTP_AUTOPROXY_CONFIG_URL
1040 );
1041 assert_eq!(WinHttpPacSource::AutoDetect.url(), None);
1042 assert_eq!(
1043 WinHttpPacSource::AutoDetectThenUrl(url.clone()).url(),
1044 Some(&url)
1045 );
1046 }
1047
1048 // The degrade-to-`Direct` answer is spelled per request, not per status code: a
1049 // configured PAC URL that failed must not be reported as "there is no proxy".
1050 #[test]
1051 fn only_a_request_that_asked_for_auto_detect_can_report_a_wpad_miss() {
1052 let url = Url::parse("http://wpad.corp/proxy.pac").unwrap();
1053 for source in [
1054 WinHttpPacSource::AutoDetect,
1055 WinHttpPacSource::AutoDetectThenUrl(url.clone()),
1056 ] {
1057 assert!(
1058 is_wpad_miss(source.flags(), ERROR_WINHTTP_AUTODETECTION_FAILED),
1059 "{source:?}"
1060 );
1061 }
1062 assert!(!is_wpad_miss(
1063 WinHttpPacSource::Url(url).flags(),
1064 ERROR_WINHTTP_AUTODETECTION_FAILED
1065 ));
1066 // Every other status stays whatever it already was, auto-detect or not.
1067 assert!(!is_wpad_miss(
1068 WINHTTP_AUTOPROXY_AUTO_DETECT,
1069 ERROR_SUCCESS.0
1070 ));
1071 }
1072
1073 // What `WinHttpGetProxyForUrlEx` is actually handed. The two WebSocket rows are the
1074 // scheme WinHTTP will not take; the rest are there so the map cannot grow past them —
1075 // `wsx` in particular, which shares the prefix and is an unrelated scheme.
1076 #[test]
1077 fn a_websocket_destination_reaches_winhttp_as_the_scheme_it_understands() {
1078 let cases = [
1079 // Path and query survive `ws`, as they do `http`.
1080 ("ws://chat.corp/room?id=1#f", "http://chat.corp/room?id=1"),
1081 // And are stripped from `wss`, as they are from `https`.
1082 ("wss://chat.corp/room?id=1", "https://chat.corp/"),
1083 ("http://alice:pw@a.corp/x?q=1#f", "http://a.corp/x?q=1"),
1084 ("https://a.corp/x?q=1", "https://a.corp/"),
1085 ("wsx://a.corp/x", "wsx://a.corp/x"),
1086 ];
1087 for (input, expected) in cases {
1088 assert_eq!(query_url(&Url::parse(input).unwrap()), expected, "{input}");
1089 }
1090 }
1091
1092 #[test]
1093 fn proxy_hosts_parse_in_every_shape_winhttp_produces() {
1094 assert_eq!(
1095 parse_proxy_host("proxy.corp"),
1096 Some(Host::Domain("proxy.corp".to_owned()))
1097 );
1098 assert_eq!(
1099 parse_proxy_host("10.0.0.1"),
1100 Some(Host::Ipv4("10.0.0.1".parse().unwrap()))
1101 );
1102 // Bare and bracketed IPv6 both arrive in practice.
1103 assert_eq!(
1104 parse_proxy_host("::1"),
1105 Some(Host::Ipv6("::1".parse().unwrap()))
1106 );
1107 assert_eq!(
1108 parse_proxy_host("[2001:db8::1]"),
1109 Some(Host::Ipv6("2001:db8::1".parse().unwrap()))
1110 );
1111 assert_eq!(parse_proxy_host(""), None);
1112 }
1113
1114 // The zero branch of `entry_to_step` has no observed call — WinHTTP fills the port in
1115 // itself on every run seen here — so this test is the only thing holding the guard it
1116 // stands on. A hand-built entry asks the question a live run cannot: `ProxyPort` is a
1117 // bare `u16` with no documented default, and 0 is
1118 // an endpoint that connects nowhere, so a step carrying it would fail every attempt
1119 // while reading like a routable proxy.
1120 #[test]
1121 fn a_proxy_entry_without_a_port_takes_its_scheme_default() {
1122 use windows::core::PWSTR;
1123
1124 for (native, scheme, port, expected) in [
1125 (WINHTTP_INTERNET_SCHEME_HTTP, ProxyScheme::Http, 0u16, 80u16),
1126 (WINHTTP_INTERNET_SCHEME_HTTPS, ProxyScheme::Https, 0, 443),
1127 (WINHTTP_INTERNET_SCHEME_SOCKS, ProxyScheme::Socks4, 0, 1080),
1128 // A port WinHTTP did fill in passes through untouched, so the guard cannot be
1129 // a rule that rewrites every entry.
1130 (WINHTTP_INTERNET_SCHEME_HTTP, ProxyScheme::Http, 8080, 8080),
1131 ] {
1132 let mut host = wide("proxy.corp");
1133 let entry = WINHTTP_PROXY_RESULT_ENTRY {
1134 fProxy: true.into(),
1135 fBypass: false.into(),
1136 ProxyScheme: native,
1137 pwszProxy: PWSTR(host.as_mut_ptr()),
1138 ProxyPort: port,
1139 };
1140 // SAFETY: `host` is a NUL terminated UTF-16 buffer that outlives the call and
1141 // is not aliased while it runs.
1142 let step = unsafe { entry_to_step(&entry) };
1143 assert_eq!(
1144 step,
1145 Some(ProxyStep::from_endpoint(
1146 ProxyEndpoint::new(Host::Domain("proxy.corp".to_owned()), expected)
1147 .with_scheme_hint(scheme)
1148 )),
1149 "{native:?} port {port}"
1150 );
1151 }
1152 }
1153
1154 // One proxy candidate, built the way WinHTTP would have filled it in.
1155 fn proxy_entry(
1156 scheme: WINHTTP_INTERNET_SCHEME,
1157 host: &mut [u16],
1158 port: u16,
1159 ) -> WINHTTP_PROXY_RESULT_ENTRY {
1160 WINHTTP_PROXY_RESULT_ENTRY {
1161 fProxy: true.into(),
1162 fBypass: false.into(),
1163 ProxyScheme: scheme,
1164 pwszProxy: windows::core::PWSTR(host.as_mut_ptr()),
1165 ProxyPort: port,
1166 }
1167 }
1168
1169 // Drive the real [`ProxyResult::to_steps`] over an array the test owns.
1170 //
1171 // `ProxyResult`'s `Drop` hands its array to `WinHttpFreeProxyResult`, and this one came
1172 // from a `let`, so the wrapper must not run it. Built as the wrapper rather than as a
1173 // conversion core split off onto a plain entry slice — which is what the finding
1174 // proposed — because the count and the pointer are what the last test below disagrees
1175 // about, and a slice is a shape that cannot hold that disagreement.
1176 fn steps_of(
1177 count: u32,
1178 entries: *mut WINHTTP_PROXY_RESULT_ENTRY,
1179 ) -> Result<Vec<ProxyStep>, Error> {
1180 let result = std::mem::ManuallyDrop::new(ProxyResult(WINHTTP_PROXY_RESULT {
1181 cEntries: count,
1182 pEntries: entries,
1183 }));
1184 result.to_steps()
1185 }
1186
1187 fn endpoint_step(host: &str, port: u16, scheme: ProxyScheme) -> ProxyStep {
1188 ProxyStep::from_endpoint(
1189 ProxyEndpoint::new(Host::Domain(host.to_owned()), port).with_scheme_hint(scheme),
1190 )
1191 }
1192
1193 // Which candidates of a mixed result reach the chain, and in which order. Four kinds are
1194 // dropped here without ending the resolution — a transport this crate does not model, a
1195 // pointer with no string behind it, a host that parses as nothing, and a repeat — and two
1196 // kinds survive.
1197 //
1198 // Three of the four drops can be told apart here; the null pointer cannot, because it is
1199 // refused twice over — `wide_ptr_to_string` gives nothing back, and the
1200 // empty string that would stand in for it is not a host either. Its entry is here for the
1201 // one thing that is its own: a null `pwszProxy` is a pointer this reader must not follow.
1202 //
1203 // This test is the only thing holding any of it. The live test in `tests/pac_winhttp.rs`
1204 // reads whatever the machine's own script answers and asks only for a non-empty chain,
1205 // and it is the only other caller that can reach `to_steps` at all, because the entry
1206 // array is WinHTTP's to allocate. It therefore cannot tell a chain that stops at the
1207 // first junk candidate from one that keeps the junk and hands the caller a step naming a
1208 // transport it cannot open.
1209 #[test]
1210 fn a_mixed_chain_keeps_the_usable_candidates_in_order() {
1211 use windows::Win32::Networking::WinHttp::WINHTTP_INTERNET_SCHEME_FTP;
1212
1213 let mut good = wide("proxy.corp");
1214 let mut secure = wide("secure.corp");
1215 let mut files = wide("files.corp");
1216 let mut malformed = wide("proxy corp");
1217 let mut entries = [
1218 proxy_entry(WINHTTP_INTERNET_SCHEME_FTP, &mut files, 21),
1219 proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut good, 3128),
1220 WINHTTP_PROXY_RESULT_ENTRY {
1221 pwszProxy: windows::core::PWSTR(ptr::null_mut()),
1222 ..proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut good, 8080)
1223 },
1224 proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut malformed, 8080),
1225 // The candidate already in the chain, named a second time.
1226 proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut good, 3128),
1227 // `DIRECT`, which every other field of the entry is silent about.
1228 WINHTTP_PROXY_RESULT_ENTRY {
1229 fProxy: false.into(),
1230 ..proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut good, 0)
1231 },
1232 proxy_entry(WINHTTP_INTERNET_SCHEME_HTTPS, &mut secure, 8443),
1233 ];
1234
1235 let count = entries.len() as u32;
1236 assert_eq!(
1237 steps_of(count, entries.as_mut_ptr()).expect("three candidates were usable"),
1238 vec![
1239 endpoint_step("proxy.corp", 3128, ProxyScheme::Http),
1240 ProxyStep::Direct,
1241 endpoint_step("secure.corp", 8443, ProxyScheme::Https),
1242 ]
1243 );
1244 }
1245
1246 // The all-junk chain, which is the one shape that must not fail open: a managed PAC whose
1247 // every candidate this reader refuses is a machine with no answer, not a machine allowed
1248 // to connect straight out. The portable parser holds the same rule over its own input,
1249 // and holds nothing about this reader — the two share a `FindProxyForURL` and no code.
1250 #[test]
1251 fn a_chain_with_nothing_usable_in_it_is_an_error_rather_than_a_direct_connection() {
1252 use windows::Win32::Networking::WinHttp::WINHTTP_INTERNET_SCHEME_FTP;
1253
1254 let mut files = wide("files.corp");
1255 let mut malformed = wide("proxy corp");
1256 let mut entries = [
1257 proxy_entry(WINHTTP_INTERNET_SCHEME_FTP, &mut files, 21),
1258 proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut malformed, 8080),
1259 ];
1260
1261 let count = entries.len() as u32;
1262 let error = steps_of(count, entries.as_mut_ptr()).unwrap_err();
1263 assert!(matches!(error, Error::PacInvalidResult { .. }), "{error:?}");
1264 // Candidates arrived and were refused, which is not the event a script returning
1265 // nothing at all would be. The number is the only place the message says which.
1266 assert!(error.to_string().contains('2'), "{error}");
1267 }
1268
1269 // A count with no array behind it. WinHTTP promises `cEntries` initialised entries on
1270 // success and the null check says that promise is not what the reader rests on, so the
1271 // number beside it is not to be believed either: a `u32` of them is a reservation the
1272 // allocator refuses and the process aborts on, in place of the error below. Reaching this
1273 // needs the count and the pointer to disagree, which is why the helper above takes them
1274 // apart.
1275 #[test]
1276 fn an_entry_count_with_no_array_behind_it_is_not_believed() {
1277 let error = steps_of(u32::MAX, ptr::null_mut()).unwrap_err();
1278 assert!(matches!(error, Error::PacInvalidResult { .. }), "{error:?}");
1279 }
1280
1281 #[test]
1282 fn a_zero_timeout_is_refused_rather_than_meaning_forever() {
1283 let error = WinHttpPacResolver::with_timeout(Duration::ZERO).unwrap_err();
1284 assert!(matches!(error, Error::PacTimeout { .. }), "{error:?}");
1285 }
1286
1287 // No caller passes a `context` like this one, which is the point: the masking must not
1288 // depend on that staying true, because `PacEvaluation` documents it as a property of
1289 // the variant rather than of who happens to build it.
1290 #[test]
1291 fn a_script_error_masks_its_context_even_when_that_context_carries_credentials() {
1292 let error = winhttp_error(
1293 "fetching http://alice:hunter2@wpad.corp/proxy.pac",
1294 ERROR_WINHTTP_BAD_AUTO_PROXY_SCRIPT,
1295 );
1296 assert!(matches!(error, Error::PacEvaluation { .. }), "{error:?}");
1297 let rendered = format!("{error} {error:?}");
1298 assert!(!rendered.contains("hunter2"), "{rendered}");
1299 // The password is the secret; the user name stays, as it does in `ProxyAuth`'s
1300 // own `Debug`.
1301 assert!(rendered.contains("alice:"), "{rendered}");
1302 assert!(rendered.contains("wpad.corp"), "{rendered}");
1303 }
1304}