1//! The proxy a relay connection, and only a relay connection, goes through: `HTTPS_PROXY`
2//! (`HTTP_PROXY` for `ws://`), then `ALL_PROXY`, each in lower case too, short of `NO_PROXY`;
3//! else the system's: macOS's network settings, Windows's Internet settings, GNOME's. Only
4//! HTTP proxies, reached by `CONNECT`, with a name and password where the URL has them; a
5//! proxy auto-configuration script is not read.
6
7use base64::Engine;
8use std::sync::Mutex;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct Proxy {
12 pub host: String,
13 pub port: u16,
14 /// The name and password `Proxy-Authorization` sends.
15 pub credentials: Option<(String, String)>,
16}
17
18impl Proxy {
19 /// `http://[name:password@]host[:port][/]`, or `host:port`; none for another scheme.
20 pub fn parse(url: &str) -> Option<Self> {
21 let url = url.trim();
22 let rest = match url.split_once("://") {
23 Some(("http" | "https", rest)) => rest,
24 Some(_) => return None,
25 None => url,
26 };
27 let rest = rest.split('/').next()?;
28 let (credentials, authority) = match rest.rsplit_once('@') {
29 Some((credentials, authority)) => {
30 let (name, password) = credentials.split_once(':').unwrap_or((credentials, ""));
31 (
32 Some((crate::live::decode(name), crate::live::decode(password))),
33 authority,
34 )
35 }
36 None => (None, rest),
37 };
38 let (host, port) = match authority.rsplit_once(':') {
39 Some((host, port)) if !port.contains(']') => (host, port.parse().ok()?),
40 _ => (authority, 80),
41 };
42 let host = host.trim_start_matches('[').trim_end_matches(']');
43 (!host.is_empty()).then(|| Self {
44 host: host.to_owned(),
45 port,
46 credentials,
47 })
48 }
49
50 /// The `Proxy-Authorization` header's value, where it has credentials.
51 pub fn authorization(&self) -> Option<String> {
52 let (name, password) = self.credentials.as_ref()?;
53 let token = base64::engine::general_purpose::STANDARD.encode(format!("{name}:{password}"));
54 Some(format!("Basic {token}"))
55 }
56}
57
58impl std::fmt::Display for Proxy {
59 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
60 write!(f, "{}:{}", self.host, self.port)
61 }
62}
63
64/// A proxy `use_proxy` set in place of what the environment and the system say.
65static CHOSEN: Mutex<Option<Option<Proxy>>> = Mutex::new(None);
66
67/// Uses `proxy`, or no proxy with `Some(None)`, for every relay connection from now on; `None`
68/// goes back to the environment's and the system's.
69pub fn use_proxy(proxy: Option<Option<Proxy>>) {
70 *CHOSEN
71 .lock()
72 .unwrap_or_else(|poisoned| poisoned.into_inner()) = proxy;
73}
74
75/// The proxy a connection to `host` goes through, over TLS where `tls`.
76pub fn for_host(host: &str, tls: bool) -> Option<Proxy> {
77 if let Some(chosen) = CHOSEN.lock().unwrap_or_else(|p| p.into_inner()).clone() {
78 return chosen;
79 }
80 let variable = |name: &str| {
81 [name.to_owned(), name.to_lowercase()]
82 .into_iter()
83 .find_map(|name| std::env::var(name).ok().filter(|value| !value.is_empty()))
84 };
85 let named = [if tls { "HTTPS_PROXY" } else { "HTTP_PROXY" }, "ALL_PROXY"]
86 .into_iter()
87 .find_map(variable);
88 match named {
89 Some(url) => {
90 let bypass = variable("NO_PROXY").unwrap_or_default();
91 (!bypassed(host, bypass.split(','))).then(|| Proxy::parse(&url))?
92 }
93 None => system(host, tls),
94 }
95}
96
97/// Whether `host` is one of `list`'s: a name, a suffix after a dot, or `*` for every host.
98fn bypassed<'a>(host: &str, list: impl Iterator<Item = &'a str>) -> bool {
99 let host = host.to_ascii_lowercase();
100 list.map(|entry| {
101 entry
102 .trim()
103 .trim_start_matches("*.")
104 .trim_start_matches('.')
105 })
106 .filter(|entry| !entry.is_empty())
107 .any(|entry| {
108 let entry = entry.to_ascii_lowercase();
109 entry == "*" || host == entry || host.ends_with(&format!(".{entry}"))
110 })
111}
112
113/// What `scutil --proxy` says of the network settings in use.
114#[cfg(target_os = "macos")]
115fn system(host: &str, tls: bool) -> Option<Proxy> {
116 let output = std::process::Command::new("/usr/sbin/scutil")
117 .arg("--proxy")
118 .output()
119 .ok()?;
120 let text = String::from_utf8(output.stdout).ok()?;
121 let value = |key: &str| {
122 text.lines().find_map(|line| {
123 let (name, value) = line.split_once(" : ")?;
124 (name.trim() == key).then(|| value.trim().to_owned())
125 })
126 };
127 let kind = if tls { "HTTPS" } else { "HTTP" };
128 if value(&format!("{kind}Enable")).as_deref() != Some("1") {
129 return None;
130 }
131 let exceptions: Vec<String> = text
132 .lines()
133 .skip_while(|line| !line.contains("ExceptionsList"))
134 .skip(1)
135 .take_while(|line| !line.contains('}'))
136 .filter_map(|line| Some(line.split_once(" : ")?.1.trim().to_owned()))
137 .collect();
138 if bypassed(host, exceptions.iter().map(String::as_str)) {
139 return None;
140 }
141 Some(Proxy {
142 host: value(&format!("{kind}Proxy"))?,
143 port: value(&format!("{kind}Port"))?.parse().ok()?,
144 credentials: None,
145 })
146}
147
148/// What Windows's Internet settings say, as WinHTTP and Internet Explorer read them for this
149/// user: `ProxyServer`, as `host:port` or `https=host:port;http=...`, where `ProxyEnable`.
150#[cfg(windows)]
151fn system(host: &str, tls: bool) -> Option<Proxy> {
152 let key = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings";
153 if registry::dword(key, "ProxyEnable")? == 0 {
154 return None;
155 }
156 let server = registry::string(key, "ProxyServer")?;
157 let overrides = registry::string(key, "ProxyOverride").unwrap_or_default();
158 if bypassed(
159 host,
160 overrides.split(';').filter(|entry| *entry != "<local>"),
161 ) {
162 return None;
163 }
164 let scheme = if tls { "https=" } else { "http=" };
165 let server = match server.contains('=') {
166 true => server
167 .split(';')
168 .find_map(|entry| entry.trim().strip_prefix(scheme))?,
169 false => server.as_str(),
170 };
171 Proxy::parse(server)
172}
173
174#[cfg(windows)]
175#[allow(unsafe_code)]
176mod registry {
177 use windows_sys::Win32::System::Registry::{
178 HKEY_CURRENT_USER, RRF_RT_REG_DWORD, RRF_RT_REG_SZ, RegGetValueW,
179 };
180
181 fn wide(text: &str) -> Vec<u16> {
182 text.encode_utf16().chain([0]).collect()
183 }
184
185 pub fn dword(key: &str, name: &str) -> Option<u32> {
186 let (key, name) = (wide(key), wide(name));
187 let mut value = 0u32;
188 let mut size = 4u32;
189 // SAFETY: the key and name are NUL-terminated and outlive the call; the value is four
190 // bytes, as `size` says.
191 let result = unsafe {
192 RegGetValueW(
193 HKEY_CURRENT_USER,
194 key.as_ptr(),
195 name.as_ptr(),
196 RRF_RT_REG_DWORD,
197 std::ptr::null_mut(),
198 (&mut value as *mut u32).cast(),
199 &mut size,
200 )
201 };
202 (result == 0).then_some(value)
203 }
204
205 pub fn string(key: &str, name: &str) -> Option<String> {
206 let (key, name) = (wide(key), wide(name));
207 let mut buffer = vec![0u16; 2048];
208 let mut size = (buffer.len() * 2) as u32;
209 // SAFETY: as in `dword`, with `size` the buffer's length in bytes.
210 let result = unsafe {
211 RegGetValueW(
212 HKEY_CURRENT_USER,
213 key.as_ptr(),
214 name.as_ptr(),
215 RRF_RT_REG_SZ,
216 std::ptr::null_mut(),
217 buffer.as_mut_ptr().cast(),
218 &mut size,
219 )
220 };
221 if result != 0 {
222 return None;
223 }
224 let length = buffer
225 .iter()
226 .position(|unit| *unit == 0)
227 .unwrap_or(buffer.len());
228 Some(String::from_utf16_lossy(&buffer[..length]))
229 }
230}
231
232/// What GNOME's proxy settings say, where they are manual.
233#[cfg(not(any(target_os = "macos", windows)))]
234fn system(host: &str, tls: bool) -> Option<Proxy> {
235 let get = |schema: &str, key: &str| {
236 let output = std::process::Command::new("gsettings")
237 .args(["get", schema, key])
238 .output()
239 .ok()?;
240 let text = String::from_utf8(output.stdout).ok()?;
241 Some(text.trim().trim_matches('\'').to_owned())
242 };
243 if get("org.gnome.system.proxy", "mode")? != "manual" {
244 return None;
245 }
246 let ignored = get("org.gnome.system.proxy", "ignore-hosts").unwrap_or_default();
247 let ignored = ignored.trim_matches(['[', ']']).replace('\'', "");
248 if bypassed(host, ignored.split(',')) {
249 return None;
250 }
251 let schema = if tls {
252 "org.gnome.system.proxy.https"
253 } else {
254 "org.gnome.system.proxy.http"
255 };
256 Some(Proxy {
257 host: get(schema, "host").filter(|host| !host.is_empty())?,
258 port: get(schema, "port")?
259 .parse()
260 .ok()
261 .filter(|port| *port != 0)?,
262 credentials: None,
263 })
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn proxies_read_as_written() {
272 assert_eq!(
273 Proxy::parse("http://ada:p%40ss@proxy.example:3128/"),
274 Some(Proxy {
275 host: "proxy.example".into(),
276 port: 3128,
277 credentials: Some(("ada".into(), "p@ss".into())),
278 })
279 );
280 assert_eq!(Proxy::parse("proxy:8080").unwrap().port, 8080);
281 assert_eq!(Proxy::parse("http://[::1]:8080").unwrap().host, "::1");
282 assert!(Proxy::parse("socks5://proxy:1080").is_none());
283 let proxy = Proxy::parse("http://ada:secret@proxy:1").unwrap();
284 assert_eq!(proxy.authorization().unwrap(), "Basic YWRhOnNlY3JldA==");
285 let list = || ["localhost", ".internal", "*.corp.example"].into_iter();
286 assert!(bypassed("localhost", list()) && bypassed("files.internal", list()));
287 assert!(bypassed("a.corp.example", list()) && !bypassed("relay.example", list()));
288 assert!(bypassed("anything", ["*"].into_iter()));
289 }
290}