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