pub(crate) mod ffi;
mod notify;
use windows::Win32::Foundation::{ERROR_FILE_NOT_FOUND, GlobalFree, HGLOBAL};
use windows::Win32::Networking::WinHttp::{
WINHTTP_ACCESS_TYPE, WINHTTP_ACCESS_TYPE_NAMED_PROXY, WINHTTP_ACCESS_TYPE_NO_PROXY,
WINHTTP_CURRENT_USER_IE_PROXY_CONFIG, WINHTTP_PROXY_INFO, WinHttpGetDefaultProxyConfiguration,
WinHttpGetIEProxyConfigForCurrentUser,
};
use windows::Win32::System::Registry::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ};
use windows::core::HRESULT;
use crate::config::{ProxyConfig, ProxyConfigSource};
use crate::error::Error;
use crate::mode::ProxyMode;
use crate::parse;
use crate::watch::WatchOptions;
pub(crate) use self::notify::Watch;
use self::ffi::{RegKey, hresult_error, wide_ptr_to_string};
const INTERNET_SETTINGS: &str = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings";
const POLICY_INTERNET_SETTINGS: &str =
r"Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings";
pub(crate) fn read_config(options: &WatchOptions) -> Result<ProxyConfig, Error> {
let mut fallbacks = Vec::new();
let policy = if options.watch_group_policy {
group_policy_source(read_group_policy_mode, &mut fallbacks)
} else {
None
};
let user = read_user_mode(IeProxyConfig::query, &mut fallbacks)?;
let machine = winhttp_default_source(WinHttpDefaultConfig::query, &mut fallbacks);
let config = ProxyConfig::from_ordered_sources(in_precedence_order(policy, user, machine))
.with_fallbacks(fallbacks);
crate::trace::debug!(
config = %crate::trace::ConfigSummary(&config),
"read the Windows proxy configuration"
);
Ok(config)
}
fn in_precedence_order(
policy: Option<(ProxyConfigSource, ProxyMode)>,
user: ProxyMode,
machine: Option<(ProxyConfigSource, ProxyMode)>,
) -> Vec<(ProxyConfigSource, ProxyMode)> {
let mut sources = vec![(ProxyConfigSource::Registry, user)];
sources.extend(policy);
sources.extend(machine);
sources
}
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
fn group_policy_source(
read: impl FnOnce() -> Result<Option<ProxyMode>, Error>,
fallbacks: &mut Vec<ProxyConfigSource>,
) -> Option<(ProxyConfigSource, ProxyMode)> {
match read() {
Ok(Some(mode)) => Some((ProxyConfigSource::GroupPolicy, mode)),
Ok(None) => None,
Err(error) => {
crate::trace::warning!(
error = %crate::trace::SafeError(&error),
"reading the HKLM group policy Internet Settings key failed; \
continuing with the per-user configuration alone"
);
fallbacks.push(ProxyConfigSource::GroupPolicy);
None
}
}
}
fn read_group_policy_mode() -> Result<Option<ProxyMode>, Error> {
let Some(key) = RegKey::open(
HKEY_LOCAL_MACHINE,
POLICY_INTERNET_SETTINGS,
KEY_READ,
"opening HKLM group policy Internet Settings key",
)?
else {
return Ok(None);
};
mode_from_registry(&key)
}
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
fn read_user_mode(
query: impl FnOnce() -> Result<IeProxyConfig, windows::core::Error>,
fallbacks: &mut Vec<ProxyConfigSource>,
) -> Result<ProxyMode, Error> {
match query() {
Ok(config) => config.to_mode(),
Err(error) => {
let lost_the_active_connection =
error.code() != HRESULT::from_win32(ERROR_FILE_NOT_FOUND.0);
let error = hresult_error("WinHttpGetIEProxyConfigForCurrentUser", error);
if lost_the_active_connection {
crate::trace::warning!(
error = %crate::trace::SafeError(&error),
"WinHttpGetIEProxyConfigForCurrentUser failed for a reason other than \
having no settings to report; reading the plain HKCU registry values \
instead, which are the LAN connection's and not the active one's"
);
fallbacks.push(ProxyConfigSource::Registry);
} else {
crate::trace::fallback(
"WinHttpGetIEProxyConfigForCurrentUser reports no Internet Explorer \
proxy settings; reading the documented plain HKCU registry values \
instead",
&error,
);
}
let key = RegKey::open(
HKEY_CURRENT_USER,
INTERNET_SETTINGS,
KEY_READ,
"opening HKCU Internet Settings key",
)?;
match key {
Some(key) => Ok(mode_from_registry(&key)?.unwrap_or(ProxyMode::Direct)),
None => Ok(ProxyMode::Direct),
}
}
}
}
fn mode_from_registry(key: &RegKey) -> Result<Option<ProxyMode>, Error> {
let enable = key.dword_value("ProxyEnable")?;
let server = key.string_value("ProxyServer")?;
let overrides = key.string_value("ProxyOverride")?;
let auto_config_url = key.string_value("AutoConfigURL")?;
let auto_detect = key.dword_value("AutoDetect")?;
if !decides_whether_to_proxy(enable, auto_config_url.as_deref(), auto_detect) {
return Ok(None);
}
let server = server.filter(|_| static_server_is_enabled(enable));
Ok(Some(resolve_mode(
auto_detect == Some(1),
auto_config_url.as_deref(),
server.as_deref(),
overrides.as_deref(),
)?))
}
fn decides_whether_to_proxy(
enable: Option<u32>,
auto_config_url: Option<&str>,
auto_detect: Option<u32>,
) -> bool {
let has_pac_url = auto_config_url.is_some_and(|url| !url.trim().is_empty());
enable.is_some() || has_pac_url || auto_detect.is_some()
}
fn static_server_is_enabled(enable: Option<u32>) -> bool {
enable == Some(1)
}
fn resolve_mode(
auto_detect: bool,
auto_config_url: Option<&str>,
server: Option<&str>,
overrides: Option<&str>,
) -> Result<ProxyMode, Error> {
if auto_detect {
return Ok(ProxyMode::WpadAutoDetect);
}
if let Some(url) = auto_config_url.map(str::trim).filter(|u| !u.is_empty()) {
let url = url::Url::parse(url).map_err(|source| Error::invalid_proxy_url(url, source))?;
return Ok(ProxyMode::pac(url));
}
match server.map(str::trim).filter(|s| !s.is_empty()) {
Some(server) => Ok(parse::windows_manual(server, overrides.unwrap_or_default())),
None => Ok(ProxyMode::Direct),
}
}
#[cfg(feature = "pac-windows-native")]
pub(crate) fn wpad_fallback() -> Result<(Option<url::Url>, ProxyMode), Error> {
match IeProxyConfig::query() {
Ok(config) => Ok(config.to_fallback_beneath()),
Err(error) => {
crate::trace::fallback(
"WinHttpGetIEProxyConfigForCurrentUser failed while re-reading the \
WPAD fallback; reading the documented plain HKCU registry values \
instead",
&hresult_error("WinHttpGetIEProxyConfigForCurrentUser", error),
);
let Some(key) = RegKey::open(
HKEY_CURRENT_USER,
INTERNET_SETTINGS,
KEY_READ,
"opening HKCU Internet Settings key for the WPAD fallback",
)?
else {
return Ok((None, ProxyMode::Direct));
};
wpad_fallback_from_key(&key)
}
}
}
#[cfg(feature = "pac-windows-native")]
fn wpad_fallback_from_key(key: &RegKey) -> Result<(Option<url::Url>, ProxyMode), Error> {
let enable = key.dword_value("ProxyEnable")?;
let server = key.string_value("ProxyServer")?;
let overrides = key.string_value("ProxyOverride")?;
let auto_config_url = key.string_value("AutoConfigURL")?;
let server = server.filter(|_| static_server_is_enabled(enable));
Ok(wpad_fallback_beneath(
auto_config_url.as_deref(),
server.as_deref(),
overrides.as_deref(),
))
}
#[cfg(feature = "pac-windows-native")]
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
fn wpad_fallback_beneath(
auto_config_url: Option<&str>,
server: Option<&str>,
overrides: Option<&str>,
) -> (Option<url::Url>, ProxyMode) {
let pac = auto_config_url
.map(str::trim)
.filter(|u| !u.is_empty())
.and_then(|raw_url| match url::Url::parse(raw_url) {
Ok(url) => Some(url),
Err(source) => {
crate::trace::warning!(
error = %crate::trace::SafeError(&Error::invalid_proxy_url(raw_url, source)),
"AutoConfigURL configured beneath WPAD auto-detect could not be \
parsed while recovering the WPAD fallback; continuing with any \
static proxy configured beneath it instead of discarding it as \
Direct"
);
None
}
});
let beneath = match server.map(str::trim).filter(|s| !s.is_empty()) {
Some(server) => parse::windows_manual(server, overrides.unwrap_or_default()),
None => ProxyMode::Direct,
};
(pac, beneath)
}
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
fn winhttp_default_source(
query: impl FnOnce() -> Result<WinHttpDefaultConfig, windows::core::Error>,
fallbacks: &mut Vec<ProxyConfigSource>,
) -> Option<(ProxyConfigSource, ProxyMode)> {
match query() {
Ok(config) => Some((ProxyConfigSource::WinHttpDefault, config.to_mode())),
Err(error) if error.code() == HRESULT::from_win32(ERROR_FILE_NOT_FOUND.0) => {
crate::trace::fallback(
"WinHttpGetDefaultProxyConfiguration reports no WinHTTP machine default \
has ever been recorded on this machine (netsh winhttp); the \
WinHttpDefault source contributes nothing",
&hresult_error("WinHttpGetDefaultProxyConfiguration", error),
);
None
}
Err(error) => {
crate::trace::warning!(
error = %crate::trace::SafeError(&hresult_error(
"WinHttpGetDefaultProxyConfiguration",
error,
)),
"reading the WinHTTP per-machine default proxy configuration failed; \
continuing without the WinHttpDefault source"
);
fallbacks.push(ProxyConfigSource::WinHttpDefault);
None
}
}
}
fn winhttp_default_mode(
access_type: WINHTTP_ACCESS_TYPE,
server: Option<&str>,
overrides: Option<&str>,
) -> ProxyMode {
match access_type {
WINHTTP_ACCESS_TYPE_NO_PROXY => ProxyMode::Direct,
WINHTTP_ACCESS_TYPE_NAMED_PROXY => match server.map(str::trim).filter(|s| !s.is_empty()) {
Some(server) => parse::windows_manual(server, overrides.unwrap_or_default()),
None => ProxyMode::Direct,
},
_ => ProxyMode::Direct,
}
}
struct IeProxyConfig(WINHTTP_CURRENT_USER_IE_PROXY_CONFIG);
impl IeProxyConfig {
fn query() -> Result<Self, windows::core::Error> {
let mut raw = WINHTTP_CURRENT_USER_IE_PROXY_CONFIG::default();
unsafe { WinHttpGetIEProxyConfigForCurrentUser(&raw mut raw) }?;
Ok(Self(raw))
}
fn strings(&self) -> (Option<String>, Option<String>, Option<String>) {
unsafe {
(
wide_ptr_to_string(self.0.lpszAutoConfigUrl.0),
wide_ptr_to_string(self.0.lpszProxy.0),
wide_ptr_to_string(self.0.lpszProxyBypass.0),
)
}
}
fn to_mode(&self) -> Result<ProxyMode, Error> {
let (auto_config_url, server, overrides) = self.strings();
resolve_mode(
self.0.fAutoDetect.as_bool(),
auto_config_url.as_deref(),
server.as_deref(),
overrides.as_deref(),
)
}
#[cfg(feature = "pac-windows-native")]
fn to_fallback_beneath(&self) -> (Option<url::Url>, ProxyMode) {
let (auto_config_url, server, overrides) = self.strings();
wpad_fallback_beneath(
auto_config_url.as_deref(),
server.as_deref(),
overrides.as_deref(),
)
}
}
impl Drop for IeProxyConfig {
fn drop(&mut self) {
for ptr in [
self.0.lpszAutoConfigUrl.0,
self.0.lpszProxy.0,
self.0.lpszProxyBypass.0,
] {
if !ptr.is_null() {
unsafe {
let _ = GlobalFree(Some(HGLOBAL(ptr.cast())));
}
}
}
}
}
struct WinHttpDefaultConfig(WINHTTP_PROXY_INFO);
impl WinHttpDefaultConfig {
fn query() -> Result<Self, windows::core::Error> {
let mut raw = WINHTTP_PROXY_INFO::default();
unsafe { WinHttpGetDefaultProxyConfiguration(&raw mut raw) }?;
Ok(Self(raw))
}
fn to_mode(&self) -> ProxyMode {
let (server, overrides) = unsafe {
(
wide_ptr_to_string(self.0.lpszProxy.0),
wide_ptr_to_string(self.0.lpszProxyBypass.0),
)
};
winhttp_default_mode(self.0.dwAccessType, server.as_deref(), overrides.as_deref())
}
}
impl Drop for WinHttpDefaultConfig {
fn drop(&mut self) {
for ptr in [self.0.lpszProxy.0, self.0.lpszProxyBypass.0] {
if !ptr.is_null() {
unsafe {
let _ = GlobalFree(Some(HGLOBAL(ptr.cast())));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use windows::Win32::Foundation::{ERROR_MORE_DATA, ERROR_NOT_ENOUGH_MEMORY};
use windows::Win32::Networking::WinHttp::ERROR_WINHTTP_INTERNAL_ERROR;
#[test]
fn auto_detect_wins_over_everything() {
let mode = resolve_mode(true, Some("http://wpad/x.pac"), Some("h:8080"), None).unwrap();
assert_eq!(mode, ProxyMode::WpadAutoDetect);
}
#[test]
fn pac_url_wins_over_static_servers() {
let mode = resolve_mode(false, Some("http://wpad/x.pac"), Some("h:8080"), None).unwrap();
assert!(matches!(mode, ProxyMode::Pac { .. }));
}
#[test]
fn blank_values_are_ignored() {
let mode = resolve_mode(false, Some(" "), Some(""), Some("<local>")).unwrap();
assert_eq!(mode, ProxyMode::Direct);
}
#[test]
fn manual_uses_the_shared_parsers() {
let mode = resolve_mode(false, None, Some("h:8080"), Some("<local>;*.corp")).unwrap();
let bypass = mode.bypass().expect("manual mode has bypass rules");
assert!(bypass.excludes_simple_hostnames());
assert!(bypass.matches_authority("api.corp"));
}
#[test]
fn every_policy_read_that_failed_is_reported_as_no_policy_and_recorded() {
let mut fallbacks = Vec::new();
let entries = [
group_policy_source(|| Ok(None), &mut fallbacks),
group_policy_source(|| Ok(Some(ProxyMode::Direct)), &mut fallbacks),
group_policy_source(
|| {
Err(Error::io(
"opening HKLM group policy Internet Settings key",
std::io::Error::from_raw_os_error(5),
))
},
&mut fallbacks,
),
group_policy_source(
|| {
Err(Error::invalid_proxy_url(
"proxy.corp/wpad.dat",
url::ParseError::RelativeUrlWithoutBase,
))
},
&mut fallbacks,
),
group_policy_source(
|| {
Err(ffi::win32_error(
"reading HKLM group policy Internet Settings value AutoConfigURL",
ERROR_MORE_DATA,
))
},
&mut fallbacks,
),
];
let named: Vec<bool> = entries
.iter()
.map(|entry| match entry {
None => false,
Some((ProxyConfigSource::GroupPolicy, _)) => true,
other => panic!("{other:?}"),
})
.collect();
assert_eq!(
named,
[false, true, false, false, false],
"only a policy key that was read and understood contributes an entry: {entries:?}"
);
assert_eq!(
fallbacks,
[ProxyConfigSource::GroupPolicy; 3],
"a policy key this process could not read is degraded; one that is absent is not"
);
}
#[test]
fn only_a_winhttp_default_that_did_not_answer_is_recorded_as_degraded() {
let mut fallbacks = Vec::new();
let contributed: Vec<bool> = [
winhttp_default_source(
|| Ok(WinHttpDefaultConfig(WINHTTP_PROXY_INFO::default())),
&mut fallbacks,
),
winhttp_default_source(
|| {
Err(windows::core::Error::from_hresult(HRESULT::from_win32(
ERROR_FILE_NOT_FOUND.0,
)))
},
&mut fallbacks,
),
winhttp_default_source(
|| {
Err(windows::core::Error::from_hresult(HRESULT::from_win32(
ERROR_MORE_DATA.0,
)))
},
&mut fallbacks,
),
]
.iter()
.map(Option::is_some)
.collect();
assert_eq!(contributed, [true, false, false]);
assert_eq!(fallbacks, [ProxyConfigSource::WinHttpDefault]);
}
#[test]
fn only_a_per_user_query_that_lost_the_active_connection_is_recorded_as_degraded() {
let mut fallbacks = Vec::new();
for code in [
ERROR_FILE_NOT_FOUND.0,
ERROR_WINHTTP_INTERNAL_ERROR,
ERROR_NOT_ENOUGH_MEMORY.0,
ERROR_MORE_DATA.0,
] {
let _ = read_user_mode(
|| {
Err(windows::core::Error::from_hresult(HRESULT::from_win32(
code,
)))
},
&mut fallbacks,
);
}
assert_eq!(
fallbacks,
[ProxyConfigSource::Registry; 3],
"a per-user call that had no settings to report is not a degraded read; one that \
failed some other way answered from the LAN store instead and is"
);
let mut fallbacks = Vec::new();
let mode = read_user_mode(
|| {
Ok(IeProxyConfig(
WINHTTP_CURRENT_USER_IE_PROXY_CONFIG::default(),
))
},
&mut fallbacks,
)
.unwrap();
assert_eq!(mode, ProxyMode::Direct);
assert!(fallbacks.is_empty(), "{fallbacks:?}");
}
#[test]
fn the_per_user_store_answers_and_the_other_two_are_only_reported() {
let policy = ProxyMode::WpadAutoDetect;
let user = resolve_mode(false, None, Some("user.example:8080"), None).unwrap();
let machine = ProxyMode::Direct;
let effective =
|sources: Vec<_>| ProxyConfig::from_ordered_sources(sources).effective.clone();
let sources = |sources: Vec<(ProxyConfigSource, ProxyMode)>| {
sources
.iter()
.map(|(source, _)| *source)
.collect::<Vec<_>>()
};
let all_three = in_precedence_order(
Some((ProxyConfigSource::GroupPolicy, policy)),
user.clone(),
Some((ProxyConfigSource::WinHttpDefault, machine.clone())),
);
assert_eq!(
sources(all_three.clone()),
[
ProxyConfigSource::Registry,
ProxyConfigSource::GroupPolicy,
ProxyConfigSource::WinHttpDefault
],
"both reported-only stores follow the one that answers, in a fixed order"
);
assert_eq!(
effective(all_three),
user,
"the per-user store answers even when a policy value is present"
);
let without_policy = in_precedence_order(
None,
user.clone(),
Some((ProxyConfigSource::WinHttpDefault, machine)),
);
assert_eq!(
sources(without_policy.clone()),
[
ProxyConfigSource::Registry,
ProxyConfigSource::WinHttpDefault
]
);
assert_eq!(
effective(without_policy),
user,
"a per-machine default must not answer for an interactive user"
);
assert_eq!(
effective(in_precedence_order(None, user.clone(), None)),
user
);
}
#[test]
fn a_policy_key_with_no_switch_in_it_decides_nothing() {
assert!(!decides_whether_to_proxy(None, None, None));
assert!(decides_whether_to_proxy(Some(0), None, None));
assert!(decides_whether_to_proxy(Some(1), None, None));
assert!(decides_whether_to_proxy(
None,
Some("http://wpad/x.pac"),
None
));
assert!(decides_whether_to_proxy(None, None, Some(0)));
assert!(decides_whether_to_proxy(None, None, Some(1)));
assert!(!decides_whether_to_proxy(None, Some(""), None));
assert!(!decides_whether_to_proxy(None, Some(" "), None));
}
#[test]
fn the_static_server_switch_is_exactly_one() {
let readings: Vec<bool> = [None, Some(0), Some(1), Some(2), Some(u32::MAX)]
.into_iter()
.map(static_server_is_enabled)
.collect();
assert_eq!(readings, [false, false, true, false, false]);
}
#[test]
fn resolve_mode_still_fails_hard_on_an_unparsable_auto_config_url() {
let error = resolve_mode(false, Some("not a url"), None, None).unwrap_err();
assert!(matches!(error, Error::InvalidProxyUrl { .. }), "{error:?}");
}
#[cfg(feature = "pac-windows-native")]
#[test]
fn wpad_fallback_degrades_an_unparsable_auto_config_url_to_direct() {
assert_eq!(
wpad_fallback_beneath(Some("not a url"), None, None),
(None, ProxyMode::Direct)
);
}
#[cfg(feature = "pac-windows-native")]
#[test]
fn wpad_fallback_keeps_a_valid_auto_config_url() {
let (pac, _) = wpad_fallback_beneath(Some("http://wpad/proxy.pac"), Some("h:8080"), None);
assert_eq!(pac.map(String::from), Some("http://wpad/proxy.pac".into()));
}
#[cfg(feature = "pac-windows-native")]
#[test]
fn a_static_server_survives_the_auto_config_url_configured_above_it() {
let (pac, beneath) = wpad_fallback_beneath(
Some("http://wpad/proxy.pac"),
Some("h:8080"),
Some("<local>"),
);
assert!(pac.is_some());
let bypass = beneath.bypass().unwrap_or_else(|| {
panic!(
"the static server beneath the PAC URL was dropped as \
{beneath:?}"
)
});
assert!(bypass.excludes_simple_hostnames());
}
#[cfg(feature = "pac-windows-native")]
#[test]
fn wpad_fallback_keeps_a_static_server_when_the_auto_config_url_is_unparsable() {
let (pac, beneath) =
wpad_fallback_beneath(Some("not a url"), Some("h:8080"), Some("<local>"));
assert_eq!(pac, None);
assert!(
matches!(beneath, ProxyMode::Manual { .. }),
"a broken AutoConfigURL must not discard the static server sitting beneath \
it; got {beneath:?}"
);
}
#[test]
fn winhttp_no_proxy_is_direct() {
let mode = winhttp_default_mode(WINHTTP_ACCESS_TYPE_NO_PROXY, Some("h:8080"), None);
assert_eq!(mode, ProxyMode::Direct);
}
#[test]
fn winhttp_named_proxy_uses_the_shared_parser() {
let mode = winhttp_default_mode(
WINHTTP_ACCESS_TYPE_NAMED_PROXY,
Some("h:8080"),
Some("<local>;*.corp"),
);
let bypass = mode.bypass().expect("manual mode has bypass rules");
assert!(bypass.excludes_simple_hostnames());
assert!(bypass.matches_authority("api.corp"));
}
#[test]
fn winhttp_named_proxy_with_a_blank_server_is_direct() {
let mode = winhttp_default_mode(WINHTTP_ACCESS_TYPE_NAMED_PROXY, Some(" "), None);
assert_eq!(mode, ProxyMode::Direct);
}
#[test]
fn winhttp_named_proxy_with_no_server_is_direct() {
let mode = winhttp_default_mode(WINHTTP_ACCESS_TYPE_NAMED_PROXY, None, None);
assert_eq!(mode, ProxyMode::Direct);
}
#[test]
fn the_access_types_this_match_declines_are_direct() {
use windows::Win32::Networking::WinHttp::WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY;
for access_type in [WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_ACCESS_TYPE(2)] {
let mode = winhttp_default_mode(access_type, Some("h:8080"), None);
assert_eq!(mode, ProxyMode::Direct, "access type {access_type:?}");
}
}
#[cfg(feature = "pac-windows-native")]
mod registry_guard {
use windows::Win32::Foundation::ERROR_FILE_NOT_FOUND;
use windows::Win32::System::Registry::{
HKEY, HKEY_CURRENT_USER, KEY_READ, KEY_SET_VALUE, REG_DWORD, REG_EXPAND_SZ, REG_SZ,
REG_VALUE_TYPE, RegCloseKey, RegDeleteValueW, RegOpenKeyExW, RegQueryValueExW,
RegSetValueExW,
};
use windows::core::PCWSTR;
use crate::sys::win::ffi::wide;
use super::super::INTERNET_SETTINGS;
const VALUES: [&str; 5] = [
"ProxyEnable",
"ProxyServer",
"ProxyOverride",
"AutoConfigURL",
"AutoDetect",
];
const CONNECTIONS: &str =
r"Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections";
const CONNECTION_VALUES: [&str; 2] = ["DefaultConnectionSettings", "SavedLegacySettings"];
static REGISTRY_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn read_raw(key: HKEY, name: &str) -> Option<(u32, Vec<u8>)> {
let name_w = wide(name);
let mut kind = REG_VALUE_TYPE::default();
let mut len: u32 = 0;
let status = unsafe {
RegQueryValueExW(
key,
PCWSTR(name_w.as_ptr()),
None,
Some(&mut kind),
None,
Some(&mut len),
)
};
if status == ERROR_FILE_NOT_FOUND {
return None;
}
assert_eq!(status.0, 0, "sizing {name}");
let mut buf = vec![0u8; len as usize];
let status = unsafe {
RegQueryValueExW(
key,
PCWSTR(name_w.as_ptr()),
None,
Some(&mut kind),
Some(buf.as_mut_ptr()),
Some(&mut len),
)
};
assert_eq!(status.0, 0, "reading {name}");
Some((kind.0, buf))
}
type SavedValue = (&'static str, Option<(u32, Vec<u8>)>);
fn open_key(path: &str) -> Option<HKEY> {
let path_w = wide(path);
let mut key = HKEY::default();
let status = unsafe {
RegOpenKeyExW(
HKEY_CURRENT_USER,
PCWSTR(path_w.as_ptr()),
None,
KEY_READ | KEY_SET_VALUE,
&raw mut key,
)
};
(status.0 == 0).then_some(key)
}
fn restore(key: HKEY, saved: &[SavedValue]) {
for (name, value) in saved {
let name_w = wide(name);
match value {
Some((kind, bytes)) => {
let _ = unsafe {
RegSetValueExW(
key,
PCWSTR(name_w.as_ptr()),
None,
REG_VALUE_TYPE(*kind),
Some(bytes),
)
};
}
None => unsafe {
let _ = RegDeleteValueW(key, PCWSTR(name_w.as_ptr()));
},
}
}
}
pub(super) struct Guard {
key: HKEY,
saved: Vec<SavedValue>,
connections: Option<HKEY>,
connections_saved: Vec<SavedValue>,
_lock: std::sync::MutexGuard<'static, ()>,
}
impl Guard {
pub(super) fn open() -> Self {
let _lock = REGISTRY_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let key = open_key(INTERNET_SETTINGS)
.expect("opening HKCU Internet Settings for the test");
let saved = VALUES
.iter()
.map(|name| (*name, read_raw(key, name)))
.collect();
let connections = open_key(CONNECTIONS);
let connections_saved = connections
.map(|key| {
CONNECTION_VALUES
.iter()
.map(|name| (*name, read_raw(key, name)))
.collect()
})
.unwrap_or_default();
Self {
key,
saved,
connections,
connections_saved,
_lock,
}
}
pub(super) fn write_dword(&self, name: &str, value: u32) {
self.write_dword_bytes(name, &value.to_le_bytes());
}
pub(super) fn write_dword_bytes(&self, name: &str, bytes: &[u8]) {
let name_w = wide(name);
let status = unsafe {
RegSetValueExW(
self.key,
PCWSTR(name_w.as_ptr()),
None,
REG_DWORD,
Some(bytes),
)
};
assert_eq!(status.0, 0, "writing {name}");
}
pub(super) fn write_sz(&self, name: &str, value: &str) {
self.write_string(name, REG_SZ, value);
}
pub(super) fn write_expand_sz(&self, name: &str, value: &str) {
self.write_string(name, REG_EXPAND_SZ, value);
}
fn write_string(&self, name: &str, kind: REG_VALUE_TYPE, value: &str) {
let name_w = wide(name);
let data_w = wide(value);
let bytes: &[u8] =
unsafe { std::slice::from_raw_parts(data_w.as_ptr().cast(), data_w.len() * 2) };
let status = unsafe {
RegSetValueExW(self.key, PCWSTR(name_w.as_ptr()), None, kind, Some(bytes))
};
assert_eq!(status.0, 0, "writing {name}");
}
pub(super) fn delete(&self, name: &str) {
let name_w = wide(name);
unsafe {
let _ = RegDeleteValueW(self.key, PCWSTR(name_w.as_ptr()));
}
}
}
impl Drop for Guard {
fn drop(&mut self) {
restore(self.key, &self.saved);
if let Some(connections) = self.connections {
restore(connections, &self.connections_saved);
unsafe {
let _ = RegCloseKey(connections);
}
}
unsafe {
let _ = RegCloseKey(self.key);
}
}
}
}
#[cfg(feature = "pac-windows-native")]
#[test]
#[ignore = "rewrites this machine's real Internet Settings; CI runs it with --include-ignored"]
fn wpad_fallback_recovers_the_static_proxy_auto_detect_would_otherwise_discard() {
use crate::Scheme;
let guard = registry_guard::Guard::open();
guard.write_dword("ProxyEnable", 1);
guard.write_sz("ProxyServer", "127.0.0.1:18080");
guard.write_sz("ProxyOverride", "<local>");
guard.delete("AutoConfigURL");
let (pac, beneath) =
wpad_fallback().expect("recovering the WPAD fallback from HKCU must succeed");
assert_eq!(pac, None, "no `AutoConfigURL` is configured above it");
assert!(
matches!(beneath, ProxyMode::Manual { .. }),
"a static proxy configured underneath auto-detect (which `effective` \
discards) must still be recoverable as the WPAD fallback, not silently \
lost as `Direct`; got {beneath:?}"
);
assert_eq!(
beneath.endpoint_for(Scheme::Http).map(|e| e.authority()),
Some("127.0.0.1:18080".to_owned()),
"{beneath:?}"
);
drop(guard);
}
#[cfg(feature = "pac-windows-native")]
#[test]
#[ignore = "rewrites this machine's real Internet Settings; CI runs it with --include-ignored"]
fn wpad_fallback_survives_an_unparsable_auto_config_url_instead_of_erroring() {
let guard = registry_guard::Guard::open();
guard.delete("ProxyEnable");
guard.delete("ProxyServer");
guard.delete("ProxyOverride");
guard.write_sz("AutoConfigURL", "not a url");
let (pac, _) =
wpad_fallback().expect("an unparsable AutoConfigURL must degrade rather than error");
assert_eq!(
pac, None,
"a broken AutoConfigURL must not be mistaken for a usable PAC URL"
);
drop(guard);
}
#[cfg(feature = "pac-windows-native")]
#[test]
#[ignore = "rewrites this machine's real Internet Settings; CI runs it with --include-ignored"]
fn wpad_fallback_falls_through_to_a_static_server_when_the_auto_config_url_is_unparsable() {
use crate::Scheme;
let guard = registry_guard::Guard::open();
guard.write_dword("ProxyEnable", 1);
guard.write_sz("ProxyServer", "127.0.0.1:18080");
guard.write_sz("ProxyOverride", "<local>");
guard.write_sz("AutoConfigURL", "not a url");
let (pac, beneath) =
wpad_fallback().expect("a malformed AutoConfigURL must degrade rather than error");
assert_eq!(pac, None, "the AutoConfigURL written above does not parse");
assert!(
matches!(beneath, ProxyMode::Manual { .. }),
"a static proxy configured beneath auto-detect must survive a broken \
AutoConfigURL sitting above it in the same registry key, not be silently \
bypassed as Direct; got {beneath:?}"
);
assert_eq!(
beneath.endpoint_for(Scheme::Http).map(|e| e.authority()),
Some("127.0.0.1:18080".to_owned()),
"{beneath:?}"
);
drop(guard);
}
#[cfg(feature = "pac-windows-native")]
#[test]
#[ignore = "rewrites this machine's real Internet Settings; CI runs it with --include-ignored"]
fn a_disabled_switch_drops_the_static_server_left_beside_it() {
let guard = registry_guard::Guard::open();
guard.write_dword("ProxyEnable", 0);
guard.write_sz("ProxyServer", "127.0.0.1:18080");
guard.write_sz("ProxyOverride", "<local>");
guard.delete("AutoConfigURL");
guard.delete("AutoDetect");
let key = RegKey::open(
HKEY_CURRENT_USER,
INTERNET_SETTINGS,
KEY_READ,
"opening HKCU Internet Settings for the test",
)
.expect("opening the key the guard just wrote to must succeed")
.expect("the guard could not have written to a key that is absent");
assert_eq!(
mode_from_registry(&key).expect("reading back what the guard wrote must succeed"),
Some(ProxyMode::Direct),
"a `ProxyServer` beside `ProxyEnable = 0` is switched off, not in force"
);
assert_eq!(
wpad_fallback_from_key(&key).expect("the same read must succeed for the fallback"),
(None, ProxyMode::Direct),
"the WPAD fallback reads the same switch and must reach the same answer"
);
drop(guard);
}
#[cfg(feature = "pac-windows-native")]
#[test]
#[ignore = "rewrites this machine's real Internet Settings; CI runs it with --include-ignored"]
fn registry_strings_arrive_without_their_terminator_and_unexpanded() {
let guard = registry_guard::Guard::open();
guard.write_sz("ProxyServer", "127.0.0.1:18080");
guard.write_expand_sz("AutoConfigURL", "http://%COMPUTERNAME%.corp/proxy.pac");
let key = RegKey::open(
HKEY_CURRENT_USER,
INTERNET_SETTINGS,
KEY_READ,
"opening HKCU Internet Settings for the test",
)
.expect("opening the key the guard just wrote to must succeed")
.expect("the guard could not have written to a key that is absent");
assert_eq!(
key.string_value("ProxyServer")
.expect("reading back a REG_SZ must succeed")
.as_deref(),
Some("127.0.0.1:18080")
);
assert_eq!(
key.string_value("AutoConfigURL")
.expect("reading back a REG_EXPAND_SZ must succeed")
.as_deref(),
Some("http://%COMPUTERNAME%.corp/proxy.pac")
);
drop(guard);
}
#[cfg(feature = "pac-windows-native")]
#[test]
#[ignore = "rewrites this machine's real Internet Settings; CI runs it with --include-ignored"]
fn an_over_long_dword_keeps_its_low_word_rather_than_being_refused() {
let guard = registry_guard::Guard::open();
guard.write_dword_bytes("ProxyEnable", &[1, 0, 0, 0, 0xEF, 0xBE, 0xAD, 0xDE]);
let key = RegKey::open(
HKEY_CURRENT_USER,
INTERNET_SETTINGS,
KEY_READ,
"opening HKCU Internet Settings for the test",
)
.expect("opening the key the guard just wrote to must succeed")
.expect("the guard could not have written to a key that is absent");
assert_eq!(
key.dword_value("ProxyEnable")
.expect("reading back an over-long REG_DWORD must succeed"),
Some(1)
);
drop(guard);
}
#[cfg(feature = "pac-windows-native")]
#[test]
#[ignore = "rewrites this machine's real Internet Settings; CI runs it with --include-ignored"]
fn a_dword_stored_under_a_string_type_silences_the_key_it_sits_in() {
let guard = registry_guard::Guard::open();
guard.write_sz("ProxyEnable", "1");
guard.write_sz("ProxyServer", "127.0.0.1:18080");
guard.write_sz("ProxyOverride", "<local>");
guard.delete("AutoConfigURL");
guard.delete("AutoDetect");
let key = RegKey::open(
HKEY_CURRENT_USER,
INTERNET_SETTINGS,
KEY_READ,
"opening HKCU Internet Settings for the test",
)
.expect("opening the key the guard just wrote to must succeed")
.expect("the guard could not have written to a key that is absent");
assert_eq!(
key.dword_value("ProxyEnable")
.expect("a wrongly typed value is absent, not a read failure"),
None
);
assert_eq!(
mode_from_registry(&key).expect("reading back what the guard wrote must succeed"),
None,
"a key whose switch is unreadable has said nothing, so it decides nothing"
);
drop(guard);
}
}