diff --git a/src/client/http.rs b/src/client/http.rs index e4791427..b1245fea 100644 --- a/src/client/http.rs +++ b/src/client/http.rs @@ -6,6 +6,7 @@ use std::time::Duration; use std::{collections::HashMap, convert::TryInto, net::SocketAddr}; use std::{fmt, str}; +use crate::util::client::{InnerRequest, NetworkScheme}; use crate::util::{ self, client::connect::HttpConnector, client::Builder, common::Exec, rt::TokioExecutor, }; @@ -24,7 +25,7 @@ use std::task::{Context, Poll}; use tokio::time::Sleep; use super::decoder::Accepts; -use super::request::{InnerRequest, Request, RequestBuilder}; +use super::request::{Request, RequestBuilder}; use super::response::Response; use super::Body; use crate::connect::Connector; @@ -204,8 +205,6 @@ impl ClientBuilder { if config.auto_sys_proxy { proxies.push(Proxy::system()); } - let proxies = Arc::new(proxies); - let proxies_maybe_http_auth = proxies.iter().any(|p| p.maybe_has_http_auth()); let mut connector = { @@ -233,17 +232,7 @@ impl ClientBuilder { http.set_connect_timeout(config.connect_timeout); let tls = BoringTlsConnector::new(config.tls)?; - Connector::new_boring_tls( - http, - tls, - proxies, - config.local_address_ipv4, - config.local_address_ipv6, - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - config.interface, - config.nodelay, - config.tls_info, - ) + Connector::new_boring_tls(http, tls, config.nodelay, config.tls_info) }; connector.set_timeout(config.connect_timeout); @@ -272,6 +261,12 @@ impl ClientBuilder { proxies_maybe_http_auth, base_url: config.base_url.map(Arc::new), http2_max_retry_count: config.http2_max_retry_count, + + proxies, + local_addr_v4: config.local_address_ipv4, + local_addr_v6: config.local_address_ipv6, + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + interface: config.interface, }), }) } @@ -1303,15 +1298,14 @@ impl Client { self.inner.proxy_auth(&uri, &mut headers); let in_flight = { - let pool_key = self.inner.hyper.pool_key(&uri); - let req = InnerRequest::new() + let req = InnerRequest::::builder() + .network_scheme(self.inner.network_scheme(&uri, None)) .uri(uri) .method(method.clone()) .version(version) .headers(headers.clone()) .headers_order(self.inner.headers_order.as_deref()) - .pool_key(pool_key) - .build(body); + .body(body); ResponseFuture::Default(self.inner.hyper.request(req)) }; @@ -1411,33 +1405,31 @@ impl Client { /// /// Returns the old proxies. #[inline] - pub fn set_proxies(&mut self, proxies: impl Into>) -> Vec { - let (inner, proxies) = self.apply_proxies(proxies); - inner.set_proxies(proxies) + pub fn set_proxies(&mut self, proxies: impl Into>) { + self.apply_proxies(proxies, true); } /// Append the proxies to the client. #[inline] pub fn append_proxies(&mut self, proxies: impl Into>) { - let (inner, proxies) = self.apply_proxies(proxies); - inner.append_proxies(proxies); - } - - /// Private helper to handle setting or appending proxies. - fn apply_proxies( - &mut self, - proxies: impl Into>, - ) -> (&mut HyperClient, Cow<'static, [Proxy]>) { - let proxies = proxies.into(); - let inner = self.inner_mut(); - inner.proxies_maybe_http_auth = proxies.iter().any(|p| p.maybe_has_http_auth()); - (&mut inner.hyper, proxies) + self.apply_proxies(proxies, false); } /// Unset the proxies for this client. #[inline] pub fn unset_proxies(&mut self) { - self.inner_mut().hyper.clear_proxies(); + self.inner_mut().proxies.clear(); + } + + /// Private helper to handle setting or appending proxies. + fn apply_proxies(&mut self, proxies: impl Into>, r#override: bool) { + let inner = self.inner_mut(); + let proxies = proxies.into(); + inner.proxies_maybe_http_auth = proxies.iter().any(|p| p.maybe_has_http_auth()); + if r#override { + inner.proxies.clear(); + } + inner.proxies.extend(proxies.into_owned()); } /// Set that all sockets are bound to the configured address before connection. @@ -1450,23 +1442,28 @@ impl Client { where T: Into>, { - self.inner_mut().hyper.set_local_address(addr.into()); + let inner = self.inner_mut(); + match addr.into() { + Some(IpAddr::V4(a)) => inner.local_addr_v4 = Some(a), + Some(IpAddr::V6(a)) => inner.local_addr_v6 = Some(a), + _ => (), + } } /// Set that all sockets are bound to the configured IPv4 or IPv6 address /// (depending on host's preferences) before connection. #[inline] pub fn set_local_addresses(&mut self, addr_ipv4: Ipv4Addr, addr_ipv6: Ipv6Addr) { - self.inner_mut() - .hyper - .set_local_addresses(addr_ipv4, addr_ipv6); + let inner = self.inner_mut(); + inner.local_addr_v4 = Some(addr_ipv4); + inner.local_addr_v6 = Some(addr_ipv6); } /// Bind to an interface by `SO_BINDTODEVICE`. #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] #[inline] pub fn set_interface(&mut self, interface: impl Into>) { - self.inner_mut().hyper.set_interface(interface.into()); + self.inner_mut().interface = Some(interface.into()); } /// Set the headers order for this client. @@ -1676,6 +1673,12 @@ struct ClientRef { proxies_maybe_http_auth: bool, base_url: Option>, http2_max_retry_count: usize, + + proxies: Vec, + local_addr_v4: Option, + local_addr_v6: Option, + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + interface: Option>, } impl_debug!( @@ -1712,8 +1715,7 @@ impl ClientRef { // Find the first proxy that matches the destination URI // If a matching proxy provides an HTTP basic auth header, insert it into the headers if let Some(header) = self - .hyper - .get_proxies() + .proxies .iter() .find(|proxy| proxy.maybe_has_http_auth() && proxy.is_match(dst)) .and_then(|proxy| proxy.http_basic_auth(dst)) @@ -1721,6 +1723,35 @@ impl ClientRef { headers.insert(PROXY_AUTHORIZATION, header); } } + + #[inline] + fn network_scheme(&self, uri: &Uri, request_proxy: Option) -> NetworkScheme { + // If the request has no proxy, use the client's local addresses + #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] + let mut builder = NetworkScheme::builder().iface((self.local_addr_v4, self.local_addr_v6)); + + // Use the client's interface if it's set + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + let mut builder = NetworkScheme::builder().iface(( + self.interface.clone(), + self.local_addr_v4, + self.local_addr_v6, + )); + + // If the request has a proxy, use it + if let Some(proxy_scheme) = request_proxy.and_then(|p| p.intercept(uri)) { + builder = builder.proxy(proxy_scheme); + } else { + // Otherwise, use the client's proxies + for proxy in self.proxies.iter() { + if let Some(proxy_scheme) = proxy.intercept(uri) { + return builder.proxy(proxy_scheme).build(); + } + } + } + + builder.build() + } } pin_project! { @@ -1821,15 +1852,14 @@ impl PendingRequest { }; *self.as_mut().in_flight().get_mut() = { - let pool_key = self.client.hyper.pool_key(&uri); - let req = InnerRequest::new() + let req = InnerRequest::::builder() + .network_scheme(self.client.network_scheme(&uri, None)) .uri(uri) .method(self.method.clone()) .version(self.version) .headers(self.headers.clone()) .headers_order(self.client.headers_order.as_deref()) - .pool_key(pool_key) - .build(body); + .body(body); ResponseFuture::Default(self.client.hyper.request(req)) }; @@ -2068,15 +2098,14 @@ impl Future for PendingRequest { self.client.proxy_auth(&uri, &mut headers); *self.as_mut().in_flight().get_mut() = { - let pool_key = self.client.hyper.pool_key(&uri); - let req = InnerRequest::new() + let req = InnerRequest::::builder() + .network_scheme(self.client.network_scheme(&uri, None)) .uri(uri) .method(self.method.clone()) .version(self.version) .headers(headers.clone()) .headers_order(self.client.headers_order.as_deref()) - .pool_key(pool_key) - .build(body); + .body(body); std::mem::swap(self.as_mut().headers(), &mut headers); ResponseFuture::Default(self.client.hyper.request(req)) }; diff --git a/src/client/request.rs b/src/client/request.rs index b525a0ed..ae2e2550 100644 --- a/src/client/request.rs +++ b/src/client/request.rs @@ -3,8 +3,7 @@ use std::fmt; use std::future::Future; use std::time::Duration; -use http::header::CONTENT_LENGTH; -use http::{request::Parts, Request as HttpRequest, Uri, Version}; +use http::{request::Parts, Request as HttpRequest, Version}; use serde::Serialize; #[cfg(feature = "json")] use serde_json; @@ -14,11 +13,9 @@ use super::http::{Client, Pending}; #[cfg(feature = "multipart")] use super::multipart; use super::response::Response; -use super::HttpVersionPref; #[cfg(feature = "cookies")] use crate::cookie; use crate::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE, HOST}; -use crate::util::ext::{ConnectExtension, PoolKeyExtension, VersionExtension}; use crate::{redirect, Method, Url}; #[cfg(feature = "cookies")] use std::sync::Arc; @@ -706,110 +703,3 @@ impl TryFrom for HttpRequest { Ok(req) } } - -/// A builder for constructing HTTP requests. -pub(crate) struct InnerRequest<'a> { - builder: http::request::Builder, - headers_order: Option<&'a [HeaderName]>, -} - -impl<'a> InnerRequest<'a> { - /// Create a new `RequestBuilder` with required fields. - #[inline] - pub fn new() -> Self { - Self { - builder: hyper2::Request::builder(), - headers_order: None, - } - } - - /// Set the method for the request. - #[inline] - pub fn method(mut self, method: Method) -> Self { - self.builder = self.builder.method(method); - self - } - - /// Set the URI for the request. - #[inline] - pub fn uri(mut self, uri: Uri) -> Self { - self.builder = self.builder.uri(uri); - self - } - - /// Set the version for the request. - #[inline] - pub fn version(mut self, version: Option) -> Self { - if let Some(version) = version { - match version { - Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09 => { - self.builder = self - .builder - .extension(ConnectExtension(VersionExtension(HttpVersionPref::Http1))); - } - Version::HTTP_2 => { - self.builder = self - .builder - .extension(ConnectExtension(VersionExtension(HttpVersionPref::Http2))); - } - _ => {} - }; - } - self - } - - /// Set the headers for the request. - #[inline] - pub fn headers(mut self, mut headers: HeaderMap) -> Self { - if let Some(h) = self.builder.headers_mut() { - std::mem::swap(h, &mut headers); - } - self - } - - /// Set the headers order for the request. - #[inline] - pub fn headers_order(mut self, order: Option<&'a [HeaderName]>) -> Self { - self.headers_order = order; - self - } - - /// Set an pool key extension for the request. - #[inline] - pub fn pool_key(mut self, pool_key: Option) -> Self { - if let Some(pool_key) = pool_key { - self.builder = self.builder.extension(ConnectExtension(pool_key)); - } - self - } - - /// Build and return the constructed request. - pub fn build(mut self, body: Body) -> http::Request { - // Sort headers if headers_order is provided - if let Some(order) = self.headers_order { - let method = self.builder.method_ref().cloned(); - let headers_mut = self.builder.headers_mut(); - if let (Some(headers), Some(method)) = (headers_mut, method) { - { - // Add CONTENT_LENGTH header if required - if let Some(len) = http_body::Body::size_hint(&body).exact() { - let needs_content_length = len != 0 - || !matches!( - method, - Method::GET | Method::HEAD | Method::DELETE | Method::CONNECT - ); - if needs_content_length { - headers - .entry(CONTENT_LENGTH) - .or_insert_with(|| HeaderValue::from(len)); - } - } - // Sort headers - crate::util::sort_headers(headers, order); - } - } - } - - self.builder.body(body).expect("valid request parts") - } -} diff --git a/src/connect.rs b/src/connect.rs index f598ad82..af3b3ee3 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -1,42 +1,35 @@ use self::tls_conn::BoringTlsConn; -use crate::tls::{BoringTlsConnector, MaybeHttpsStream}; +use crate::tls::{BoringTlsConnector, HttpsConnector, MaybeHttpsStream}; use crate::util::client::connect::{Connected, Connection}; use crate::util::client::Dst; -use crate::util::ext::PoolKeyExtension; use crate::util::rt::TokioIo; use crate::util::{self, into_uri}; use http::uri::Scheme; -use http::Uri; use hyper2::rt::{Read, ReadBufCursor, Write}; use tokio_boring::SslStream; use tower_service::Service; use pin_project_lite::pin_project; -use std::borrow::Cow; use std::future::Future; use std::io::{self, IoSlice}; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::ops::Deref; use std::pin::Pin; -use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; use crate::dns::DynResolver; use crate::error::BoxError; -use crate::proxy::{Proxy, ProxyScheme}; +use crate::proxy::ProxyScheme; pub(crate) type HttpConnector = util::client::connect::HttpConnector; #[derive(Clone)] pub(crate) struct Connector { inner: Inner, - proxies: Arc>, verbose: verbose::Wrapper, timeout: Option, nodelay: bool, tls_info: bool, - pool_key_ext: Option, } #[derive(Clone)] @@ -50,47 +43,17 @@ impl Connector { pub(crate) fn new_boring_tls( mut http: HttpConnector, tls: BoringTlsConnector, - proxies: Arc>, - local_addr_v4: Option, - local_addr_v6: Option, - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - interface: Option>, nodelay: bool, tls_info: bool, ) -> Connector { - match (local_addr_v4, local_addr_v6) { - (Some(v4), Some(v6)) => http.set_local_addresses(v4, v6), - (Some(v4), None) => http.set_local_address(Some(IpAddr::from(v4))), - (None, Some(v6)) => http.set_local_address(Some(IpAddr::from(v6))), - _ => {} - } - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - if let Some(ref interface) = interface { - http.set_interface(interface.clone()); - } http.enforce_http(false); - - let mut connector = Connector { + Connector { inner: Inner { http, tls }, - proxies, verbose: verbose::OFF, timeout: None, nodelay, tls_info, - pool_key_ext: None, - }; - - #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] - connector.init_pool_key(local_addr_v4.map(IpAddr::V4), local_addr_v6.map(IpAddr::V6)); - - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - connector.init_pool_key( - local_addr_v4.map(IpAddr::V4), - local_addr_v6.map(IpAddr::V6), - interface, - ); - - connector + } } #[inline] @@ -108,109 +71,13 @@ impl Connector { self.verbose.0 = enabled; } - #[inline] - pub(crate) fn get_proxies(&self) -> &[Proxy] { - self.proxies.as_ref() - } - - #[inline] - pub(crate) fn set_proxies(&mut self, proxies: Cow<'static, [Proxy]>) -> Vec { - std::mem::replace(self.proxies_mut(), proxies.into_owned()) - } - - #[inline] - pub(crate) fn append_proxies(&mut self, proxies: Cow<'static, [Proxy]>) { - self.proxies_mut().extend(proxies.into_owned()); - } - - #[inline] - pub(crate) fn clear_proxies(&mut self) { - self.proxies_mut().clear(); - } - - #[inline] - fn proxies_mut(&mut self) -> &mut Vec { - Arc::make_mut(&mut self.proxies) - } - - #[inline] - pub(crate) fn set_local_address(&mut self, addr: Option) { - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - self.init_pool_key(addr, None, None); - - #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] - self.init_pool_key(addr, None); - - self.inner.http.set_local_address(addr); - } - - #[inline] - pub(crate) fn set_local_addresses(&mut self, addr_ipv4: Ipv4Addr, addr_ipv6: Ipv6Addr) { - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - self.init_pool_key(IpAddr::V4(addr_ipv4), IpAddr::V6(addr_ipv6), None); - - #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] - self.init_pool_key(IpAddr::V4(addr_ipv4), IpAddr::V6(addr_ipv6)); - - self.inner.http.set_local_addresses(addr_ipv4, addr_ipv6); - } - - #[inline] - fn init_pool_key( - &mut self, - addr_ipv4: impl Into>, - addr_ipv6: impl Into>, - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - interface: impl Into>>, - ) { - if self.proxies.is_empty() { - let ipv4 = addr_ipv4.into(); - let ipv6 = addr_ipv6.into(); - match (&ipv4, &ipv6) { - (Some(_), Some(_)) | (None, Some(_)) | (Some(_), None) => { - self.pool_key_ext = Some(PoolKeyExtension::Address(ipv4, ipv6)); - } - _ => - { - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - if let Some(interface) = interface.into() { - self.pool_key_ext = Some(PoolKeyExtension::Interface(interface)); - } - } - } - } - } - - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - #[inline] - pub(crate) fn set_interface(&mut self, interface: std::borrow::Cow<'static, str>) { - if self.proxies.is_empty() { - self.pool_key_ext = Some(PoolKeyExtension::Interface(interface.clone())); - } - - match &mut self.inner { - Inner::BoringTls { http, .. } => http.set_interface(interface), - }; - } - #[inline] pub(crate) fn set_connector(&mut self, connector: BoringTlsConnector) { self.inner.tls = connector; } - #[inline] - pub(crate) fn pool_key(&self, uri: &Uri) -> Option { - for proxy in self.proxies.as_ref() { - if let Some(proxy_scheme) = proxy.intercept(uri) { - return Some(PoolKeyExtension::Proxy(proxy_scheme)); - } - } - - self.pool_key_ext.clone() - } - #[cfg(feature = "socks")] - async fn connect_socks(&self, dst: Dst, proxy: ProxyScheme) -> Result { + async fn connect_socks(&self, mut dst: Dst, proxy: ProxyScheme) -> Result { let dns = match proxy { ProxyScheme::Socks4 { .. } => socks::DnsResolve::Local, ProxyScheme::Socks5 { @@ -226,11 +93,15 @@ impl Connector { let Inner { http, tls } = &self.inner; if dst.scheme() == Some(&Scheme::HTTPS) { - let host = dst.host().ok_or(crate::error::uri_bad_host())?; + let http = HttpsConnector::builder(http.clone()) + .with_version_pref(dst.version_pref()) + .with_iface(dst.take_iface()) + .build(tls); + log::trace!("socks HTTPS over proxy"); + let host = dst.host().ok_or(crate::error::uri_bad_host())?; let conn = socks::connect(proxy, &dst, dns).await?; - let http = tls.create_connector(http.clone(), dst.version_pref()); let io = http.connect(&dst, host, TokioIo::new(conn)).await?; return Ok(Conn { @@ -249,7 +120,11 @@ impl Connector { }) } - async fn connect_with_maybe_proxy(self, dst: Dst, is_proxy: bool) -> Result { + async fn connect_with_maybe_proxy( + self, + mut dst: Dst, + is_proxy: bool, + ) -> Result { let Inner { http, tls } = &self.inner; let mut http = http.clone(); @@ -261,7 +136,10 @@ impl Connector { } log::trace!("connect with maybe proxy"); - let mut http = tls.create_connector(http, dst.version_pref()); + let mut http = HttpsConnector::builder(http) + .with_version_pref(dst.version_pref()) + .with_iface(dst.take_iface()) + .build(tls); let io = http.call(dst.deref().clone()).await?; if let MaybeHttpsStream::Https(stream) = io { @@ -305,14 +183,18 @@ impl Connector { let Inner { http, tls } = &self.inner; if dst.scheme() == Some(&Scheme::HTTPS) { + let mut http = HttpsConnector::builder(http.clone()) + .with_version_pref(dst.version_pref()) + .with_iface(dst.take_iface()) + .build(tls); + let host = dst.host().ok_or(crate::error::uri_bad_host())?; let port = dst.port_u16().unwrap_or(443); - let mut http = tls.create_connector(http.clone(), dst.version_pref()); - let conn = http.call(proxy_dst).await?; - log::trace!("tunneling HTTPS over proxy"); + let conn = http.call(proxy_dst).await?; let tunneled = tunnel::connect(conn, host, port, auth).await?; + let io = http.connect(&dst, host, tunneled).await?; return Ok(Conn { @@ -324,7 +206,7 @@ impl Connector { }); } - dst.next_dst(proxy_dst); + dst.set_dst(proxy_dst); self.connect_with_maybe_proxy(dst, true).await } @@ -354,16 +236,15 @@ impl Service for Connector { Poll::Ready(Ok(())) } - fn call(&mut self, dst: Dst) -> Self::Future { + fn call(&mut self, mut dst: Dst) -> Self::Future { log::debug!("starting new connection: {:?}", dst); let timeout = self.timeout; - for prox in self.proxies.iter() { - if let Some(proxy_scheme) = prox.intercept(dst.deref()) { - return Box::pin(with_timeout( - self.clone().connect_via_proxy(dst, proxy_scheme), - timeout, - )); - } + + if let Some(proxy_scheme) = dst.take_proxy() { + return Box::pin(with_timeout( + self.clone().connect_via_proxy(dst, proxy_scheme), + timeout, + )); } Box::pin(with_timeout( diff --git a/src/error.rs b/src/error.rs index dae19842..6698bb8f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -239,6 +239,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: http::Error) -> Error { + Error::new(Kind::Builder, Some(format!("http error: {:?}", err))) + } +} + #[derive(Debug)] pub(crate) enum Kind { Builder, diff --git a/src/tls/conn/layer.rs b/src/tls/conn/layer.rs index bb15f8a8..2a8261fa 100644 --- a/src/tls/conn/layer.rs +++ b/src/tls/conn/layer.rs @@ -1,10 +1,13 @@ /// referrer: https://github.com/cloudflare/boring/blob/master/hyper-boring/src/lib.rs use super::cache::{SessionCache, SessionKey}; use super::{key_index, HttpsLayerSettings, MaybeHttpsStream}; +use crate::connect::HttpConnector; use crate::error::BoxError; -use crate::tls::TlsConnectExtension; +use crate::tls::ext::TlsExtension; +use crate::tls::{BoringTlsConnector, TlsConnectExtension}; use crate::util::client::connect::Connection; use crate::util::rt::TokioIo; +use crate::HttpVersionPref; use antidote::Mutex; use boring::error::ErrorStack; use boring::ssl::{ @@ -18,13 +21,93 @@ use std::fmt::{self, Debug}; use std::future::Future; use tokio_boring::SslStream; -use std::net; +use std::net::{self, IpAddr, Ipv4Addr, Ipv6Addr}; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; use tower_layer::Layer; use tower_service::Service; +pub(crate) struct HttpsConnectorBuilder { + version: Option, + http: HttpConnector, +} + +#[allow(unused)] +impl HttpsConnectorBuilder { + #[inline] + pub fn new(http: HttpConnector) -> HttpsConnectorBuilder { + HttpsConnectorBuilder { + version: None, + http, + } + } + + #[inline] + pub fn with_version_pref(mut self, version: impl Into>) -> Self { + self.version = version.into(); + self + } + + #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] + #[inline] + pub fn with_iface(mut self, (ipv4, ipv6): (Option, Option)) -> Self { + match (ipv4, ipv6) { + (Some(a), Some(b)) => self.http.set_local_addresses(a, b), + (Some(a), None) => self.http.set_local_address(Some(IpAddr::V4(a))), + (None, Some(b)) => self.http.set_local_address(Some(IpAddr::V6(b))), + _ => (), + } + self + } + + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + #[inline] + pub fn with_iface( + mut self, + (interface, (address_ipv4, address_ipv6)): ( + Option>, + (Option, Option), + ), + ) -> Self { + match (interface, address_ipv4, address_ipv6) { + (Some(a), Some(b), Some(c)) => { + self.http.set_interface(a); + self.http.set_local_addresses(b, c); + } + (None, Some(b), Some(c)) => { + self.http.set_local_addresses(b, c); + } + (Some(a), None, None) => { + self.http.set_interface(a); + } + (Some(a), Some(b), None) => { + self.http.set_interface(a); + self.http.set_local_address(Some(IpAddr::V4(b))); + } + (Some(a), None, Some(b)) => { + self.http.set_interface(a); + self.http.set_local_address(Some(IpAddr::V6(b))); + } + (None, Some(b), None) => { + self.http.set_local_address(Some(IpAddr::V4(b))); + } + (None, None, Some(c)) => { + self.http.set_local_address(Some(IpAddr::V6(c))); + } + _ => (), + } + self + } + + #[inline] + pub(crate) fn build(self, tls: &BoringTlsConnector) -> HttpsConnector { + let mut connector = HttpsConnector::with_connector_layer(self.http, tls.0.clone()); + connector.set_ssl_callback(move |ssl, _| ssl.configure_alpn_protos(self.version)); + connector + } +} + /// A Connector using BoringSSL to support `http` and `https` schemes. #[derive(Clone)] pub struct HttpsConnector { @@ -32,12 +115,19 @@ pub struct HttpsConnector { inner: Inner, } +impl HttpsConnector { + /// Creates a new `HttpsConnectorBuilder` + pub fn builder(http: HttpConnector) -> HttpsConnectorBuilder { + HttpsConnectorBuilder::new(http) + } +} + impl HttpsConnector where S: Service + Send, S::Error: Into>, S::Future: Unpin + Send + 'static, - T: Read + Write + Connection + Unpin + fmt::Debug + Sync + Send + 'static, + T: Read + Write + Connection + Unpin + Debug + Sync + Send + 'static, { /// Creates a new `HttpsConnector` with a given `HttpConnector` pub fn with_connector_layer(http: S, layer: HttpsLayer) -> HttpsConnector { diff --git a/src/tls/ext/mod.rs b/src/tls/ext/mod.rs index 3cffd020..41ef4cd8 100644 --- a/src/tls/ext/mod.rs +++ b/src/tls/ext/mod.rs @@ -247,18 +247,11 @@ impl TlsConnectExtension for ConnectConfiguration { impl TlsExtension for SslRef { #[inline] fn configure_alpn_protos(&mut self, version: Option) -> TlsResult<()> { - if let Some(HttpVersionPref::Http1) = version { - self.set_alpn_protos(HTTP_1_ALPN)?; + match version { + Some(HttpVersionPref::Http1) => self.set_alpn_protos(HTTP_1_ALPN), + Some(HttpVersionPref::Http2) => self.set_alpn_protos(HTTP_2_ALPN), + Some(HttpVersionPref::All) => self.set_alpn_protos(HTTP_1_OR_2_ALPN), + None => Ok(()), } - - if let Some(HttpVersionPref::Http2) = version { - self.set_alpn_protos(HTTP_2_ALPN)?; - } - - if let Some(HttpVersionPref::All) = version { - self.set_alpn_protos(HTTP_1_OR_2_ALPN)?; - } - - Ok(()) } } diff --git a/src/tls/mimic/macros.rs b/src/tls/mimic/macros.rs index 1430c1bd..df51dde9 100644 --- a/src/tls/mimic/macros.rs +++ b/src/tls/mimic/macros.rs @@ -105,13 +105,9 @@ macro_rules! header_firefox_accept { header::ACCEPT_LANGUAGE, HeaderValue::from_static("en-US,en;q=0.5"), ); - }; } - - - #[macro_export] macro_rules! header_firefox_accpet_with_zstd { ($headers:expr) => { diff --git a/src/tls/mod.rs b/src/tls/mod.rs index 10aeceb2..1dd19a38 100644 --- a/src/tls/mod.rs +++ b/src/tls/mod.rs @@ -5,20 +5,19 @@ //! - Various parts of TLS can also be configured or even disabled on the //! `ClientBuilder`. +#![allow(missing_debug_implementations)] #![allow(missing_docs)] mod conn; mod ext; mod mimic; mod settings; -use crate::{connect::HttpConnector, HttpVersionPref}; use boring::{ error::ErrorStack, ssl::{SslConnector, SslMethod, SslOptions, SslVersion}, }; -pub use conn::MaybeHttpsStream; -use conn::{HttpsConnector, HttpsLayer, HttpsLayerSettings}; -use ext::TlsExtension; +pub use conn::{HttpsConnector, MaybeHttpsStream}; +use conn::{HttpsLayer, HttpsLayerSettings}; pub use ext::{cert_compression, TlsBuilderExtension, TlsConnectExtension}; pub use mimic::{chrome, firefox, okhttp, safari, tls_settings, Impersonate}; pub use settings::{Http2Settings, ImpersonateSettings, RootCertsStore, TlsSettings}; @@ -27,153 +26,132 @@ type TlsResult = Result; /// A wrapper around a `SslConnectorBuilder` that allows for additional settings. #[derive(Clone)] -#[allow(missing_debug_implementations)] -pub struct BoringTlsConnector { - inner: HttpsLayer, -} +pub struct BoringTlsConnector(HttpsLayer); impl BoringTlsConnector { /// Create a new `BoringTlsConnector` with the given function. #[inline] pub fn new(settings: TlsSettings) -> TlsResult { - connect_layer(settings).map(|layer| Self { inner: layer }) - } - - /// Create a new `HttpsConnector` with the settings from the `HttpConnector`. - #[inline] - pub(crate) fn create_connector( - &self, - http: HttpConnector, - version: Option, - ) -> HttpsConnector { - // Create the `HttpsConnector` with the given `HttpConnector` and `ConnectLayer`. - let mut http = HttpsConnector::with_connector_layer(http, self.inner.clone()); - // Set the SSL callback to configure ALPN protos. - http.set_ssl_callback(move |ssl, _| ssl.configure_alpn_protos(version)); - http - } -} - -/// Create a new `ConnectLayer` with the given `Tls` settings. -#[inline] -fn connect_layer(settings: TlsSettings) -> TlsResult { - // Create the default connector. - let connector = if cfg!(any(feature = "native-roots", feature = "webpki-roots")) { - SslConnector::no_default_verify_builder(SslMethod::tls_client()) - } else { - SslConnector::builder(SslMethod::tls_client()) - }?; - - // Create the `SslConnectorBuilder` and configure it. - let mut connector = connector - .configure_cert_verification(settings.certs_verification)? - .configure_alpn_protos(settings.alpn_protos)? - .configure_min_tls_version(settings.min_tls_version)? - .configure_max_tls_version(settings.max_tls_version)?; - - // Set enable ocsp stapling if it is set. - if settings.enable_ocsp_stapling { - connector.enable_ocsp_stapling(); - } - - // Set enable signed cert timestamps if it is set. - if settings.enable_signed_cert_timestamps { - connector.enable_signed_cert_timestamps(); - } - - // Set no session ticket if it is set. - if let Some(false) = settings.session_ticket { - connector.set_options(SslOptions::NO_TICKET); - } + // Create the default connector. + let connector = if cfg!(any(feature = "native-roots", feature = "webpki-roots")) { + SslConnector::no_default_verify_builder(SslMethod::tls_client()) + } else { + SslConnector::builder(SslMethod::tls_client()) + }?; + + // Create the `SslConnectorBuilder` and configure it. + let mut connector = connector + .configure_cert_verification(settings.certs_verification)? + .configure_alpn_protos(settings.alpn_protos)? + .configure_min_tls_version(settings.min_tls_version)? + .configure_max_tls_version(settings.max_tls_version)?; + + // Set enable ocsp stapling if it is set. + if settings.enable_ocsp_stapling { + connector.enable_ocsp_stapling(); + } - // Set grease enabled if it is set. - if let Some(grease_enabled) = settings.grease_enabled { - connector.set_grease_enabled(grease_enabled); - } + // Set enable signed cert timestamps if it is set. + if settings.enable_signed_cert_timestamps { + connector.enable_signed_cert_timestamps(); + } - // Set permute extensions if it is set. - if let Some(permute_extensions) = settings.permute_extensions { - connector.set_permute_extensions(permute_extensions); - } + // Set no session ticket if it is set. + if let Some(false) = settings.session_ticket { + connector.set_options(SslOptions::NO_TICKET); + } - // Set the curves if they are set. - if let Some(curves) = settings.curves.as_deref() { - connector.set_curves(curves)?; - } + // Set grease enabled if it is set. + if let Some(grease_enabled) = settings.grease_enabled { + connector.set_grease_enabled(grease_enabled); + } - // Set the signature algorithms list if it is set. - if let Some(sigalgs_list) = settings.sigalgs_list.as_deref() { - connector.set_sigalgs_list(sigalgs_list)?; - } + // Set permute extensions if it is set. + if let Some(permute_extensions) = settings.permute_extensions { + connector.set_permute_extensions(permute_extensions); + } - // Set the delegated credentials if it is set. - if let Some(delegated_credentials) = settings.delegated_credentials.as_deref() { - connector.set_delegated_credentials(delegated_credentials)?; - } + // Set the curves if they are set. + if let Some(curves) = settings.curves.as_deref() { + connector.set_curves(curves)?; + } - // Set the cipher list if it is set. - if let Some(cipher_list) = settings.cipher_list.as_deref() { - connector.set_cipher_list(cipher_list)?; - } + // Set the signature algorithms list if it is set. + if let Some(sigalgs_list) = settings.sigalgs_list.as_deref() { + connector.set_sigalgs_list(sigalgs_list)?; + } - // Set the certificate compression algorithm if it is set. - if let Some(cert_compression_algorithm) = settings.cert_compression_algorithm { - for algorithm in cert_compression_algorithm.iter() { - connector = connector.configure_add_cert_compression_alg(*algorithm)?; + // Set the delegated credentials if it is set. + if let Some(delegated_credentials) = settings.delegated_credentials.as_deref() { + connector.set_delegated_credentials(delegated_credentials)?; } - } - // Set the record size limit if it is set. - if let Some(record_size_limit) = settings.record_size_limit { - connector.set_record_size_limit(record_size_limit); - } + // Set the cipher list if it is set. + if let Some(cipher_list) = settings.cipher_list.as_deref() { + connector.set_cipher_list(cipher_list)?; + } - // Set the key shares length limit if it is set. - if let Some(limit) = settings.key_shares_length_limit { - connector.set_key_shares_length_limit(limit); - } + // Set the certificate compression algorithm if it is set. + if let Some(cert_compression_algorithm) = settings.cert_compression_algorithm { + for algorithm in cert_compression_algorithm.iter() { + connector = connector.configure_add_cert_compression_alg(*algorithm)?; + } + } - // Set the extension permutation if it is set. - if let Some(extensions) = settings.extension_permutation { - connector.set_extension_permutation(extensions.as_ref())?; - } + // Set the record size limit if it is set. + if let Some(record_size_limit) = settings.record_size_limit { + connector.set_record_size_limit(record_size_limit); + } - // Set the extension permutation index if it is set. - if let Some(indices) = settings.extension_permutation_indices { - connector.set_extension_permutation_indices(indices.as_ref())?; - } + // Set the key shares length limit if it is set. + if let Some(limit) = settings.key_shares_length_limit { + connector.set_key_shares_length_limit(limit); + } - // Conditionally configure the TLS builder based on the "native-roots" feature. - // If no custom CA cert store, use the system's native certificate store if the feature is enabled. - let connector = if settings.root_certs_store.is_none() { - // WebPKI root certificates are enabled (regardless of whether native-roots is also enabled). - #[cfg(any(feature = "webpki-roots", feature = "native-roots"))] - { - connector.configure_set_verify_cert_store()? + // Set the extension permutation if it is set. + if let Some(extensions) = settings.extension_permutation { + connector.set_extension_permutation(extensions.as_ref())?; } - // Neither native-roots nor WebPKI roots are enabled, proceed with the default builder. - #[cfg(not(any(feature = "native-roots", feature = "webpki-roots")))] - { - connector + // Set the extension permutation index if it is set. + if let Some(indices) = settings.extension_permutation_indices { + connector.set_extension_permutation_indices(indices.as_ref())?; } - } else { - // If a custom CA certificate store is provided, configure it. - connector.configure_ca_cert_store(settings.root_certs_store)? - }; - - // Create the `HttpsLayerSettings` with the default session cache capacity. - let settings = HttpsLayerSettings::builder() - .session_cache_capacity(8) - .session_cache(settings.pre_shared_key) - .skip_session_ticket(settings.psk_skip_session_ticket) - .alpn_protos(settings.alpn_protos) - .application_settings(settings.application_settings) - .enable_ech_grease(settings.enable_ech_grease) - .tls_sni(settings.tls_sni) - .build(); - - Ok(HttpsLayer::with_connector_and_settings(connector, settings)) + + // Conditionally configure the TLS builder based on the "native-roots" feature. + // If no custom CA cert store, use the system's native certificate store if the feature is enabled. + let connector = if settings.root_certs_store.is_none() { + // WebPKI root certificates are enabled (regardless of whether native-roots is also enabled). + #[cfg(any(feature = "webpki-roots", feature = "native-roots"))] + { + connector.configure_set_verify_cert_store()? + } + + // Neither native-roots nor WebPKI roots are enabled, proceed with the default builder. + #[cfg(not(any(feature = "native-roots", feature = "webpki-roots")))] + { + connector + } + } else { + // If a custom CA certificate store is provided, configure it. + connector.configure_ca_cert_store(settings.root_certs_store)? + }; + + // Create the `HttpsLayerSettings` with the default session cache capacity. + let settings = HttpsLayerSettings::builder() + .session_cache_capacity(8) + .session_cache(settings.pre_shared_key) + .skip_session_ticket(settings.psk_skip_session_ticket) + .alpn_protos(settings.alpn_protos) + .application_settings(settings.application_settings) + .enable_ech_grease(settings.enable_ech_grease) + .tls_sni(settings.tls_sni) + .build(); + + Ok(Self(HttpsLayer::with_connector_and_settings( + connector, settings, + ))) + } } /// A TLS protocol version. diff --git a/src/util/client/mod.rs b/src/util/client/mod.rs index 0b15f548..22be7441 100644 --- a/src/util/client/mod.rs +++ b/src/util/client/mod.rs @@ -9,10 +9,13 @@ pub mod connect; // Publicly available, but just for legacy purposes. A better pool will be // designed. mod pool; +mod request; +mod scheme; use std::error::Error as StdError; use std::fmt; use std::future::Future; +use std::net::{Ipv4Addr, Ipv6Addr}; use std::num::NonZeroUsize; use std::pin::Pin; use std::task::{self, Poll}; @@ -27,8 +30,8 @@ use hyper2::{body::Body, Method, Request, Response, Uri, Version}; use log::{debug, trace, warn}; use sync_wrapper::SyncWrapper; +use crate::proxy::ProxyScheme; use crate::util::common; -use crate::util::ext::PoolKeyExtension; use crate::HttpVersionPref; use connect::capture::CaptureConnectionExtension; use connect::{Alpn, Connect, Connected, Connection}; @@ -36,8 +39,9 @@ use pool::Ver; use common::{lazy as hyper_lazy, timer, Exec, Lazy}; -use super::ext::{ConnectExtension, VersionExtension}; use super::into_uri; +pub use request::InnerRequest; +pub use scheme::NetworkScheme; type BoxSendFuture = Pin + Send>>; @@ -112,36 +116,29 @@ macro_rules! e { } // We might change this... :shrug: -type PoolKey = (Uri, Option); +type PoolKey = (NetworkScheme, Uri); /// Destination of the request /// /// This is used to store the destination of the request, the http version pref, and the pool key. #[derive(Clone)] pub struct Dst { - version: Option, + dst: Uri, + http_version_pref: Option, pool_key: PoolKey, - next_dst: Option, } impl Dst { /// Create a new `Dst` from a request - pub fn new(req: &mut Request, is_http_connect: bool) -> Result { - let (version, extension) = { - ( - req.extensions_mut() - .remove::>() - .map(|e| e.into_inner()), - req.extensions_mut() - .remove::>() - .map(|e| e.into_inner()), - ) - }; - + pub fn new( + req: &mut Request, + is_http_connect: bool, + network_scheme: Option, + http_version_pref: Option, + ) -> Result { let uri = req.uri_mut(); - let (scheme, authority, extension) = match (uri.scheme().cloned(), uri.authority().cloned()) - { - (Some(scheme), Some(auth)) => (scheme, auth, extension), + let (scheme, auth) = match (uri.scheme().cloned(), uri.authority().cloned()) { + (Some(scheme), Some(auth)) => (scheme, auth), (None, Some(auth)) if is_http_connect => { let scheme = match auth.port_u16() { Some(443) => { @@ -153,7 +150,7 @@ impl Dst { Scheme::HTTP } }; - (scheme, auth.clone(), extension) + (scheme, auth) } _ => { debug!("Client requires absolute-form URIs, received: {:?}", uri); @@ -161,11 +158,12 @@ impl Dst { } }; - into_uri(scheme, authority) + // Convert the scheme and host to a URI + into_uri(scheme, auth) .map(|uri| Dst { - pool_key: (uri, extension), - version, - next_dst: None, + dst: uri.clone(), + pool_key: (network_scheme.unwrap_or_default(), uri), + http_version_pref, }) .map_err(|_| e!(UserAbsoluteUriRequired)) } @@ -176,13 +174,35 @@ impl Dst { } /// Set the next destination of the request (for proxy) - pub(crate) fn next_dst(&mut self, uri: Uri) { - self.next_dst = Some(uri); + pub(crate) fn set_dst(&mut self, uri: Uri) { + self.dst = uri; } /// Get the http version pref pub fn version_pref(&self) -> Option { - self.version.clone().map(|v| v.0) + self.http_version_pref + } + + /// Task network scheme for iface + #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] + pub fn take_iface(&mut self) -> (Option, Option) { + self.pool_key.0.take_iface() + } + + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + pub fn take_iface( + &mut self, + ) -> ( + Option>, + Option, + Option, + ) { + self.pool_key.0.take_iface() + } + + /// Take the network scheme for proxy + pub fn take_proxy(&mut self) -> Option { + self.pool_key.0.take_proxy() } } @@ -190,13 +210,13 @@ impl std::ops::Deref for Dst { type Target = Uri; fn deref(&self) -> &Self::Target { - self.next_dst.as_ref().unwrap_or(&self.pool_key.0) + &self.dst } } impl std::fmt::Debug for Dst { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.pool_key.0) + write!(f, "{}", self.dst) } } @@ -282,7 +302,8 @@ where /// # } /// # fn main() {} /// ``` - pub fn request(&self, mut req: Request) -> ResponseFuture { + pub fn request(&self, req: InnerRequest) -> ResponseFuture { + let (mut req, network_scheme, http_version_pref) = req.split(); let is_http_connect = req.method() == Method::CONNECT; match req.version() { Version::HTTP_11 => (), @@ -297,7 +318,7 @@ where other => return ResponseFuture::error_version(other), }; - let ctx = match Dst::new(&mut req, is_http_connect) { + let ctx = match Dst::new(&mut req, is_http_connect, network_scheme, http_version_pref) { Ok(s) => s, Err(err) => { return ResponseFuture::new(future::err(err)); @@ -689,7 +710,7 @@ where } } -impl tower_service::Service> for Client +impl tower_service::Service> for Client where C: Connect + Clone + Send + Sync + 'static, B: Body + Send + 'static + Unpin, @@ -704,12 +725,12 @@ where Poll::Ready(Ok(())) } - fn call(&mut self, req: Request) -> Self::Future { + fn call(&mut self, req: InnerRequest) -> Self::Future { self.request(req) } } -impl tower_service::Service> for &'_ Client +impl tower_service::Service> for &'_ Client where C: Connect + Clone + Send + Sync + 'static, B: Body + Send + 'static + Unpin, @@ -724,7 +745,7 @@ where Poll::Ready(Ok(())) } - fn call(&mut self, req: Request) -> Self::Future { + fn call(&mut self, req: InnerRequest) -> Self::Future { self.request(req) } } diff --git a/src/util/client/request.rs b/src/util/client/request.rs new file mode 100644 index 00000000..deae0ee2 --- /dev/null +++ b/src/util/client/request.rs @@ -0,0 +1,153 @@ +#![allow(missing_debug_implementations)] + +use super::NetworkScheme; +use crate::HttpVersionPref; +use core::error::Error as StdError; +use http::{header::CONTENT_LENGTH, HeaderMap, HeaderName, HeaderValue, Method, Uri, Version}; +use http_body::Body; + +pub struct InnerRequest +where + B: Body + Send + 'static + Unpin, + B::Data: Send, + B::Error: Into>, +{ + request: http::Request, + http_version_pref: Option, + network_scheme: Option, +} + +impl InnerRequest +where + B: Body + Send + 'static + Unpin, + B::Data: Send, + B::Error: Into>, +{ + pub fn builder<'a>() -> InnerRequestBuilder<'a> { + InnerRequestBuilder::new() + } + + pub fn split( + self, + ) -> ( + http::Request, + Option, + Option, + ) { + (self.request, self.network_scheme, self.http_version_pref) + } +} + +/// A builder for constructing HTTP requests. +pub struct InnerRequestBuilder<'a> { + builder: http::request::Builder, + http_version_pref: Option, + network_scheme: Option, + headers_order: Option<&'a [HeaderName]>, +} + +impl<'a> InnerRequestBuilder<'a> { + /// Create a new `RequestBuilder` with required fields. + #[inline] + pub fn new() -> Self { + Self { + builder: hyper2::Request::builder(), + http_version_pref: None, + network_scheme: None, + headers_order: None, + } + } + + /// Set the method for the request. + #[inline] + pub fn method(mut self, method: Method) -> Self { + self.builder = self.builder.method(method); + self + } + + /// Set the URI for the request. + #[inline] + pub fn uri(mut self, uri: Uri) -> Self { + self.builder = self.builder.uri(uri); + self + } + + /// Set the version for the request. + #[inline] + pub fn version(mut self, version: impl Into>) -> Self { + if let Some(version) = version.into() { + let pref = match version { + Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09 => HttpVersionPref::Http1, + Version::HTTP_2 => HttpVersionPref::Http2, + _ => HttpVersionPref::default(), + }; + self.builder = self.builder.version(version); + self.http_version_pref = Some(pref); + } + self + } + + /// Set the headers for the request. + #[inline] + pub fn headers(mut self, mut headers: HeaderMap) -> Self { + if let Some(h) = self.builder.headers_mut() { + std::mem::swap(h, &mut headers); + } + self + } + + /// Set the headers order for the request. + #[inline] + pub fn headers_order(mut self, order: Option<&'a [HeaderName]>) -> Self { + self.headers_order = order; + self + } + + /// Set network scheme for the request. + #[inline] + pub fn network_scheme(mut self, network_scheme: impl Into>) -> Self { + self.network_scheme = network_scheme.into(); + self + } + + /// Set the body for the request. + #[inline] + pub fn body(mut self, body: B) -> InnerRequest + where + B: Body + Send + 'static + Unpin, + B::Data: Send, + B::Error: Into>, + { + // Sort headers if headers_order is provided + if let Some(order) = self.headers_order { + let method = self.builder.method_ref().cloned(); + let headers_mut = self.builder.headers_mut(); + + if let (Some(headers), Some(method)) = (headers_mut, method) { + { + // Add CONTENT_LENGTH header if required + if let Some(len) = http_body::Body::size_hint(&body).exact() { + let needs_content_length = len != 0 + || !matches!( + method, + Method::GET | Method::HEAD | Method::DELETE | Method::CONNECT + ); + if needs_content_length { + headers + .entry(CONTENT_LENGTH) + .or_insert_with(|| HeaderValue::from(len)); + } + } + // Sort headers + crate::util::sort_headers(headers, order); + } + } + } + + InnerRequest { + request: self.builder.body(body).expect("failed to build request"), + http_version_pref: self.http_version_pref, + network_scheme: self.network_scheme, + } + } +} diff --git a/src/util/client/scheme.rs b/src/util/client/scheme.rs new file mode 100644 index 00000000..caf176f8 --- /dev/null +++ b/src/util/client/scheme.rs @@ -0,0 +1,184 @@ +use crate::proxy::ProxyScheme; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[derive(Clone, Hash, PartialEq, Eq, Default)] +pub enum NetworkScheme { + /// Network scheme with an interface. + Iface { + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + interface: Option>, + addresses: (Option, Option), + }, + + /// Network scheme with a proxy. + Proxy(Option), + + /// No network scheme. + #[default] + None, +} + +impl NetworkScheme { + pub fn builder() -> NetworkSchemeBuilder { + NetworkSchemeBuilder::new() + } + + pub fn take_proxy(&mut self) -> Option { + match self { + NetworkScheme::Proxy(proxy) => proxy.take(), + _ => None, + } + } + + #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] + pub fn take_iface(&mut self) -> (Option, Option) { + match self { + NetworkScheme::Iface { addresses, .. } => (addresses.0.take(), addresses.1.take()), + _ => (None, None), + } + } + + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + pub fn take_iface( + &mut self, + ) -> ( + Option>, + Option, + Option, + ) { + match self { + NetworkScheme::Iface { + interface, + addresses, + } => (interface.take(), addresses.0.take(), addresses.1.take()), + _ => (None, None, None), + } + } +} + +impl std::fmt::Debug for NetworkScheme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NetworkScheme::Proxy(proxy) => write!(f, "proxy: {:?}", proxy), + NetworkScheme::Iface { addresses, .. } => { + write!(f, "iface: {:?}, {:?}", addresses.0, addresses.1) + } + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + NetworkScheme::Iface { + interface, + addresses, + } => { + write!( + f, + "iface: {:?}, {:?}, {:?}", + interface, addresses.0, addresses.1 + ) + } + NetworkScheme::None => write!(f, "None"), + } + } +} + +#[allow(missing_debug_implementations)] +pub struct NetworkSchemeBuilder { + addresses: (Option, Option), + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + interface: Option>, + proxy: Option, +} + +impl NetworkSchemeBuilder { + fn new() -> Self { + Self { + addresses: (None, None), + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + interface: None, + proxy: None, + } + } + + pub fn iface(mut self, iface: (Option, Option)) -> Self { + self.addresses = iface; + self + } + + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + pub fn interface( + mut self, + (addresses, interface): ( + (Option, Option), + Option>, + ), + ) -> Self { + self.addresses = addresses; + self.interface = interface; + self + } + + pub fn proxy(mut self, proxy: impl Into>) -> Self { + self.proxy = proxy.into(); + self + } + + pub fn build(self) -> NetworkScheme { + if self.proxy.is_some() { + NetworkScheme::Proxy(self.proxy) + } else { + NetworkScheme::Iface { + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + interface: self.interface, + addresses: self.addresses, + } + } + } +} + +impl From> for NetworkScheme { + fn from(value: Option) -> Self { + NetworkScheme::Iface { + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + interface: None, + addresses: match value { + Some(IpAddr::V4(a)) => (Some(a), None), + Some(IpAddr::V6(b)) => (None, Some(b)), + _ => (None, None), + }, + } + } +} + +impl From<(Option, Option)> for NetworkScheme { + fn from(value: (Option, Option)) -> Self { + NetworkScheme::Iface { + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + interface: None, + addresses: value, + } + } +} + +impl From for NetworkScheme { + fn from(value: ProxyScheme) -> Self { + NetworkScheme::Proxy(Some(value)) + } +} + +#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] +impl From for NetworkScheme { + fn from(value: String) -> Self { + NetworkScheme::Iface { + interface: Some(std::borrow::Cow::Owned(value)), + addresses: (None, None), + } + } +} + +#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] +impl From<&'static str> for NetworkScheme { + fn from(value: &'static str) -> Self { + NetworkScheme::Iface { + interface: Some(std::borrow::Cow::Borrowed(value)), + addresses: (None, None), + } + } +} diff --git a/src/util/ext.rs b/src/util/ext.rs index 452b9751..8b137891 100644 --- a/src/util/ext.rs +++ b/src/util/ext.rs @@ -1,26 +1 @@ -use crate::{proxy::ProxyScheme, HttpVersionPref}; -use std::net::IpAddr; -/// Extension for connect. -#[derive(Clone)] -pub(crate) struct ConnectExtension(pub T); - -impl ConnectExtension { - /// Get the inner value. - pub(crate) fn into_inner(self) -> T { - self.0 - } -} - -/// Extension for http version preference. -#[derive(Debug, Clone)] -pub(crate) struct VersionExtension(pub HttpVersionPref); - -/// Extension for pool key suffix -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) enum PoolKeyExtension { - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - Interface(std::borrow::Cow<'static, str>), - Address(Option, Option), - Proxy(ProxyScheme), -}