use colored::*; use reqwest::blocking; use reqwest::Url; /// Where the proxy URL came from — used for status messages. #[derive(Debug, Clone, PartialEq)] pub enum ProxySource { /// --proxy on the command line Flag, /// HTTPS_PROXY or HTTP_PROXY environment variable Env(String), } /// Resolved proxy configuration. A `None` url means "go direct". #[derive(Debug, Clone)] pub struct ProxyConfig { pub url: Option, pub source: Option, } impl ProxyConfig { /// Resolve proxy with this priority: /// 1. --no-proxy → direct, ignore everything /// 2. --proxy → use that URL /// 3. HTTPS_PROXY / HTTP_PROXY env vars /// 4. Nothing → direct pub fn resolve(proxy_flag: Option<&str>, no_proxy: bool) -> Result { if no_proxy { return Ok(Self { url: None, source: None }); } if let Some(raw) = proxy_flag { let url = Url::parse(raw) .map_err(|e| anyhow::anyhow!("Invalid --proxy URL '{}': {}", raw, e))?; return Ok(Self { url: Some(url), source: Some(ProxySource::Flag) }); } // Walk the env vars in priority order for var in &["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"] { if let Ok(val) = std::env::var(var) { if val.is_empty() { continue; } let url = Url::parse(&val) .map_err(|e| anyhow::anyhow!("Invalid {} env var '{}': {}", var, val, e))?; return Ok(Self { url: Some(url), source: Some(ProxySource::Env(var.to_string())), }); } } Ok(Self { url: None, source: None }) } /// True when the user explicitly passed --proxy (as opposed to env-var pickup). /// Used for distinguishing "you asked for this proxy and it failed" from /// "we picked up an ambient proxy from your environment" in error paths. #[allow(dead_code)] pub fn is_explicit(&self) -> bool { matches!(self.source, Some(ProxySource::Flag)) } /// Human-readable description of where the proxy came from, for status output. pub fn source_description(&self) -> String { match &self.source { Some(ProxySource::Flag) => "--proxy flag".to_string(), Some(ProxySource::Env(var)) => format!("{} env var", var), None => "none".to_string(), } } /// Print a one-line proxy status line (used in setup output). pub fn print_status(&self) { match &self.url { Some(url) => println!( " {} Proxy: {} ({})", "✓".green(), url.as_str().bright_white(), self.source_description() ), None => println!( " {} Proxy: direct (no proxy)", "○".bright_black() ), } } /// Build a reqwest blocking Client that honours this proxy config. /// /// * `Some(url)` → all HTTP/HTTPS traffic goes through that proxy. /// * `None` → direct, no proxy at all. /// /// We always go through the builder (never plain `Client::new()`) because /// `Client::new()` silently picks up env-var proxies. When the user says /// `--no-proxy` we need to actively disable that. pub fn client(&self) -> anyhow::Result { let mut builder = blocking::ClientBuilder::new(); match &self.url { Some(url) => { builder = builder.proxy(reqwest::Proxy::all(url.clone())?); } None => { // Actively suppress env-var auto-detection. builder = builder.no_proxy(); } } builder.build().map_err(|e| anyhow::anyhow!("Failed to build HTTP client: {}", e)) } } /// RAII guard that temporarily sets HTTPS_PROXY / HTTP_PROXY for the lifetime /// of the guard, then restores the previous values on drop. /// /// libgit2 (the C library behind the `git2` crate) reads these env vars /// directly for its HTTP transport. This is the cleanest way to make /// `git2::Repository::clone` and `remote.fetch()` honour a `--proxy` flag /// without touching the user's global `~/.gitconfig`. /// /// When the ProxyConfig has no URL (direct / --no-proxy) the guard *clears* /// the vars so libgit2 won't accidentally pick up an ambient proxy the user /// didn't intend for this operation. pub struct GitProxyGuard { prev_https: Option, prev_http: Option, } impl GitProxyGuard { pub fn new(config: &ProxyConfig) -> Self { let prev_https = std::env::var("HTTPS_PROXY").ok(); let prev_http = std::env::var("HTTP_PROXY").ok(); match &config.url { Some(url) => { let s = url.as_str(); std::env::set_var("HTTPS_PROXY", s); std::env::set_var("HTTP_PROXY", s); } None => { std::env::remove_var("HTTPS_PROXY"); std::env::remove_var("HTTP_PROXY"); } } Self { prev_https, prev_http } } } impl Drop for GitProxyGuard { fn drop(&mut self) { match &self.prev_https { Some(v) => std::env::set_var("HTTPS_PROXY", v), None => std::env::remove_var("HTTPS_PROXY"), } match &self.prev_http { Some(v) => std::env::set_var("HTTP_PROXY", v), None => std::env::remove_var("HTTP_PROXY"), } } } /// Print clear, actionable instructions for obtaining dependencies without /// an internet connection. Called whenever a network download fails so the /// user always sees their escape hatch. pub fn print_offline_instructions() { println!(); println!("{}", "── Offline / Air-Gapped Installation ──────────────────────".bright_yellow().bold()); println!(); println!("If you have no internet access (or the proxy is not working),"); println!("you can obtain the required SDKs on a connected machine and"); println!("copy them over."); println!(); println!("{}", "1. FTC SDK (git clone)".bright_cyan().bold()); println!(" On a connected machine:"); println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git"); println!(" cd FtcRobotController"); println!(" git checkout v10.1.1"); println!(); println!(" Copy the entire FtcRobotController/ directory to this machine"); println!(" at ~/.weevil/ftc-sdk/ (or wherever your .weevil.toml points)."); println!(); println!("{}", "2. Android SDK (command-line tools)".bright_cyan().bold()); println!(" Download the zip for your OS from a connected machine:"); println!(" Linux: https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"); println!(" macOS: https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip"); println!(" Windows: https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip"); println!(); println!(" Extract to ~/.weevil/android-sdk/, then run sdkmanager:"); println!(" ./cmdline-tools/latest/bin/sdkmanager \\"); println!(" platform-tools platforms;android-34 build-tools;34.0.0"); println!(); println!(" Copy the resulting android-sdk/ directory to this machine."); println!(); println!("{}", "3. Gradle distribution".bright_cyan().bold()); println!(" Gradle fetches its own distribution the first time ./gradlew"); println!(" runs. If that fails offline, download manually:"); println!(" https://services.gradle.org/distributions/gradle-8.9-bin.zip"); println!(" Extract into ~/.gradle/wrapper/dists/gradle-8.9-bin/"); println!(" (the exact subdirectory is printed by gradlew on failure)."); println!(); println!("{}", "4. Proxy quick reference".bright_cyan().bold()); println!(" • Use a specific proxy: weevil --proxy http://proxy:3128 sdk install"); println!(" • Skip the proxy entirely: weevil --no-proxy sdk install"); println!(" • Gradle also reads HTTPS_PROXY / HTTP_PROXY, so set those"); println!(" in your shell before running ./gradlew if the build needs a proxy."); println!(); } #[cfg(test)] mod tests { use super::*; use std::sync::Mutex; // Env vars are process-global. cargo test runs tests in parallel within // a single binary, so any test that sets/removes HTTPS_PROXY or HTTP_PROXY // must hold this lock for its entire duration. static ENV_MUTEX: Mutex<()> = Mutex::new(()); #[test] fn no_proxy_flag_forces_direct() { let _env_lock = ENV_MUTEX.lock().unwrap(); std::env::set_var("HTTPS_PROXY", "http://proxy.example.com:3128"); let config = ProxyConfig::resolve(None, true).unwrap(); assert!(config.url.is_none()); std::env::remove_var("HTTPS_PROXY"); } #[test] fn explicit_flag_overrides_env() { let _env_lock = ENV_MUTEX.lock().unwrap(); std::env::set_var("HTTPS_PROXY", "http://env-proxy.example.com:3128"); let config = ProxyConfig::resolve(Some("http://flag-proxy.example.com:8080"), false).unwrap(); assert_eq!(config.url.as_ref().unwrap().host_str(), Some("flag-proxy.example.com")); assert!(config.is_explicit()); std::env::remove_var("HTTPS_PROXY"); } #[test] fn picks_up_env_var() { let _env_lock = ENV_MUTEX.lock().unwrap(); std::env::remove_var("HTTPS_PROXY"); std::env::remove_var("https_proxy"); std::env::set_var("HTTP_PROXY", "http://env-proxy.example.com:3128"); let config = ProxyConfig::resolve(None, false).unwrap(); assert_eq!(config.url.as_ref().unwrap().host_str(), Some("env-proxy.example.com")); assert_eq!(config.source, Some(ProxySource::Env("HTTP_PROXY".to_string()))); std::env::remove_var("HTTP_PROXY"); } #[test] fn direct_when_nothing_set() { let _env_lock = ENV_MUTEX.lock().unwrap(); std::env::remove_var("HTTPS_PROXY"); std::env::remove_var("https_proxy"); std::env::remove_var("HTTP_PROXY"); std::env::remove_var("http_proxy"); let config = ProxyConfig::resolve(None, false).unwrap(); assert!(config.url.is_none()); } #[test] fn rejects_garbage_url() { let result = ProxyConfig::resolve(Some("not a url at all"), false); assert!(result.is_err()); } #[test] fn client_builds_with_proxy() { let config = ProxyConfig::resolve(Some("http://proxy.example.com:3128"), false).unwrap(); assert!(config.client().is_ok()); } #[test] fn client_builds_direct() { let _env_lock = ENV_MUTEX.lock().unwrap(); std::env::remove_var("HTTPS_PROXY"); std::env::remove_var("https_proxy"); std::env::remove_var("HTTP_PROXY"); std::env::remove_var("http_proxy"); let config = ProxyConfig::resolve(None, true).unwrap(); assert!(config.client().is_ok()); } #[test] fn git_proxy_guard_sets_and_restores() { let _env_lock = ENV_MUTEX.lock().unwrap(); std::env::set_var("HTTPS_PROXY", "http://original:1111"); std::env::set_var("HTTP_PROXY", "http://original:2222"); let config = ProxyConfig::resolve(Some("http://guarded:9999"), false).unwrap(); { let _guard = GitProxyGuard::new(&config); // Url::parse normalises — trailing slash is expected assert_eq!(std::env::var("HTTPS_PROXY").unwrap(), "http://guarded:9999/"); assert_eq!(std::env::var("HTTP_PROXY").unwrap(), "http://guarded:9999/"); } assert_eq!(std::env::var("HTTPS_PROXY").unwrap(), "http://original:1111"); assert_eq!(std::env::var("HTTP_PROXY").unwrap(), "http://original:2222"); std::env::remove_var("HTTPS_PROXY"); std::env::remove_var("HTTP_PROXY"); } #[test] fn git_proxy_guard_clears_for_direct() { let _env_lock = ENV_MUTEX.lock().unwrap(); std::env::set_var("HTTPS_PROXY", "http://should-be-cleared:1111"); std::env::set_var("HTTP_PROXY", "http://should-be-cleared:2222"); let config = ProxyConfig { url: None, source: None }; { let _guard = GitProxyGuard::new(&config); assert!(std::env::var("HTTPS_PROXY").is_err()); assert!(std::env::var("HTTP_PROXY").is_err()); } assert_eq!(std::env::var("HTTPS_PROXY").unwrap(), "http://should-be-cleared:1111"); assert_eq!(std::env::var("HTTP_PROXY").unwrap(), "http://should-be-cleared:2222"); std::env::remove_var("HTTPS_PROXY"); std::env::remove_var("HTTP_PROXY"); } }