1use std::collections::HashMap;
2use std::fmt;
3
4use crate::apply::Command;
5use crate::backends;
6use crate::capability::{
7 system_proxy_platform_info, SystemProxyCapability, SystemProxyCapabilityReport,
8 SystemProxyStatus,
9};
10use crate::command_runner::{CommandRunner, RealCommandRunner};
11use crate::redaction::redact_proxy_settings;
12
13#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
15pub struct SystemProxySettings {
16 pub source: String,
18 pub http_proxy: Option<String>,
20 pub https_proxy: Option<String>,
22 pub socks_proxy: Option<String>,
24 pub no_proxy: Option<String>,
26 pub raw: HashMap<String, String>,
28}
29
30impl fmt::Display for SystemProxySettings {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 write!(f, "System proxy settings (source: {})", self.source)?;
33 if let Some(ref http) = self.http_proxy {
34 write!(f, "\n HTTP proxy: {http}")?;
35 }
36 if let Some(ref https) = self.https_proxy {
37 write!(f, "\n HTTPS proxy: {https}")?;
38 }
39 if let Some(ref socks) = self.socks_proxy {
40 write!(f, "\n SOCKS proxy: {socks}")?;
41 }
42 if let Some(ref no_proxy) = self.no_proxy {
43 write!(f, "\n No proxy: {no_proxy}")?;
44 }
45 Ok(())
46 }
47}
48
49#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
51pub struct InspectionResult {
52 pub platform: String,
54 pub capabilities: Vec<SystemProxyCapabilityReport>,
56 pub settings: Option<SystemProxySettings>,
58 pub redacted_settings: Option<SystemProxySettings>,
60 pub apply_supported: bool,
62 pub dry_run_commands: Vec<Command>,
64}
65
66impl fmt::Display for InspectionResult {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 writeln!(f, "Platform: {}", self.platform)?;
69 writeln!(f, "\nCapabilities:")?;
70 for cap in &self.capabilities {
71 writeln!(f, " {cap}")?;
72 }
73 if let Some(ref settings) = self.settings {
74 writeln!(f, "\nSettings:")?;
75 writeln!(f, "{settings}")?;
76 }
77 writeln!(f, "\nApply supported: {}", self.apply_supported)?;
78 if !self.dry_run_commands.is_empty() {
79 writeln!(f, "\nDry-run commands:")?;
80 for cmd in &self.dry_run_commands {
81 writeln!(f, " {cmd}")?;
82 }
83 }
84 Ok(())
85 }
86}
87
88pub fn detect_platform() -> String {
90 if cfg!(target_os = "macos") {
91 "macos".to_string()
92 } else if cfg!(target_os = "linux") {
93 "linux".to_string()
94 } else if cfg!(target_os = "windows") {
95 "windows".to_string()
96 } else {
97 "unknown".to_string()
98 }
99}
100
101pub fn inspect_system_proxy() -> InspectionResult {
106 inspect_system_proxy_with_runner(&RealCommandRunner)
107}
108
109pub fn inspect_system_proxy_with_runner(runner: &dyn CommandRunner) -> InspectionResult {
111 let platform = detect_platform();
112 let capabilities = system_proxy_platform_info();
113
114 let settings = match platform.as_str() {
115 "macos" => inspect_macos_best(runner, &capabilities),
116 "windows" => inspect_windows_best(runner, &capabilities),
117 "linux" => inspect_linux_best(runner, &capabilities),
118 _ => inspect_env_only(runner),
119 };
120
121 let redacted_settings = settings.as_ref().map(|s| SystemProxySettings {
122 source: s.source.clone(),
123 http_proxy: s.http_proxy.as_ref().map(|v| redact_proxy_value(v)),
124 https_proxy: s.https_proxy.as_ref().map(|v| redact_proxy_value(v)),
125 socks_proxy: s.socks_proxy.as_ref().map(|v| redact_proxy_value(v)),
126 no_proxy: s.no_proxy.clone(),
127 raw: redact_proxy_settings(&s.raw),
128 });
129
130 let apply_supported = capabilities.iter().any(|c| {
131 matches!(c.status, SystemProxyStatus::Available)
132 && matches!(
133 c.capability,
134 SystemProxyCapability::ApplyMacosNetworksetup
135 | SystemProxyCapability::ApplyWindowsInternetSettings
136 | SystemProxyCapability::ApplyGnomeSettings
137 | SystemProxyCapability::ApplyKdeSettings
138 )
139 });
140
141 let dry_run_commands = generate_dry_run_commands(&platform, settings.as_ref());
142
143 InspectionResult {
144 platform,
145 capabilities,
146 settings,
147 redacted_settings,
148 apply_supported,
149 dry_run_commands,
150 }
151}
152
153fn inspect_macos_best(
154 runner: &dyn CommandRunner,
155 capabilities: &[SystemProxyCapabilityReport],
156) -> Option<SystemProxySettings> {
157 let has_networksetup = capabilities.iter().any(|c| {
158 c.capability == SystemProxyCapability::InspectMacosNetworksetup
159 && c.status == SystemProxyStatus::Available
160 });
161
162 if has_networksetup {
163 if let Ok(services) = backends::macos::list_network_services(runner) {
164 if let Some(service) = services.first() {
165 if let Ok(settings) = backends::macos::inspect_macos_proxy(runner, service) {
166 return Some(settings);
167 }
168 }
169 }
170 }
171
172 Some(backends::env::inspect_environment(runner))
173}
174
175fn inspect_windows_best(
176 runner: &dyn CommandRunner,
177 capabilities: &[SystemProxyCapabilityReport],
178) -> Option<SystemProxySettings> {
179 let has_registry = capabilities.iter().any(|c| {
180 c.capability == SystemProxyCapability::InspectWindowsInternetSettings
181 && c.status == SystemProxyStatus::Available
182 });
183
184 if has_registry {
185 if let Ok(settings) = backends::windows::inspect_windows_proxy(runner) {
186 return Some(settings);
187 }
188 }
189
190 Some(backends::env::inspect_environment(runner))
191}
192
193fn inspect_linux_best(
194 runner: &dyn CommandRunner,
195 capabilities: &[SystemProxyCapabilityReport],
196) -> Option<SystemProxySettings> {
197 let has_gnome = capabilities.iter().any(|c| {
198 c.capability == SystemProxyCapability::InspectGnomeSettings
199 && c.status == SystemProxyStatus::Available
200 });
201
202 if has_gnome {
203 if let Ok(settings) = backends::linux::inspect_gnome_proxy(runner) {
204 return Some(settings);
205 }
206 }
207
208 Some(backends::env::inspect_environment(runner))
209}
210
211fn inspect_env_only(runner: &dyn CommandRunner) -> Option<SystemProxySettings> {
212 Some(backends::env::inspect_environment(runner))
213}
214
215fn generate_dry_run_commands(
216 platform: &str,
217 settings: Option<&SystemProxySettings>,
218) -> Vec<Command> {
219 let settings = match settings {
220 Some(s) => s,
221 None => return Vec::new(),
222 };
223
224 match platform {
225 "macos" => {
226 let service = settings.source.split(':').next_back().unwrap_or("*Wi-Fi");
227 backends::macos::generate_macos_apply_commands(
228 service,
229 settings.http_proxy.as_deref(),
230 settings.https_proxy.as_deref(),
231 settings.socks_proxy.as_deref(),
232 settings.no_proxy.as_deref(),
233 )
234 }
235 "windows" => backends::windows::generate_windows_apply_commands(
236 settings.http_proxy.as_deref(),
237 settings.https_proxy.as_deref(),
238 settings.socks_proxy.as_deref(),
239 settings.no_proxy.as_deref(),
240 ),
241 "linux" => backends::linux::generate_gnome_apply_commands(
242 settings.http_proxy.as_deref(),
243 settings.https_proxy.as_deref(),
244 settings.socks_proxy.as_deref(),
245 settings.no_proxy.as_deref(),
246 ),
247 _ => Vec::new(),
248 }
249}
250
251fn redact_proxy_value(value: &str) -> String {
252 crate::redaction::redact_proxy_uri(value)
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::command_runner::MockCommandRunner;
259
260 #[test]
261 fn detect_platform_returns_known_value() {
262 let platform = detect_platform();
263 assert!(["macos", "linux", "windows", "unknown"].contains(&platform.as_str()));
264 }
265
266 #[test]
267 fn inspection_result_serializes() {
268 let result = InspectionResult {
269 platform: "test".to_string(),
270 capabilities: Vec::new(),
271 settings: None,
272 redacted_settings: None,
273 apply_supported: false,
274 dry_run_commands: Vec::new(),
275 };
276 let json = serde_json::to_string(&result).unwrap();
277 assert!(json.contains("\"platform\":\"test\""));
278 }
279
280 #[test]
281 fn settings_display_format() {
282 let settings = SystemProxySettings {
283 source: "test".to_string(),
284 http_proxy: Some("http://proxy:8080".to_string()),
285 https_proxy: None,
286 socks_proxy: None,
287 no_proxy: Some("localhost".to_string()),
288 raw: std::collections::HashMap::new(),
289 };
290 let display = settings.to_string();
291 assert!(display.contains("HTTP proxy: http://proxy:8080"));
292 assert!(display.contains("No proxy: localhost"));
293 }
294
295 #[test]
296 fn inspection_with_mock_runner() {
297 let runner = MockCommandRunner::new();
298 let result = inspect_system_proxy_with_runner(&runner);
299 assert!(!result.platform.is_empty());
300 assert!(!result.capabilities.is_empty());
301 }
302}