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| {
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
65pub 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
176pub 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 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}