|
| 1 | +use super::no_proxy::NoProxy; |
| 2 | +use super::utils::{get_first_env, parse_env_uri}; |
| 3 | +use super::Matcher; |
| 4 | + |
| 5 | +#[derive(Default)] |
| 6 | +pub struct Builder { |
| 7 | + pub(crate) is_cgi: bool, |
| 8 | + pub(crate) all: String, |
| 9 | + pub(crate) http: String, |
| 10 | + pub(crate) https: String, |
| 11 | + pub(crate) no: String, |
| 12 | +} |
| 13 | + |
| 14 | +// ===== impl Builder ===== |
| 15 | +impl Builder { |
| 16 | + pub(crate) fn from_env() -> Self { |
| 17 | + Builder { |
| 18 | + is_cgi: std::env::var_os("REQUEST_METHOD").is_some(), |
| 19 | + all: get_first_env(&["ALL_PROXY", "all_proxy"]), |
| 20 | + http: get_first_env(&["HTTP_PROXY", "http_proxy"]), |
| 21 | + https: get_first_env(&["HTTPS_PROXY", "https_proxy"]), |
| 22 | + no: get_first_env(&["NO_PROXY", "no_proxy"]), |
| 23 | + } |
| 24 | + } |
| 25 | + |
| 26 | + /// Set a proxy for all schemes (ALL_PROXY equivalent). |
| 27 | + pub fn all_proxy(mut self, proxy: impl Into<String>) -> Self { |
| 28 | + self.all = proxy.into(); |
| 29 | + self |
| 30 | + } |
| 31 | + |
| 32 | + /// Set a proxy for HTTP schemes (HTTP_PROXY equivalent). |
| 33 | + pub fn http_proxy(mut self, proxy: impl Into<String>) -> Self { |
| 34 | + self.http = proxy.into(); |
| 35 | + self |
| 36 | + } |
| 37 | + |
| 38 | + /// Set a proxy for HTTPS schemes (HTTPS_PROXY equivalent). |
| 39 | + pub fn https_proxy(mut self, proxy: impl Into<String>) -> Self { |
| 40 | + self.https = proxy.into(); |
| 41 | + self |
| 42 | + } |
| 43 | + |
| 44 | + /// Set no-proxy rules (NO_PROXY equivalent). |
| 45 | + pub fn no_proxy(mut self, no_proxy: impl Into<String>) -> Self { |
| 46 | + self.no = no_proxy.into(); |
| 47 | + self |
| 48 | + } |
| 49 | + |
| 50 | + pub(crate) fn build(self) -> Matcher { |
| 51 | + if self.is_cgi { |
| 52 | + return Matcher { |
| 53 | + http: None, |
| 54 | + https: None, |
| 55 | + no: NoProxy::empty(), |
| 56 | + }; |
| 57 | + } |
| 58 | + |
| 59 | + let all = parse_env_uri(&self.all); |
| 60 | + |
| 61 | + Matcher { |
| 62 | + http: parse_env_uri(&self.http).or_else(|| all.clone()), |
| 63 | + https: parse_env_uri(&self.https).or(all), |
| 64 | + no: NoProxy::from_string(&self.no), |
| 65 | + } |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +// ===== MacOS Builder System Proxies ===== |
| 70 | +#[cfg(feature = "system-proxies")] |
| 71 | +#[cfg(target_os = "macos")] |
| 72 | +mod macos_proxies { |
| 73 | + use super::*; |
| 74 | + |
| 75 | + use system_configuration::core_foundation::array::CFArray; |
| 76 | + use system_configuration::core_foundation::base::{CFType, TCFType, TCFTypeRef}; |
| 77 | + use system_configuration::core_foundation::dictionary::CFDictionary; |
| 78 | + use system_configuration::core_foundation::number::CFNumber; |
| 79 | + use system_configuration::core_foundation::string::{CFString, CFStringRef}; |
| 80 | + use system_configuration::dynamic_store::{SCDynamicStore, SCDynamicStoreBuilder}; |
| 81 | + |
| 82 | + impl Builder { |
| 83 | + // Helper function to check if a proxy is enabled |
| 84 | + fn is_proxy_enabled(&self, prefix: &str, proxies: &CFDictionary<CFString, CFType>) -> bool { |
| 85 | + let key = format!("{}Enable", prefix); |
| 86 | + proxies |
| 87 | + .find(CFString::new(&key)) |
| 88 | + .map(|val| { |
| 89 | + // Try to get the value as i32 directly |
| 90 | + unsafe { |
| 91 | + let num_ref = val.as_concrete_TypeRef(); |
| 92 | + if num_ref.is_null() { |
| 93 | + return false; |
| 94 | + } |
| 95 | + let num = CFNumber::wrap_under_get_rule(num_ref as *const _); |
| 96 | + num.to_i32() == Some(1) |
| 97 | + } |
| 98 | + }) |
| 99 | + .unwrap_or(false) |
| 100 | + } |
| 101 | + // Helper function to get a string value |
| 102 | + fn get_string( |
| 103 | + &self, |
| 104 | + key: &str, |
| 105 | + proxies: &CFDictionary<CFString, CFType>, |
| 106 | + ) -> Option<String> { |
| 107 | + proxies |
| 108 | + .find(CFString::new(key)) |
| 109 | + .map(|val| unsafe { |
| 110 | + let str_ref = val.as_concrete_TypeRef(); |
| 111 | + if str_ref.is_null() { |
| 112 | + return None; |
| 113 | + } |
| 114 | + let cfstr = CFString::wrap_under_get_rule(str_ref as *const _); |
| 115 | + Some(cfstr.to_string()) |
| 116 | + }) |
| 117 | + .flatten() |
| 118 | + } |
| 119 | + // Helper function to get an integer value |
| 120 | + fn get_int(&self, key: &str, proxies: &CFDictionary<CFString, CFType>) -> Option<i32> { |
| 121 | + proxies |
| 122 | + .find(CFString::new(key)) |
| 123 | + .map(|val| unsafe { |
| 124 | + let num_ref = val.as_concrete_TypeRef(); |
| 125 | + if num_ref.is_null() { |
| 126 | + return None; |
| 127 | + } |
| 128 | + let num = CFNumber::wrap_under_get_rule(num_ref as *const _); |
| 129 | + num.to_i32() |
| 130 | + }) |
| 131 | + .flatten() |
| 132 | + } |
| 133 | + |
| 134 | + pub fn from_system_proxy(mut self) -> Self { |
| 135 | + let store = SCDynamicStoreBuilder::new("proxy-fetcher").build(); |
| 136 | + |
| 137 | + if let Some(proxies) = store.get_proxies() { |
| 138 | + let (http, https, no) = self.extract_system_proxy(proxies); |
| 139 | + |
| 140 | + if let Some(http_proxy) = http { |
| 141 | + self.http = http_proxy; |
| 142 | + } |
| 143 | + if let Some(https_proxy) = https { |
| 144 | + self.https = https_proxy; |
| 145 | + } |
| 146 | + if let Some(no_proxy) = no { |
| 147 | + self.no = no_proxy; |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + self |
| 152 | + } |
| 153 | + pub(crate) fn extract_system_proxy( |
| 154 | + &self, |
| 155 | + proxies: CFDictionary<CFString, CFType>, |
| 156 | + ) -> (Option<String>, Option<String>, Option<String>) { |
| 157 | + let mut http: Option<String> = None; |
| 158 | + let mut https: Option<String> = None; |
| 159 | + let mut no: Option<String> = None; |
| 160 | + |
| 161 | + // Process HTTP proxy |
| 162 | + if self.is_proxy_enabled("HTTP", &proxies) { |
| 163 | + if let Some(host) = self.get_string("HTTPProxy", &proxies) { |
| 164 | + let port = self.get_int("HTTPPort", &proxies); |
| 165 | + http = match port { |
| 166 | + Some(p) => Some(format!("http://{}:{}", host, p)), |
| 167 | + None => Some(format!("http://{}", host)), |
| 168 | + }; |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + // Process HTTPS proxy |
| 173 | + if self.is_proxy_enabled("HTTPS", &proxies) { |
| 174 | + if let Some(host) = self.get_string("HTTPSProxy", &proxies) { |
| 175 | + let port = self.get_int("HTTPSPort", &proxies); |
| 176 | + https = match port { |
| 177 | + Some(p) => Some(format!("https://{}:{}", host, p)), |
| 178 | + None => Some(format!("https://{}", host)), |
| 179 | + }; |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + // Process exceptions (NO_PROXY) |
| 184 | + if let Some(exceptions_ref) = proxies.find(CFString::new("ExceptionsList")) { |
| 185 | + if let Some(arr) = exceptions_ref.downcast::<CFArray>() { |
| 186 | + let exceptions: Vec<String> = arr |
| 187 | + .iter() |
| 188 | + .filter_map(|item| unsafe { |
| 189 | + // Get the raw pointer value |
| 190 | + let ptr = item.as_void_ptr(); |
| 191 | + if ptr.is_null() { |
| 192 | + return None; |
| 193 | + } |
| 194 | + // Try to convert it to a CFString |
| 195 | + let cfstr = CFString::wrap_under_get_rule(ptr as *const _); |
| 196 | + Some(cfstr.to_string()) |
| 197 | + }) |
| 198 | + .collect(); |
| 199 | + no = Some(exceptions.join(",")); |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + (http, https, no) |
| 204 | + } |
| 205 | + } |
| 206 | + |
| 207 | + #[cfg(test)] |
| 208 | + mod tests { |
| 209 | + use super::*; |
| 210 | + use crate::client::proxy::Matcher; |
| 211 | + use system_configuration::core_foundation::array::CFArray; |
| 212 | + use std::{net::IpAddr, str::FromStr}; |
| 213 | + |
| 214 | + struct MockSCDynamicStore { |
| 215 | + pairs: Vec<(CFString, CFType)>, |
| 216 | + } |
| 217 | + |
| 218 | + impl MockSCDynamicStore { |
| 219 | + fn new() -> Self { |
| 220 | + let mut keys = Vec::new(); |
| 221 | + let mut values = Vec::new(); |
| 222 | + |
| 223 | + // HTTP proxy enabled |
| 224 | + keys.push(CFString::new("HTTPEnable")); |
| 225 | + values.push(CFNumber::from(1).as_CFType()); |
| 226 | + |
| 227 | + // HTTP proxy host and port |
| 228 | + keys.push(CFString::new("HTTPProxy")); |
| 229 | + values.push(CFString::new("test-proxy.example.com").as_CFType()); |
| 230 | + keys.push(CFString::new("HTTPPort")); |
| 231 | + values.push(CFNumber::from(8080).as_CFType()); |
| 232 | + |
| 233 | + // HTTPS proxy enabled |
| 234 | + keys.push(CFString::new("HTTPSEnable")); |
| 235 | + values.push(CFNumber::from(1).as_CFType()); |
| 236 | + // HTTPS proxy host and port |
| 237 | + keys.push(CFString::new("HTTPSProxy")); |
| 238 | + values.push(CFString::new("secure-proxy.example.com").as_CFType()); |
| 239 | + keys.push(CFString::new("HTTPSPort")); |
| 240 | + values.push(CFNumber::from(8443).as_CFType()); |
| 241 | + |
| 242 | + // Exception list |
| 243 | + keys.push(CFString::new("ExceptionsList")); |
| 244 | + let exceptions = vec![ |
| 245 | + CFString::new("localhost").as_CFType(), |
| 246 | + CFString::new("127.0.0.1").as_CFType(), |
| 247 | + CFString::new("*.local").as_CFType(), |
| 248 | + ]; |
| 249 | + values.push(CFArray::from_CFTypes(&exceptions).as_CFType()); |
| 250 | + |
| 251 | + let pairs = keys |
| 252 | + .iter() |
| 253 | + .map(|k| k.clone()) |
| 254 | + .zip(values.iter().map(|v| v.as_CFType())) |
| 255 | + .collect::<Vec<_>>(); |
| 256 | + |
| 257 | + MockSCDynamicStore { pairs } |
| 258 | + } |
| 259 | + |
| 260 | + fn get_proxies(&self) -> Option<CFDictionary<CFString, CFType>> { |
| 261 | + let proxies = CFDictionary::from_CFType_pairs(&self.pairs.clone()); |
| 262 | + Some(proxies) |
| 263 | + } |
| 264 | + } |
| 265 | + |
| 266 | + #[test] |
| 267 | + fn test_mac_os_proxy_mocked() { |
| 268 | + let mock_store = MockSCDynamicStore::new(); |
| 269 | + let proxies = mock_store.get_proxies().unwrap(); |
| 270 | + let (http, https, ns) = Matcher::builder().extract_system_proxy(proxies); |
| 271 | + |
| 272 | + assert!(http.is_some()); |
| 273 | + assert!(https.is_some()); |
| 274 | + assert!(ns.is_some()); |
| 275 | + } |
| 276 | + |
| 277 | + #[ignore] |
| 278 | + #[test] |
| 279 | + fn test_mac_os_proxy() { |
| 280 | + let matcher = Matcher::builder().from_system_proxy().build(); |
| 281 | + assert!(matcher |
| 282 | + .http |
| 283 | + .unwrap() |
| 284 | + .uri |
| 285 | + .eq("http://proxy.example.com:8080")); |
| 286 | + assert!(matcher |
| 287 | + .https |
| 288 | + .unwrap() |
| 289 | + .uri |
| 290 | + .eq("https://proxy.example.com:8080")); |
| 291 | + |
| 292 | + assert!(matcher.no.domains.contains("ebay.com")); |
| 293 | + assert!(matcher.no.domains.contains("amazon.com")); |
| 294 | + |
| 295 | + let ip = IpAddr::from_str("54.239.28.85").unwrap(); |
| 296 | + assert!(matcher.no.ips.contains(ip)); |
| 297 | + } |
| 298 | + } |
| 299 | +} |
| 300 | + |
| 301 | +// ===== Windows Builder System Proxies ===== |
| 302 | +#[cfg(feature = "system-proxies")] |
| 303 | +#[cfg(target_os = "win")] |
| 304 | +mod win_proxies { |
| 305 | + impl Builder { |
| 306 | + pub fn from_system_proxy(mut self) -> Self { |
| 307 | + todo!("Load Win system proxy settings"); |
| 308 | + } |
| 309 | + } |
| 310 | + |
| 311 | + #[cfg(test)] |
| 312 | + mod tests { |
| 313 | + use super::*; |
| 314 | + } |
| 315 | +} |
0 commit comments