Skip to main content

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}