From dbf5d728fe1f8a6b72ee51ed72c719cea934271a Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Sat, 19 Jul 2025 22:00:29 +0800 Subject: [PATCH 1/4] refactor(connect): modularize components by responsibility --- src/client/http/connect/conn.rs | 187 ++++++++ .../http/{connect.rs => connect/connector.rs} | 442 +----------------- src/client/http/connect/mod.rs | 32 ++ src/client/http/connect/tls_info.rs | 54 +++ src/client/http/connect/verbose.rs | 160 +++++++ 5 files changed, 442 insertions(+), 433 deletions(-) create mode 100644 src/client/http/connect/conn.rs rename src/client/http/{connect.rs => connect/connector.rs} (52%) create mode 100644 src/client/http/connect/mod.rs create mode 100644 src/client/http/connect/tls_info.rs create mode 100644 src/client/http/connect/verbose.rs diff --git a/src/client/http/connect/conn.rs b/src/client/http/connect/conn.rs new file mode 100644 index 000000000..3606c6a80 --- /dev/null +++ b/src/client/http/connect/conn.rs @@ -0,0 +1,187 @@ +use std::{ + io::{self, IoSlice}, + pin::Pin, + task::{Context, Poll}, +}; + +use pin_project_lite::pin_project; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::TcpStream, +}; +use tokio_boring2::SslStream; + +use super::{AsyncConnWithInfo, TlsInfoFactory}; +use crate::{ + core::{ + client::{ + ConnRequest, + connect::{Connected, Connection}, + }, + rt::{Read, ReadBufCursor, TokioIo, Write}, + }, + tls::{MaybeHttpsStream, TlsInfo}, +}; + +pub struct Unnameable(pub(super) ConnRequest); + +pin_project! { + /// Note: the `is_proxy` member means *is plain text HTTP proxy*. + /// This tells core whether the URI should be written in + /// * origin-form (`GET /just/a/path HTTP/1.1`), when `is_proxy == false`, or + /// * absolute-form (`GET http://foo.bar/and/a/path HTTP/1.1`), otherwise. + pub struct Conn { + #[pin] + pub(super) inner: Box, + pub(super) is_proxy: bool, + pub(super) tls_info: bool, + } +} + +// ==== impl Conn ==== + +impl Connection for Conn { + fn connected(&self) -> Connected { + let connected = self.inner.connected().proxy(self.is_proxy); + + if self.tls_info { + if let Some(tls_info) = self.inner.tls_info() { + connected.extra(tls_info) + } else { + connected + } + } else { + connected + } + } +} + +impl Read for Conn { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context, + buf: ReadBufCursor<'_>, + ) -> Poll> { + let this = self.project(); + Read::poll_read(this.inner, cx, buf) + } +} + +impl Write for Conn { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + let this = self.project(); + Write::poll_write(this.inner, cx, buf) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[IoSlice<'_>], + ) -> Poll> { + let this = self.project(); + Write::poll_write_vectored(this.inner, cx, bufs) + } + + fn is_write_vectored(&self) -> bool { + self.inner.is_write_vectored() + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let this = self.project(); + Write::poll_flush(this.inner, cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let this = self.project(); + Write::poll_shutdown(this.inner, cx) + } +} + +pin_project! { + pub(super) struct TlsConn { + #[pin] + pub(super) inner: TokioIo>, + } +} + +// ==== impl TlsConn ==== + +impl Connection for TlsConn { + fn connected(&self) -> Connected { + let connected = self.inner.inner().get_ref().connected(); + if self.inner.inner().ssl().selected_alpn_protocol() == Some(b"h2") { + connected.negotiated_h2() + } else { + connected + } + } +} + +impl Connection for TlsConn>> { + fn connected(&self) -> Connected { + let connected = self.inner.inner().get_ref().connected(); + if self.inner.inner().ssl().selected_alpn_protocol() == Some(b"h2") { + connected.negotiated_h2() + } else { + connected + } + } +} + +impl Read for TlsConn { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context, + buf: ReadBufCursor<'_>, + ) -> Poll> { + let this = self.project(); + Read::poll_read(this.inner, cx, buf) + } +} + +impl Write for TlsConn { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + let this = self.project(); + Write::poll_write(this.inner, cx, buf) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[IoSlice<'_>], + ) -> Poll> { + let this = self.project(); + Write::poll_write_vectored(this.inner, cx, bufs) + } + + fn is_write_vectored(&self) -> bool { + self.inner.is_write_vectored() + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let this = self.project(); + Write::poll_flush(this.inner, cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let this = self.project(); + Write::poll_shutdown(this.inner, cx) + } +} + +impl TlsInfoFactory for TlsConn +where + TokioIo>: TlsInfoFactory, +{ + fn tls_info(&self) -> Option { + self.inner.tls_info() + } +} diff --git a/src/client/http/connect.rs b/src/client/http/connect/connector.rs similarity index 52% rename from src/client/http/connect.rs rename to src/client/http/connect/connector.rs index cb4e3e7f5..c6d3059cd 100644 --- a/src/client/http/connect.rs +++ b/src/client/http/connect/connector.rs @@ -1,6 +1,5 @@ use std::{ future::Future, - io::{self, IoSlice}, pin::Pin, sync::Arc, task::{Context, Poll}, @@ -8,32 +7,28 @@ use std::{ }; use http::uri::Scheme; -use pin_project_lite::pin_project; -use tls_conn::TlsConn; -use tokio::net::TcpStream; -use tokio_boring2::SslStream; use tower::{ Service, ServiceBuilder, timeout::TimeoutLayer, util::{BoxCloneSyncService, MapRequestLayer}, }; -pub(super) use self::conn::{Conn, Unnameable}; -use super::aliases::{BoxedConnectorLayer, BoxedConnectorService, HttpConnector}; +use super::{ + super::aliases::{BoxedConnectorLayer, BoxedConnectorService, HttpConnector}, + conn::{Conn, TlsConn, Unnameable}, + verbose, +}; use crate::{ core::{ - client::{ - ConnExtra, ConnRequest, - connect::{Connected, Connection, proxy}, - }, - rt::{Read, ReadBufCursor, TokioIo, Write}, + client::{ConnExtra, ConnRequest, connect::proxy}, + rt::TokioIo, }, dns::DynResolver, error::{BoxError, TimedOut, map_timeout_to_connector_error}, proxy::{Intercepted, Matcher as ProxyMatcher}, tls::{ EstablishedConn, HttpsConnector, MaybeHttpsStream, TlsConnector, TlsConnectorBuilder, - TlsInfo, TlsOptions, + TlsOptions, }, }; @@ -204,7 +199,7 @@ impl Connector { ConnectorBuilder { config: Config { proxies, - verbose: verbose::OFF, + verbose: verbose::Wrapper(false), tcp_nodelay: false, tls_info: false, timeout: None, @@ -448,422 +443,3 @@ impl Service for ConnectorService { Box::pin(self.clone().connect_auto(req)) } } - -trait TlsInfoFactory { - fn tls_info(&self) -> Option; -} - -impl TlsInfoFactory for TcpStream { - fn tls_info(&self) -> Option { - None - } -} - -impl TlsInfoFactory for TokioIo { - fn tls_info(&self) -> Option { - self.inner().tls_info() - } -} - -impl TlsInfoFactory for SslStream { - fn tls_info(&self) -> Option { - self.ssl() - .peer_certificate() - .and_then(|c| c.to_der().ok()) - .map(|c| TlsInfo { - peer_certificate: Some(c), - }) - } -} - -impl TlsInfoFactory for MaybeHttpsStream { - fn tls_info(&self) -> Option { - match self { - MaybeHttpsStream::Https(tls) => tls.tls_info(), - MaybeHttpsStream::Http(_) => None, - } - } -} - -impl TlsInfoFactory for SslStream>> { - fn tls_info(&self) -> Option { - self.ssl() - .peer_certificate() - .and_then(|c| c.to_der().ok()) - .map(|c| TlsInfo { - peer_certificate: Some(c), - }) - } -} - -pub(crate) trait AsyncConn: - Read + Write + Connection + Send + Sync + Unpin + 'static -{ -} - -impl AsyncConn for T {} - -trait AsyncConnWithInfo: AsyncConn + TlsInfoFactory {} - -impl AsyncConnWithInfo for T {} - -type BoxConn = Box; - -mod conn { - use super::*; - - #[derive(Debug)] - pub struct Unnameable(pub(super) ConnRequest); - - pin_project! { - /// Note: the `is_proxy` member means *is plain text HTTP proxy*. - /// This tells core whether the URI should be written in - /// * origin-form (`GET /just/a/path HTTP/1.1`), when `is_proxy == false`, or - /// * absolute-form (`GET http://foo.bar/and/a/path HTTP/1.1`), otherwise. - pub struct Conn { - #[pin] - pub(super) inner: BoxConn, - pub(super) is_proxy: bool, - pub(super) tls_info: bool, - } - } - - impl Connection for Conn { - fn connected(&self) -> Connected { - let connected = self.inner.connected().proxy(self.is_proxy); - - if self.tls_info { - if let Some(tls_info) = self.inner.tls_info() { - connected.extra(tls_info) - } else { - connected - } - } else { - connected - } - } - } - - impl Read for Conn { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context, - buf: ReadBufCursor<'_>, - ) -> Poll> { - let this = self.project(); - Read::poll_read(this.inner, cx, buf) - } - } - - impl Write for Conn { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context, - buf: &[u8], - ) -> Poll> { - let this = self.project(); - Write::poll_write(this.inner, cx, buf) - } - - fn poll_write_vectored( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - bufs: &[IoSlice<'_>], - ) -> Poll> { - let this = self.project(); - Write::poll_write_vectored(this.inner, cx, bufs) - } - - fn is_write_vectored(&self) -> bool { - self.inner.is_write_vectored() - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { - let this = self.project(); - Write::poll_flush(this.inner, cx) - } - - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { - let this = self.project(); - Write::poll_shutdown(this.inner, cx) - } - } -} - -mod tls_conn { - use std::{ - io::{self, IoSlice}, - pin::Pin, - task::{Context, Poll}, - }; - - use pin_project_lite::pin_project; - use tokio::{ - io::{AsyncRead, AsyncWrite}, - net::TcpStream, - }; - use tokio_boring2::SslStream; - - use super::{TlsInfo, TlsInfoFactory}; - use crate::{ - core::{ - client::connect::{Connected, Connection}, - rt::{Read, ReadBufCursor, TokioIo, Write}, - }, - tls::MaybeHttpsStream, - }; - - pin_project! { - pub(super) struct TlsConn { - #[pin] - pub(super) inner: TokioIo>, - } - } - - impl Connection for TlsConn { - fn connected(&self) -> Connected { - let connected = self.inner.inner().get_ref().connected(); - if self.inner.inner().ssl().selected_alpn_protocol() == Some(b"h2") { - connected.negotiated_h2() - } else { - connected - } - } - } - - impl Connection for TlsConn>> { - fn connected(&self) -> Connected { - let connected = self.inner.inner().get_ref().connected(); - if self.inner.inner().ssl().selected_alpn_protocol() == Some(b"h2") { - connected.negotiated_h2() - } else { - connected - } - } - } - - impl Read for TlsConn { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context, - buf: ReadBufCursor<'_>, - ) -> Poll> { - let this = self.project(); - Read::poll_read(this.inner, cx, buf) - } - } - - impl Write for TlsConn { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context, - buf: &[u8], - ) -> Poll> { - let this = self.project(); - Write::poll_write(this.inner, cx, buf) - } - - fn poll_write_vectored( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - bufs: &[IoSlice<'_>], - ) -> Poll> { - let this = self.project(); - Write::poll_write_vectored(this.inner, cx, bufs) - } - - fn is_write_vectored(&self) -> bool { - self.inner.is_write_vectored() - } - - fn poll_flush( - self: Pin<&mut Self>, - cx: &mut Context, - ) -> Poll> { - let this = self.project(); - Write::poll_flush(this.inner, cx) - } - - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut Context, - ) -> Poll> { - let this = self.project(); - Write::poll_shutdown(this.inner, cx) - } - } - - impl TlsInfoFactory for TlsConn - where - TokioIo>: TlsInfoFactory, - { - fn tls_info(&self) -> Option { - self.inner.tls_info() - } - } -} - -mod verbose { - use super::{AsyncConnWithInfo, BoxConn}; - - pub(super) const OFF: Wrapper = Wrapper(false); - - #[derive(Clone, Copy)] - pub(super) struct Wrapper(pub(super) bool); - - impl Wrapper { - #[cfg_attr(not(feature = "tracing"), inline(always))] - pub(super) fn wrap(&self, conn: T) -> BoxConn { - #[cfg(feature = "tracing")] - { - if self.0 { - return Box::new(sealed::Verbose { - // truncate is fine - id: crate::util::fast_random() as u32, - inner: conn, - }); - } - } - - Box::new(conn) - } - } - - #[cfg(feature = "tracing")] - mod sealed { - use std::{ - fmt, - io::{self, IoSlice}, - pin::Pin, - task::{Context, Poll}, - }; - - use super::super::TlsInfoFactory; - use crate::{ - core::{ - client::connect::{Connected, Connection}, - rt::{Read, ReadBufCursor, Write}, - }, - tls::TlsInfo, - util::Escape, - }; - - pub(super) struct Verbose { - pub(super) id: u32, - pub(super) inner: T, - } - - impl Connection for Verbose { - fn connected(&self) -> Connected { - self.inner.connected() - } - } - - impl Read for Verbose { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context, - mut buf: ReadBufCursor<'_>, - ) -> Poll> { - // TODO: This _does_ forget the `init` len, so it could result in - // re-initializing twice. Needs upstream support, perhaps. - // SAFETY: Passing to a ReadBuf will never de-initialize any bytes. - let mut vbuf = crate::core::rt::ReadBuf::uninit(unsafe { buf.as_mut() }); - match Pin::new(&mut self.inner).poll_read(cx, vbuf.unfilled()) { - Poll::Ready(Ok(())) => { - trace!("{:08x} read: {:?}", self.id, Escape::new(vbuf.filled())); - let len = vbuf.filled().len(); - // SAFETY: The two cursors were for the same buffer. What was - // filled in one is safe in the other. - unsafe { - buf.advance(len); - } - Poll::Ready(Ok(())) - } - Poll::Ready(Err(e)) => Poll::Ready(Err(e)), - Poll::Pending => Poll::Pending, - } - } - } - - impl Write for Verbose { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context, - buf: &[u8], - ) -> Poll> { - match Pin::new(&mut self.inner).poll_write(cx, buf) { - Poll::Ready(Ok(n)) => { - trace!("{:08x} write: {:?}", self.id, Escape::new(&buf[..n])); - Poll::Ready(Ok(n)) - } - Poll::Ready(Err(e)) => Poll::Ready(Err(e)), - Poll::Pending => Poll::Pending, - } - } - - fn poll_write_vectored( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - bufs: &[IoSlice<'_>], - ) -> Poll> { - match Pin::new(&mut self.inner).poll_write_vectored(cx, bufs) { - Poll::Ready(Ok(nwritten)) => { - trace!( - "{:08x} write (vectored): {:?}", - self.id, - Vectored { bufs, nwritten } - ); - Poll::Ready(Ok(nwritten)) - } - Poll::Ready(Err(e)) => Poll::Ready(Err(e)), - Poll::Pending => Poll::Pending, - } - } - - fn is_write_vectored(&self) -> bool { - self.inner.is_write_vectored() - } - - fn poll_flush( - mut self: Pin<&mut Self>, - cx: &mut Context, - ) -> Poll> { - Pin::new(&mut self.inner).poll_flush(cx) - } - - fn poll_shutdown( - mut self: Pin<&mut Self>, - cx: &mut Context, - ) -> Poll> { - Pin::new(&mut self.inner).poll_shutdown(cx) - } - } - - impl TlsInfoFactory for Verbose { - fn tls_info(&self) -> Option { - self.inner.tls_info() - } - } - - struct Vectored<'a, 'b> { - bufs: &'a [IoSlice<'b>], - nwritten: usize, - } - - impl fmt::Debug for Vectored<'_, '_> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut left = self.nwritten; - for buf in self.bufs.iter() { - if left == 0 { - break; - } - let n = std::cmp::min(left, buf.len()); - Escape::new(&buf[..n]).fmt(f)?; - left -= n; - } - Ok(()) - } - } - } -} diff --git a/src/client/http/connect/mod.rs b/src/client/http/connect/mod.rs new file mode 100644 index 000000000..7df04b63d --- /dev/null +++ b/src/client/http/connect/mod.rs @@ -0,0 +1,32 @@ +mod conn; +mod connector; +mod tls_info; +mod verbose; + +pub(super) use self::{ + conn::{Conn, Unnameable}, + connector::Connector, + tls_info::TlsInfoFactory, +}; +use crate::core::{ + client::connect::Connection, + rt::{Read, Write}, +}; + +/// A trait alias for types that can be used as async connections. +/// +/// This trait is automatically implemented for any type that satisfies the required bounds: +/// - [`Read`] + [`Write`]: For I/O operations +/// - [`Connection`]: For connection metadata +/// - [`Send`] + [`Sync`] + [`Unpin`] + `'static`: For async/await compatibility +trait AsyncConn: Read + Write + Connection + Send + Sync + Unpin + 'static {} + +/// An async connection that can also provide TLS information. +/// +/// This extends [`AsyncConn`] with the ability to extract TLS certificate information +/// when available. Useful for connections that may be either plain TCP or TLS-encrypted. +trait AsyncConnWithInfo: AsyncConn + TlsInfoFactory {} + +impl AsyncConn for T where T: Read + Write + Connection + Send + Sync + Unpin + 'static {} + +impl AsyncConnWithInfo for T where T: AsyncConn + TlsInfoFactory {} diff --git a/src/client/http/connect/tls_info.rs b/src/client/http/connect/tls_info.rs new file mode 100644 index 000000000..9d041d801 --- /dev/null +++ b/src/client/http/connect/tls_info.rs @@ -0,0 +1,54 @@ +use tokio::net::TcpStream; +use tokio_boring2::SslStream; + +use crate::{ + core::rt::TokioIo, + tls::{MaybeHttpsStream, TlsInfo}, +}; + +pub trait TlsInfoFactory { + fn tls_info(&self) -> Option; +} + +impl TlsInfoFactory for TcpStream { + fn tls_info(&self) -> Option { + None + } +} + +impl TlsInfoFactory for TokioIo { + fn tls_info(&self) -> Option { + self.inner().tls_info() + } +} + +impl TlsInfoFactory for SslStream { + fn tls_info(&self) -> Option { + self.ssl() + .peer_certificate() + .and_then(|c| c.to_der().ok()) + .map(|c| TlsInfo { + peer_certificate: Some(c), + }) + } +} + +impl TlsInfoFactory for MaybeHttpsStream { + fn tls_info(&self) -> Option { + match self { + MaybeHttpsStream::Https(tls) => tls.tls_info(), + MaybeHttpsStream::Http(_) => None, + } + } +} + +impl TlsInfoFactory for SslStream>> { + fn tls_info(&self) -> Option { + self.ssl() + .peer_certificate() + .and_then(|c| c.to_der().ok()) + .map(|c| TlsInfo { + peer_certificate: Some(c), + }) + } +} diff --git a/src/client/http/connect/verbose.rs b/src/client/http/connect/verbose.rs new file mode 100644 index 000000000..6ced28a5b --- /dev/null +++ b/src/client/http/connect/verbose.rs @@ -0,0 +1,160 @@ +use super::AsyncConnWithInfo; + +#[derive(Clone, Copy)] +pub(super) struct Wrapper(pub(super) bool); + +impl Wrapper { + #[cfg_attr(not(feature = "tracing"), inline(always))] + pub(super) fn wrap(&self, conn: T) -> Box { + #[cfg(feature = "tracing")] + { + if self.0 { + return Box::new(sealed::Verbose { + // truncate is fine + id: crate::util::fast_random() as u32, + inner: conn, + }); + } + } + + Box::new(conn) + } +} + +#[cfg(feature = "tracing")] +mod sealed { + use std::{ + fmt, + io::{self, IoSlice}, + pin::Pin, + task::{Context, Poll}, + }; + + use super::super::TlsInfoFactory; + use crate::{ + core::{ + client::connect::{Connected, Connection}, + rt::{Read, ReadBufCursor, Write}, + }, + tls::TlsInfo, + util::Escape, + }; + + pub(super) struct Verbose { + pub(super) id: u32, + pub(super) inner: T, + } + + impl Connection for Verbose { + fn connected(&self) -> Connected { + self.inner.connected() + } + } + + impl Read for Verbose { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context, + mut buf: ReadBufCursor<'_>, + ) -> Poll> { + // TODO: This _does_ forget the `init` len, so it could result in + // re-initializing twice. Needs upstream support, perhaps. + // SAFETY: Passing to a ReadBuf will never de-initialize any bytes. + let mut vbuf = crate::core::rt::ReadBuf::uninit(unsafe { buf.as_mut() }); + match Pin::new(&mut self.inner).poll_read(cx, vbuf.unfilled()) { + Poll::Ready(Ok(())) => { + trace!("{:08x} read: {:?}", self.id, Escape::new(vbuf.filled())); + let len = vbuf.filled().len(); + // SAFETY: The two cursors were for the same buffer. What was + // filled in one is safe in the other. + unsafe { + buf.advance(len); + } + Poll::Ready(Ok(())) + } + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + Poll::Pending => Poll::Pending, + } + } + } + + impl Write for Verbose { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + match Pin::new(&mut self.inner).poll_write(cx, buf) { + Poll::Ready(Ok(n)) => { + trace!("{:08x} write: {:?}", self.id, Escape::new(&buf[..n])); + Poll::Ready(Ok(n)) + } + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + Poll::Pending => Poll::Pending, + } + } + + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[IoSlice<'_>], + ) -> Poll> { + match Pin::new(&mut self.inner).poll_write_vectored(cx, bufs) { + Poll::Ready(Ok(nwritten)) => { + trace!( + "{:08x} write (vectored): {:?}", + self.id, + Vectored { bufs, nwritten } + ); + Poll::Ready(Ok(nwritten)) + } + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + Poll::Pending => Poll::Pending, + } + } + + fn is_write_vectored(&self) -> bool { + self.inner.is_write_vectored() + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context, + ) -> Poll> { + Pin::new(&mut self.inner).poll_flush(cx) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context, + ) -> Poll> { + Pin::new(&mut self.inner).poll_shutdown(cx) + } + } + + impl TlsInfoFactory for Verbose { + fn tls_info(&self) -> Option { + self.inner.tls_info() + } + } + + struct Vectored<'a, 'b> { + bufs: &'a [IoSlice<'b>], + nwritten: usize, + } + + impl fmt::Debug for Vectored<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut left = self.nwritten; + for buf in self.bufs.iter() { + if left == 0 { + break; + } + let n = std::cmp::min(left, buf.len()); + Escape::new(&buf[..n]).fmt(f)?; + left -= n; + } + Ok(()) + } + } +} From 2300470f8770dee8ce16df658db1be65921f7ead Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Sat, 19 Jul 2025 22:40:07 +0800 Subject: [PATCH 2/4] fmt --- src/client/http/connect/conn.rs | 48 ++++++++++++++++++++++------ src/client/http/connect/connector.rs | 45 ++++++++++---------------- src/client/http/connect/verbose.rs | 15 ++++----- 3 files changed, 62 insertions(+), 46 deletions(-) diff --git a/src/client/http/connect/conn.rs b/src/client/http/connect/conn.rs index 3606c6a80..f071eae57 100644 --- a/src/client/http/connect/conn.rs +++ b/src/client/http/connect/conn.rs @@ -32,14 +32,38 @@ pin_project! { /// * absolute-form (`GET http://foo.bar/and/a/path HTTP/1.1`), otherwise. pub struct Conn { #[pin] - pub(super) inner: Box, - pub(super) is_proxy: bool, - pub(super) tls_info: bool, + inner: Box, + is_proxy: bool, + tls_info: bool, + } +} + +pin_project! { + /// A wrapper around `SslStream` that adapts it for use as a generic async connection. + /// + /// This type enables unified handling of plain TCP and TLS-encrypted streams by providing + /// implementations of `Connection`, `Read`, `Write`, and `TlsInfoFactory`. + /// It is mainly used internally to abstract over different connection types. + pub(super) struct TlsConn { + #[pin] + inner: TokioIo>, } } // ==== impl Conn ==== +impl Conn { + /// Creates a new `Conn` instance with the given inner connection and TLS info flag. + #[inline(always)] + pub(super) fn new(inner: Box, is_proxy: bool, tls_info: bool) -> Self { + Self { + inner, + is_proxy, + tls_info, + } + } +} + impl Connection for Conn { fn connected(&self) -> Connected { let connected = self.inner.connected().proxy(self.is_proxy); @@ -101,15 +125,21 @@ impl Write for Conn { } } -pin_project! { - pub(super) struct TlsConn { - #[pin] - pub(super) inner: TokioIo>, +// ==== impl TlsConn ==== + +impl TlsConn +where + T: AsyncRead + AsyncWrite + Unpin, +{ + /// Creates a new `TlsConn` wrapping the provided `SslStream`. + #[inline(always)] + pub fn new(inner: SslStream) -> Self { + Self { + inner: TokioIo::new(inner), + } } } -// ==== impl TlsConn ==== - impl Connection for TlsConn { fn connected(&self) -> Connected { let connected = self.inner.inner().get_ref().connected(); diff --git a/src/client/http/connect/connector.rs b/src/client/http/connect/connector.rs index c6d3059cd..77e83e902 100644 --- a/src/client/http/connect/connector.rs +++ b/src/client/http/connect/connector.rs @@ -275,18 +275,12 @@ impl ConnectorService { if !self.config.tcp_nodelay { stream.get_ref().set_nodelay(false)?; } - self.config.verbose.wrap(TlsConn { - inner: TokioIo::new(stream), - }) + self.config.verbose.wrap(TlsConn::new(stream)) } else { self.config.verbose.wrap(io) }; - Ok(Conn { - inner, - is_proxy, - tls_info: self.config.tls_info, - }) + Ok(Conn::new(inner, is_proxy, self.config.tls_info)) } /// Establishes a connection through a specified proxy. @@ -325,7 +319,7 @@ impl ConnectorService { let is_https = uri.scheme() == Some(&Scheme::HTTPS); let conn = socks.call(uri).await?; - return if is_https { + let conn = if is_https { trace!("socks HTTPS over proxy"); // Create a TLS connector for the established connection. @@ -334,20 +328,16 @@ impl ConnectorService { let established_conn = EstablishedConn::new(req, conn); let io = connector.call(established_conn).await?; - Ok(Conn { - inner: self.config.verbose.wrap(TlsConn { - inner: TokioIo::new(io), - }), - is_proxy: false, - tls_info: self.config.tls_info, - }) + Conn::new( + self.config.verbose.wrap(TlsConn::new(io)), + false, + self.config.tls_info, + ) } else { - Ok(Conn { - inner: self.config.verbose.wrap(conn), - is_proxy: false, - tls_info: false, - }) + Conn::new(self.config.verbose.wrap(conn), false, false) }; + + return Ok(conn); } } @@ -379,13 +369,12 @@ impl ConnectorService { let established_conn = EstablishedConn::new(req, tunneled); let io = connector.call(established_conn).await?; - return Ok(Conn { - inner: self.config.verbose.wrap(TlsConn { - inner: TokioIo::new(io), - }), - is_proxy: false, - tls_info: self.config.tls_info, - }); + let conn = Conn::new( + self.config.verbose.wrap(TlsConn::new(io)), + false, + self.config.tls_info, + ); + return Ok(conn); } *req.uri_mut() = proxy_uri; diff --git a/src/client/http/connect/verbose.rs b/src/client/http/connect/verbose.rs index 6ced28a5b..d3572030d 100644 --- a/src/client/http/connect/verbose.rs +++ b/src/client/http/connect/verbose.rs @@ -7,14 +7,11 @@ impl Wrapper { #[cfg_attr(not(feature = "tracing"), inline(always))] pub(super) fn wrap(&self, conn: T) -> Box { #[cfg(feature = "tracing")] - { - if self.0 { - return Box::new(sealed::Verbose { - // truncate is fine - id: crate::util::fast_random() as u32, - inner: conn, - }); - } + if self.0 { + return Box::new(sealed::Verbose { + id: crate::util::fast_random(), + inner: conn, + }); } Box::new(conn) @@ -41,7 +38,7 @@ mod sealed { }; pub(super) struct Verbose { - pub(super) id: u32, + pub(super) id: u64, pub(super) inner: T, } From be88c796b45fd8a567153ce7a4308973b4dfec9e Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Sat, 19 Jul 2025 22:59:15 +0800 Subject: [PATCH 3/4] fmt --- src/client/http/connect/conn.rs | 7 +------ src/client/http/connect/connector.rs | 9 +++++---- src/client/http/connect/mod.rs | 15 +++++++++------ src/client/http/connect/verbose.rs | 23 ++++++++++++++--------- 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/client/http/connect/conn.rs b/src/client/http/connect/conn.rs index f071eae57..daf285dca 100644 --- a/src/client/http/connect/conn.rs +++ b/src/client/http/connect/conn.rs @@ -14,17 +14,12 @@ use tokio_boring2::SslStream; use super::{AsyncConnWithInfo, TlsInfoFactory}; use crate::{ core::{ - client::{ - ConnRequest, - connect::{Connected, Connection}, - }, + client::connect::{Connected, Connection}, rt::{Read, ReadBufCursor, TokioIo, Write}, }, tls::{MaybeHttpsStream, TlsInfo}, }; -pub struct Unnameable(pub(super) ConnRequest); - pin_project! { /// Note: the `is_proxy` member means *is plain text HTTP proxy*. /// This tells core whether the URI should be written in diff --git a/src/client/http/connect/connector.rs b/src/client/http/connect/connector.rs index 77e83e902..d97605b76 100644 --- a/src/client/http/connect/connector.rs +++ b/src/client/http/connect/connector.rs @@ -15,8 +15,9 @@ use tower::{ use super::{ super::aliases::{BoxedConnectorLayer, BoxedConnectorService, HttpConnector}, - conn::{Conn, TlsConn, Unnameable}, - verbose, + Unnameable, + conn::{Conn, TlsConn}, + verbose::Verbose, }; use crate::{ core::{ @@ -38,7 +39,7 @@ type Connecting = Pin> + Send>>; #[derive(Clone)] struct Config { proxies: Arc>, - verbose: verbose::Wrapper, + verbose: Verbose, tcp_nodelay: bool, tls_info: bool, /// When there is a single timeout layer and no other layers, @@ -199,7 +200,7 @@ impl Connector { ConnectorBuilder { config: Config { proxies, - verbose: verbose::Wrapper(false), + verbose: Verbose::OFF, tcp_nodelay: false, tls_info: false, timeout: None, diff --git a/src/client/http/connect/mod.rs b/src/client/http/connect/mod.rs index 7df04b63d..e23845d7e 100644 --- a/src/client/http/connect/mod.rs +++ b/src/client/http/connect/mod.rs @@ -3,16 +3,19 @@ mod connector; mod tls_info; mod verbose; -pub(super) use self::{ - conn::{Conn, Unnameable}, - connector::Connector, - tls_info::TlsInfoFactory, -}; +pub(super) use self::{conn::Conn, connector::Connector, tls_info::TlsInfoFactory}; use crate::core::{ - client::connect::Connection, + client::{ConnRequest, connect::Connection}, rt::{Read, Write}, }; +/// A wrapper type for [`ConnRequest`] used to erase its concrete type. +/// +/// [`Unnameable`] allows passing connection requests through trait objects or +/// type-erased interfaces where the concrete type of the request is not important. +/// This is mainly used internally to simplify service composition and dynamic dispatch. +pub struct Unnameable(pub(super) ConnRequest); + /// A trait alias for types that can be used as async connections. /// /// This trait is automatically implemented for any type that satisfies the required bounds: diff --git a/src/client/http/connect/verbose.rs b/src/client/http/connect/verbose.rs index d3572030d..d8f3cbc04 100644 --- a/src/client/http/connect/verbose.rs +++ b/src/client/http/connect/verbose.rs @@ -1,14 +1,19 @@ use super::AsyncConnWithInfo; #[derive(Clone, Copy)] -pub(super) struct Wrapper(pub(super) bool); +pub struct Verbose(pub(super) bool); + +impl Verbose { + pub const OFF: Verbose = Verbose(false); -impl Wrapper { #[cfg_attr(not(feature = "tracing"), inline(always))] - pub(super) fn wrap(&self, conn: T) -> Box { + pub(super) fn wrap(&self, conn: T) -> Box + where + T: AsyncConnWithInfo + 'static, + { #[cfg(feature = "tracing")] if self.0 { - return Box::new(sealed::Verbose { + return Box::new(sealed::Wrapper { id: crate::util::fast_random(), inner: conn, }); @@ -37,18 +42,18 @@ mod sealed { util::Escape, }; - pub(super) struct Verbose { + pub(super) struct Wrapper { pub(super) id: u64, pub(super) inner: T, } - impl Connection for Verbose { + impl Connection for Wrapper { fn connected(&self) -> Connected { self.inner.connected() } } - impl Read for Verbose { + impl Read for Wrapper { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context, @@ -75,7 +80,7 @@ mod sealed { } } - impl Write for Verbose { + impl Write for Wrapper { fn poll_write( mut self: Pin<&mut Self>, cx: &mut Context, @@ -129,7 +134,7 @@ mod sealed { } } - impl TlsInfoFactory for Verbose { + impl TlsInfoFactory for Wrapper { fn tls_info(&self) -> Option { self.inner.tls_info() } From bc59204b86da9a56e575c318bb62afbc94d3b1d7 Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Sat, 19 Jul 2025 23:01:14 +0800 Subject: [PATCH 4/4] fmt --- src/client/http/connect/tls_info.rs | 4 ++++ src/client/http/connect/verbose.rs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/client/http/connect/tls_info.rs b/src/client/http/connect/tls_info.rs index 9d041d801..8ee2275f4 100644 --- a/src/client/http/connect/tls_info.rs +++ b/src/client/http/connect/tls_info.rs @@ -6,6 +6,10 @@ use crate::{ tls::{MaybeHttpsStream, TlsInfo}, }; +/// A trait for extracting TLS information from a connection. +/// +/// Implementors can provide access to peer certificate data or other TLS-related metadata. +/// For non-TLS connections, this typically returns `None`. pub trait TlsInfoFactory { fn tls_info(&self) -> Option; } diff --git a/src/client/http/connect/verbose.rs b/src/client/http/connect/verbose.rs index d8f3cbc04..e38b3980b 100644 --- a/src/client/http/connect/verbose.rs +++ b/src/client/http/connect/verbose.rs @@ -1,5 +1,9 @@ use super::AsyncConnWithInfo; +/// Controls whether to enable verbose tracing for connections. +/// +/// When enabled (with the `tracing` feature), connections are wrapped to log I/O operations for +/// debugging. #[derive(Clone, Copy)] pub struct Verbose(pub(super) bool);