diff --git a/Cargo.lock b/Cargo.lock index 2e7f6dbdc..7f67fadfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,12 @@ version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -1334,6 +1340,17 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1526,6 +1543,7 @@ dependencies = [ "rustls-pki-types", "serde", "serde_json", + "socks", "url", "webpki-roots 0.26.11", ] @@ -1656,6 +1674,28 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" diff --git a/crates/context/Cargo.toml b/crates/context/Cargo.toml index bc370e250..81fb7a352 100644 --- a/crates/context/Cargo.toml +++ b/crates/context/Cargo.toml @@ -20,7 +20,7 @@ regex = { workspace = true } once_cell = { workspace = true } sha2 = "0.10" flate2 = { version = "1", default-features = false, features = ["zlib-rs"] } -ureq = { version = "2", default-features = false, features = ["tls", "json"] } +ureq = { version = "2", default-features = false, features = ["tls", "json", "socks-proxy"] } rustls-native-certs = "0.8" webpki-roots = "1" tiny_http = "0.12" diff --git a/crates/context/src/generate_image.rs b/crates/context/src/generate_image.rs index 62d212b6f..dec59049c 100644 --- a/crates/context/src/generate_image.rs +++ b/crates/context/src/generate_image.rs @@ -485,6 +485,7 @@ mod tests { use std::time::Duration; fn round_trip(edit: bool, override_model: Option<&str>, background: Option<&str>) { + let _proxy_lock = crate::http::PROXY_ENV_LOCK.lock().unwrap(); let server = tiny_http::Server::http("127.0.0.1:0").unwrap(); let api_base = format!("http://{}", server.server_addr()); let temp = std::env::temp_dir().join(format!("impeccable-image-{}-{}", std::process::id(), server.server_addr().to_ip().unwrap().port())); diff --git a/crates/context/src/http.rs b/crates/context/src/http.rs index c5423984d..e040c8580 100644 --- a/crates/context/src/http.rs +++ b/crates/context/src/http.rs @@ -15,18 +15,30 @@ //! fails to load, verifies against the bundled roots exactly as before. //! `SSL_CERT_FILE` / `SSL_CERT_DIR` stand in for the OS store, as they do //! for OpenSSL and curl; the bundled roots stay either way. +//! +//! The shared agent builder also honors `ALL_PROXY`, `HTTPS_PROXY`, and +//! `HTTP_PROXY` (and their lowercase forms) so `update` and `install` work +//! behind a corporate proxy (#823). Live-mode localhost HTTP does not use +//! this builder. The `socks-proxy` feature is enabled because ureq 2.x +//! prefers `ALL_PROXY`, which is often `socks5://`. We opt in on this +//! builder only, not globally via ureq's `proxy-from-env` feature. use std::sync::Arc; +#[cfg(test)] +pub(crate) static PROXY_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + use once_cell::sync::Lazy; use ureq::rustls::pki_types::CertificateDer; use ureq::rustls::{self, ClientConfig, RootCertStore}; -/// `ureq::AgentBuilder::new()` with the engine's trust store installed. -/// Every HTTPS call site builds its agent from this; the plain-HTTP calls -/// to the live server on localhost do not need it. +/// `ureq::AgentBuilder::new()` with the engine's trust store installed and +/// env proxy vars honored. Every HTTPS call site builds its agent from this; +/// the plain-HTTP calls to the live server on localhost do not use it. pub fn agent_builder() -> ureq::AgentBuilder { - ureq::AgentBuilder::new().tls_config(tls_config()) + ureq::AgentBuilder::new() + .tls_config(tls_config()) + .try_proxy_from_env(true) } fn tls_config() -> Arc { @@ -99,6 +111,104 @@ tB0WGTOG3QIgdJa8gBPU9Y6WsrursItsnUeGTYHKDCZZ6MjlekLFuoc= fn agent_builds_from_this_hosts_store() { // Runs the real rustls-native-certs load: it must not panic, and the // shared config must be accepted by a ureq agent. + let _lock = PROXY_ENV_LOCK.lock().unwrap(); let _agent = agent_builder().build(); } + + #[test] + fn agent_honors_http_proxy_from_env() { + use std::io::{Read, Write}; + use std::net::TcpListener; + use std::sync::MutexGuard; + use std::time::Duration; + + struct ProxyEnvGuard { + saved: Vec<(String, Option)>, + } + + impl ProxyEnvGuard { + fn set(vars: &[(&str, Option<&str>)]) -> Self { + let saved = vars + .iter() + .map(|(key, _)| (key.to_string(), std::env::var(key).ok())) + .collect(); + for (key, value) in vars { + match value { + // SAFETY: PROXY_ENV_LOCK serializes every test that + // reads or writes these process-global proxy vars. + Some(v) => unsafe { std::env::set_var(key, v) }, + None => unsafe { std::env::remove_var(key) }, + } + } + Self { saved } + } + } + + impl Drop for ProxyEnvGuard { + fn drop(&mut self) { + for (key, value) in &self.saved { + match value { + // SAFETY: same lock as set(); restore before unlock. + Some(v) => unsafe { std::env::set_var(key, v) }, + None => unsafe { std::env::remove_var(key) }, + } + } + } + } + + let _lock: MutexGuard<'_, ()> = PROXY_ENV_LOCK.lock().unwrap(); + + let listener = TcpListener::bind("127.0.0.1:0").expect("bind proxy listener"); + let proxy_addr = listener.local_addr().expect("proxy listener addr"); + + let request = std::sync::Arc::new(std::sync::Mutex::new(Vec::::new())); + let request_for_thread = request.clone(); + let handle = std::thread::spawn(move || { + listener + .set_nonblocking(true) + .expect("nonblocking proxy listener"); + let deadline = std::time::Instant::now() + Duration::from_secs(5); + while std::time::Instant::now() < deadline { + let Ok((mut stream, _)) = listener.accept() else { + std::thread::sleep(Duration::from_millis(10)); + continue; + }; + + let mut buf = [0u8; 4096]; + let n = stream.read(&mut buf).unwrap_or(0); + let chunk = &buf[..n]; + if !chunk.is_empty() && String::from_utf8_lossy(chunk).contains("proxy-test.invalid") { + request_for_thread.lock().unwrap().extend_from_slice(chunk); + let response = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok"; + let _ = stream.write_all(response); + return; + } + } + }); + + let proxy_url = format!("http://127.0.0.1:{}", proxy_addr.port()); + let _env_guard = ProxyEnvGuard::set(&[ + ("ALL_PROXY", None), + ("all_proxy", None), + ("HTTPS_PROXY", None), + ("https_proxy", None), + ("HTTP_PROXY", Some(&proxy_url)), + ("http_proxy", Some(&proxy_url)), + ]); + + let agent = agent_builder() + .timeout(Duration::from_secs(2)) + .build(); + let response = agent.get("http://proxy-test.invalid/").call(); + assert!(response.is_ok(), "expected proxy-routed GET to succeed"); + + handle.join().expect("proxy thread"); + + let request_bytes = request.lock().unwrap().clone(); + let request_text = String::from_utf8_lossy(&request_bytes); + assert!( + request_text.contains("proxy-test.invalid"), + "proxy should receive request for target host, got: {request_text:?}" + ); + } }