Skip to main content

eggress_system_proxy/backends/
linux.rs

1use crate::apply::Command;
2use crate::command_runner::CommandRunner;
3use crate::inspection::SystemProxySettings;
4
5/// Inspect GNOME proxy settings via `gsettings`.
6pub fn inspect_gnome_proxy(runner: &dyn CommandRunner) -> Result<SystemProxySettings, String> {
7    let mut raw = std::collections::HashMap::new();
8
9    let mode = get_gsettings_value(runner, "org.gnome.system.proxy", "mode")?;
10    raw.insert("mode".to_string(), mode.clone().unwrap_or_default());
11
12    if mode.as_deref() == Some("none") || mode.is_none() {
13        return Ok(SystemProxySettings {
14            source: "linux:gnome:gsettings".to_string(),
15            http_proxy: None,
16            https_proxy: None,
17            socks_proxy: None,
18            no_proxy: None,
19            raw,
20        });
21    }
22
23    let http_host = get_gsettings_value(runner, "org.gnome.system.proxy.http", "host")?;
24    let http_port = get_gsettings_value(runner, "org.gnome.system.proxy.http", "port")?;
25    let https_host = get_gsettings_value(runner, "org.gnome.system.proxy.https", "host")?;
26    let https_port = get_gsettings_value(runner, "org.gnome.system.proxy.https", "port")?;
27    let socks_host = get_gsettings_value(runner, "org.gnome.system.proxy.socks", "host")?;
28    let socks_port = get_gsettings_value(runner, "org.gnome.system.proxy.socks", "port")?;
29    let ignore_hosts = get_gsettings_value(runner, "org.gnome.system.proxy", "ignore-hosts")?;
30
31    let http_proxy = format_proxy_address(&http_host, &http_port);
32    let https_proxy = format_proxy_address(&https_host, &https_port);
33    let socks_proxy = format_proxy_address(&socks_host, &socks_port);
34
35    if let Some(ref v) = http_proxy {
36        raw.insert("http_proxy".to_string(), v.clone());
37    }
38    if let Some(ref v) = https_proxy {
39        raw.insert("https_proxy".to_string(), v.clone());
40    }
41    if let Some(ref v) = socks_proxy {
42        raw.insert("socks_proxy".to_string(), v.clone());
43    }
44
45    let no_proxy = ignore_hosts.map(|h| {
46        h.trim_start_matches('[')
47            .trim_end_matches(']')
48            .replace("', '", ",")
49    });
50
51    if let Some(ref v) = no_proxy {
52        raw.insert("no_proxy".to_string(), v.clone());
53    }
54
55    Ok(SystemProxySettings {
56        source: "linux:gnome:gsettings".to_string(),
57        http_proxy,
58        https_proxy,
59        socks_proxy,
60        no_proxy,
61        raw,
62    })
63}
64
65/// Generate `gsettings` commands to apply GNOME proxy settings (dry-run).
66pub fn generate_gnome_apply_commands(
67    http_proxy: Option<&str>,
68    https_proxy: Option<&str>,
69    socks_proxy: Option<&str>,
70    no_proxy: Option<&str>,
71) -> Vec<Command> {
72    let mut commands = Vec::new();
73
74    let has_any = http_proxy.is_some() || https_proxy.is_some() || socks_proxy.is_some();
75    if has_any {
76        commands.push(Command::new(
77            "gsettings",
78            vec![
79                "set".into(),
80                "org.gnome.system.proxy".into(),
81                "mode".into(),
82                "manual".into(),
83            ],
84        ));
85    }
86
87    if let Some(http) = http_proxy {
88        if let Some((host, port)) = parse_proxy_address(http) {
89            commands.push(Command::new(
90                "gsettings",
91                vec![
92                    "set".into(),
93                    "org.gnome.system.proxy.http".into(),
94                    "host".into(),
95                    host,
96                ],
97            ));
98            commands.push(Command::new(
99                "gsettings",
100                vec![
101                    "set".into(),
102                    "org.gnome.system.proxy.http".into(),
103                    "port".into(),
104                    port.to_string(),
105                ],
106            ));
107        }
108    }
109
110    if let Some(https) = https_proxy {
111        if let Some((host, port)) = parse_proxy_address(https) {
112            commands.push(Command::new(
113                "gsettings",
114                vec![
115                    "set".into(),
116                    "org.gnome.system.proxy.https".into(),
117                    "host".into(),
118                    host,
119                ],
120            ));
121            commands.push(Command::new(
122                "gsettings",
123                vec![
124                    "set".into(),
125                    "org.gnome.system.proxy.https".into(),
126                    "port".into(),
127                    port.to_string(),
128                ],
129            ));
130        }
131    }
132
133    if let Some(socks) = socks_proxy {
134        if let Some((host, port)) = parse_proxy_address(socks) {
135            commands.push(Command::new(
136                "gsettings",
137                vec![
138                    "set".into(),
139                    "org.gnome.system.proxy.socks".into(),
140                    "host".into(),
141                    host,
142                ],
143            ));
144            commands.push(Command::new(
145                "gsettings",
146                vec![
147                    "set".into(),
148                    "org.gnome.system.proxy.socks".into(),
149                    "port".into(),
150                    port.to_string(),
151                ],
152            ));
153        }
154    }
155
156    if let Some(no_proxy) = no_proxy {
157        let gsettings_list: Vec<String> = no_proxy
158            .split(',')
159            .map(|s| format!("'{}'", s.trim()))
160            .collect();
161        let value = format!("[{}]", gsettings_list.join(", "));
162        commands.push(Command::new(
163            "gsettings",
164            vec![
165                "set".into(),
166                "org.gnome.system.proxy".into(),
167                "ignore-hosts".into(),
168                value,
169            ],
170        ));
171    }
172
173    commands
174}
175
176/// Generate `gsettings` commands to disable GNOME proxy (dry-run).
177pub fn generate_gnome_disable_commands() -> Vec<Command> {
178    vec![Command::new(
179        "gsettings",
180        vec![
181            "set".into(),
182            "org.gnome.system.proxy".into(),
183            "mode".into(),
184            "none".into(),
185        ],
186    )]
187}
188
189fn get_gsettings_value(
190    runner: &dyn CommandRunner,
191    schema: &str,
192    key: &str,
193) -> Result<Option<String>, String> {
194    let output = runner
195        .run("gsettings", &["get", schema, key])
196        .map_err(|e| format!("failed to run gsettings: {e}"))?;
197
198    if !output.status.success() {
199        return Ok(None);
200    }
201
202    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
203    if stdout.is_empty() || stdout == "undefined" {
204        return Ok(None);
205    }
206
207    // Strip surrounding quotes from gsettings output
208    let value = if stdout.starts_with('\'') && stdout.ends_with('\'') {
209        stdout[1..stdout.len() - 1].to_string()
210    } else {
211        stdout
212    };
213
214    Ok(Some(value))
215}
216
217fn format_proxy_address(host: &Option<String>, port: &Option<String>) -> Option<String> {
218    match (host, port) {
219        (Some(h), Some(p)) if !h.is_empty() => Some(format!("{h}:{p}")),
220        (Some(h), _) if !h.is_empty() => Some(h.clone()),
221        _ => None,
222    }
223}
224
225fn parse_proxy_address(addr: &str) -> Option<(String, u16)> {
226    let addr = addr
227        .strip_prefix("http://")
228        .or_else(|| addr.strip_prefix("https://"))
229        .or_else(|| addr.strip_prefix("socks://"))
230        .or_else(|| addr.strip_prefix("socks5://"))
231        .unwrap_or(addr);
232
233    if let Some(pos) = addr.rfind(':') {
234        let host = addr[..pos].to_string();
235        let port_str = &addr[pos + 1..];
236        if let Ok(port) = port_str.parse::<u16>() {
237            return Some((host, port));
238        }
239    }
240    None
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::command_runner::MockCommandRunner;
247
248    fn success_output(stdout: &str) -> std::process::Output {
249        #[cfg(unix)]
250        {
251            use std::os::unix::process::ExitStatusExt;
252            std::process::Output {
253                status: std::process::ExitStatus::from_raw(0),
254                stdout: stdout.as_bytes().to_vec(),
255                stderr: Vec::new(),
256            }
257        }
258        #[cfg(not(unix))]
259        {
260            std::process::Output {
261                status: std::process::ExitStatus::default(),
262                stdout: stdout.as_bytes().to_vec(),
263                stderr: Vec::new(),
264            }
265        }
266    }
267
268    #[test]
269    fn inspect_gnome_manual_mode() {
270        let runner = MockCommandRunner::new()
271            .add_response(
272                "gsettings",
273                vec![
274                    "get".to_string(),
275                    "org.gnome.system.proxy".to_string(),
276                    "mode".to_string(),
277                ],
278                Ok(success_output("'manual'\n")),
279            )
280            .add_response(
281                "gsettings",
282                vec![
283                    "get".to_string(),
284                    "org.gnome.system.proxy.http".to_string(),
285                    "host".to_string(),
286                ],
287                Ok(success_output("'proxy.example.com'\n")),
288            )
289            .add_response(
290                "gsettings",
291                vec![
292                    "get".to_string(),
293                    "org.gnome.system.proxy.http".to_string(),
294                    "port".to_string(),
295                ],
296                Ok(success_output("8080\n")),
297            )
298            .add_response(
299                "gsettings",
300                vec![
301                    "get".to_string(),
302                    "org.gnome.system.proxy.https".to_string(),
303                    "host".to_string(),
304                ],
305                Ok(success_output("'proxy.example.com'\n")),
306            )
307            .add_response(
308                "gsettings",
309                vec![
310                    "get".to_string(),
311                    "org.gnome.system.proxy.https".to_string(),
312                    "port".to_string(),
313                ],
314                Ok(success_output("8443\n")),
315            )
316            .add_response(
317                "gsettings",
318                vec![
319                    "get".to_string(),
320                    "org.gnome.system.proxy.socks".to_string(),
321                    "host".to_string(),
322                ],
323                Ok(success_output("''\n")),
324            )
325            .add_response(
326                "gsettings",
327                vec![
328                    "get".to_string(),
329                    "org.gnome.system.proxy.socks".to_string(),
330                    "port".to_string(),
331                ],
332                Ok(success_output("0\n")),
333            )
334            .add_response(
335                "gsettings",
336                vec![
337                    "get".to_string(),
338                    "org.gnome.system.proxy".to_string(),
339                    "ignore-hosts".to_string(),
340                ],
341                Ok(success_output("['localhost', '127.0.0.1']\n")),
342            );
343
344        let settings = inspect_gnome_proxy(&runner).unwrap();
345        assert_eq!(
346            settings.http_proxy.as_deref(),
347            Some("proxy.example.com:8080")
348        );
349        assert_eq!(
350            settings.https_proxy.as_deref(),
351            Some("proxy.example.com:8443")
352        );
353        assert_eq!(settings.socks_proxy, None);
354        assert!(settings.no_proxy.unwrap().contains("localhost"));
355    }
356
357    #[test]
358    fn generate_gnome_apply_commands_structure() {
359        let commands = generate_gnome_apply_commands(
360            Some("proxy:8080"),
361            Some("proxy:8443"),
362            None,
363            Some("localhost"),
364        );
365        assert!(commands.iter().any(|c| {
366            let s = c.to_string();
367            s.contains("mode") && s.contains("manual")
368        }));
369        assert!(commands.iter().any(|c| c.to_string().contains("http host")));
370        assert!(commands
371            .iter()
372            .any(|c| c.to_string().contains("https host")));
373        assert!(commands
374            .iter()
375            .any(|c| c.to_string().contains("ignore-hosts")));
376    }
377
378    #[test]
379    fn generate_gnome_disable_commands_works() {
380        let commands = generate_gnome_disable_commands();
381        assert_eq!(commands.len(), 1);
382        assert!(commands[0].to_string().contains("none"));
383    }
384
385    #[test]
386    fn parse_proxy_address_with_scheme() {
387        assert_eq!(
388            parse_proxy_address("http://proxy:8080"),
389            Some(("proxy".to_string(), 8080))
390        );
391    }
392
393    #[test]
394    fn parse_proxy_address_without_scheme() {
395        assert_eq!(
396            parse_proxy_address("proxy:8080"),
397            Some(("proxy".to_string(), 8080))
398        );
399    }
400}