#![allow(dead_code)]
use super::config::{NetworkConfig, TlsConfig};
use super::resolve::ResolvedProxy;
use std::collections::HashMap;
use std::process::Command;
pub mod env_keys {
pub const HTTP_PROXY_UPPER: &str = "HTTP_PROXY";
pub const HTTP_PROXY_LOWER: &str = "http_proxy";
pub const HTTPS_PROXY_UPPER: &str = "HTTPS_PROXY";
pub const HTTPS_PROXY_LOWER: &str = "https_proxy";
pub const NO_PROXY_UPPER: &str = "NO_PROXY";
pub const NO_PROXY_LOWER: &str = "no_proxy";
pub const ALL_PROXY_UPPER: &str = "ALL_PROXY";
pub const ALL_PROXY_LOWER: &str = "all_proxy";
pub const CURL_CA_BUNDLE: &str = "CURL_CA_BUNDLE";
pub const SSL_CERT_FILE: &str = "SSL_CERT_FILE";
pub const SSL_CERT_DIR: &str = "SSL_CERT_DIR";
pub const REQUESTS_CA_BUNDLE: &str = "REQUESTS_CA_BUNDLE";
pub const NODE_EXTRA_CA_CERTS: &str = "NODE_EXTRA_CA_CERTS";
pub const GIT_SSL_CAINFO: &str = "GIT_SSL_CAINFO";
pub const AWS_CA_BUNDLE: &str = "AWS_CA_BUNDLE";
}
const TRUSTED_CA_BUNDLE_DIRS: &[&str] = &[
"/etc/ssl/",
"/etc/pki/",
"/etc/ca-certificates/",
"/usr/share/ca-certificates/",
"/usr/local/share/ca-certificates/",
"/opt/homebrew/etc/ca-certificates/",
"/opt/homebrew/share/ca-certificates/",
"/Library/Keychains/",
"/System/Library/Keychains/",
];
fn ca_bundle_path_is_trusted(path: &str) -> bool {
use std::path::{Path, PathBuf};
if std::env::var("JARVY_ALLOW_CUSTOM_CA")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes"))
.unwrap_or(false)
{
return true;
}
let canon: PathBuf = std::fs::canonicalize(path).unwrap_or_else(|_| PathBuf::from(path));
let canon_str = canon.to_string_lossy();
if TRUSTED_CA_BUNDLE_DIRS
.iter()
.any(|dir| canon_str.starts_with(dir))
{
return true;
}
if let Ok(jarvy_dir) = crate::paths::jarvy_home() {
let curated_ca_dir = jarvy_dir.join("ca");
if Path::new(&canon).starts_with(&curated_ca_dir) {
return true;
}
}
false
}
pub fn generate_env_vars(
proxy: &ResolvedProxy,
tls: Option<&TlsConfig>,
) -> HashMap<String, String> {
let mut vars = proxy.to_env_vars();
if let Some(tls_config) = tls {
if tls_config.insecure {
tracing::warn!(
event = "network.tls.insecure_ignored",
"[network.tls] insecure=true is parsed but never applied; \
jarvy refuses to disable TLS verification from project config"
);
}
if let Some(ref ca_bundle) = tls_config.ca_bundle {
if ca_bundle_path_is_trusted(ca_bundle) {
vars.insert(env_keys::CURL_CA_BUNDLE.to_string(), ca_bundle.clone());
vars.insert(env_keys::SSL_CERT_FILE.to_string(), ca_bundle.clone());
vars.insert(env_keys::REQUESTS_CA_BUNDLE.to_string(), ca_bundle.clone());
vars.insert(env_keys::NODE_EXTRA_CA_CERTS.to_string(), ca_bundle.clone());
vars.insert(env_keys::GIT_SSL_CAINFO.to_string(), ca_bundle.clone());
vars.insert(env_keys::AWS_CA_BUNDLE.to_string(), ca_bundle.clone());
} else {
tracing::warn!(
event = "network.tls.refused_untrusted_ca_bundle",
path = %ca_bundle,
"refused [network.tls] ca_bundle outside trusted dirs; \
set JARVY_ALLOW_CUSTOM_CA=1 to override"
);
}
}
}
vars
}
pub fn apply_to_command(cmd: &mut Command, proxy: &ResolvedProxy, tls: Option<&TlsConfig>) {
let vars = generate_env_vars(proxy, tls);
for (key, value) in vars {
cmd.env(key, value);
}
}
pub fn apply_network_config(cmd: &mut Command, config: &NetworkConfig, tool_name: &str) {
use super::resolve::ProxyResolver;
let resolver = ProxyResolver::new(Some(config));
let resolved = resolver.resolve_for_tool(tool_name);
apply_to_command(cmd, &resolved, config.tls.as_ref());
}
pub fn generate_shell_exports(proxy: &ResolvedProxy, tls: Option<&TlsConfig>) -> String {
let vars = generate_env_vars(proxy, tls);
let mut lines = Vec::new();
for (key, value) in vars {
let escaped = value.replace('\'', "'\\''");
lines.push(format!("export {}='{}'", key, escaped));
}
lines.sort(); lines.join("\n")
}
pub fn clear_proxy_env(cmd: &mut Command) {
cmd.env_remove(env_keys::HTTP_PROXY_UPPER);
cmd.env_remove(env_keys::HTTP_PROXY_LOWER);
cmd.env_remove(env_keys::HTTPS_PROXY_UPPER);
cmd.env_remove(env_keys::HTTPS_PROXY_LOWER);
cmd.env_remove(env_keys::NO_PROXY_UPPER);
cmd.env_remove(env_keys::NO_PROXY_LOWER);
cmd.env_remove(env_keys::ALL_PROXY_UPPER);
cmd.env_remove(env_keys::ALL_PROXY_LOWER);
}
#[cfg(test)]
mod tests {
use super::super::resolve::ProxySource;
use super::*;
#[test]
fn test_generate_env_vars_basic() {
let proxy = ResolvedProxy {
http_proxy: Some("http://proxy:8080".to_string()),
https_proxy: Some("https://proxy:8443".to_string()),
socks_proxy: None,
no_proxy: Some("localhost".to_string()),
source: ProxySource::GlobalConfig,
};
let vars = generate_env_vars(&proxy, None);
assert_eq!(
vars.get("HTTP_PROXY"),
Some(&"http://proxy:8080".to_string())
);
assert_eq!(
vars.get("http_proxy"),
Some(&"http://proxy:8080".to_string())
);
assert_eq!(
vars.get("HTTPS_PROXY"),
Some(&"https://proxy:8443".to_string())
);
assert_eq!(vars.get("NO_PROXY"), Some(&"localhost".to_string()));
}
#[test]
fn test_generate_env_vars_with_ca() {
let proxy = ResolvedProxy::default();
let tls = TlsConfig {
ca_bundle: Some("/etc/ssl/certs/ca.crt".to_string()),
insecure: false,
};
let vars = generate_env_vars(&proxy, Some(&tls));
assert_eq!(
vars.get("CURL_CA_BUNDLE"),
Some(&"/etc/ssl/certs/ca.crt".to_string())
);
assert_eq!(
vars.get("SSL_CERT_FILE"),
Some(&"/etc/ssl/certs/ca.crt".to_string())
);
assert_eq!(
vars.get("NODE_EXTRA_CA_CERTS"),
Some(&"/etc/ssl/certs/ca.crt".to_string())
);
assert_eq!(
vars.get("GIT_SSL_CAINFO"),
Some(&"/etc/ssl/certs/ca.crt".to_string())
);
}
#[test]
fn ca_bundle_outside_trusted_dirs_is_dropped() {
if std::env::var("JARVY_ALLOW_CUSTOM_CA").is_ok() {
return;
}
let proxy = ResolvedProxy::default();
let tls = TlsConfig {
ca_bundle: Some("/tmp/attacker.crt".to_string()),
insecure: false,
};
let vars = generate_env_vars(&proxy, Some(&tls));
assert!(
!vars.contains_key("CURL_CA_BUNDLE"),
"untrusted ca_bundle must NOT be propagated; got {:?}",
vars.get("CURL_CA_BUNDLE")
);
assert!(!vars.contains_key("SSL_CERT_FILE"));
assert!(!vars.contains_key("NODE_EXTRA_CA_CERTS"));
}
#[test]
fn ca_bundle_path_predicate_accepts_system_dirs() {
assert!(ca_bundle_path_is_trusted("/etc/ssl/certs/ca-bundle.crt"));
assert!(ca_bundle_path_is_trusted(
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"
));
assert!(ca_bundle_path_is_trusted(
"/usr/share/ca-certificates/cacert.org/cacert.org.crt"
));
}
#[test]
fn ca_bundle_path_predicate_rejects_user_writable_paths() {
if std::env::var("JARVY_ALLOW_CUSTOM_CA").is_ok() {
return;
}
assert!(!ca_bundle_path_is_trusted("/tmp/attacker.crt"));
assert!(!ca_bundle_path_is_trusted("/var/tmp/x"));
}
#[test]
fn ca_bundle_path_under_jarvy_cache_is_refused() {
if std::env::var("JARVY_ALLOW_CUSTOM_CA").is_ok() {
return;
}
if let Ok(jarvy_dir) = crate::paths::jarvy_home() {
let cached = jarvy_dir.join("cache").join("configs").join("abc.toml");
assert!(
!ca_bundle_path_is_trusted(&cached.to_string_lossy()),
"cache file under ~/.jarvy/ must not be a trusted CA bundle"
);
let sibling = jarvy_dir.join("ca-attacker").join("evil.crt");
assert!(!ca_bundle_path_is_trusted(&sibling.to_string_lossy()));
}
}
#[test]
fn ca_bundle_path_under_curated_ca_dir_is_trusted() {
if std::env::var("JARVY_ALLOW_CUSTOM_CA").is_ok() {
return;
}
if let Ok(jarvy_dir) = crate::paths::jarvy_home() {
let curated = jarvy_dir.join("ca").join("corp.crt");
assert!(
ca_bundle_path_is_trusted(&curated.to_string_lossy()),
"~/.jarvy/ca/<file> must remain trusted (the dir Jarvy never auto-writes)"
);
}
}
#[test]
fn test_generate_shell_exports() {
let proxy = ResolvedProxy {
http_proxy: Some("http://proxy:8080".to_string()),
https_proxy: None,
socks_proxy: None,
no_proxy: None,
source: ProxySource::GlobalConfig,
};
let exports = generate_shell_exports(&proxy, None);
assert!(exports.contains("export HTTP_PROXY='http://proxy:8080'"));
assert!(exports.contains("export http_proxy='http://proxy:8080'"));
}
#[test]
fn test_generate_shell_exports_escaping() {
let proxy = ResolvedProxy {
http_proxy: Some("http://user:p'ass@proxy:8080".to_string()),
https_proxy: None,
socks_proxy: None,
no_proxy: None,
source: ProxySource::GlobalConfig,
};
let exports = generate_shell_exports(&proxy, None);
assert!(exports.contains("'\\''"));
}
}