diff --git a/CHANGELOG.md b/CHANGELOG.md index 18cebafd..9965bd6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 0.1.19 (2025-12-03) + +- Add `client::pool` module for composable pools. Enable with the `client-pool` feature. +- Add `pool::singleton` for sharing a single cloneable connection. +- Add `pool::cache` for caching a list of connections. +- Add `pool::negotiate` for combining two pools with upgrade and fallback negotiation. +- Add `pool::map` for customizable mapping of keys and connections. + # 0.1.18 (2025-11-13) - Fix `rt::TokioTimer` to support Tokio's paused time. diff --git a/Cargo.toml b/Cargo.toml index 525b15ee..d13bf7e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hyper-util" -version = "0.1.18" +version = "0.1.19" description = "hyper utilities" readme = "README.md" homepage = "https://hyper.rs" @@ -77,7 +77,7 @@ full = [ client = ["hyper/client", "tokio/net", "dep:tracing", "dep:futures-channel", "dep:tower-service"] client-legacy = ["client", "dep:socket2", "tokio/sync", "dep:libc", "dep:futures-util"] -client-pool = ["dep:futures-util", "dep:tower-layer"] +client-pool = ["client", "dep:futures-util", "dep:tower-layer"] client-proxy = ["client", "dep:base64", "dep:ipnet", "dep:percent-encoding"] client-proxy-system = ["dep:system-configuration", "dep:windows-registry"] diff --git a/src/client/conn.rs b/src/client/conn.rs index f2e2ef99..61ffe63b 100644 --- a/src/client/conn.rs +++ b/src/client/conn.rs @@ -1,4 +1,8 @@ -//! todo +//! Tower layers and services for HTTP/1 and HTTP/2 client connections. +//! +//! This module provides Tower-compatible layers that wrap Hyper's low-level +//! HTTP client connection types, making them easier to compose with other +//! middleware and connection pooling strategies. use std::future::Future; use std::marker::PhantomData; @@ -12,14 +16,39 @@ use crate::common::future::poll_fn; type BoxError = Box; -/// todo +/// A Tower [`Layer`](tower_layer::Layer) for creating HTTP/1 client connections. +/// +/// This layer wraps a connection service (typically a TCP or TLS connector) and +/// performs the HTTP/1 handshake, producing an [`Http1ClientService`] that can +/// send requests. +/// +/// Use [`http1()`] to create a layer with default settings, or construct from +/// a [`hyper::client::conn::http1::Builder`] for custom configuration. +/// +/// # Example +/// +/// ```ignore +/// use hyper_util::client::conn::http1; +/// use hyper::{client::connect::HttpConnector, body::Bytes}; +/// use tower:: ServiceBuilder; +/// use http_body_util::Empty; +/// +/// let connector = HttpConnector::new(); +/// let layer: Http1Layer> = http1(); +/// let client = ServiceBuilder::new() +/// .layer(layer) +/// .service(connector); +/// ``` #[cfg(feature = "http1")] pub struct Http1Layer { builder: hyper::client::conn::http1::Builder, _body: PhantomData, } -/// todo +/// Creates an [`Http1Layer`] with default HTTP/1 settings. +/// +/// For custom settings, construct an [`Http1Layer`] from a +/// [`hyper::client::conn::http1::Builder`] using `.into()`. #[cfg(feature = "http1")] pub fn http1() -> Http1Layer { Http1Layer { @@ -60,7 +89,11 @@ impl From for Http1Layer { } } -/// todo +/// A Tower [`Service`] that establishes HTTP/1 connections. +/// +/// This service wraps an underlying connection service (e.g., TCP or TLS) and +/// performs the HTTP/1 handshake when called. The resulting service can be used +/// to send HTTP requests over the established connection. #[cfg(feature = "http1")] pub struct Http1Connect { inner: M, @@ -118,21 +151,56 @@ impl Clone for Http1Connect { } } -/// todo +/// A Tower [`Layer`](tower_layer::Layer) for creating HTTP/2 client connections. +/// +/// This layer wraps a connection service (typically a TCP or TLS connector) and +/// performs the HTTP/2 handshake, producing an [`Http2ClientService`] that can +/// send requests. +/// +/// Use [`http2()`] to create a layer with a specific executor, or construct from +/// a [`hyper::client::conn::http2::Builder`] for custom configuration. +/// +/// # Example +/// +/// ```ignore +/// use hyper_util::client::conn::http2; +/// use hyper::{client::connect::HttpConnector, body::Bytes}; +/// use tower:: ServiceBuilder; +/// use http_body_util::Empty; +/// +/// let connector = HttpConnector::new(); +/// let layer: Http2Layer> = http2(); +/// let client = ServiceBuilder::new() +/// .layer(layer) +/// .service(connector); +/// ``` #[cfg(feature = "http2")] -pub struct Http2Layer { +pub struct Http2Layer { + builder: hyper::client::conn::http2::Builder, _body: PhantomData, } -/// todo +/// Creates an [`Http2Layer`] with default HTTP/1 settings. +/// +/// For custom settings, construct an [`Http2Layer`] from a +/// [`hyper::client::conn::http2::Builder`] using `.into()`. #[cfg(feature = "http2")] -pub fn http2() -> Http2Layer { - Http2Layer { _body: PhantomData } +pub fn http2(executor: E) -> Http2Layer +where + E: Clone, +{ + Http2Layer { + builder: hyper::client::conn::http2::Builder::new(executor), + _body: PhantomData, + } } #[cfg(feature = "http2")] -impl tower_layer::Layer for Http2Layer { - type Service = Http2Connect; +impl tower_layer::Layer for Http2Layer +where + E: Clone, +{ + type Service = Http2Connect; fn layer(&self, inner: M) -> Self::Service { Http2Connect { inner, @@ -143,25 +211,40 @@ impl tower_layer::Layer for Http2Layer { } #[cfg(feature = "http2")] -impl Clone for Http2Layer { +impl Clone for Http2Layer { fn clone(&self) -> Self { Self { + builder: self.builder.clone(), _body: self._body.clone(), } } } -/// todo +#[cfg(feature = "http2")] +impl From> for Http2Layer { + fn from(builder: hyper::client::conn::http2::Builder) -> Self { + Self { + builder, + _body: PhantomData, + } + } +} + +/// A Tower [`Service`] that establishes HTTP/2 connections. +/// +/// This service wraps an underlying connection service (e.g., TCP or TLS) and +/// performs the HTTP/2 handshake when called. The resulting service can be used +/// to send HTTP requests over the established connection. #[cfg(feature = "http2")] #[derive(Debug)] -pub struct Http2Connect { +pub struct Http2Connect { inner: M, - builder: hyper::client::conn::http2::Builder, + builder: hyper::client::conn::http2::Builder, _body: PhantomData, } #[cfg(feature = "http2")] -impl Service for Http2Connect +impl Service for Http2Connect where M: Service, M::Future: Send + 'static, @@ -170,6 +253,7 @@ where B: hyper::body::Body + Unpin + Send + 'static, B::Data: Send + 'static, B::Error: Into, + E: hyper::rt::bounds::Http2ClientConnExec + Unpin + Clone + Send + 'static, { type Response = Http2ClientService; type Error = BoxError; @@ -199,7 +283,7 @@ where } #[cfg(feature = "http2")] -impl Clone for Http2Connect { +impl Clone for Http2Connect { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -209,7 +293,14 @@ impl Clone for Http2Connect { } } -/// A thin adapter over hyper HTTP/1 client SendRequest. +/// A Tower [`Service`] that sends HTTP/1 requests over an established connection. +/// +/// This is a thin wrapper around [`hyper::client::conn::http1::SendRequest`] that implements +/// the Tower `Service` trait, making it composable with other Tower middleware. +/// +/// The service maintains a single HTTP/1 connection and can be used to send multiple +/// sequential requests. For concurrent requests or connection pooling, wrap this service +/// with appropriate middleware. #[cfg(feature = "http1")] #[derive(Debug)] pub struct Http1ClientService { @@ -218,7 +309,10 @@ pub struct Http1ClientService { #[cfg(feature = "http1")] impl Http1ClientService { - /// todo + /// Constructs a new HTTP/1 client service from a Hyper `SendRequest`. + /// + /// Typically you won't call this directly; instead, use [`Http1Connect`] to + /// establish connections and produce this service. pub fn new(tx: hyper::client::conn::http1::SendRequest) -> Self { Self { tx } } @@ -248,7 +342,14 @@ where } } -/// todo +/// A Tower [`Service`] that sends HTTP/2 requests over an established connection. +/// +/// This is a thin wrapper around [`hyper::client::conn::http2::SendRequest`] that implements +/// the Tower `Service` trait, making it composable with other Tower middleware. +/// +/// The service maintains a single HTTP/2 connection and supports multiplexing multiple +/// concurrent requests over that connection. The service can be cloned to send requests +/// concurrently, or used with the [`Singleton`](crate::client::pool::singleton::Singleton) pool service. #[cfg(feature = "http2")] #[derive(Debug)] pub struct Http2ClientService { @@ -257,7 +358,10 @@ pub struct Http2ClientService { #[cfg(feature = "http2")] impl Http2ClientService { - /// todo + /// Constructs a new HTTP/2 client service from a Hyper `SendRequest`. + /// + /// Typically you won't call this directly; instead, use [`Http2Connect`] to + /// establish connections and produce this service. pub fn new(tx: hyper::client::conn::http2::SendRequest) -> Self { Self { tx } } diff --git a/src/client/legacy/connect/http.rs b/src/client/legacy/connect/http.rs index 56324e79..0236cc85 100644 --- a/src/client/legacy/connect/http.rs +++ b/src/client/legacy/connect/http.rs @@ -397,7 +397,7 @@ impl HttpConnector { /// - macOS, iOS, visionOS, watchOS, and tvOS /// /// [VRF]: https://www.kernel.org/doc/Documentation/networking/vrf.txt - /// [`man 7 socket`] https://man7.org/linux/man-pages/man7/socket.7.html + /// [`man 7 socket`]: https://man7.org/linux/man-pages/man7/socket.7.html /// [`man 7p ip`]: https://docs.oracle.com/cd/E86824_01/html/E54777/ip-7p.html #[cfg(any( target_os = "android", diff --git a/src/client/pool/map.rs b/src/client/pool/map.rs index 83b56480..8ff4179d 100644 --- a/src/client/pool/map.rs +++ b/src/client/pool/map.rs @@ -48,7 +48,7 @@ where // impl Map impl Map { - /// Create a [`Builder`] to configure a new `Map`. + /// Create a `Builder` to configure a new `Map`. pub fn builder() -> builder::Builder { builder::Builder::new() diff --git a/src/client/pool/mod.rs b/src/client/pool/mod.rs index 9f1a8fce..fd505416 100644 --- a/src/client/pool/mod.rs +++ b/src/client/pool/mod.rs @@ -1,4 +1,8 @@ //! Composable pool services +//! +//! This module contains various concepts of a connection pool separated into +//! their own concerns. This allows for users to compose the layers, along with +//! any other layers, when constructing custom connection pools. pub mod cache; pub mod map; diff --git a/src/client/proxy/matcher.rs b/src/client/proxy/matcher.rs index a883133e..41990089 100644 --- a/src/client/proxy/matcher.rs +++ b/src/client/proxy/matcher.rs @@ -515,9 +515,15 @@ impl DomainMatcher { fn contains(&self, domain: &str) -> bool { let domain_len = domain.len(); for d in &self.0 { - if d == domain || d.strip_prefix('.') == Some(domain) { + if d.eq_ignore_ascii_case(domain) + || d.strip_prefix('.') + .map_or(false, |s| s.eq_ignore_ascii_case(domain)) + { return true; - } else if domain.ends_with(d) { + } else if domain + .get(domain_len.saturating_sub(d.len())..) + .map_or(false, |s| s.eq_ignore_ascii_case(d)) + { if d.starts_with('.') { // If the first character of d is a dot, that means the first character of domain // must also be a dot, so we are looking at a subdomain of d and that matches @@ -698,13 +704,19 @@ mod tests { // domains match with leading `.` assert!(matcher.contains("foo.bar")); + assert!(matcher.contains("FOO.BAR")); + // subdomains match with leading `.` assert!(matcher.contains("www.foo.bar")); + assert!(matcher.contains("WWW.FOO.BAR")); // domains match with no leading `.` assert!(matcher.contains("bar.foo")); + assert!(matcher.contains("Bar.foo")); + // subdomains match with no leading `.` assert!(matcher.contains("www.bar.foo")); + assert!(matcher.contains("WWW.BAR.FOO")); // non-subdomain string prefixes don't match assert!(!matcher.contains("notfoo.bar")); @@ -866,4 +878,48 @@ mod tests { assert!(m.intercept(&"http://rick.roll".parse().unwrap()).is_none()); } + + #[test] + fn test_domain_matcher_case_insensitive() { + let domains = vec![".foo.bar".into()]; + let matcher = DomainMatcher(domains); + + assert!(matcher.contains("foo.bar")); + assert!(matcher.contains("FOO.BAR")); + assert!(matcher.contains("Foo.Bar")); + + assert!(matcher.contains("www.foo.bar")); + assert!(matcher.contains("WWW.FOO.BAR")); + assert!(matcher.contains("Www.Foo.Bar")); + } + + #[test] + fn test_no_proxy_case_insensitive() { + let p = p! { + all = "http://proxy.local", + no = ".example.com", + }; + + // should bypass proxy (case insensitive match) + assert!(p + .intercept(&"http://example.com".parse().unwrap()) + .is_none()); + assert!(p + .intercept(&"http://EXAMPLE.COM".parse().unwrap()) + .is_none()); + assert!(p + .intercept(&"http://Example.com".parse().unwrap()) + .is_none()); + + // subdomain should bypass proxy (case insensitive match) + assert!(p + .intercept(&"http://www.example.com".parse().unwrap()) + .is_none()); + assert!(p + .intercept(&"http://WWW.EXAMPLE.COM".parse().unwrap()) + .is_none()); + assert!(p + .intercept(&"http://Www.Example.Com".parse().unwrap()) + .is_none()); + } }