diff --git a/crates/tinytools-std/src/network/test_support.rs b/crates/tinytools-std/src/network/test_support.rs index 33ead40..8425aaa 100644 --- a/crates/tinytools-std/src/network/test_support.rs +++ b/crates/tinytools-std/src/network/test_support.rs @@ -46,6 +46,7 @@ pub struct TestNetGate { used: AtomicUsize, local_only: bool, disclosed: Mutex>, + prepared: Mutex>, } impl TestNetGate { @@ -62,6 +63,7 @@ impl TestNetGate { used: AtomicUsize::new(0), local_only: false, disclosed: Mutex::new(Vec::new()), + prepared: Mutex::new(Vec::new()), }) } @@ -73,6 +75,7 @@ impl TestNetGate { used: AtomicUsize::new(0), local_only: true, disclosed: Mutex::new(Vec::new()), + prepared: Mutex::new(Vec::new()), }) } @@ -80,6 +83,11 @@ impl TestNetGate { pub fn disclosed(&self) -> Vec<(String, bool, bool)> { self.disclosed.lock().unwrap().clone() } + + /// The service names `prepare_client` was called with, in order. + pub fn prepared(&self) -> Vec { + self.prepared.lock().unwrap().clone() + } } impl NetGate for TestNetGate { @@ -114,9 +122,10 @@ impl NetGate for TestNetGate { fn prepare_client( &self, - _service: &str, + service: &str, builder: reqwest::ClientBuilder, ) -> reqwest::ClientBuilder { + self.prepared.lock().unwrap().push(service.to_string()); builder } diff --git a/crates/tinytools-std/src/network/web_fetch.rs b/crates/tinytools-std/src/network/web_fetch.rs index ba45a46..b55a3cf 100644 --- a/crates/tinytools-std/src/network/web_fetch.rs +++ b/crates/tinytools-std/src/network/web_fetch.rs @@ -361,11 +361,13 @@ impl WebFetchTool { // redirects by default, and a redirect target may be on a host // outside the allowed-domains list. We surface 3xx responses to // the caller so they can decide whether to refetch the new URL. - let client = match reqwest::Client::builder() + let builder = reqwest::Client::builder() .timeout(Duration::from_secs(self.timeout_secs)) .user_agent(USER_AGENT) - .redirect(reqwest::redirect::Policy::none()) - // Connect only to the vetted addresses; the URL keeps its host name. + .redirect(reqwest::redirect::Policy::none()); + let builder = self.gate.prepare_client("tool.web_fetch", builder); + // Connect only to the vetted addresses; the URL keeps its host name. + let client = match builder .resolve_to_addrs(&target.host, &target.addrs) .build() { @@ -672,3 +674,7 @@ pub use tinytools::status::web_fetch_status as status_of; #[cfg(test)] #[path = "web_fetch_tests.rs"] mod tests; + +#[cfg(test)] +#[path = "web_fetch_proxy_tests.rs"] +mod proxy_tests; diff --git a/crates/tinytools-std/src/network/web_fetch_proxy_tests.rs b/crates/tinytools-std/src/network/web_fetch_proxy_tests.rs new file mode 100644 index 0000000..a0a2eea --- /dev/null +++ b/crates/tinytools-std/src/network/web_fetch_proxy_tests.rs @@ -0,0 +1,36 @@ +//! web_fetch builds its client through the host's `prepare_client`, so the +//! host's proxy settings apply to it as they do to http_request and curl. + +#![allow(clippy::unwrap_used)] + +use super::*; +use crate::network::test_support::{DEFAULT_LIMITS, TestHtml, TestNetGate, pinned_to_local}; + +#[tokio::test] +async fn web_fetch_builds_its_client_through_prepare_client() { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tokio::spawn(async move { + let (mut socket, _) = listener.accept().await.unwrap(); + let _ = socket.read(&mut [0u8; 8192]).await; + let _ = socket + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok") + .await; + }); + let gate = TestNetGate::supervised(); + let tool = WebFetchTool::new( + gate.clone(), + vec![], + None, + None, + DEFAULT_LIMITS, + Arc::new(TestHtml), + ); + let result = tool + .fetch_validated(&pinned_to_local(&format!("http://{addr}/p")), 1_000, false) + .await + .unwrap(); + assert!(!result.is_error, "{}", result.output()); + assert_eq!(gate.prepared(), vec!["tool.web_fetch".to_string()]); +}