1use crate::apply::Command;
2use crate::command_runner::CommandRunner;
3use crate::inspection::SystemProxySettings;
4
5pub 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| parse_gsettings_string_array(&h));
46
47 if let Some(ref v) = no_proxy {
48 raw.insert("no_proxy".to_string(), v.clone());
49 }
50
51 Ok(SystemProxySettings {
52 source: "linux:gnome:gsettings".to_string(),
53 http_proxy,
54 https_proxy,
55 socks_proxy,
56 no_proxy,
57 raw,
58 })
59}
60
61pub fn generate_gnome_apply_commands(
63 http_proxy: Option<&str>,
64 https_proxy: Option<&str>,
65 socks_proxy: Option<&str>,
66 no_proxy: Option<&str>,
67) -> Vec<Command> {
68 let mut commands = Vec::new();
69
70 let has_any = http_proxy.is_some() || https_proxy.is_some() || socks_proxy.is_some();
71 if has_any {
72 commands.push(Command::new(
73 "gsettings",
74 vec![
75 "set".into(),
76 "org.gnome.system.proxy".into(),
77 "mode".into(),
78 "manual".into(),
79 ],
80 ));
81 }
82
83 if let Some(http) = http_proxy {
84 if let Some((host, port)) = parse_proxy_address(http) {
85 commands.push(Command::new(
86 "gsettings",
87 vec![
88 "set".into(),
89 "org.gnome.system.proxy.http".into(),
90 "host".into(),
91 host,
92 ],
93 ));
94 commands.push(Command::new(
95 "gsettings",
96 vec![
97 "set".into(),
98 "org.gnome.system.proxy.http".into(),
99 "port".into(),
100 port.to_string(),
101 ],
102 ));
103 }
104 }
105
106 if let Some(https) = https_proxy {
107 if let Some((host, port)) = parse_proxy_address(https) {
108 commands.push(Command::new(
109 "gsettings",
110 vec![
111 "set".into(),
112 "org.gnome.system.proxy.https".into(),
113 "host".into(),
114 host,
115 ],
116 ));
117 commands.push(Command::new(
118 "gsettings",
119 vec![
120 "set".into(),
121 "org.gnome.system.proxy.https".into(),
122 "port".into(),
123 port.to_string(),
124 ],
125 ));
126 }
127 }
128
129 if let Some(socks) = socks_proxy {
130 if let Some((host, port)) = parse_proxy_address(socks) {
131 commands.push(Command::new(
132 "gsettings",
133 vec![
134 "set".into(),
135 "org.gnome.system.proxy.socks".into(),
136 "host".into(),
137 host,
138 ],
139 ));
140 commands.push(Command::new(
141 "gsettings",
142 vec![
143 "set".into(),
144 "org.gnome.system.proxy.socks".into(),
145 "port".into(),
146 port.to_string(),
147 ],
148 ));
149 }
150 }
151
152 if let Some(no_proxy) = no_proxy {
153 let gsettings_list: Vec<String> = no_proxy
154 .split(',')
155 .map(|s| format!("'{}'", s.trim().replace('\'', "\\'")))
159 .collect();
160 let value = format!("[{}]", gsettings_list.join(", "));
161 commands.push(Command::new(
162 "gsettings",
163 vec![
164 "set".into(),
165 "org.gnome.system.proxy".into(),
166 "ignore-hosts".into(),
167 value,
168 ],
169 ));
170 }
171
172 commands
173}
174
175pub fn generate_gnome_disable_commands() -> Vec<Command> {
177 vec![Command::new(
178 "gsettings",
179 vec![
180 "set".into(),
181 "org.gnome.system.proxy".into(),
182 "mode".into(),
183 "none".into(),
184 ],
185 )]
186}
187
188fn get_gsettings_value(
189 runner: &dyn CommandRunner,
190 schema: &str,
191 key: &str,
192) -> Result<Option<String>, String> {
193 let output = runner
194 .run("gsettings", &["get", schema, key])
195 .map_err(|e| format!("failed to run gsettings: {e}"))?;
196
197 if !output.status.success() {
198 return Ok(None);
199 }
200
201 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
202 if stdout.is_empty() || stdout == "undefined" {
203 return Ok(None);
204 }
205
206 let value = if stdout.starts_with('\'') && stdout.ends_with('\'') {
208 stdout[1..stdout.len() - 1].to_string()
209 } else {
210 stdout
211 };
212
213 Ok(Some(value))
214}
215
216fn format_proxy_address(host: &Option<String>, port: &Option<String>) -> Option<String> {
217 match (host, port) {
218 (Some(h), Some(p)) if !h.is_empty() => Some(format!("{h}:{p}")),
219 (Some(h), _) if !h.is_empty() => Some(h.clone()),
220 _ => None,
221 }
222}
223
224fn parse_proxy_address(addr: &str) -> Option<(String, u16)> {
225 let addr = addr
226 .strip_prefix("http://")
227 .or_else(|| addr.strip_prefix("https://"))
228 .or_else(|| addr.strip_prefix("socks://"))
229 .or_else(|| addr.strip_prefix("socks5://"))
230 .unwrap_or(addr);
231
232 if let Some(addr) = addr.strip_prefix('[') {
233 let end = addr.find(']')?;
234 let host = &addr[..end];
235 let port = addr.get(end + 1..)?.strip_prefix(':')?.parse().ok()?;
236 return Some((host.to_string(), port));
237 }
238
239 if let Some(pos) = addr.rfind(':') {
240 let host = addr[..pos].to_string();
241 let port_str = &addr[pos + 1..];
242 if !host.contains(':') {
243 if let Ok(port) = port_str.parse::<u16>() {
244 return Some((host, port));
245 }
246 }
247 }
248 None
249}
250
251fn parse_gsettings_string_array(value: &str) -> String {
252 let value = value.trim();
253 let Some(value) = value.strip_prefix('[').and_then(|v| v.strip_suffix(']')) else {
254 return value.to_string();
255 };
256
257 let mut entries = Vec::new();
258 let mut current = String::new();
259 let mut quoted = false;
260 let mut escaped = false;
261 for ch in value.chars() {
262 if escaped {
263 current.push(ch);
264 escaped = false;
265 } else if ch == '\\' && quoted {
266 escaped = true;
267 } else if ch == '\'' {
268 quoted = !quoted;
269 } else if ch == ',' && !quoted {
270 let entry = current.trim();
271 if !entry.is_empty() {
272 entries.push(entry.to_string());
273 }
274 current.clear();
275 } else {
276 current.push(ch);
277 }
278 }
279 let entry = current.trim();
280 if !entry.is_empty() {
281 entries.push(entry.to_string());
282 }
283 entries.join(",")
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use crate::command_runner::MockCommandRunner;
290
291 fn success_output(stdout: &str) -> std::process::Output {
292 #[cfg(unix)]
293 {
294 use std::os::unix::process::ExitStatusExt;
295 std::process::Output {
296 status: std::process::ExitStatus::from_raw(0),
297 stdout: stdout.as_bytes().to_vec(),
298 stderr: Vec::new(),
299 }
300 }
301 #[cfg(not(unix))]
302 {
303 std::process::Output {
304 status: std::process::ExitStatus::default(),
305 stdout: stdout.as_bytes().to_vec(),
306 stderr: Vec::new(),
307 }
308 }
309 }
310
311 #[test]
312 fn inspect_gnome_manual_mode() {
313 let runner = MockCommandRunner::new()
314 .add_response(
315 "gsettings",
316 vec![
317 "get".to_string(),
318 "org.gnome.system.proxy".to_string(),
319 "mode".to_string(),
320 ],
321 Ok(success_output("'manual'\n")),
322 )
323 .add_response(
324 "gsettings",
325 vec![
326 "get".to_string(),
327 "org.gnome.system.proxy.http".to_string(),
328 "host".to_string(),
329 ],
330 Ok(success_output("'proxy.example.com'\n")),
331 )
332 .add_response(
333 "gsettings",
334 vec![
335 "get".to_string(),
336 "org.gnome.system.proxy.http".to_string(),
337 "port".to_string(),
338 ],
339 Ok(success_output("8080\n")),
340 )
341 .add_response(
342 "gsettings",
343 vec![
344 "get".to_string(),
345 "org.gnome.system.proxy.https".to_string(),
346 "host".to_string(),
347 ],
348 Ok(success_output("'proxy.example.com'\n")),
349 )
350 .add_response(
351 "gsettings",
352 vec![
353 "get".to_string(),
354 "org.gnome.system.proxy.https".to_string(),
355 "port".to_string(),
356 ],
357 Ok(success_output("8443\n")),
358 )
359 .add_response(
360 "gsettings",
361 vec![
362 "get".to_string(),
363 "org.gnome.system.proxy.socks".to_string(),
364 "host".to_string(),
365 ],
366 Ok(success_output("''\n")),
367 )
368 .add_response(
369 "gsettings",
370 vec![
371 "get".to_string(),
372 "org.gnome.system.proxy.socks".to_string(),
373 "port".to_string(),
374 ],
375 Ok(success_output("0\n")),
376 )
377 .add_response(
378 "gsettings",
379 vec![
380 "get".to_string(),
381 "org.gnome.system.proxy".to_string(),
382 "ignore-hosts".to_string(),
383 ],
384 Ok(success_output("['localhost', '127.0.0.1']\n")),
385 );
386
387 let settings = inspect_gnome_proxy(&runner).unwrap();
388 assert_eq!(
389 settings.http_proxy.as_deref(),
390 Some("proxy.example.com:8080")
391 );
392 assert_eq!(
393 settings.https_proxy.as_deref(),
394 Some("proxy.example.com:8443")
395 );
396 assert_eq!(settings.socks_proxy, None);
397 assert!(settings.no_proxy.unwrap().contains("localhost"));
398 }
399
400 #[test]
401 fn generate_gnome_apply_commands_structure() {
402 let commands = generate_gnome_apply_commands(
403 Some("proxy:8080"),
404 Some("proxy:8443"),
405 None,
406 Some("localhost"),
407 );
408 assert!(commands.iter().any(|c| {
409 let s = c.to_string();
410 s.contains("mode") && s.contains("manual")
411 }));
412 assert!(commands.iter().any(|c| c.to_string().contains("http host")));
413 assert!(commands
414 .iter()
415 .any(|c| c.to_string().contains("https host")));
416 assert!(commands
417 .iter()
418 .any(|c| c.to_string().contains("ignore-hosts")));
419 }
420
421 #[test]
422 fn generate_gnome_disable_commands_works() {
423 let commands = generate_gnome_disable_commands();
424 assert_eq!(commands.len(), 1);
425 assert!(commands[0].to_string().contains("none"));
426 }
427
428 #[test]
429 fn parse_proxy_address_with_scheme() {
430 assert_eq!(
431 parse_proxy_address("http://proxy:8080"),
432 Some(("proxy".to_string(), 8080))
433 );
434 }
435
436 #[test]
437 fn parse_proxy_address_without_scheme() {
438 assert_eq!(
439 parse_proxy_address("proxy:8080"),
440 Some(("proxy".to_string(), 8080))
441 );
442 }
443
444 #[test]
445 fn parse_proxy_address_ipv6() {
446 assert_eq!(
447 parse_proxy_address("http://[::1]:8080"),
448 Some(("::1".to_string(), 8080))
449 );
450 }
451
452 #[test]
453 fn parse_gsettings_single_item_array() {
454 assert_eq!(parse_gsettings_string_array("['localhost']"), "localhost");
455 }
456
457 #[test]
458 fn parse_gsettings_array_preserves_commas_inside_values() {
459 assert_eq!(
460 parse_gsettings_string_array("['localhost', 'foo,bar']"),
461 "localhost,foo,bar"
462 );
463 }
464}