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}