use std::path::Path;
pub const DEFAULT_NO_PROXY: &str =
"localhost,127.0.0.1,::1,169.254.0.0/16,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16";
pub const PROXY_PORT_ENV_KEY: &str = "KODA_PROXY_PORT";
pub fn proxy_env_vars(port: u16, ca_bundle: Option<&Path>) -> Vec<(String, String)> {
let proxy_url = format!("http://127.0.0.1:{port}");
let mut vars = vec![
("HTTPS_PROXY".to_string(), proxy_url.clone()),
("https_proxy".to_string(), proxy_url.clone()),
("HTTP_PROXY".to_string(), proxy_url.clone()),
("http_proxy".to_string(), proxy_url),
("NO_PROXY".to_string(), DEFAULT_NO_PROXY.to_string()),
("no_proxy".to_string(), DEFAULT_NO_PROXY.to_string()),
];
if let Some(ca) = ca_bundle {
let ca_str = ca.to_string_lossy().to_string();
vars.push(("SSL_CERT_FILE".to_string(), ca_str.clone()));
vars.push(("NODE_EXTRA_CA_CERTS".to_string(), ca_str.clone()));
vars.push(("REQUESTS_CA_BUNDLE".to_string(), ca_str.clone()));
vars.push(("CURL_CA_BUNDLE".to_string(), ca_str));
}
vars.sort_by(|a, b| a.0.cmp(&b.0));
vars
}
pub fn socks5_env_vars(port: u16) -> Vec<(String, String)> {
let url = format!("socks5h://127.0.0.1:{port}");
let mut vars = vec![
("ALL_PROXY".to_string(), url.clone()),
("all_proxy".to_string(), url),
];
vars.sort_by(|a, b| a.0.cmp(&b.0));
vars
}
pub fn ca_bundle_for_policy(net: &crate::policy::NetPolicy) -> Option<&Path> {
net.mitm.as_ref().map(|m| m.ca_bundle.as_path())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn proxy_env_vars_includes_all_six_proxy_keys() {
let vars = proxy_env_vars(8080, None);
let keys: Vec<&str> = vars.iter().map(|(k, _)| k.as_str()).collect();
assert!(keys.contains(&"HTTPS_PROXY"));
assert!(keys.contains(&"https_proxy"));
assert!(keys.contains(&"HTTP_PROXY"));
assert!(keys.contains(&"http_proxy"));
assert!(keys.contains(&"NO_PROXY"));
assert!(keys.contains(&"no_proxy"));
}
#[test]
fn proxy_env_vars_omits_ca_bundle_when_none() {
let vars = proxy_env_vars(8080, None);
let keys: Vec<&str> = vars.iter().map(|(k, _)| k.as_str()).collect();
assert!(!keys.contains(&"SSL_CERT_FILE"));
assert!(!keys.contains(&"NODE_EXTRA_CA_CERTS"));
assert!(!keys.contains(&"REQUESTS_CA_BUNDLE"));
assert!(!keys.contains(&"CURL_CA_BUNDLE"));
}
#[test]
fn proxy_env_vars_includes_all_four_ca_keys_when_some() {
let bundle = PathBuf::from("/etc/ssl/corp-ca.pem");
let vars = proxy_env_vars(8080, Some(&bundle));
let keys: Vec<&str> = vars.iter().map(|(k, _)| k.as_str()).collect();
assert!(keys.contains(&"SSL_CERT_FILE"));
assert!(keys.contains(&"NODE_EXTRA_CA_CERTS"));
assert!(keys.contains(&"REQUESTS_CA_BUNDLE"));
assert!(keys.contains(&"CURL_CA_BUNDLE"));
for key in [
"SSL_CERT_FILE",
"NODE_EXTRA_CA_CERTS",
"REQUESTS_CA_BUNDLE",
"CURL_CA_BUNDLE",
] {
let v = vars
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
.unwrap();
assert_eq!(v, "/etc/ssl/corp-ca.pem");
}
}
#[test]
fn proxy_url_format_uses_loopback_ipv4() {
let vars = proxy_env_vars(31415, None);
let url = vars
.iter()
.find(|(k, _)| k == "HTTPS_PROXY")
.map(|(_, v)| v.as_str())
.unwrap();
assert_eq!(url, "http://127.0.0.1:31415");
}
#[test]
fn no_proxy_default_covers_loopback_and_rfc1918() {
assert!(DEFAULT_NO_PROXY.contains("127.0.0.1"));
assert!(DEFAULT_NO_PROXY.contains("::1"));
assert!(DEFAULT_NO_PROXY.contains("10.0.0.0/8"));
assert!(DEFAULT_NO_PROXY.contains("172.16.0.0/12"));
assert!(DEFAULT_NO_PROXY.contains("192.168.0.0/16"));
assert!(DEFAULT_NO_PROXY.contains("169.254.0.0/16"));
}
#[test]
fn socks5_env_vars_uses_socks5h_scheme() {
let vars = socks5_env_vars(1080);
for (_, v) in &vars {
assert!(v.starts_with("socks5h://"), "got: {v}");
}
}
#[test]
fn socks5_env_vars_includes_upper_and_lower() {
let vars = socks5_env_vars(1080);
let keys: Vec<&str> = vars.iter().map(|(k, _)| k.as_str()).collect();
assert!(keys.contains(&"ALL_PROXY"));
assert!(keys.contains(&"all_proxy"));
}
#[test]
fn socks5_env_vars_uses_loopback_ipv4() {
let vars = socks5_env_vars(31415);
let url = vars
.iter()
.find(|(k, _)| k == "ALL_PROXY")
.map(|(_, v)| v.as_str())
.unwrap();
assert_eq!(url, "socks5h://127.0.0.1:31415");
}
#[test]
fn ca_bundle_for_policy_handles_no_mitm() {
let policy = crate::policy::NetPolicy::default();
assert!(ca_bundle_for_policy(&policy).is_none());
}
#[test]
fn ca_bundle_for_policy_returns_path_when_mitm_set() {
let policy = crate::policy::NetPolicy {
mitm: Some(crate::policy::MitmConfig {
ca_bundle: PathBuf::from("/x/ca.pem"),
socket_map: vec![],
}),
..Default::default()
};
assert_eq!(ca_bundle_for_policy(&policy), Some(Path::new("/x/ca.pem")));
}
#[test]
fn policy_with_mitm_yields_full_ca_bouquet() {
let policy = crate::policy::NetPolicy {
mitm: Some(crate::policy::MitmConfig {
ca_bundle: PathBuf::from("/etc/ssl/corp.pem"),
socket_map: vec![],
}),
..Default::default()
};
let ca = ca_bundle_for_policy(&policy);
let vars = proxy_env_vars(8080, ca);
for key in [
"SSL_CERT_FILE",
"NODE_EXTRA_CA_CERTS",
"REQUESTS_CA_BUNDLE",
"CURL_CA_BUNDLE",
] {
let v = vars
.iter()
.find(|(k, _)| k == key)
.unwrap_or_else(|| panic!("missing {key} in {vars:?}"));
assert_eq!(
v.1, "/etc/ssl/corp.pem",
"{key} must point at the policy's ca_bundle"
);
}
}
#[test]
fn policy_without_mitm_yields_no_ca_bouquet() {
let policy = crate::policy::NetPolicy::default();
let ca = ca_bundle_for_policy(&policy);
let vars = proxy_env_vars(8080, ca);
for key in [
"SSL_CERT_FILE",
"NODE_EXTRA_CA_CERTS",
"REQUESTS_CA_BUNDLE",
"CURL_CA_BUNDLE",
] {
assert!(
!vars.iter().any(|(k, _)| k == key),
"default policy must not advertise {key}; got {vars:?}"
);
}
}
}