eggress_system_proxy/backends/
macos.rs1use crate::apply::Command;
2use crate::command_runner::CommandRunner;
3use crate::inspection::SystemProxySettings;
4
5pub fn list_network_services(runner: &dyn CommandRunner) -> Result<Vec<String>, String> {
7 let output = runner
8 .run("networksetup", &["-listallnetworkservices"])
9 .map_err(|e| format!("failed to run networksetup: {e}"))?;
10
11 if !output.status.success() {
12 return Err(String::from_utf8_lossy(&output.stderr).to_string());
13 }
14
15 let stdout = String::from_utf8_lossy(&output.stdout);
16 Ok(stdout
17 .lines()
18 .skip(1) .map(|s| s.trim().to_string())
20 .filter(|s| !s.is_empty())
21 .collect())
22}
23
24pub fn inspect_macos_proxy(
26 runner: &dyn CommandRunner,
27 service: &str,
28) -> Result<SystemProxySettings, String> {
29 let mut raw = std::collections::HashMap::new();
30
31 let http_proxy = get_macos_proxy_field(runner, service, "-getwebproxy", "Server")?;
32 let https_proxy = get_macos_proxy_field(runner, service, "-getsecurewebproxy", "Server")?;
33 let socks_proxy = get_macos_proxy_field(runner, service, "-getsocksfirewallproxy", "Server")?;
34 let no_proxy = get_macos_proxy_field(runner, service, "-getwebproxy", "BypassDomains")?;
35
36 if let Some(ref v) = http_proxy {
37 raw.insert("http_proxy".to_string(), v.clone());
38 }
39 if let Some(ref v) = https_proxy {
40 raw.insert("https_proxy".to_string(), v.clone());
41 }
42 if let Some(ref v) = socks_proxy {
43 raw.insert("socks_proxy".to_string(), v.clone());
44 }
45 if let Some(ref v) = no_proxy {
46 raw.insert("no_proxy".to_string(), v.clone());
47 }
48
49 Ok(SystemProxySettings {
50 source: format!("macos:networksetup:{service}"),
51 http_proxy,
52 https_proxy,
53 socks_proxy,
54 no_proxy,
55 raw,
56 })
57}
58
59fn get_macos_proxy_field(
60 runner: &dyn CommandRunner,
61 service: &str,
62 flag: &str,
63 field: &str,
64) -> Result<Option<String>, String> {
65 let output = runner
66 .run("networksetup", &[flag, service])
67 .map_err(|e| format!("failed to run networksetup {flag}: {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 let Some(pos) = line.find(':') {
76 let key = line[..pos].trim();
77 let value = line[pos + 1..].trim();
78 if key == field && !value.is_empty() && value != "Off" {
79 return Ok(Some(value.to_string()));
80 }
81 }
82 }
83 Ok(None)
84}
85
86pub fn generate_macos_apply_commands(
88 service: &str,
89 http_proxy: Option<&str>,
90 https_proxy: Option<&str>,
91 socks_proxy: Option<&str>,
92 no_proxy: Option<&str>,
93) -> Vec<Command> {
94 let mut commands = Vec::new();
95 if let Some(http) = http_proxy {
96 commands.push(Command::new(
97 "networksetup",
98 vec!["-setwebproxy".into(), service.into(), "on".into()],
99 ));
100 commands.push(Command::new(
101 "networksetup",
102 vec!["-setwebproxyservers".into(), service.into(), http.into()],
103 ));
104 }
105 if let Some(https) = https_proxy {
106 commands.push(Command::new(
107 "networksetup",
108 vec!["-setsecurewebproxy".into(), service.into(), "on".into()],
109 ));
110 commands.push(Command::new(
111 "networksetup",
112 vec![
113 "-setsecurewebproxyservers".into(),
114 service.into(),
115 https.into(),
116 ],
117 ));
118 }
119 if let Some(socks) = socks_proxy {
120 commands.push(Command::new(
121 "networksetup",
122 vec!["-setsocksfirewallproxy".into(), service.into(), "on".into()],
123 ));
124 commands.push(Command::new(
125 "networksetup",
126 vec![
127 "-setsocksfirewallproxyserver".into(),
128 service.into(),
129 socks.into(),
130 ],
131 ));
132 }
133 if let Some(no_proxy) = no_proxy {
134 commands.push(Command::new(
135 "networksetup",
136 vec![
137 "-setwebproxybypassdomains".into(),
138 service.into(),
139 no_proxy.into(),
140 ],
141 ));
142 }
143 commands
144}
145
146pub fn generate_macos_disable_commands(service: &str) -> Vec<Command> {
148 vec![
149 Command::new(
150 "networksetup",
151 vec!["-setwebproxy".into(), service.into(), "off".into()],
152 ),
153 Command::new(
154 "networksetup",
155 vec!["-setsecurewebproxy".into(), service.into(), "off".into()],
156 ),
157 Command::new(
158 "networksetup",
159 vec![
160 "-setsocksfirewallproxy".into(),
161 service.into(),
162 "off".into(),
163 ],
164 ),
165 ]
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::command_runner::MockCommandRunner;
172
173 fn success_output(stdout: &str) -> std::process::Output {
174 #[cfg(unix)]
175 {
176 use std::os::unix::process::ExitStatusExt;
177 std::process::Output {
178 status: std::process::ExitStatus::from_raw(0),
179 stdout: stdout.as_bytes().to_vec(),
180 stderr: Vec::new(),
181 }
182 }
183 #[cfg(not(unix))]
184 {
185 std::process::Output {
186 status: std::process::ExitStatus::default(),
187 stdout: stdout.as_bytes().to_vec(),
188 stderr: Vec::new(),
189 }
190 }
191 }
192
193 #[test]
194 fn list_services_parses_output() {
195 let runner = MockCommandRunner::new().add_response(
196 "networksetup",
197 vec!["-listallnetworkservices".to_string()],
198 Ok(success_output("An asterisk (*) denotes that a network service is disabled.\n*Ethernet\n*Wi-Fi\nUSB 10/100/1000 LAN\n")),
199 );
200
201 let services = list_network_services(&runner).unwrap();
202 assert_eq!(services.len(), 3);
203 assert!(services.contains(&"*Wi-Fi".to_string()));
204 }
205
206 #[test]
207 fn inspect_proxy_parses_web_proxy() {
208 let runner = MockCommandRunner::new()
209 .add_response(
210 "networksetup",
211 vec!["-getwebproxy".to_string(), "*Wi-Fi".to_string()],
212 Ok(success_output(
213 "Enabled: Yes\nServer: proxy.example.com:8080\nBypassDomains: localhost\n",
214 )),
215 )
216 .add_response(
217 "networksetup",
218 vec!["-getsecurewebproxy".to_string(), "*Wi-Fi".to_string()],
219 Ok(success_output(
220 "Enabled: Yes\nServer: proxy.example.com:8443\nBypassDomains: localhost\n",
221 )),
222 )
223 .add_response(
224 "networksetup",
225 vec!["-getsocksfirewallproxy".to_string(), "*Wi-Fi".to_string()],
226 Ok(success_output("Enabled: No\nServer: \nPort: 0\n")),
227 );
228
229 let settings = inspect_macos_proxy(&runner, "*Wi-Fi").unwrap();
230 assert_eq!(
231 settings.http_proxy.as_deref(),
232 Some("proxy.example.com:8080")
233 );
234 assert_eq!(
235 settings.https_proxy.as_deref(),
236 Some("proxy.example.com:8443")
237 );
238 assert_eq!(settings.socks_proxy, None);
239 assert!(settings.source.contains("*Wi-Fi"));
240 }
241
242 #[test]
243 fn generate_apply_commands() {
244 let commands = generate_macos_apply_commands(
245 "*Wi-Fi",
246 Some("proxy:8080"),
247 Some("proxy:8443"),
248 Some("socks:1080"),
249 Some("localhost,127.0.0.1"),
250 );
251 assert!(commands
252 .iter()
253 .any(|c| c.to_string().contains("-setwebproxy")));
254 assert!(commands
255 .iter()
256 .any(|c| c.to_string().contains("-setsecurewebproxy")));
257 assert!(commands
258 .iter()
259 .any(|c| c.to_string().contains("-setsocksfirewallproxy")));
260 assert!(commands
261 .iter()
262 .any(|c| c.to_string().contains("-setwebproxybypassdomains")));
263 }
264
265 #[test]
266 fn generate_disable_commands() {
267 let commands = generate_macos_disable_commands("*Wi-Fi");
268 assert_eq!(commands.len(), 3);
269 assert!(commands.iter().all(|c| c.to_string().contains("off")));
270 }
271}