Skip to main content

eggress_system_proxy/backends/
windows.rs

1use crate::apply::Command;
2use crate::command_runner::CommandRunner;
3use crate::inspection::SystemProxySettings;
4
5/// Inspect proxy settings from Windows Internet Settings registry keys.
6///
7/// On non-Windows platforms, returns an error.
8pub fn inspect_windows_proxy(runner: &dyn CommandRunner) -> Result<SystemProxySettings, String> {
9    let _ = runner;
10    #[cfg(target_os = "windows")]
11    {
12        inspect_windows_registry(runner)
13    }
14    #[cfg(not(target_os = "windows"))]
15    {
16        Err("Windows Internet Settings not available on this platform".to_string())
17    }
18}
19
20#[cfg(target_os = "windows")]
21fn inspect_windows_registry(runner: &dyn CommandRunner) -> Result<SystemProxySettings, String> {
22    let mut raw = std::collections::HashMap::new();
23
24    // HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings
25    let key = r#"Software\Microsoft\Windows\CurrentVersion\Internet Settings"#;
26
27    let proxy_enable = read_reg_value(runner, key, "ProxyEnable")?;
28    let proxy_server = read_reg_value(runner, key, "ProxyServer")?;
29    let proxy_override = read_reg_value(runner, key, "ProxyOverride")?;
30
31    raw.insert("ProxyEnable".to_string(), proxy_enable.unwrap_or_default());
32    raw.insert(
33        "ProxyServer".to_string(),
34        proxy_server.clone().unwrap_or_default(),
35    );
36    raw.insert(
37        "ProxyOverride".to_string(),
38        proxy_override.clone().unwrap_or_default(),
39    );
40
41    let enabled = raw.get("ProxyEnable").map_or(false, |v| v == "1");
42
43    let (http_proxy, https_proxy, socks_proxy) = if enabled {
44        parse_windows_proxy_server(&proxy_server.unwrap_or_default())
45    } else {
46        (None, None, None)
47    };
48
49    Ok(SystemProxySettings {
50        source: "windows:internet_settings".to_string(),
51        http_proxy,
52        https_proxy,
53        socks_proxy,
54        no_proxy: proxy_override,
55        raw,
56    })
57}
58
59#[cfg(target_os = "windows")]
60fn read_reg_value(
61    runner: &dyn CommandRunner,
62    key: &str,
63    value: &str,
64) -> Result<Option<String>, String> {
65    let output = runner
66        .run("reg", &["query", &format!("HKCU\\{key}"), "/v", value])
67        .map_err(|e| format!("failed to run reg: {e}"))?;
68
69    if !output.status.success() {
70        return Ok(None);
71    }
72
73    let stdout = String::from_utf8_lossy(&output.stdout);
74    for line in stdout.lines() {
75        if line.contains(value) {
76            if let Some(pos) = line.rfind("REG_SZ") {
77                return Ok(Some(line[pos + 6..].trim().to_string()));
78            }
79        }
80    }
81    Ok(None)
82}
83
84#[cfg(target_os = "windows")]
85fn parse_windows_proxy_server(server: &str) -> (Option<String>, Option<String>, Option<String>) {
86    let mut http = None;
87    let mut https = None;
88    let mut socks = None;
89
90    for part in server.split(';') {
91        let part = part.trim();
92        if let Some(addr) = part.strip_prefix("http=") {
93            http = Some(addr.to_string());
94        } else if let Some(addr) = part.strip_prefix("https=") {
95            https = Some(addr.to_string());
96        } else if let Some(addr) = part.strip_prefix("socks=") {
97            socks = Some(addr.to_string());
98        } else if !part.is_empty() {
99            // Bare address applies to all protocols
100            if http.is_none() {
101                http = Some(part.to_string());
102            }
103            if https.is_none() {
104                https = Some(part.to_string());
105            }
106        }
107    }
108
109    (http, https, socks)
110}
111
112/// Generate commands to apply Windows proxy settings (dry-run only).
113pub fn generate_windows_apply_commands(
114    http_proxy: Option<&str>,
115    https_proxy: Option<&str>,
116    socks_proxy: Option<&str>,
117    no_proxy: Option<&str>,
118) -> Vec<Command> {
119    let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings";
120    let mut commands = Vec::new();
121
122    // Build ProxyServer value
123    let mut parts = Vec::new();
124    if let Some(http) = http_proxy {
125        parts.push(format!("http={http}"));
126    }
127    if let Some(https) = https_proxy {
128        parts.push(format!("https={https}"));
129    }
130    if let Some(socks) = socks_proxy {
131        parts.push(format!("socks={socks}"));
132    }
133
134    if !parts.is_empty() {
135        let proxy_value = parts.join(";");
136        commands.push(Command::new(
137            "reg",
138            vec![
139                "add".into(),
140                key.into(),
141                "/v".into(),
142                "ProxyServer".into(),
143                "/t".into(),
144                "REG_SZ".into(),
145                "/d".into(),
146                proxy_value,
147                "/f".into(),
148            ],
149        ));
150        commands.push(Command::new(
151            "reg",
152            vec![
153                "add".into(),
154                key.into(),
155                "/v".into(),
156                "ProxyEnable".into(),
157                "/t".into(),
158                "REG_DWORD".into(),
159                "/d".into(),
160                "1".into(),
161                "/f".into(),
162            ],
163        ));
164    }
165
166    if let Some(no_proxy) = no_proxy {
167        commands.push(Command::new(
168            "reg",
169            vec![
170                "add".into(),
171                key.into(),
172                "/v".into(),
173                "ProxyOverride".into(),
174                "/t".into(),
175                "REG_SZ".into(),
176                "/d".into(),
177                no_proxy.into(),
178                "/f".into(),
179            ],
180        ));
181    }
182
183    commands
184}
185
186/// Generate commands to disable Windows proxy settings (dry-run only).
187pub fn generate_windows_disable_commands() -> Vec<Command> {
188    let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings";
189    vec![Command::new(
190        "reg",
191        vec![
192            "add".into(),
193            key.into(),
194            "/v".into(),
195            "ProxyEnable".into(),
196            "/t".into(),
197            "REG_DWORD".into(),
198            "/d".into(),
199            "0".into(),
200            "/f".into(),
201        ],
202    )]
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[cfg(target_os = "windows")]
210    #[test]
211    fn parse_server_bare_address() {
212        let (http, https, socks) = parse_windows_proxy_server("proxy:8080");
213        assert_eq!(http.as_deref(), Some("proxy:8080"));
214        assert_eq!(https.as_deref(), Some("proxy:8080"));
215        assert_eq!(socks, None);
216    }
217
218    #[cfg(target_os = "windows")]
219    #[test]
220    fn parse_server_explicit_protocols() {
221        let (http, https, socks) =
222            parse_windows_proxy_server("http=proxy:8080;https=proxy:8443;socks=proxy:1080");
223        assert_eq!(http.as_deref(), Some("proxy:8080"));
224        assert_eq!(https.as_deref(), Some("proxy:8443"));
225        assert_eq!(socks.as_deref(), Some("proxy:1080"));
226    }
227
228    #[cfg(target_os = "windows")]
229    #[test]
230    fn parse_server_empty() {
231        let (http, https, socks) = parse_windows_proxy_server("");
232        assert_eq!(http, None);
233        assert_eq!(https, None);
234        assert_eq!(socks, None);
235    }
236
237    #[test]
238    fn generate_apply_commands_produces_reg_commands() {
239        let commands = generate_windows_apply_commands(
240            Some("proxy:8080"),
241            Some("proxy:8443"),
242            None,
243            Some("localhost"),
244        );
245        assert!(commands
246            .iter()
247            .any(|c| c.to_string().contains("ProxyServer")));
248        assert!(commands
249            .iter()
250            .any(|c| c.to_string().contains("ProxyEnable")));
251        assert!(commands
252            .iter()
253            .any(|c| c.to_string().contains("ProxyOverride")));
254    }
255
256    #[test]
257    fn generate_disable_commands() {
258        let commands = generate_windows_disable_commands();
259        assert_eq!(commands.len(), 1);
260        assert!(commands[0].to_string().contains("ProxyEnable"));
261        assert!(commands[0].to_string().contains("/d 0"));
262    }
263}