Skip to main content

windows_erg/proxy/
mod.rs

1//! Windows proxy discovery and URL-specific proxy resolution.
2
3mod types;
4
5pub use types::{
6    IeProxyConfig, ProxyConfig, ProxyResolution, parse_proxy_bypass, parse_proxy_server,
7};
8
9use crate::error::{
10    InvalidParameterError, ProxyConfigError, ProxyError, ProxyResolutionError, RegistryError,
11    WindowsApiError,
12};
13use crate::registry::{self, Hive};
14use crate::{Error, Result};
15use windows::Win32::Foundation::{GetLastError, GlobalFree, HGLOBAL};
16use windows::Win32::Networking::WinHttp::{
17    WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_AUTO_DETECT_TYPE_DHCP,
18    WINHTTP_AUTO_DETECT_TYPE_DNS_A, WINHTTP_AUTOPROXY_AUTO_DETECT, WINHTTP_AUTOPROXY_CONFIG_URL,
19    WINHTTP_AUTOPROXY_OPTIONS, WINHTTP_CURRENT_USER_IE_PROXY_CONFIG, WINHTTP_PROXY_INFO,
20    WinHttpCloseHandle, WinHttpGetIEProxyConfigForCurrentUser, WinHttpGetProxyForUrl, WinHttpOpen,
21};
22use windows::core::{HSTRING, PCWSTR, PWSTR};
23
24const INTERNET_SETTINGS_PATH: &str = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings";
25const POLICIES_PATH: &str = r"SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings";
26
27/// Read system-level proxy configuration from registry.
28pub fn get_system_proxy() -> Result<Option<ProxyConfig>> {
29    get_proxy_from_hive(Hive::LocalMachine)
30}
31
32/// Read user-level proxy configuration from registry.
33pub fn get_user_proxy() -> Result<Option<ProxyConfig>> {
34    get_proxy_from_hive(Hive::CurrentUser)
35}
36
37/// Read effective proxy configuration based on `ProxySettingsPerUser` policy.
38pub fn get_effective_proxy() -> Result<Option<ProxyConfig>> {
39    if use_user_proxy_settings()? {
40        return get_user_proxy();
41    }
42    get_system_proxy()
43}
44
45/// Read IE/WinHTTP proxy discovery configuration for the current user.
46pub fn get_ie_proxy_config() -> Result<IeProxyConfig> {
47    unsafe {
48        let mut config = WINHTTP_CURRENT_USER_IE_PROXY_CONFIG::default();
49        WinHttpGetIEProxyConfigForCurrentUser(&mut config).map_err(|err| {
50            Error::WindowsApi(WindowsApiError::with_context(
51                err,
52                "WinHttpGetIEProxyConfigForCurrentUser",
53            ))
54        })?;
55
56        let auto_config_url = take_optional_pwstr(config.lpszAutoConfigUrl)?;
57        let proxy = take_optional_pwstr(config.lpszProxy)?;
58        let proxy_bypass = take_optional_pwstr(config.lpszProxyBypass)?;
59
60        Ok(IeProxyConfig {
61            auto_detect: config.fAutoDetect.as_bool(),
62            auto_config_url,
63            proxy,
64            proxy_bypass,
65        })
66    }
67}
68
69/// Resolve proxy for a specific URL using WinHTTP auto-proxy behavior.
70pub fn get_proxy_for_url(url: &str) -> Result<Option<ProxyResolution>> {
71    if url.trim().is_empty() {
72        return Err(Error::InvalidParameter(InvalidParameterError::new(
73            "url",
74            "url cannot be empty",
75        )));
76    }
77
78    let ie_config = get_ie_proxy_config()?;
79
80    unsafe {
81        let session = WinHttpSession::new()?;
82        let mut proxy_info = WINHTTP_PROXY_INFO::default();
83
84        let mut options = WINHTTP_AUTOPROXY_OPTIONS::default();
85        if ie_config.auto_detect {
86            options.dwFlags |= WINHTTP_AUTOPROXY_AUTO_DETECT;
87            options.dwAutoDetectFlags =
88                WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A;
89        }
90
91        if let Some(auto_url) = &ie_config.auto_config_url {
92            options.dwFlags |= WINHTTP_AUTOPROXY_CONFIG_URL;
93            let auto_url_wide = HSTRING::from(auto_url);
94            options.lpszAutoConfigUrl = PCWSTR(auto_url_wide.as_ptr());
95
96            let url_wide = HSTRING::from(url);
97            let result = WinHttpGetProxyForUrl(
98                session.handle,
99                PCWSTR(url_wide.as_ptr()),
100                &mut options,
101                &mut proxy_info,
102            );
103
104            if result.is_err() {
105                return Ok(None);
106            }
107        } else if ie_config.auto_detect {
108            let url_wide = HSTRING::from(url);
109            let result = WinHttpGetProxyForUrl(
110                session.handle,
111                PCWSTR(url_wide.as_ptr()),
112                &mut options,
113                &mut proxy_info,
114            );
115
116            if result.is_err() {
117                return Ok(None);
118            }
119        } else if let Some(proxy) = ie_config.proxy {
120            return Ok(Some(ProxyResolution::from_winhttp(
121                url,
122                Some(proxy),
123                ie_config.proxy_bypass,
124                false,
125                false,
126            )));
127        } else {
128            return Ok(None);
129        }
130
131        let proxy = take_optional_pwstr(proxy_info.lpszProxy)?;
132        let bypass = take_optional_pwstr(proxy_info.lpszProxyBypass)?;
133
134        Ok(Some(ProxyResolution::from_winhttp(
135            url,
136            proxy,
137            bypass,
138            ie_config.auto_detect,
139            ie_config.auto_config_url.is_some(),
140        )))
141    }
142}
143
144fn get_proxy_from_hive(hive: Hive) -> Result<Option<ProxyConfig>> {
145    let proxy_enabled =
146        read_u32_optional(hive, INTERNET_SETTINGS_PATH, "ProxyEnable")?.unwrap_or(0) == 1;
147
148    let server = read_string_optional(hive, INTERNET_SETTINGS_PATH, "ProxyServer")?
149        .filter(|value| !value.trim().is_empty());
150
151    let bypass = read_string_optional(hive, INTERNET_SETTINGS_PATH, "ProxyOverride")?;
152    let auto_detect =
153        read_u32_optional(hive, INTERNET_SETTINGS_PATH, "AutoDetect")?.unwrap_or(0) == 1;
154    let auto_config_url = read_string_optional(hive, INTERNET_SETTINGS_PATH, "AutoConfigURL")?;
155
156    if !proxy_enabled && !auto_detect && auto_config_url.is_none() {
157        return Ok(None);
158    }
159
160    if proxy_enabled {
161        if let Some(server) = server {
162            return Ok(Some(ProxyConfig::from_registry_values(
163                server,
164                bypass,
165                auto_detect,
166                auto_config_url,
167            )));
168        }
169
170        return Err(Error::Proxy(ProxyError::InvalidConfig(
171            ProxyConfigError::new("ProxyServer", "ProxyEnable is 1 but ProxyServer is missing"),
172        )));
173    }
174
175    Ok(Some(ProxyConfig {
176        server: String::new(),
177        bypass: parse_proxy_bypass(bypass.as_deref()),
178        by_scheme: Default::default(),
179        auto_detect,
180        auto_config_url,
181    }))
182}
183
184fn use_user_proxy_settings() -> Result<bool> {
185    Ok(
186        read_u32_optional(Hive::CurrentUser, POLICIES_PATH, "ProxySettingsPerUser")?.unwrap_or(0)
187            == 1,
188    )
189}
190
191fn read_string_optional(hive: Hive, path: &str, value_name: &str) -> Result<Option<String>> {
192    match registry::read_string(hive, path, value_name) {
193        Ok(value) => Ok(Some(value)),
194        Err(Error::Registry(RegistryError::KeyNotFound(_))) => Ok(None),
195        Err(Error::Registry(RegistryError::ValueNotFound(_))) => Ok(None),
196        Err(err) => Err(err),
197    }
198}
199
200fn read_u32_optional(hive: Hive, path: &str, value_name: &str) -> Result<Option<u32>> {
201    match registry::read_u32(hive, path, value_name) {
202        Ok(value) => Ok(Some(value)),
203        Err(Error::Registry(RegistryError::KeyNotFound(_))) => Ok(None),
204        Err(Error::Registry(RegistryError::ValueNotFound(_))) => Ok(None),
205        Err(err) => Err(err),
206    }
207}
208
209fn take_optional_pwstr(pwstr: PWSTR) -> Result<Option<String>> {
210    if pwstr.is_null() {
211        return Ok(None);
212    }
213
214    let mut len = 0usize;
215    unsafe {
216        while *pwstr.0.add(len) != 0 {
217            len += 1;
218        }
219    }
220
221    let data = unsafe { std::slice::from_raw_parts(pwstr.0, len) };
222    let value = String::from_utf16_lossy(data);
223
224    unsafe {
225        GlobalFree(HGLOBAL(pwstr.0 as _)).map_err(|err| {
226            Error::WindowsApi(WindowsApiError::with_context(err, "GlobalFree(PWSTR)"))
227        })?;
228    }
229
230    Ok(Some(value))
231}
232
233struct WinHttpSession {
234    handle: *mut core::ffi::c_void,
235}
236
237impl WinHttpSession {
238    fn new() -> Result<Self> {
239        unsafe {
240            let agent = HSTRING::from("windows-erg/proxy");
241            let handle = WinHttpOpen(
242                PCWSTR(agent.as_ptr()),
243                WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
244                PCWSTR::null(),
245                PCWSTR::null(),
246                0,
247            );
248
249            if handle.is_null() {
250                let code = GetLastError();
251                return Err(Error::Proxy(ProxyError::ResolutionFailed(
252                    ProxyResolutionError::new(
253                        "WinHttpOpen",
254                        format!("WinHttpOpen returned null (error: 0x{:08X})", code.0),
255                    ),
256                )));
257            }
258
259            Ok(WinHttpSession { handle })
260        }
261    }
262}
263
264impl Drop for WinHttpSession {
265    fn drop(&mut self) {
266        unsafe {
267            let _ = WinHttpCloseHandle(self.handle);
268        }
269    }
270}