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}