use std::ffi::c_void;
use std::fmt;
use std::net::Ipv6Addr;
use std::ptr;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};
use url::{Host, Url};
use windows::Win32::Foundation::{ERROR_IO_PENDING, ERROR_SUCCESS, WAIT_OBJECT_0, WAIT_TIMEOUT};
use windows::Win32::Networking::WinHttp::{
ERROR_WINHTTP_AUTODETECTION_FAILED, ERROR_WINHTTP_BAD_AUTO_PROXY_SCRIPT,
ERROR_WINHTTP_INTERNAL_ERROR, WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_ASYNC_RESULT,
WINHTTP_AUTO_DETECT_TYPE_DHCP, WINHTTP_AUTO_DETECT_TYPE_DNS_A, WINHTTP_AUTOPROXY_AUTO_DETECT,
WINHTTP_AUTOPROXY_CONFIG_URL, WINHTTP_AUTOPROXY_OPTIONS,
WINHTTP_CALLBACK_FLAG_GETPROXYFORURL_COMPLETE, WINHTTP_CALLBACK_FLAG_REQUEST_ERROR,
WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE, WINHTTP_CALLBACK_STATUS_REQUEST_ERROR,
WINHTTP_FLAG_ASYNC, WINHTTP_INTERNET_SCHEME_HTTP, WINHTTP_INTERNET_SCHEME_HTTPS,
WINHTTP_INTERNET_SCHEME_SOCKS, WINHTTP_PROXY_RESULT, WINHTTP_PROXY_RESULT_ENTRY,
WINHTTP_RESET_ALL, WINHTTP_RESET_OUT_OF_PROC, WinHttpCloseHandle, WinHttpCreateProxyResolver,
WinHttpFreeProxyResult, WinHttpGetProxyForUrlEx, WinHttpGetProxyResult, WinHttpOpen,
WinHttpResetAutoProxy, WinHttpSetStatusCallback, WinHttpSetTimeouts,
};
use windows::Win32::System::Threading::{SetEvent, WaitForSingleObject};
use windows::core::PCWSTR;
use crate::config::{ProxyConfig, ProxyConfigSource};
use crate::endpoint::{ProxyEndpoint, ProxyScheme};
use crate::error::Error;
use crate::mode::ProxyMode;
use crate::resolve::ProxyStep;
use crate::sys::win::ffi::{Event, wide, wide_ptr_to_string};
use super::result::Chain;
pub const DEFAULT_WINHTTP_PAC_TIMEOUT: Duration = Duration::from_secs(5);
const USER_AGENT: &str = "proxy-watch";
#[derive(Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum WinHttpPacSource {
AutoDetect,
Url(Url),
AutoDetectThenUrl(Url),
}
impl fmt::Debug for WinHttpPacSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AutoDetect => f.write_str("AutoDetect"),
Self::Url(url) => f
.debug_tuple("Url")
.field(&format_args!(
"{}",
crate::util::redact_userinfo(url.as_str())
))
.finish(),
Self::AutoDetectThenUrl(url) => f
.debug_tuple("AutoDetectThenUrl")
.field(&format_args!(
"{}",
crate::util::redact_userinfo(url.as_str())
))
.finish(),
}
}
}
impl WinHttpPacSource {
#[must_use]
pub fn from_mode(mode: &ProxyMode) -> Option<Self> {
match mode {
ProxyMode::Pac { url, .. } => Some(Self::Url(url.clone())),
ProxyMode::WpadAutoDetect => Some(Self::AutoDetect),
_ => None,
}
}
fn flags(&self) -> u32 {
match self {
Self::AutoDetect => WINHTTP_AUTOPROXY_AUTO_DETECT,
Self::Url(_) => WINHTTP_AUTOPROXY_CONFIG_URL,
Self::AutoDetectThenUrl(_) => {
WINHTTP_AUTOPROXY_AUTO_DETECT | WINHTTP_AUTOPROXY_CONFIG_URL
}
}
}
fn url(&self) -> Option<&Url> {
match self {
Self::AutoDetect => None,
Self::Url(url) | Self::AutoDetectThenUrl(url) => Some(url),
}
}
}
fn is_wpad_miss(flags: u32, status: u32) -> bool {
status == ERROR_WINHTTP_AUTODETECTION_FAILED && flags & WINHTTP_AUTOPROXY_AUTO_DETECT != 0
}
fn wait_millis(timeout: Duration) -> u32 {
u32::try_from(timeout.as_millis())
.unwrap_or(u32::MAX)
.min(u32::MAX - 1)
}
fn session_timeout_millis(timeout: Duration) -> i32 {
i32::try_from(timeout.as_millis())
.unwrap_or(i32::MAX)
.max(1)
}
#[derive(Debug)]
pub struct WinHttpPacResolver {
session: Session,
timeout: Duration,
wpad: bool,
}
impl WinHttpPacResolver {
pub fn new() -> Result<Self, Error> {
Self::with_timeout(DEFAULT_WINHTTP_PAC_TIMEOUT)
}
pub fn with_timeout(timeout: Duration) -> Result<Self, Error> {
if timeout.is_zero() {
return Err(Error::PacTimeout { timeout });
}
let session = Session::open(timeout)?;
Ok(Self {
session,
timeout,
wpad: false,
})
}
#[must_use]
pub fn timeout(&self) -> Duration {
self.timeout
}
#[must_use]
pub fn with_wpad(mut self, wpad: bool) -> Self {
self.wpad = wpad;
self
}
#[must_use]
pub fn wpad(&self) -> bool {
self.wpad
}
pub fn reset_auto_proxy(&self) -> Result<AutoProxyReset, Error> {
let mut last = LAST_RESET.lock().unwrap_or_else(PoisonError::into_inner);
let now = Instant::now();
if let Some(retry_after) = reset_wait(*last, now) {
return Ok(AutoProxyReset::TooSoon { retry_after });
}
let status = unsafe {
WinHttpResetAutoProxy(
self.session.0,
WINHTTP_RESET_ALL | WINHTTP_RESET_OUT_OF_PROC,
)
};
if status != ERROR_SUCCESS.0 {
return Err(winhttp_error("WinHttpResetAutoProxy", status));
}
*last = Some(now);
Ok(AutoProxyReset::Requested)
}
pub fn resolve(&self, url: &Url, source: &WinHttpPacSource) -> Result<Vec<ProxyStep>, Error> {
match self.resolve_raw(url, source)? {
WpadOutcome::Resolved(steps) => Ok(steps),
WpadOutcome::AutoDetectionFailed => Ok(vec![ProxyStep::Direct]),
}
}
fn resolve_raw(&self, url: &Url, source: &WinHttpPacSource) -> Result<WpadOutcome, Error> {
if !self.wpad && source.flags() & WINHTTP_AUTOPROXY_AUTO_DETECT != 0 {
return Err(Error::PacNotSupported { mode: "wpad" });
}
let resolver = Resolver::create(&self.session)?;
let url_w = wide(&query_url(url));
let config_url_w = source.url().map(|url| wide(url.as_str()));
let options = WINHTTP_AUTOPROXY_OPTIONS {
dwFlags: source.flags(),
dwAutoDetectFlags: if source.flags() & WINHTTP_AUTOPROXY_AUTO_DETECT == 0 {
0
} else {
WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A
},
lpszAutoConfigUrl: PCWSTR(config_url_w.as_ref().map_or(ptr::null(), Vec::as_ptr)),
lpvReserved: ptr::null_mut(),
dwReserved: 0,
fAutoLogonIfChallenged: false.into(),
};
let pending = Arc::new(Pending::new()?);
let context = Arc::into_raw(Arc::clone(&pending));
let status = unsafe {
WinHttpGetProxyForUrlEx(
resolver.0,
PCWSTR(url_w.as_ptr()),
&raw const options,
Some(context as usize),
)
};
if status != ERROR_IO_PENDING.0 {
unsafe { drop(Arc::from_raw(context)) };
if is_wpad_miss(options.dwFlags, status) {
return Ok(WpadOutcome::AutoDetectionFailed);
}
if status != ERROR_SUCCESS.0 {
return Err(winhttp_error("WinHttpGetProxyForUrlEx", status));
}
} else {
let waited =
unsafe { WaitForSingleObject(pending.done.raw(), wait_millis(self.timeout)) };
if waited != WAIT_OBJECT_0 {
let failed = (waited != WAIT_TIMEOUT).then(std::io::Error::last_os_error);
drop(resolver);
return Err(match failed {
Some(error) => Error::io("waiting for WinHTTP's proxy answer", error),
None => Error::PacTimeout {
timeout: self.timeout,
},
});
}
let code = pending.status.load(Ordering::Acquire);
if is_wpad_miss(options.dwFlags, code) {
return Ok(WpadOutcome::AutoDetectionFailed);
}
if code != ERROR_SUCCESS.0 {
return Err(winhttp_error("resolving the proxy for a URL", code));
}
}
let mut raw = WINHTTP_PROXY_RESULT::default();
let status = unsafe { WinHttpGetProxyResult(resolver.0, &raw mut raw) };
if status != ERROR_SUCCESS.0 {
return Err(winhttp_error("WinHttpGetProxyResult", status));
}
ProxyResult(raw).to_steps().map(WpadOutcome::Resolved)
}
pub fn resolve_config(&self, config: &ProxyConfig, url: &Url) -> Result<Vec<ProxyStep>, Error> {
let mode = &config.effective;
let has_host = crate::endpoint::has_request_host(url);
match mode {
ProxyMode::Direct | ProxyMode::Manual { .. } => crate::resolve::resolve(config, url),
ProxyMode::PacInline { .. } => Err(Error::PacNotSupported { mode: "pac-inline" }),
ProxyMode::WpadAutoDetect if !has_host => Ok(vec![ProxyStep::Direct]),
ProxyMode::WpadAutoDetect if !self.wpad => Err(Error::PacNotSupported { mode: "wpad" }),
ProxyMode::WpadAutoDetect => self.resolve_wpad_with_fallback(config, url),
_ => match WinHttpPacSource::from_mode(mode) {
Some(source) if has_host => self.resolve(url, &source),
Some(_) => Ok(vec![ProxyStep::Direct]),
None => Err(Error::PacNotSupported { mode: "unknown" }),
},
}
}
fn resolve_wpad_with_fallback(
&self,
config: &ProxyConfig,
url: &Url,
) -> Result<Vec<ProxyStep>, Error> {
let (pac_url, beneath) =
if config.source(ProxyConfigSource::Registry) == Some(&config.effective) {
crate::sys::win::wpad_fallback()?
} else {
(None, ProxyMode::Direct)
};
let source = match &pac_url {
Some(pac_url) => WinHttpPacSource::AutoDetectThenUrl(pac_url.clone()),
None => WinHttpPacSource::AutoDetect,
};
match self.resolve_raw(url, &source)? {
WpadOutcome::Resolved(steps) => Ok(steps),
WpadOutcome::AutoDetectionFailed => match beneath {
ProxyMode::Manual { .. } => {
let manual = ProxyConfig::from_source(ProxyConfigSource::Registry, beneath);
crate::resolve::resolve(&manual, url)
}
_ => Ok(vec![ProxyStep::Direct]),
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AutoProxyReset {
Requested,
TooSoon {
retry_after: Duration,
},
}
const RESET_SPACING: Duration = Duration::from_secs(30);
static LAST_RESET: Mutex<Option<Instant>> = Mutex::new(None);
fn reset_wait(last: Option<Instant>, now: Instant) -> Option<Duration> {
let elapsed = now.saturating_duration_since(last?);
RESET_SPACING
.checked_sub(elapsed)
.filter(|wait| !wait.is_zero())
}
#[derive(Debug)]
enum WpadOutcome {
Resolved(Vec<ProxyStep>),
AutoDetectionFailed,
}
fn winhttp_error(context: &str, code: u32) -> Error {
if code == ERROR_WINHTTP_BAD_AUTO_PROXY_SCRIPT {
return Error::pac_evaluation(format!(
"{context}: the PAC script could not be executed by WinHTTP"
));
}
Error::io(
format!("{context} (WinHTTP status {code:#010x})"),
std::io::Error::from_raw_os_error(code as i32),
)
}
#[derive(Debug)]
struct Pending {
done: Event,
status: AtomicU32,
}
unsafe impl Sync for Pending {}
impl Pending {
fn new() -> Result<Self, Error> {
Ok(Self {
done: Event::new(true, "creating the PAC resolution completion event")?,
status: AtomicU32::new(ERROR_SUCCESS.0),
})
}
fn finish(&self, code: u32) {
self.status.store(code, Ordering::Release);
unsafe {
let _ = SetEvent(self.done.raw());
}
}
}
unsafe extern "system" fn status_callback(
_handle: *mut c_void,
context: usize,
status: u32,
info: *mut c_void,
_info_len: u32,
) {
let code = match status {
WINHTTP_CALLBACK_STATUS_GETPROXYFORURL_COMPLETE => ERROR_SUCCESS.0,
WINHTTP_CALLBACK_STATUS_REQUEST_ERROR => {
if info.is_null() {
ERROR_WINHTTP_INTERNAL_ERROR
} else {
unsafe { (*info.cast::<WINHTTP_ASYNC_RESULT>()).dwError }
}
}
_ => return,
};
if context == 0 {
return;
}
let pending: Arc<Pending> = unsafe { Arc::from_raw(context as *const Pending) };
pending.finish(code);
}
#[derive(Debug)]
struct Session(*mut c_void);
unsafe impl Send for Session {}
unsafe impl Sync for Session {}
impl Session {
fn open(timeout: Duration) -> Result<Self, Error> {
let agent = wide(USER_AGENT);
let handle = unsafe {
WinHttpOpen(
PCWSTR(agent.as_ptr()),
WINHTTP_ACCESS_TYPE_NO_PROXY,
PCWSTR::null(),
PCWSTR::null(),
WINHTTP_FLAG_ASYNC,
)
};
if handle.is_null() {
return Err(last_error("WinHttpOpen"));
}
let session = Self(handle);
let millis = session_timeout_millis(timeout);
unsafe { WinHttpSetTimeouts(session.0, millis, millis, millis, millis) }
.map_err(|e| crate::sys::win::ffi::hresult_error("WinHttpSetTimeouts", e))?;
let previous = unsafe {
WinHttpSetStatusCallback(
session.0,
Some(status_callback),
WINHTTP_CALLBACK_FLAG_GETPROXYFORURL_COMPLETE | WINHTTP_CALLBACK_FLAG_REQUEST_ERROR,
0,
)
};
if previous.is_some_and(|callback| callback as usize == usize::MAX) {
return Err(last_error("WinHttpSetStatusCallback"));
}
Ok(session)
}
}
impl Drop for Session {
fn drop(&mut self) {
unsafe {
let _ = WinHttpCloseHandle(self.0);
}
}
}
#[derive(Debug)]
struct Resolver(*mut c_void);
impl Resolver {
fn create(session: &Session) -> Result<Self, Error> {
let mut handle: *mut c_void = ptr::null_mut();
let status = unsafe { WinHttpCreateProxyResolver(session.0, &raw mut handle) };
if status != ERROR_SUCCESS.0 {
return Err(winhttp_error("WinHttpCreateProxyResolver", status));
}
Ok(Self(handle))
}
}
impl Drop for Resolver {
fn drop(&mut self) {
unsafe {
let _ = WinHttpCloseHandle(self.0);
}
}
}
struct ProxyResult(WINHTTP_PROXY_RESULT);
impl ProxyResult {
fn to_steps(&self) -> Result<Vec<ProxyStep>, Error> {
let count = self.0.cEntries as usize;
let entries = if count == 0 || self.0.pEntries.is_null() {
&[][..]
} else {
unsafe { std::slice::from_raw_parts(self.0.pEntries.cast_const(), count) }
};
let mut chain = Chain::new();
for entry in entries {
if let Some(step) = unsafe { entry_to_step(entry) } {
chain.push(step);
if chain.is_full() {
break;
}
}
}
chain.finish().ok_or_else(|| {
Error::pac_invalid_result(format!(
"WinHTTP returned {count} proxy entries, none of them usable"
))
})
}
}
impl Drop for ProxyResult {
fn drop(&mut self) {
unsafe {
WinHttpFreeProxyResult(&raw mut self.0);
}
}
}
unsafe fn entry_to_step(entry: &WINHTTP_PROXY_RESULT_ENTRY) -> Option<ProxyStep> {
if !entry.fProxy.as_bool() {
return Some(ProxyStep::Direct);
}
let scheme = match entry.ProxyScheme.0 {
s if s == WINHTTP_INTERNET_SCHEME_HTTP.0 => ProxyScheme::Http,
s if s == WINHTTP_INTERNET_SCHEME_HTTPS.0 => ProxyScheme::Https,
s if s == WINHTTP_INTERNET_SCHEME_SOCKS.0 => ProxyScheme::Socks4,
_ => return None,
};
let host_text = unsafe { wide_ptr_to_string(entry.pwszProxy.0) }?;
let host = parse_proxy_host(host_text.trim())?;
let port = if entry.ProxyPort == 0 {
scheme.default_port()
} else {
entry.ProxyPort
};
let endpoint = ProxyEndpoint::new(host, port).with_scheme_hint(scheme);
Some(ProxyStep::from_endpoint(endpoint))
}
fn query_url(url: &Url) -> String {
let sanitized = crate::pac::sanitize_url(url);
match url.scheme() {
"ws" | "wss" => format!("http{}", &sanitized.as_str()[2..]),
_ => sanitized.into(),
}
}
fn parse_proxy_host(text: &str) -> Option<Host> {
if text.is_empty() {
return None;
}
if let Ok(address) = text.parse::<Ipv6Addr>() {
return Some(Host::Ipv6(address));
}
Host::parse(text).ok()
}
fn last_error(context: &str) -> Error {
Error::io(context.to_owned(), std::io::Error::last_os_error())
}
#[cfg(test)]
mod tests {
use super::*;
use windows::Win32::Networking::WinHttp::WINHTTP_INTERNET_SCHEME;
#[test]
fn a_reset_inside_thirty_seconds_waits_out_the_rest() {
let start = Instant::now();
assert_eq!(reset_wait(None, start), None);
assert_eq!(
reset_wait(Some(start), start + Duration::from_secs(10)),
Some(Duration::from_secs(20))
);
assert_eq!(reset_wait(Some(start), start + RESET_SPACING), None);
assert_eq!(
reset_wait(Some(start), start + Duration::from_secs(31)),
None
);
assert_eq!(
reset_wait(Some(start + Duration::from_secs(5)), start),
Some(RESET_SPACING)
);
}
#[test]
#[ignore = "flushes the machine-wide WinHTTP autoproxy cache"]
fn a_real_reset_is_accepted_and_a_second_one_waits() {
let resolver = WinHttpPacResolver::new().unwrap();
assert_eq!(
resolver.reset_auto_proxy().unwrap(),
AutoProxyReset::Requested
);
match resolver.reset_auto_proxy().unwrap() {
AutoProxyReset::TooSoon { retry_after } => assert!(retry_after <= RESET_SPACING),
other => panic!("{other:?}"),
}
}
#[test]
fn every_pac_source_debug_names_itself_and_keeps_its_url() {
let url = Url::parse("https://wpad.corp/proxy.pac").unwrap();
for (source, expected) in [
(WinHttpPacSource::AutoDetect, "AutoDetect"),
(
WinHttpPacSource::Url(url.clone()),
"Url(https://wpad.corp/proxy.pac)",
),
(
WinHttpPacSource::AutoDetectThenUrl(url.clone()),
"AutoDetectThenUrl(https://wpad.corp/proxy.pac)",
),
] {
assert_eq!(format!("{source:?}"), expected);
}
}
#[test]
fn an_overlong_timeout_is_clamped_below_infinite() {
use windows::Win32::System::Threading::INFINITE;
for timeout in [
Duration::MAX,
Duration::from_millis(u64::from(u32::MAX)),
Duration::from_millis(u64::from(u32::MAX) + 1),
] {
let millis = wait_millis(timeout);
assert_eq!(millis, u32::MAX - 1, "{timeout:?}");
assert!(millis < INFINITE, "{timeout:?}");
}
}
#[test]
fn an_ordinary_timeout_converts_to_milliseconds() {
assert_eq!(wait_millis(DEFAULT_WINHTTP_PAC_TIMEOUT), 5_000);
assert_eq!(wait_millis(Duration::from_millis(250)), 250);
assert_eq!(wait_millis(Duration::ZERO), 0);
}
#[test]
fn a_sub_millisecond_budget_never_reaches_winhttp_as_infinite() {
assert_eq!(session_timeout_millis(Duration::from_micros(500)), 1);
assert_eq!(session_timeout_millis(Duration::from_nanos(1)), 1);
}
#[test]
fn an_ordinary_budget_reaches_winhttp_unchanged() {
assert_eq!(
session_timeout_millis(DEFAULT_WINHTTP_PAC_TIMEOUT),
5_000_i32
);
assert_eq!(session_timeout_millis(Duration::from_millis(250)), 250_i32);
assert_eq!(session_timeout_millis(Duration::MAX), i32::MAX);
}
#[test]
fn sources_come_from_the_auto_config_modes_only() {
let url = Url::parse("http://wpad.corp/proxy.pac").unwrap();
assert_eq!(
WinHttpPacSource::from_mode(&ProxyMode::pac(url.clone())),
Some(WinHttpPacSource::Url(url))
);
assert_eq!(
WinHttpPacSource::from_mode(&ProxyMode::WpadAutoDetect),
Some(WinHttpPacSource::AutoDetect)
);
assert_eq!(WinHttpPacSource::from_mode(&ProxyMode::Direct), None);
assert_eq!(
WinHttpPacSource::from_mode(&ProxyMode::pac_inline("body".to_owned())),
None
);
assert_eq!(
WinHttpPacSource::from_mode(&ProxyMode::manual(
std::collections::HashMap::new(),
crate::BypassRules::new()
)),
None
);
}
#[test]
fn autoproxy_flags_match_the_variant() {
let url = Url::parse("http://wpad.corp/proxy.pac").unwrap();
assert_eq!(
WinHttpPacSource::AutoDetect.flags(),
WINHTTP_AUTOPROXY_AUTO_DETECT
);
assert_eq!(
WinHttpPacSource::Url(url.clone()).flags(),
WINHTTP_AUTOPROXY_CONFIG_URL
);
assert_eq!(
WinHttpPacSource::AutoDetectThenUrl(url.clone()).flags(),
WINHTTP_AUTOPROXY_AUTO_DETECT | WINHTTP_AUTOPROXY_CONFIG_URL
);
assert_eq!(WinHttpPacSource::AutoDetect.url(), None);
assert_eq!(
WinHttpPacSource::AutoDetectThenUrl(url.clone()).url(),
Some(&url)
);
}
#[test]
fn only_a_request_that_asked_for_auto_detect_can_report_a_wpad_miss() {
let url = Url::parse("http://wpad.corp/proxy.pac").unwrap();
for source in [
WinHttpPacSource::AutoDetect,
WinHttpPacSource::AutoDetectThenUrl(url.clone()),
] {
assert!(
is_wpad_miss(source.flags(), ERROR_WINHTTP_AUTODETECTION_FAILED),
"{source:?}"
);
}
assert!(!is_wpad_miss(
WinHttpPacSource::Url(url).flags(),
ERROR_WINHTTP_AUTODETECTION_FAILED
));
assert!(!is_wpad_miss(
WINHTTP_AUTOPROXY_AUTO_DETECT,
ERROR_SUCCESS.0
));
}
#[test]
fn a_websocket_destination_reaches_winhttp_as_the_scheme_it_understands() {
let cases = [
("ws://chat.corp/room?id=1#f", "http://chat.corp/room?id=1"),
("wss://chat.corp/room?id=1", "https://chat.corp/"),
("http://alice:pw@a.corp/x?q=1#f", "http://a.corp/x?q=1"),
("https://a.corp/x?q=1", "https://a.corp/"),
("wsx://a.corp/x", "wsx://a.corp/x"),
];
for (input, expected) in cases {
assert_eq!(query_url(&Url::parse(input).unwrap()), expected, "{input}");
}
}
#[test]
fn proxy_hosts_parse_in_every_shape_winhttp_produces() {
assert_eq!(
parse_proxy_host("proxy.corp"),
Some(Host::Domain("proxy.corp".to_owned()))
);
assert_eq!(
parse_proxy_host("10.0.0.1"),
Some(Host::Ipv4("10.0.0.1".parse().unwrap()))
);
assert_eq!(
parse_proxy_host("::1"),
Some(Host::Ipv6("::1".parse().unwrap()))
);
assert_eq!(
parse_proxy_host("[2001:db8::1]"),
Some(Host::Ipv6("2001:db8::1".parse().unwrap()))
);
assert_eq!(parse_proxy_host(""), None);
}
#[test]
fn a_proxy_entry_without_a_port_takes_its_scheme_default() {
use windows::core::PWSTR;
for (native, scheme, port, expected) in [
(WINHTTP_INTERNET_SCHEME_HTTP, ProxyScheme::Http, 0u16, 80u16),
(WINHTTP_INTERNET_SCHEME_HTTPS, ProxyScheme::Https, 0, 443),
(WINHTTP_INTERNET_SCHEME_SOCKS, ProxyScheme::Socks4, 0, 1080),
(WINHTTP_INTERNET_SCHEME_HTTP, ProxyScheme::Http, 8080, 8080),
] {
let mut host = wide("proxy.corp");
let entry = WINHTTP_PROXY_RESULT_ENTRY {
fProxy: true.into(),
fBypass: false.into(),
ProxyScheme: native,
pwszProxy: PWSTR(host.as_mut_ptr()),
ProxyPort: port,
};
let step = unsafe { entry_to_step(&entry) };
assert_eq!(
step,
Some(ProxyStep::from_endpoint(
ProxyEndpoint::new(Host::Domain("proxy.corp".to_owned()), expected)
.with_scheme_hint(scheme)
)),
"{native:?} port {port}"
);
}
}
fn proxy_entry(
scheme: WINHTTP_INTERNET_SCHEME,
host: &mut [u16],
port: u16,
) -> WINHTTP_PROXY_RESULT_ENTRY {
WINHTTP_PROXY_RESULT_ENTRY {
fProxy: true.into(),
fBypass: false.into(),
ProxyScheme: scheme,
pwszProxy: windows::core::PWSTR(host.as_mut_ptr()),
ProxyPort: port,
}
}
fn steps_of(
count: u32,
entries: *mut WINHTTP_PROXY_RESULT_ENTRY,
) -> Result<Vec<ProxyStep>, Error> {
let result = std::mem::ManuallyDrop::new(ProxyResult(WINHTTP_PROXY_RESULT {
cEntries: count,
pEntries: entries,
}));
result.to_steps()
}
fn endpoint_step(host: &str, port: u16, scheme: ProxyScheme) -> ProxyStep {
ProxyStep::from_endpoint(
ProxyEndpoint::new(Host::Domain(host.to_owned()), port).with_scheme_hint(scheme),
)
}
#[test]
fn a_mixed_chain_keeps_the_usable_candidates_in_order() {
use windows::Win32::Networking::WinHttp::WINHTTP_INTERNET_SCHEME_FTP;
let mut good = wide("proxy.corp");
let mut secure = wide("secure.corp");
let mut files = wide("files.corp");
let mut malformed = wide("proxy corp");
let mut entries = [
proxy_entry(WINHTTP_INTERNET_SCHEME_FTP, &mut files, 21),
proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut good, 3128),
WINHTTP_PROXY_RESULT_ENTRY {
pwszProxy: windows::core::PWSTR(ptr::null_mut()),
..proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut good, 8080)
},
proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut malformed, 8080),
proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut good, 3128),
WINHTTP_PROXY_RESULT_ENTRY {
fProxy: false.into(),
..proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut good, 0)
},
proxy_entry(WINHTTP_INTERNET_SCHEME_HTTPS, &mut secure, 8443),
];
let count = entries.len() as u32;
assert_eq!(
steps_of(count, entries.as_mut_ptr()).expect("three candidates were usable"),
vec![
endpoint_step("proxy.corp", 3128, ProxyScheme::Http),
ProxyStep::Direct,
endpoint_step("secure.corp", 8443, ProxyScheme::Https),
]
);
}
#[test]
fn the_chain_keeps_its_first_steps_and_drops_the_rest() {
use crate::pac::result::MAX_CHAIN;
let mut host = wide("proxy.corp");
let mut entries = (1..=MAX_CHAIN as u16 + 1)
.map(|port| proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut host, port))
.collect::<Vec<_>>();
entries.push(WINHTTP_PROXY_RESULT_ENTRY {
fProxy: false.into(),
..proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut host, 0)
});
let count = entries.len() as u32;
let steps = steps_of(count, entries.as_mut_ptr()).expect("every candidate is usable");
assert_eq!(steps.len(), MAX_CHAIN);
assert_eq!(
steps[MAX_CHAIN - 1],
endpoint_step("proxy.corp", MAX_CHAIN as u16, ProxyScheme::Http)
);
assert!(!steps.contains(&ProxyStep::Direct));
}
#[test]
fn a_chain_with_nothing_usable_in_it_is_an_error_rather_than_a_direct_connection() {
use windows::Win32::Networking::WinHttp::WINHTTP_INTERNET_SCHEME_FTP;
let mut files = wide("files.corp");
let mut malformed = wide("proxy corp");
let mut entries = [
proxy_entry(WINHTTP_INTERNET_SCHEME_FTP, &mut files, 21),
proxy_entry(WINHTTP_INTERNET_SCHEME_HTTP, &mut malformed, 8080),
];
let count = entries.len() as u32;
let error = steps_of(count, entries.as_mut_ptr()).unwrap_err();
assert!(matches!(error, Error::PacInvalidResult { .. }), "{error:?}");
assert!(error.to_string().contains('2'), "{error}");
}
#[test]
fn an_entry_count_with_no_array_behind_it_is_not_believed() {
let error = steps_of(u32::MAX, ptr::null_mut()).unwrap_err();
assert!(matches!(error, Error::PacInvalidResult { .. }), "{error:?}");
}
#[test]
fn a_zero_timeout_is_refused_rather_than_meaning_forever() {
let error = WinHttpPacResolver::with_timeout(Duration::ZERO).unwrap_err();
assert!(matches!(error, Error::PacTimeout { .. }), "{error:?}");
}
#[test]
fn a_script_error_masks_its_context_even_when_that_context_carries_credentials() {
let error = winhttp_error(
"fetching http://alice:hunter2@wpad.corp/proxy.pac",
ERROR_WINHTTP_BAD_AUTO_PROXY_SCRIPT,
);
assert!(matches!(error, Error::PacEvaluation { .. }), "{error:?}");
let rendered = format!("{error} {error:?}");
assert!(!rendered.contains("hunter2"), "{rendered}");
assert!(rendered.contains("alice:"), "{rendered}");
assert!(rendered.contains("wpad.corp"), "{rendered}");
}
}