use crate::apply::Command;
use crate::command_runner::CommandRunner;
use crate::inspection::SystemProxySettings;
pub fn inspect_gnome_proxy(runner: &dyn CommandRunner) -> Result<SystemProxySettings, String> {
let mut raw = std::collections::HashMap::new();
let mode = get_gsettings_value(runner, "org.gnome.system.proxy", "mode")?;
raw.insert("mode".to_string(), mode.clone().unwrap_or_default());
if mode.as_deref() == Some("none") || mode.is_none() {
return Ok(SystemProxySettings {
source: "linux:gnome:gsettings".to_string(),
http_proxy: None,
https_proxy: None,
socks_proxy: None,
no_proxy: None,
raw,
});
}
let http_host = get_gsettings_value(runner, "org.gnome.system.proxy.http", "host")?;
let http_port = get_gsettings_value(runner, "org.gnome.system.proxy.http", "port")?;
let https_host = get_gsettings_value(runner, "org.gnome.system.proxy.https", "host")?;
let https_port = get_gsettings_value(runner, "org.gnome.system.proxy.https", "port")?;
let socks_host = get_gsettings_value(runner, "org.gnome.system.proxy.socks", "host")?;
let socks_port = get_gsettings_value(runner, "org.gnome.system.proxy.socks", "port")?;
let ignore_hosts = get_gsettings_value(runner, "org.gnome.system.proxy", "ignore-hosts")?;
let http_proxy = format_proxy_address(&http_host, &http_port);
let https_proxy = format_proxy_address(&https_host, &https_port);
let socks_proxy = format_proxy_address(&socks_host, &socks_port);
if let Some(ref v) = http_proxy {
raw.insert("http_proxy".to_string(), v.clone());
}
if let Some(ref v) = https_proxy {
raw.insert("https_proxy".to_string(), v.clone());
}
if let Some(ref v) = socks_proxy {
raw.insert("socks_proxy".to_string(), v.clone());
}
let no_proxy = ignore_hosts.map(|h| parse_gsettings_string_array(&h));
if let Some(ref v) = no_proxy {
raw.insert("no_proxy".to_string(), v.clone());
}
Ok(SystemProxySettings {
source: "linux:gnome:gsettings".to_string(),
http_proxy,
https_proxy,
socks_proxy,
no_proxy,
raw,
})
}
pub fn generate_gnome_apply_commands(
http_proxy: Option<&str>,
https_proxy: Option<&str>,
socks_proxy: Option<&str>,
no_proxy: Option<&str>,
) -> Vec<Command> {
let mut commands = Vec::new();
let has_any = http_proxy.is_some() || https_proxy.is_some() || socks_proxy.is_some();
if has_any {
commands.push(Command::new(
"gsettings",
vec![
"set".into(),
"org.gnome.system.proxy".into(),
"mode".into(),
"manual".into(),
],
));
}
if let Some(http) = http_proxy {
if let Some((host, port)) = parse_proxy_address(http) {
commands.push(Command::new(
"gsettings",
vec![
"set".into(),
"org.gnome.system.proxy.http".into(),
"host".into(),
host,
],
));
commands.push(Command::new(
"gsettings",
vec![
"set".into(),
"org.gnome.system.proxy.http".into(),
"port".into(),
port.to_string(),
],
));
}
}
if let Some(https) = https_proxy {
if let Some((host, port)) = parse_proxy_address(https) {
commands.push(Command::new(
"gsettings",
vec![
"set".into(),
"org.gnome.system.proxy.https".into(),
"host".into(),
host,
],
));
commands.push(Command::new(
"gsettings",
vec![
"set".into(),
"org.gnome.system.proxy.https".into(),
"port".into(),
port.to_string(),
],
));
}
}
if let Some(socks) = socks_proxy {
if let Some((host, port)) = parse_proxy_address(socks) {
commands.push(Command::new(
"gsettings",
vec![
"set".into(),
"org.gnome.system.proxy.socks".into(),
"host".into(),
host,
],
));
commands.push(Command::new(
"gsettings",
vec![
"set".into(),
"org.gnome.system.proxy.socks".into(),
"port".into(),
port.to_string(),
],
));
}
}
if let Some(no_proxy) = no_proxy {
let gsettings_list: Vec<String> = no_proxy
.split(',')
.map(|s| format!("'{}'", s.trim().replace('\'', "\\'")))
.collect();
let value = format!("[{}]", gsettings_list.join(", "));
commands.push(Command::new(
"gsettings",
vec![
"set".into(),
"org.gnome.system.proxy".into(),
"ignore-hosts".into(),
value,
],
));
}
commands
}
pub fn generate_gnome_disable_commands() -> Vec<Command> {
vec![Command::new(
"gsettings",
vec![
"set".into(),
"org.gnome.system.proxy".into(),
"mode".into(),
"none".into(),
],
)]
}
fn get_gsettings_value(
runner: &dyn CommandRunner,
schema: &str,
key: &str,
) -> Result<Option<String>, String> {
let output = runner
.run("gsettings", &["get", schema, key])
.map_err(|e| format!("failed to run gsettings: {e}"))?;
if !output.status.success() {
return Ok(None);
}
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
if stdout.is_empty() || stdout == "undefined" {
return Ok(None);
}
let value = if stdout.starts_with('\'') && stdout.ends_with('\'') {
stdout[1..stdout.len() - 1].to_string()
} else {
stdout
};
Ok(Some(value))
}
fn format_proxy_address(host: &Option<String>, port: &Option<String>) -> Option<String> {
match (host, port) {
(Some(h), Some(p)) if !h.is_empty() => Some(format!("{h}:{p}")),
(Some(h), _) if !h.is_empty() => Some(h.clone()),
_ => None,
}
}
fn parse_proxy_address(addr: &str) -> Option<(String, u16)> {
let addr = addr
.strip_prefix("http://")
.or_else(|| addr.strip_prefix("https://"))
.or_else(|| addr.strip_prefix("socks://"))
.or_else(|| addr.strip_prefix("socks5://"))
.unwrap_or(addr);
if let Some(addr) = addr.strip_prefix('[') {
let end = addr.find(']')?;
let host = &addr[..end];
let port = addr.get(end + 1..)?.strip_prefix(':')?.parse().ok()?;
return Some((host.to_string(), port));
}
if let Some(pos) = addr.rfind(':') {
let host = addr[..pos].to_string();
let port_str = &addr[pos + 1..];
if !host.contains(':') {
if let Ok(port) = port_str.parse::<u16>() {
return Some((host, port));
}
}
}
None
}
fn parse_gsettings_string_array(value: &str) -> String {
let value = value.trim();
let Some(value) = value.strip_prefix('[').and_then(|v| v.strip_suffix(']')) else {
return value.to_string();
};
let mut entries = Vec::new();
let mut current = String::new();
let mut quoted = false;
let mut escaped = false;
for ch in value.chars() {
if escaped {
current.push(ch);
escaped = false;
} else if ch == '\\' && quoted {
escaped = true;
} else if ch == '\'' {
quoted = !quoted;
} else if ch == ',' && !quoted {
let entry = current.trim();
if !entry.is_empty() {
entries.push(entry.to_string());
}
current.clear();
} else {
current.push(ch);
}
}
let entry = current.trim();
if !entry.is_empty() {
entries.push(entry.to_string());
}
entries.join(",")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::command_runner::MockCommandRunner;
fn success_output(stdout: &str) -> std::process::Output {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
std::process::Output {
status: std::process::ExitStatus::from_raw(0),
stdout: stdout.as_bytes().to_vec(),
stderr: Vec::new(),
}
}
#[cfg(not(unix))]
{
std::process::Output {
status: std::process::ExitStatus::default(),
stdout: stdout.as_bytes().to_vec(),
stderr: Vec::new(),
}
}
}
#[test]
fn inspect_gnome_manual_mode() {
let runner = MockCommandRunner::new()
.add_response(
"gsettings",
vec![
"get".to_string(),
"org.gnome.system.proxy".to_string(),
"mode".to_string(),
],
Ok(success_output("'manual'\n")),
)
.add_response(
"gsettings",
vec![
"get".to_string(),
"org.gnome.system.proxy.http".to_string(),
"host".to_string(),
],
Ok(success_output("'proxy.example.com'\n")),
)
.add_response(
"gsettings",
vec![
"get".to_string(),
"org.gnome.system.proxy.http".to_string(),
"port".to_string(),
],
Ok(success_output("8080\n")),
)
.add_response(
"gsettings",
vec![
"get".to_string(),
"org.gnome.system.proxy.https".to_string(),
"host".to_string(),
],
Ok(success_output("'proxy.example.com'\n")),
)
.add_response(
"gsettings",
vec![
"get".to_string(),
"org.gnome.system.proxy.https".to_string(),
"port".to_string(),
],
Ok(success_output("8443\n")),
)
.add_response(
"gsettings",
vec![
"get".to_string(),
"org.gnome.system.proxy.socks".to_string(),
"host".to_string(),
],
Ok(success_output("''\n")),
)
.add_response(
"gsettings",
vec![
"get".to_string(),
"org.gnome.system.proxy.socks".to_string(),
"port".to_string(),
],
Ok(success_output("0\n")),
)
.add_response(
"gsettings",
vec![
"get".to_string(),
"org.gnome.system.proxy".to_string(),
"ignore-hosts".to_string(),
],
Ok(success_output("['localhost', '127.0.0.1']\n")),
);
let settings = inspect_gnome_proxy(&runner).unwrap();
assert_eq!(
settings.http_proxy.as_deref(),
Some("proxy.example.com:8080")
);
assert_eq!(
settings.https_proxy.as_deref(),
Some("proxy.example.com:8443")
);
assert_eq!(settings.socks_proxy, None);
assert!(settings.no_proxy.unwrap().contains("localhost"));
}
#[test]
fn generate_gnome_apply_commands_structure() {
let commands = generate_gnome_apply_commands(
Some("proxy:8080"),
Some("proxy:8443"),
None,
Some("localhost"),
);
assert!(commands.iter().any(|c| {
let s = c.to_string();
s.contains("mode") && s.contains("manual")
}));
assert!(commands.iter().any(|c| c.to_string().contains("http host")));
assert!(commands
.iter()
.any(|c| c.to_string().contains("https host")));
assert!(commands
.iter()
.any(|c| c.to_string().contains("ignore-hosts")));
}
#[test]
fn generate_gnome_disable_commands_works() {
let commands = generate_gnome_disable_commands();
assert_eq!(commands.len(), 1);
assert!(commands[0].to_string().contains("none"));
}
#[test]
fn parse_proxy_address_with_scheme() {
assert_eq!(
parse_proxy_address("http://proxy:8080"),
Some(("proxy".to_string(), 8080))
);
}
#[test]
fn parse_proxy_address_without_scheme() {
assert_eq!(
parse_proxy_address("proxy:8080"),
Some(("proxy".to_string(), 8080))
);
}
#[test]
fn parse_proxy_address_ipv6() {
assert_eq!(
parse_proxy_address("http://[::1]:8080"),
Some(("::1".to_string(), 8080))
);
}
#[test]
fn parse_gsettings_single_item_array() {
assert_eq!(parse_gsettings_string_array("['localhost']"), "localhost");
}
#[test]
fn parse_gsettings_array_preserves_commas_inside_values() {
assert_eq!(
parse_gsettings_string_array("['localhost', 'foo,bar']"),
"localhost,foo,bar"
);
}
}