Skip to main content

eggress_system_proxy/
inspection.rs

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/// System proxy settings read from the platform.
14#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
15pub struct SystemProxySettings {
16    /// Source description (e.g., "environment", "macos:networksetup:*Wi-Fi").
17    pub source: String,
18    /// HTTP proxy address (e.g., "http://proxy:8080").
19    pub http_proxy: Option<String>,
20    /// HTTPS proxy address.
21    pub https_proxy: Option<String>,
22    /// SOCKS proxy address.
23    pub socks_proxy: Option<String>,
24    /// No-proxy/bypass list.
25    pub no_proxy: Option<String>,
26    /// Raw key-value pairs from the source.
27    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/// Full inspection result including capabilities and settings.
50#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
51pub struct InspectionResult {
52    /// Detected platform.
53    pub platform: String,
54    /// Capability reports for all system proxy backends.
55    pub capabilities: Vec<SystemProxyCapabilityReport>,
56    /// Current proxy settings (if readable).
57    pub settings: Option<SystemProxySettings>,
58    /// Redacted version of settings (safe for logging).
59    pub redacted_settings: Option<SystemProxySettings>,
60    /// Whether apply/revert is supported on this platform.
61    pub apply_supported: bool,
62    /// Commands that would be used for apply (dry-run).
63    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
88/// Detect the current platform name.
89pub 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
101/// Inspect system proxy settings using the best available backend.
102///
103/// This is the main entry point for read-only inspection. It probes
104/// capabilities and uses the first available backend.
105pub fn inspect_system_proxy() -> InspectionResult {
106    inspect_system_proxy_with_runner(&RealCommandRunner)
107}
108
109/// Inspect system proxy settings with a custom command runner (for testing).
110pub 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}