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