use std::collections::HashMap;
use std::fmt;
use std::time::SystemTime;
use crate::bypass::BypassRules;
use crate::config::{ProxyConfig, ProxyConfigSource};
use crate::diagnostic::{RejectedValue, RejectionKind, RejectionSource};
use crate::endpoint::{ProxyEndpoint, ProxyEntry, Scheme};
use crate::error::Error;
use crate::mode::ProxyMode;
use crate::parse;
pub const CGI_MARKER_VAR: &str = "REQUEST_METHOD";
const SCHEME_VARS: [(Scheme, &str); 4] = [
(Scheme::Http, "http_proxy"),
(Scheme::Https, "https_proxy"),
(Scheme::Ftp, "ftp_proxy"),
(Scheme::All, "all_proxy"),
];
const NO_PROXY_VAR: &str = "no_proxy";
#[derive(Clone)]
pub struct ProxyEnv {
per_scheme: HashMap<Scheme, ProxyEntry>,
bypass: BypassRules,
rejected: Vec<RejectedValue>,
captured_at: SystemTime,
}
impl ProxyEnv {
pub fn from_env() -> Result<Self, Error> {
Self::from_vars(std::env::vars_os().filter_map(readable_var))
}
pub fn from_vars<I, K, V>(vars: I) -> Result<Self, Error>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
let mut map: HashMap<String, String> = HashMap::new();
for (k, v) in vars {
map.entry(k.as_ref().to_owned())
.or_insert_with(|| v.as_ref().to_owned());
}
let in_cgi = map
.get(CGI_MARKER_VAR)
.or_else(|| any_case_on_windows(&map, CGI_MARKER_VAR).map(|(_, value)| value))
.is_some_and(|method| !method.is_empty());
if in_cgi
&& let Some(variable) = map
.keys()
.filter(|k| k.eq_ignore_ascii_case(SCHEME_VARS[0].1))
.min()
{
return Err(Error::CgiHttpProxy {
variable: variable.clone(),
});
}
let mut per_scheme = HashMap::new();
let mut rejected = Vec::new();
for (scheme, name) in SCHEME_VARS {
let Some(value) = lookup(&map, name) else {
continue;
};
let trimmed = value.trim();
if trimmed.is_empty() {
per_scheme.insert(scheme, ProxyEntry::Disabled);
continue;
}
match ProxyEndpoint::parse(trimmed, 80) {
Ok(endpoint) => {
per_scheme.insert(scheme, ProxyEntry::Use(endpoint));
}
#[cfg_attr(not(feature = "tracing"), allow(unused_variables))]
Err(err) => {
crate::trace::warning!(
variable = name,
error = %crate::trace::SafeError(&err),
"skipping an unparseable *_proxy value"
);
rejected.push(
RejectedValue::new(
RejectionKind::InvalidProxyEndpoint,
RejectionSource::EnvironmentVariable(name.to_owned()),
trimmed,
)
.for_scheme(Some(scheme)),
);
}
}
}
let bypass = match lookup(&map, NO_PROXY_VAR) {
Some(value) => parse::no_proxy(value),
None => BypassRules::new(),
};
Ok(Self {
per_scheme,
bypass,
rejected,
captured_at: SystemTime::now(),
})
}
#[must_use]
pub fn per_scheme(&self) -> &HashMap<Scheme, ProxyEntry> {
&self.per_scheme
}
#[must_use]
pub fn bypass(&self) -> &BypassRules {
&self.bypass
}
#[must_use]
pub fn rejected(&self) -> &[RejectedValue] {
&self.rejected
}
#[must_use]
pub fn captured_at(&self) -> SystemTime {
self.captured_at
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.per_scheme.is_empty() && self.rejected.is_empty()
}
#[must_use]
pub fn is_configured(&self) -> bool {
!self.per_scheme.is_empty() || !self.bypass.is_empty()
}
#[must_use]
pub fn endpoint_for(&self, scheme: Scheme) -> Option<&ProxyEndpoint> {
self.per_scheme
.get(&scheme)
.or_else(|| self.per_scheme.get(&Scheme::All))?
.endpoint()
}
#[must_use]
pub fn to_mode(&self) -> ProxyMode {
if self.is_empty() {
ProxyMode::Direct
} else {
ProxyMode::manual(self.per_scheme.clone(), self.bypass.clone())
.with_rejected(self.rejected.clone())
}
}
#[must_use]
pub fn to_config(&self) -> ProxyConfig {
let mut config = ProxyConfig::from_source(ProxyConfigSource::Env, self.to_mode());
config.captured_at = self.captured_at;
config
}
}
impl fmt::Debug for ProxyEnv {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ProxyEnv")
.field(
"per_scheme",
&self
.per_scheme
.iter()
.collect::<std::collections::BTreeMap<_, _>>(),
)
.field("bypass", &self.bypass)
.field("rejected", &self.rejected)
.field("captured_at", &self.captured_at)
.finish()
}
}
impl PartialEq for ProxyEnv {
fn eq(&self, other: &Self) -> bool {
self.per_scheme == other.per_scheme
&& self.bypass == other.bypass
&& self.rejected == other.rejected
}
}
impl Eq for ProxyEnv {}
fn readable_var(
(name, value): (std::ffi::OsString, std::ffi::OsString),
) -> Option<(String, String)> {
Some((
name.into_string().ok()?,
value.to_string_lossy().into_owned(),
))
}
fn lookup<'a>(map: &'a HashMap<String, String>, lower: &str) -> Option<&'a str> {
map.get(lower)
.or_else(|| map.get(&lower.to_ascii_uppercase()))
.or_else(|| any_case_on_windows(map, lower).map(|(_, value)| value))
.map(String::as_str)
}
fn any_case_on_windows<'a>(
map: &'a HashMap<String, String>,
name: &str,
) -> Option<(&'a String, &'a String)> {
if !cfg!(windows) {
return None;
}
map.iter()
.filter(|(key, _)| key.eq_ignore_ascii_case(name))
.min_by_key(|(key, _)| *key)
}
#[cfg(all(test, any(windows, unix)))]
mod tests {
use super::*;
#[test]
fn the_env_debug_lists_every_field_and_sorts_the_schemes() {
let env = ProxyEnv::from_vars([
("https_proxy", "http://secure.corp:8443"),
("http_proxy", "http://plain.corp:8080"),
("no_proxy", ".example.com"),
])
.expect("no REQUEST_METHOD is set here");
assert_eq!(
format!("{env:?}"),
format!(
"ProxyEnv {{ per_scheme: {{Http: {:?}, Https: {:?}}}, bypass: {:?}, \
rejected: [], captured_at: {:?} }}",
env.per_scheme[&Scheme::Http],
env.per_scheme[&Scheme::Https],
env.bypass,
env.captured_at
)
);
}
#[test]
fn an_http_proxy_that_is_not_unicode_is_refused_rather_than_dropped() {
#[cfg(windows)]
let raw = {
use std::os::windows::ffi::OsStringExt;
std::ffi::OsString::from_wide(&[0xD800])
};
#[cfg(unix)]
let raw = {
use std::os::unix::ffi::OsStringExt;
std::ffi::OsString::from_vec(vec![0xFF])
};
let (name, value) = readable_var((std::ffi::OsString::from("http_proxy"), raw))
.expect("a variable whose name is ASCII stays in the snapshot");
let env = ProxyEnv::from_vars([(name, value)]).expect("no REQUEST_METHOD is set here");
assert!(
env.endpoint_for(Scheme::Http).is_none(),
"replacement characters are not an address to send traffic to"
);
assert_eq!(
env.rejected().len(),
1,
"the variable is set, so its drop has to be visible: {:?}",
env.rejected()
);
}
#[test]
fn a_password_that_is_not_unicode_is_refused_like_a_host_that_is_not() {
#[cfg(windows)]
let raw = {
use std::os::windows::ffi::OsStringExt;
let mut units: Vec<u16> = "http://alice:".encode_utf16().collect();
units.push(0xD800);
units.extend("@proxy.corp:8080".encode_utf16());
std::ffi::OsString::from_wide(&units)
};
#[cfg(unix)]
let raw = {
use std::os::unix::ffi::OsStringExt;
let mut bytes = b"http://alice:".to_vec();
bytes.push(0xFF);
bytes.extend_from_slice(b"@proxy.corp:8080");
std::ffi::OsString::from_vec(bytes)
};
let (name, value) = readable_var((std::ffi::OsString::from("http_proxy"), raw))
.expect("a variable whose name is ASCII stays in the snapshot");
assert!(value.contains("@proxy.corp:8080"), "{value}");
let env = ProxyEnv::from_vars([(name, value)]).expect("no REQUEST_METHOD is set here");
assert!(
env.endpoint_for(Scheme::Http).is_none(),
"a credential the reader had to invent is not one to offer a proxy"
);
assert_eq!(
env.rejected().len(),
1,
"the variable is set, so its drop has to be visible: {:?}",
env.rejected()
);
}
#[test]
fn a_variable_whose_name_is_not_unicode_is_dropped_without_a_trace() {
#[cfg(windows)]
let raw = {
use std::os::windows::ffi::OsStringExt;
std::ffi::OsString::from_wide(&[0xD800])
};
#[cfg(unix)]
let raw = {
use std::os::unix::ffi::OsStringExt;
std::ffi::OsString::from_vec(vec![0xFF])
};
assert_eq!(
readable_var((raw, std::ffi::OsString::from("http://p:8080"))),
None
);
}
}