use reqwest::Url;
use crate::core::egress::config::ProxySource;
use crate::core::error::{OlError, ERR_PAC_UNAVAILABLE};
use super::{parse_discovered, rung, Context, Ladder, PacAnswer, PacBinding, Route, RungResult};
const INET_SETTINGS: &str = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings";
const INET_SETTINGS_POLICY: &str =
r"SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings";
const WINHTTP_POLICY: &str = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\WinHttp";
const AUTOPROXY_SERVICE: &str = "WinHttpAutoProxySvc";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct IeProxyConfig {
pub auto_detect: bool,
pub auto_config_url: Option<String>,
pub proxy: Option<String>,
pub proxy_bypass: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AutoProxyMode {
ConfigUrl(String),
AutoDetect,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceState {
Disabled,
Enabled,
Absent,
}
pub trait WinSource {
fn ie_config(&self) -> Option<IeProxyConfig>;
fn default_proxy(&self) -> Option<String>;
fn hklm_dword(&self, path: &str, name: &str) -> Option<u32>;
fn hklm_string(&self, path: &str, name: &str) -> Option<String>;
fn service_state(&self, name: &str) -> ServiceState;
fn get_proxy_for_url(
&self,
target: &Url,
mode: &AutoProxyMode,
) -> Result<Option<PacAnswer>, OlError>;
}
pub fn walk(ladder: &mut Ladder, ctx: Context, src: &dyn WinSource, target: &Url) {
match ctx {
Context::UserSession => {
let ie = src.ie_config();
if ladder.offer(rung::WININET_USER, user_static(ie.as_ref())) {
return;
}
if ladder.offer(rung::IE_PAC, ie_pac(src, ie.as_ref(), target)) {
return;
}
if ladder.offer(rung::WINHTTP_MACHINE, machine_winhttp(src)) {
return;
}
let hinted = ie.as_ref().is_some_and(|c| c.auto_detect);
ladder.offer(rung::WPAD, wpad(src, target, hinted));
}
Context::DaemonService => {
if ladder.offer(rung::HKLM_INETSETTINGS, hklm_internet_settings(src, target)) {
return;
}
if ladder.offer(rung::WINHTTP_MACHINE, machine_winhttp(src)) {
return;
}
ladder.offer(rung::WPAD, wpad(src, target, true));
}
}
}
fn user_static(ie: Option<&IeProxyConfig>) -> RungResult {
let list = ie.and_then(|c| c.proxy.as_deref()).unwrap_or_default();
from_proxy_list(ProxySource::Windows, list, "per-user WinINet")
}
fn machine_winhttp(src: &dyn WinSource) -> RungResult {
let list = src.default_proxy().unwrap_or_default();
from_proxy_list(ProxySource::Windows, &list, "machine WinHTTP")
}
fn from_proxy_list(source: ProxySource, list: &str, what: &str) -> RungResult {
let Some(entry) = parse_proxy_list(list) else {
return RungResult::empty(source);
};
match parse_discovered(&entry) {
Some(url) => RungResult::static_route(source, url),
None => RungResult::skipped(
source,
crate::core::error::ERR_PROXY_CONFIG_INVALID,
format!("{what} names a proxy this client cannot use: {entry}"),
),
}
}
fn ie_pac(src: &dyn WinSource, ie: Option<&IeProxyConfig>, target: &Url) -> RungResult {
let Some(raw) = ie.and_then(|c| c.auto_config_url.as_deref()) else {
return RungResult::empty(ProxySource::Pac);
};
let Some(pac_url) = Url::parse(raw).ok() else {
return RungResult::skipped(
ProxySource::Pac,
crate::core::error::ERR_PROXY_CONFIG_INVALID,
format!("AutoConfigURL is not a URL: {raw}"),
);
};
pac_rung(
src,
target,
ProxySource::Pac,
AutoProxyMode::ConfigUrl(pac_url.to_string()),
Some(pac_url),
)
}
fn wpad(src: &dyn WinSource, target: &Url, auto_detect_hinted: bool) -> RungResult {
if !auto_detect_hinted {
return RungResult::empty_with(
ProxySource::Wpad,
"per-user auto-detect (fAutoDetect) is off",
);
}
if src.hklm_dword(WINHTTP_POLICY, "DisableWpad") == Some(1) {
return RungResult::skipped(
ProxySource::Wpad,
ERR_PAC_UNAVAILABLE,
format!(r"WPAD is disabled by policy: HKLM\{WINHTTP_POLICY}\DisableWpad = 1"),
);
}
match src.service_state(AUTOPROXY_SERVICE) {
ServiceState::Disabled => {
return RungResult::skipped(
ProxySource::Wpad,
ERR_PAC_UNAVAILABLE,
format!("{AUTOPROXY_SERVICE} is disabled (Start = 4)"),
)
}
ServiceState::Absent => {
return RungResult::skipped(
ProxySource::Wpad,
ERR_PAC_UNAVAILABLE,
format!("{AUTOPROXY_SERVICE} is not registered on this host"),
)
}
ServiceState::Enabled => {}
}
pac_rung(
src,
target,
ProxySource::Wpad,
AutoProxyMode::AutoDetect,
None,
)
}
fn pac_rung(
src: &dyn WinSource,
target: &Url,
source: ProxySource,
mode: AutoProxyMode,
pac_url: Option<Url>,
) -> RungResult {
match src.get_proxy_for_url(target, &mode) {
Ok(None) => RungResult::empty_with(
source,
match mode {
AutoProxyMode::AutoDetect => "no WPAD server answered on this network",
_ => "auto-detection named no proxy",
},
),
Ok(Some(answer)) => {
let probe_via = answer.first_route();
if probe_via.is_none() && answer.names_a_proxy() {
return RungResult::skipped(
source,
crate::core::error::ERR_PROXY_CONFIG_INVALID,
format!("the PAC named no usable proxy: {:?}", answer.proxies),
);
}
RungResult::Candidate {
source,
route: Route::PacSource { pac_url },
probe_via,
}
}
Err(e) => RungResult::skipped(source, e.code, e.message),
}
}
fn hklm_internet_settings(src: &dyn WinSource, target: &Url) -> RungResult {
let machine_wide = [INET_SETTINGS_POLICY, INET_SETTINGS]
.iter()
.any(|p| src.hklm_dword(p, "ProxySettingsPerUser") == Some(0));
if !machine_wide {
return RungResult::empty_with(
ProxySource::Windows,
"machine Internet Settings do not apply: ProxySettingsPerUser is not 0",
);
}
if let Some(raw) = src.hklm_string(INET_SETTINGS, "AutoConfigURL") {
if let Ok(pac_url) = Url::parse(&raw) {
return pac_rung(
src,
target,
ProxySource::Pac,
AutoProxyMode::ConfigUrl(pac_url.to_string()),
Some(pac_url),
);
}
}
if src.hklm_dword(INET_SETTINGS, "ProxyEnable") != Some(1) {
return RungResult::empty(ProxySource::Windows);
}
let list = src
.hklm_string(INET_SETTINGS, "ProxyServer")
.unwrap_or_default();
from_proxy_list(ProxySource::Windows, &list, "HKLM Internet Settings")
}
pub fn eval_pac(
src: &dyn WinSource,
target: &Url,
binding: &PacBinding,
) -> Result<Option<Url>, OlError> {
let mode = match &binding.pac_url {
Some(u) => AutoProxyMode::ConfigUrl(u.to_string()),
None => AutoProxyMode::AutoDetect,
};
Ok(src
.get_proxy_for_url(target, &mode)?
.and_then(|answer| answer.first_route()))
}
pub const WINHTTP_LOGIN_FAILURE: u32 = 12015;
pub const WINHTTP_AUTODETECTION_FAILED: u32 = 12180;
pub fn with_autologon_retry<F>(mut attempt: F) -> Result<Option<PacAnswer>, OlError>
where
F: FnMut(bool) -> Result<PacAnswer, u32>,
{
let raw = match attempt(false) {
Ok(answer) => return Ok(Some(answer)),
Err(WINHTTP_LOGIN_FAILURE) => attempt(true),
Err(code) => Err(code),
};
match raw {
Ok(answer) => Ok(Some(answer)),
Err(WINHTTP_AUTODETECTION_FAILED) => Ok(None),
Err(code) => Err(pac_error(code)),
}
}
fn pac_error(code: u32) -> OlError {
OlError::new(
ERR_PAC_UNAVAILABLE,
format!("WinHttpGetProxyForUrl failed with Windows error {code}"),
)
.with_suggestion(
"The PAC script could not be fetched or evaluated. Set an explicit proxy with \
`openlatch proxy set <url>` if this host has no working PAC.",
)
}
pub(crate) fn parse_proxy_list(list: &str) -> Option<String> {
let mut https: Option<&str> = None;
let mut http: Option<&str> = None;
let mut bare: Option<&str> = None;
for token in list.split([';', ' ', '\t', '\r', '\n']) {
let token = token.trim();
if token.is_empty() {
continue;
}
match token.split_once('=') {
Some(("https", v)) if https.is_none() => https = Some(v),
Some(("http", v)) if http.is_none() => http = Some(v),
Some(_) => continue,
None if bare.is_none() => bare = Some(token),
None => continue,
}
}
https
.or(http)
.or(bare)
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
}
#[cfg(windows)]
pub(super) fn native() -> &'static Win32Source {
static SOURCE: std::sync::OnceLock<Win32Source> = std::sync::OnceLock::new();
SOURCE.get_or_init(Win32Source::default)
}
#[cfg(windows)]
#[derive(Default)]
pub struct Win32Source {
negative_until: std::sync::Mutex<Option<std::time::Instant>>,
}
#[cfg(windows)]
mod win32 {
use std::ffi::{OsStr, OsString};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
pub(super) const MAX_VALUE_BYTES: u32 = 64 * 1024;
pub(super) fn wide(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
pub(super) unsafe fn from_wide_ptr(p: *const u16) -> Option<String> {
if p.is_null() {
return None;
}
let mut len = 0usize;
while unsafe { *p.add(len) } != 0 {
len += 1;
if len > 64 * 1024 {
return None;
}
}
let slice = unsafe { std::slice::from_raw_parts(p, len) };
Some(OsString::from_wide(slice).to_string_lossy().into_owned())
}
pub(super) struct GlobalStr(pub *mut u16);
impl GlobalStr {
pub(super) fn take(&self) -> Option<String> {
unsafe { from_wide_ptr(self.0) }
}
}
impl Drop for GlobalStr {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
windows_sys::Win32::Foundation::GlobalFree(self.0.cast());
}
}
}
}
}
#[cfg(windows)]
impl Win32Source {
const PAC_DEADLINE: std::time::Duration = std::time::Duration::from_secs(5);
const NEGATIVE_TTL: std::time::Duration = std::time::Duration::from_secs(10);
fn in_negative_cache(&self) -> bool {
self.negative_until
.lock()
.ok()
.and_then(|g| *g)
.is_some_and(|at| at.elapsed() < Self::NEGATIVE_TTL)
}
fn record_timeout(&self) {
if let Ok(mut g) = self.negative_until.lock() {
*g = Some(std::time::Instant::now());
}
}
}
#[cfg(windows)]
fn winhttp_session() -> Option<usize> {
use windows_sys::Win32::Networking::WinHttp::{WinHttpOpen, WINHTTP_ACCESS_TYPE_NO_PROXY};
struct Handle(usize);
unsafe impl Send for Handle {}
unsafe impl Sync for Handle {}
static SESSION: std::sync::OnceLock<Option<Handle>> = std::sync::OnceLock::new();
SESSION
.get_or_init(|| {
let agent = win32::wide("openlatch-egress");
let h = unsafe {
WinHttpOpen(
agent.as_ptr(),
WINHTTP_ACCESS_TYPE_NO_PROXY,
std::ptr::null(),
std::ptr::null(),
0,
)
};
if h.is_null() {
None
} else {
Some(Handle(h as usize))
}
})
.as_ref()
.map(|h| h.0)
}
#[cfg(windows)]
impl WinSource for Win32Source {
fn ie_config(&self) -> Option<IeProxyConfig> {
use windows_sys::Win32::Networking::WinHttp::{
WinHttpGetIEProxyConfigForCurrentUser, WINHTTP_CURRENT_USER_IE_PROXY_CONFIG,
};
let mut raw = WINHTTP_CURRENT_USER_IE_PROXY_CONFIG::default();
let ok = unsafe { WinHttpGetIEProxyConfigForCurrentUser(&mut raw) };
if ok == 0 {
return None;
}
let auto_config_url = win32::GlobalStr(raw.lpszAutoConfigUrl);
let proxy = win32::GlobalStr(raw.lpszProxy);
let proxy_bypass = win32::GlobalStr(raw.lpszProxyBypass);
Some(IeProxyConfig {
auto_detect: raw.fAutoDetect != 0,
auto_config_url: auto_config_url.take().filter(|s| !s.is_empty()),
proxy: proxy.take().filter(|s| !s.is_empty()),
proxy_bypass: proxy_bypass.take().filter(|s| !s.is_empty()),
})
}
fn default_proxy(&self) -> Option<String> {
use windows_sys::Win32::Networking::WinHttp::{
WinHttpGetDefaultProxyConfiguration, WINHTTP_ACCESS_TYPE_NAMED_PROXY,
WINHTTP_PROXY_INFO,
};
let mut info = WINHTTP_PROXY_INFO::default();
let ok = unsafe { WinHttpGetDefaultProxyConfiguration(&mut info) };
if ok == 0 {
return None;
}
let proxy = win32::GlobalStr(info.lpszProxy);
let _bypass = win32::GlobalStr(info.lpszProxyBypass);
if info.dwAccessType != WINHTTP_ACCESS_TYPE_NAMED_PROXY {
return None;
}
proxy.take().filter(|s| !s.is_empty())
}
fn hklm_dword(&self, path: &str, name: &str) -> Option<u32> {
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
use windows_sys::Win32::System::Registry::{
RegGetValueW, HKEY_LOCAL_MACHINE, RRF_RT_REG_DWORD,
};
let subkey = win32::wide(path);
let value = win32::wide(name);
let mut out: u32 = 0;
let mut size: u32 = std::mem::size_of::<u32>() as u32;
let rc = unsafe {
RegGetValueW(
HKEY_LOCAL_MACHINE,
subkey.as_ptr(),
value.as_ptr(),
RRF_RT_REG_DWORD,
std::ptr::null_mut(),
std::ptr::addr_of_mut!(out).cast(),
&mut size,
)
};
(rc == ERROR_SUCCESS).then_some(out)
}
fn hklm_string(&self, path: &str, name: &str) -> Option<String> {
use windows_sys::Win32::Foundation::ERROR_SUCCESS;
use windows_sys::Win32::System::Registry::{
RegGetValueW, HKEY_LOCAL_MACHINE, RRF_RT_REG_SZ,
};
let subkey = win32::wide(path);
let value = win32::wide(name);
let mut bytes: u32 = 0;
let rc = unsafe {
RegGetValueW(
HKEY_LOCAL_MACHINE,
subkey.as_ptr(),
value.as_ptr(),
RRF_RT_REG_SZ,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut bytes,
)
};
if rc != ERROR_SUCCESS || bytes == 0 || bytes > win32::MAX_VALUE_BYTES {
return None;
}
let mut buf = vec![0u16; bytes as usize / 2 + 1];
let mut written = bytes;
let rc = unsafe {
RegGetValueW(
HKEY_LOCAL_MACHINE,
subkey.as_ptr(),
value.as_ptr(),
RRF_RT_REG_SZ,
std::ptr::null_mut(),
buf.as_mut_ptr().cast(),
&mut written,
)
};
if rc != ERROR_SUCCESS {
return None;
}
let len = (written as usize / 2).saturating_sub(1).min(buf.len());
let s = String::from_utf16_lossy(&buf[..len]);
(!s.is_empty()).then_some(s)
}
fn service_state(&self, name: &str) -> ServiceState {
match self.hklm_dword(
&format!(r"SYSTEM\CurrentControlSet\Services\{name}"),
"Start",
) {
Some(4) => return ServiceState::Disabled,
Some(_) => return ServiceState::Enabled,
None => {}
}
service_state_via_scm(name)
}
fn get_proxy_for_url(
&self,
target: &Url,
mode: &AutoProxyMode,
) -> Result<Option<PacAnswer>, OlError> {
if self.in_negative_cache() {
return Err(OlError::new(
ERR_PAC_UNAVAILABLE,
"a PAC evaluation timed out moments ago; not retrying yet",
)
.with_suggestion(
"Set an explicit proxy with `openlatch proxy set <url>` if this host has \
no working PAC responder.",
));
}
let Some(session) = winhttp_session() else {
return Err(OlError::new(
ERR_PAC_UNAVAILABLE,
"WinHttpOpen failed; this host has no usable WinHTTP session",
));
};
let url = target.to_string();
let mode = mode.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(get_proxy_for_url_blocking(session, &url, &mode));
});
match rx.recv_timeout(Self::PAC_DEADLINE) {
Ok(result) => result,
Err(_) => {
self.record_timeout();
Err(OlError::new(
ERR_PAC_UNAVAILABLE,
format!(
"the PAC evaluation did not finish within {} s",
Self::PAC_DEADLINE.as_secs()
),
)
.with_suggestion(
"A WPAD responder that never answers looks exactly like this. Set an \
explicit proxy with `openlatch proxy set <url>`.",
))
}
}
}
}
#[cfg(windows)]
fn service_state_via_scm(name: &str) -> ServiceState {
use windows_sys::Win32::Foundation::{GetLastError, ERROR_SERVICE_DOES_NOT_EXIST};
use windows_sys::Win32::System::Services::{
CloseServiceHandle, OpenSCManagerW, OpenServiceW, QueryServiceStatusEx, SC_MANAGER_CONNECT,
SC_STATUS_PROCESS_INFO, SERVICE_QUERY_STATUS, SERVICE_STATUS_PROCESS,
};
let scm = unsafe { OpenSCManagerW(std::ptr::null(), std::ptr::null(), SC_MANAGER_CONNECT) };
if scm.is_null() {
return ServiceState::Enabled;
}
let wide_name = win32::wide(name);
let svc = unsafe { OpenServiceW(scm, wide_name.as_ptr(), SERVICE_QUERY_STATUS) };
if svc.is_null() {
let missing = unsafe { GetLastError() } == ERROR_SERVICE_DOES_NOT_EXIST;
unsafe { CloseServiceHandle(scm) };
return if missing {
ServiceState::Absent
} else {
ServiceState::Enabled
};
}
let mut status = SERVICE_STATUS_PROCESS::default();
let mut needed: u32 = 0;
let ok = unsafe {
QueryServiceStatusEx(
svc,
SC_STATUS_PROCESS_INFO,
std::ptr::addr_of_mut!(status).cast(),
std::mem::size_of::<SERVICE_STATUS_PROCESS>() as u32,
&mut needed,
)
};
unsafe {
CloseServiceHandle(svc);
CloseServiceHandle(scm);
}
let _ = ok;
ServiceState::Enabled
}
#[cfg(windows)]
fn get_proxy_for_url_blocking(
session: usize,
url: &str,
mode: &AutoProxyMode,
) -> Result<Option<PacAnswer>, OlError> {
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Networking::WinHttp::{
WinHttpGetProxyForUrl, WINHTTP_ACCESS_TYPE_NAMED_PROXY, WINHTTP_AUTOPROXY_AUTO_DETECT,
WINHTTP_AUTOPROXY_CONFIG_URL, WINHTTP_AUTOPROXY_OPTIONS, WINHTTP_AUTO_DETECT_TYPE_DHCP,
WINHTTP_AUTO_DETECT_TYPE_DNS_A, WINHTTP_PROXY_INFO,
};
let url_w = win32::wide(url);
let config_url_w = match mode {
AutoProxyMode::ConfigUrl(u) => Some(win32::wide(u)),
AutoProxyMode::AutoDetect => None,
};
let attempt = |auto_logon: bool| -> Result<PacAnswer, u32> {
let mut opts = WINHTTP_AUTOPROXY_OPTIONS::default();
match &config_url_w {
Some(w) => {
opts.dwFlags = WINHTTP_AUTOPROXY_CONFIG_URL;
opts.lpszAutoConfigUrl = w.as_ptr();
}
None => {
opts.dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT;
opts.dwAutoDetectFlags =
WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A;
}
}
opts.fAutoLogonIfChallenged = i32::from(auto_logon);
let mut info = WINHTTP_PROXY_INFO::default();
let ok = unsafe {
WinHttpGetProxyForUrl(
session as *mut std::ffi::c_void,
url_w.as_ptr(),
&mut opts,
&mut info,
)
};
if ok == 0 {
return Err(unsafe { GetLastError() });
}
let proxy = win32::GlobalStr(info.lpszProxy);
let _bypass = win32::GlobalStr(info.lpszProxyBypass);
if info.dwAccessType != WINHTTP_ACCESS_TYPE_NAMED_PROXY {
return Ok(PacAnswer::default());
}
Ok(PacAnswer {
proxies: proxy
.take()
.unwrap_or_default()
.split([';', ' '])
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect(),
})
};
with_autologon_retry(attempt)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::egress::discovery::tests::ScriptedProbe;
use crate::core::egress::discovery::CandidateOutcome;
use std::cell::RefCell;
use std::collections::HashMap;
#[derive(Default)]
struct FakeWin {
ie: Option<IeProxyConfig>,
default_proxy: Option<String>,
dwords: HashMap<(String, String), u32>,
strings: HashMap<(String, String), String>,
service: Option<ServiceState>,
pac: Option<PacAnswer>,
pac_error: Option<&'static str>,
calls: RefCell<Vec<AutoProxyMode>>,
}
impl FakeWin {
fn dword(mut self, path: &str, name: &str, v: u32) -> Self {
self.dwords.insert((path.to_string(), name.to_string()), v);
self
}
fn string(mut self, path: &str, name: &str, v: &str) -> Self {
self.strings
.insert((path.to_string(), name.to_string()), v.to_string());
self
}
fn pac_calls(&self) -> usize {
self.calls.borrow().len()
}
}
impl WinSource for FakeWin {
fn ie_config(&self) -> Option<IeProxyConfig> {
self.ie.clone()
}
fn default_proxy(&self) -> Option<String> {
self.default_proxy.clone()
}
fn hklm_dword(&self, path: &str, name: &str) -> Option<u32> {
self.dwords
.get(&(path.to_string(), name.to_string()))
.copied()
}
fn hklm_string(&self, path: &str, name: &str) -> Option<String> {
self.strings
.get(&(path.to_string(), name.to_string()))
.cloned()
}
fn service_state(&self, _name: &str) -> ServiceState {
self.service.unwrap_or(ServiceState::Enabled)
}
fn get_proxy_for_url(
&self,
_target: &Url,
mode: &AutoProxyMode,
) -> Result<Option<PacAnswer>, OlError> {
self.calls.borrow_mut().push(mode.clone());
if let Some(code) = self.pac_error {
return Err(OlError::new(code, "fixture PAC failure"));
}
Ok(Some(self.pac.clone().unwrap_or_default()))
}
}
fn target() -> Url {
Url::parse("https://app.openlatch.ai/api/v1/health").expect("target")
}
fn walk_with(
src: &FakeWin,
ctx: Context,
probe: &ScriptedProbe,
) -> Vec<super::super::CandidateAttempt> {
let mut ladder = Ladder::new(probe);
walk(&mut ladder, ctx, src, &target());
ladder.finish().1
}
#[test]
fn the_user_context_walks_the_frozen_rung_order() {
let src = FakeWin {
ie: Some(IeProxyConfig {
auto_detect: true,
auto_config_url: Some("http://wpad.corp/proxy.pac".into()),
proxy: Some("https=user.corp:8080".into()),
proxy_bypass: None,
}),
default_proxy: Some("machine.corp:8080".into()),
pac: Some(PacAnswer {
proxies: vec!["PROXY pac.corp:3128".into()],
}),
..Default::default()
};
let probe = ScriptedProbe::always_fails();
let trace = walk_with(&src, Context::UserSession, &probe);
let order: Vec<_> = trace.iter().map(|a| a.rung).collect();
assert_eq!(
order,
vec![
rung::WININET_USER,
rung::IE_PAC,
rung::WINHTTP_MACHINE,
rung::WPAD
]
);
}
#[test]
fn a_winning_rung_stops_the_walk_before_the_expensive_ones() {
let src = FakeWin {
ie: Some(IeProxyConfig {
auto_detect: true,
auto_config_url: Some("http://wpad.corp/proxy.pac".into()),
proxy: Some("user.corp:8080".into()),
..Default::default()
}),
..Default::default()
};
let probe = ScriptedProbe::new(vec![Ok(5)]);
let trace = walk_with(&src, Context::UserSession, &probe);
assert_eq!(trace.len(), 1);
assert_eq!(trace[0].rung, rung::WININET_USER);
assert_eq!(
src.pac_calls(),
0,
"a won ladder must not evaluate a PAC at all"
);
}
#[test]
fn disable_wpad_skips_the_rung_and_costs_no_network() {
let src = FakeWin {
ie: Some(IeProxyConfig {
auto_detect: true,
..Default::default()
}),
..Default::default()
}
.dword(WINHTTP_POLICY, "DisableWpad", 1);
let probe = ScriptedProbe::always_fails();
let trace = walk_with(&src, Context::UserSession, &probe);
let wpad = trace
.iter()
.find(|a| a.rung == rung::WPAD)
.expect("wpad row");
assert_eq!(wpad.probe, CandidateOutcome::Skipped(ERR_PAC_UNAVAILABLE));
assert!(wpad
.detail
.as_deref()
.is_some_and(|d| d.contains("DisableWpad")));
assert_eq!(
src.pac_calls(),
0,
"a gated WPAD must not touch the network"
);
}
#[test]
fn a_disabled_autoproxy_service_skips_wpad() {
let src = FakeWin {
ie: Some(IeProxyConfig {
auto_detect: true,
..Default::default()
}),
service: Some(ServiceState::Disabled),
..Default::default()
};
let probe = ScriptedProbe::always_fails();
let trace = walk_with(&src, Context::UserSession, &probe);
let wpad = trace
.iter()
.find(|a| a.rung == rung::WPAD)
.expect("wpad row");
assert_eq!(wpad.probe, CandidateOutcome::Skipped(ERR_PAC_UNAVAILABLE));
assert!(wpad
.detail
.as_deref()
.is_some_and(|d| d.contains(AUTOPROXY_SERVICE)));
assert_eq!(src.pac_calls(), 0);
}
#[test]
fn auto_detect_off_leaves_an_empty_rung_not_a_gated_one() {
let src = FakeWin {
ie: Some(IeProxyConfig::default()),
..Default::default()
};
let probe = ScriptedProbe::always_fails();
let trace = walk_with(&src, Context::UserSession, &probe);
let wpad = trace
.iter()
.find(|a| a.rung == rung::WPAD)
.expect("wpad row");
assert_eq!(wpad.probe, CandidateOutcome::NotConfigured);
assert_eq!(src.pac_calls(), 0);
}
#[test]
fn a_pac_win_persists_the_script_and_probes_the_answer() {
let src = FakeWin {
ie: Some(IeProxyConfig {
auto_config_url: Some("http://wpad.corp/proxy.pac".into()),
..Default::default()
}),
pac: Some(PacAnswer {
proxies: vec!["PROXY pac.corp:3128".into()],
}),
..Default::default()
};
let probe = ScriptedProbe::new(vec![Ok(11)]);
let mut ladder = Ladder::new(&probe);
walk(&mut ladder, Context::UserSession, &src, &target());
let (won, trace) = ladder.finish();
let won = won.expect("the PAC rung wins");
assert_eq!(won.source, ProxySource::Pac);
match won.route {
Route::PacSource { pac_url } => assert_eq!(
pac_url.map(|u| u.to_string()).as_deref(),
Some("http://wpad.corp/proxy.pac")
),
Route::Static(_) => panic!("a PAC win must never materialise a static route"),
}
assert_eq!(
probe.seen.borrow().as_slice(),
&[Some("http://pac.corp:3128".to_string())]
);
let pac_row = trace
.iter()
.find(|a| a.rung == rung::IE_PAC)
.expect("pac row");
assert_eq!(pac_row.url_masked, "pac:http://wpad.corp/proxy.pac");
}
#[test]
fn a_pac_answering_direct_is_a_real_answer() {
let src = FakeWin {
ie: Some(IeProxyConfig {
auto_config_url: Some("http://wpad.corp/proxy.pac".into()),
..Default::default()
}),
pac: Some(PacAnswer::default()),
..Default::default()
};
let probe = ScriptedProbe::new(vec![Ok(2)]);
let mut ladder = Ladder::new(&probe);
walk(&mut ladder, Context::UserSession, &src, &target());
let (won, _) = ladder.finish();
assert!(won.is_some());
assert_eq!(probe.seen.borrow().as_slice(), &[None]);
}
#[test]
fn the_service_context_reads_hklm_only_when_it_applies() {
let src = FakeWin::default()
.dword(INET_SETTINGS, "ProxySettingsPerUser", 1)
.dword(INET_SETTINGS, "ProxyEnable", 1)
.string(INET_SETTINGS, "ProxyServer", "machine.corp:8080");
let probe = ScriptedProbe::always_fails();
let trace = walk_with(&src, Context::DaemonService, &probe);
let row = trace
.iter()
.find(|a| a.rung == rung::HKLM_INETSETTINGS)
.expect("hklm row");
assert_eq!(row.probe, CandidateOutcome::NotConfigured);
assert!(row
.detail
.as_deref()
.is_some_and(|d| d.contains("ProxySettingsPerUser")));
}
#[test]
fn the_policy_hive_alone_can_make_hklm_authoritative() {
let src = FakeWin::default()
.dword(INET_SETTINGS_POLICY, "ProxySettingsPerUser", 0)
.dword(INET_SETTINGS, "ProxyEnable", 1)
.string(INET_SETTINGS, "ProxyServer", "https=machine.corp:8080");
let probe = ScriptedProbe::new(vec![Ok(4)]);
let mut ladder = Ladder::new(&probe);
walk(&mut ladder, Context::DaemonService, &src, &target());
let (won, _) = ladder.finish();
let won = won.expect("HKLM wins");
assert_eq!(won.source, ProxySource::Windows);
assert_eq!(
won.route,
Route::Static(Url::parse("http://machine.corp:8080").expect("url"))
);
}
#[test]
fn the_service_context_walks_its_own_rung_order() {
let src = FakeWin::default();
let probe = ScriptedProbe::always_fails();
let trace = walk_with(&src, Context::DaemonService, &probe);
let order: Vec<_> = trace.iter().map(|a| a.rung).collect();
assert_eq!(
order,
vec![rung::HKLM_INETSETTINGS, rung::WINHTTP_MACHINE, rung::WPAD]
);
}
#[test]
fn the_proxy_list_grammar_prefers_https_then_http_then_bare() {
assert_eq!(
parse_proxy_list("http=a.corp:80;https=b.corp:443;ftp=c.corp:21").as_deref(),
Some("b.corp:443")
);
assert_eq!(
parse_proxy_list("http=a.corp:80 ftp=c.corp:21").as_deref(),
Some("a.corp:80")
);
assert_eq!(
parse_proxy_list("plain.corp:8080").as_deref(),
Some("plain.corp:8080")
);
assert_eq!(parse_proxy_list("").as_deref(), None);
assert_eq!(parse_proxy_list(" ; ").as_deref(), None);
assert_eq!(parse_proxy_list("socks=s.corp:1080").as_deref(), None);
}
#[test]
fn an_https_entry_names_a_destination_scheme_not_the_proxys_own() {
let src = FakeWin {
ie: Some(IeProxyConfig {
proxy: Some("https=proxy.corp:8080".into()),
..Default::default()
}),
..Default::default()
};
let probe = ScriptedProbe::new(vec![Ok(1)]);
let mut ladder = Ladder::new(&probe);
walk(&mut ladder, Context::UserSession, &src, &target());
let (won, _) = ladder.finish();
assert_eq!(
won.expect("static win").route,
Route::Static(Url::parse("http://proxy.corp:8080").expect("url"))
);
}
#[test]
fn a_failing_pac_leaves_a_skip_with_its_code() {
let src = FakeWin {
ie: Some(IeProxyConfig {
auto_config_url: Some("http://wpad.corp/proxy.pac".into()),
..Default::default()
}),
pac_error: Some(ERR_PAC_UNAVAILABLE),
..Default::default()
};
let probe = ScriptedProbe::always_fails();
let trace = walk_with(&src, Context::UserSession, &probe);
let row = trace
.iter()
.find(|a| a.rung == rung::IE_PAC)
.expect("pac row");
assert_eq!(row.probe, CandidateOutcome::Skipped(ERR_PAC_UNAVAILABLE));
assert!(!row.was_probed());
}
#[test]
fn the_autologon_retry_is_false_first_and_only_once() {
let seen = RefCell::new(Vec::new());
let answer = PacAnswer {
proxies: vec!["pac.corp:3128".into()],
};
let ok = with_autologon_retry(|auto| {
seen.borrow_mut().push(auto);
Ok(answer.clone())
});
assert_eq!(ok.expect("first attempt succeeds"), Some(answer.clone()));
assert_eq!(seen.borrow().as_slice(), &[false]);
seen.borrow_mut().clear();
let retried = with_autologon_retry(|auto| {
seen.borrow_mut().push(auto);
if auto {
Ok(answer.clone())
} else {
Err(WINHTTP_LOGIN_FAILURE)
}
});
assert_eq!(retried.expect("the retry succeeds"), Some(answer));
assert_eq!(seen.borrow().as_slice(), &[false, true]);
seen.borrow_mut().clear();
let failed = with_autologon_retry(|auto| {
seen.borrow_mut().push(auto);
Err(12002)
});
assert_eq!(
failed.expect_err("a timeout is not a login challenge").code,
ERR_PAC_UNAVAILABLE
);
assert_eq!(
seen.borrow().as_slice(),
&[false],
"only 12015 earns a second attempt"
);
}
#[cfg(windows)]
#[test]
fn the_real_backend_answers_without_faulting() {
let src = native();
for _ in 0..3 {
let _ = src.ie_config();
let _ = src.default_proxy();
}
let _ = src.hklm_dword(WINHTTP_POLICY, "DisableWpad");
assert_ne!(
src.service_state(AUTOPROXY_SERVICE),
ServiceState::Absent,
"WinHttpAutoProxySvc is registered on every supported Windows"
);
assert_eq!(
src.service_state("openlatch-no-such-service-exists"),
ServiceState::Absent
);
}
}