diff --git a/src/async_impl/client.rs b/src/async_impl/client.rs index ecb540b..b9cbee6 100644 --- a/src/async_impl/client.rs +++ b/src/async_impl/client.rs @@ -64,6 +64,7 @@ struct Config { #[cfg(feature = "__tls")] certs_verification: bool, connect_timeout: Option, + connection_verbose: bool, max_idle_per_host: usize, #[cfg(feature = "__tls")] identity: Option, @@ -111,6 +112,7 @@ impl ClientBuilder { #[cfg(feature = "__tls")] certs_verification: true, connect_timeout: None, + connection_verbose: false, max_idle_per_host: std::usize::MAX, proxies: Vec::new(), auto_sys_proxy: true, @@ -234,6 +236,7 @@ impl ClientBuilder { }; connector.set_timeout(config.connect_timeout); + connector.set_verbose(config.connection_verbose); let mut builder = hyper::Client::builder(); if config.http2_only { @@ -489,6 +492,17 @@ impl ClientBuilder { self } + /// Set whether connections should emit verbose logs. + /// + /// Enabling this option will emit [log][] messages at the `TRACE` level + /// for read and write operations on connections. + /// + /// [log]: https://crates.io/crates/log + pub fn connection_verbose(mut self, verbose: bool) -> ClientBuilder { + self.config.connection_verbose = verbose; + self + } + // HTTP options /// Sets the maximum idle connection per host allowed in the pool. diff --git a/src/async_impl/multipart.rs b/src/async_impl/multipart.rs index bf2ac7c..45b8269 100644 --- a/src/async_impl/multipart.rs +++ b/src/async_impl/multipart.rs @@ -487,6 +487,8 @@ impl PercentEncoding { } fn gen_boundary() -> String { + use crate::util::fast_random as random; + let a = random(); let b = random(); let c = random(); @@ -495,42 +497,6 @@ fn gen_boundary() -> String { format!("{:016x}-{:016x}-{:016x}-{:016x}", a, b, c, d) } -// xor-shift -fn random() -> u64 { - use std::cell::Cell; - use std::collections::hash_map::RandomState; - use std::hash::{BuildHasher, Hasher}; - use std::num::Wrapping; - - thread_local! { - static RNG: Cell> = Cell::new(Wrapping(seed())); - } - - fn seed() -> u64 { - let seed = RandomState::new(); - - let mut out = 0; - let mut cnt = 0; - while out == 0 { - cnt += 1; - let mut hasher = seed.build_hasher(); - hasher.write_usize(cnt); - out = hasher.finish(); - } - out - } - - RNG.with(|rng| { - let mut n = rng.get(); - debug_assert_ne!(n.0, 0); - n ^= n >> 12; - n ^= n << 25; - n ^= n >> 27; - rng.set(n); - n.0.wrapping_mul(0x2545_f491_4f6c_dd1d) - }) -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/blocking/client.rs b/src/blocking/client.rs index 259cc6a..b311209 100644 --- a/src/blocking/client.rs +++ b/src/blocking/client.rs @@ -273,6 +273,16 @@ impl ClientBuilder { } } + /// Set whether connections should emit verbose logs. + /// + /// Enabling this option will emit [log][] messages at the `TRACE` level + /// for read and write operations on connections. + /// + /// [log]: https://crates.io/crates/log + pub fn connection_verbose(self, verbose: bool) -> ClientBuilder { + self.with_inner(move |inner| inner.connection_verbose(verbose)) + } + // HTTP options /// Sets the maximum idle connection per host allowed in the pool. diff --git a/src/connect.rs b/src/connect.rs index 100c58b..378f2d4 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -37,6 +37,7 @@ type HttpConnector = hyper::client::HttpConnector; pub(crate) struct Connector { inner: Inner, proxies: Arc>, + verbose: verbose::Wrapper, timeout: Option, #[cfg(feature = "__tls")] nodelay: bool, @@ -73,6 +74,7 @@ impl Connector { http.set_nodelay(nodelay); Ok(Connector { inner: Inner::Http(http), + verbose: verbose::OFF, proxies, timeout: None, }) @@ -98,6 +100,7 @@ impl Connector { Ok(Connector { inner: Inner::DefaultTls(http, tls), proxies, + verbose: verbose::OFF, timeout: None, nodelay, user_agent, @@ -135,6 +138,7 @@ impl Connector { tls_proxy, }, proxies, + verbose: verbose::OFF, timeout: None, nodelay, user_agent, @@ -145,6 +149,10 @@ impl Connector { self.timeout = timeout; } + pub(crate) fn set_verbose(&mut self, enabled: bool) { + self.verbose.0 = enabled; + } + #[cfg(feature = "socks")] async fn connect_socks( &self, @@ -178,7 +186,7 @@ impl Connector { .await .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; return Ok(Conn { - inner: Box::new(NativeTlsConn { inner: io }), + inner: self.verbose.wrap(NativeTlsConn { inner: io }), is_proxy: false, }); } @@ -203,7 +211,7 @@ impl Connector { .await .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; return Ok(Conn { - inner: Box::new(RustlsTlsConn { inner: io }), + inner: self.verbose.wrap(RustlsTlsConn { inner: io }), is_proxy: false, }); } @@ -212,7 +220,10 @@ impl Connector { Inner::Http(_) => () } - socks::connect(proxy, dst, dns).await + socks::connect(proxy, dst, dns).await.map(|tcp| Conn { + inner: self.verbose.wrap(tcp), + is_proxy: false, + }) } async fn connect_with_maybe_proxy( @@ -225,7 +236,7 @@ impl Connector { Inner::Http(mut http) => { let io = http.call(dst).await?; Ok(Conn { - inner: Box::new(io), + inner: self.verbose.wrap(io), is_proxy, }) } @@ -246,7 +257,7 @@ impl Connector { //} Ok(Conn { - inner: Box::new(io), + inner: self.verbose.wrap(io), is_proxy, }) } @@ -270,7 +281,7 @@ impl Connector { } Ok(Conn { - inner: Box::new(io), + inner: self.verbose.wrap(io), is_proxy, }) } @@ -322,7 +333,7 @@ impl Connector { .await .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; return Ok(Conn { - inner: Box::new(NativeTlsConn { inner: io }), + inner: self.verbose.wrap(NativeTlsConn { inner: io }), is_proxy: false, }); } @@ -359,7 +370,7 @@ impl Connector { .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; return Ok(Conn { - inner: Box::new(RustlsTlsConn { inner: io }), + inner: self.verbose.wrap(RustlsTlsConn { inner: io }), is_proxy: false, }); } @@ -438,8 +449,11 @@ impl Service for Connector } } -pub(crate) trait AsyncConn: AsyncRead + AsyncWrite + Connection {} -impl AsyncConn for T {} +pub(crate) trait AsyncConn: AsyncRead + AsyncWrite + Connection + Send + Sync + Unpin + 'static {} + +impl AsyncConn for T {} + +type BoxConn = Box; pin_project! { /// Note: the `is_proxy` member means *is plain text HTTP proxy*. @@ -448,7 +462,7 @@ pin_project! { /// * absolute-form (`GET http://foo.bar/and/a/path HTTP/1.1`), otherwise. pub(crate) struct Conn { #[pin] - inner: Box, + inner: BoxConn, is_proxy: bool, } } @@ -795,11 +809,13 @@ mod rustls_tls_conn { #[cfg(feature = "socks")] mod socks { - use http::Uri; - use tokio_socks::tcp::Socks5Stream; use std::io; use std::net::ToSocketAddrs; + use http::Uri; + use tokio::net::TcpStream; + use tokio_socks::tcp::Socks5Stream; + use super::{BoxError, Scheme}; use crate::proxy::ProxyScheme; @@ -812,7 +828,7 @@ mod socks { proxy: ProxyScheme, dst: Uri, dns: DnsResolve, - ) -> Result { + ) -> Result { let https = dst.scheme() == Some(&Scheme::HTTPS); let original_host = dst .host() @@ -852,10 +868,123 @@ mod socks { .map_err(|e| format!("socks connect error: {}", e))? }; - Ok(super::Conn { - inner: Box::new( stream.into_inner() ), - is_proxy: false, - }) + Ok(stream.into_inner()) + } +} + +mod verbose { + use std::fmt; + use std::pin::Pin; + use std::task::{Context, Poll}; + use hyper::client::connect::{Connected, Connection}; + use tokio::io::{AsyncRead, AsyncWrite}; + + pub(super) const OFF: Wrapper = Wrapper(false); + + #[derive(Clone, Copy)] + pub(super) struct Wrapper(pub(super) bool); + + impl Wrapper { + pub(super) fn wrap(&self, conn: T) -> super::BoxConn { + if self.0 && log::log_enabled!(log::Level::Trace) { + Box::new(Verbose { + // truncate is fine + id: crate::util::fast_random() as u32, + inner: conn, + }) + } else { + Box::new(conn) + } + } + } + + struct Verbose { + id: u32, + inner: T, + } + + impl Connection for Verbose { + fn connected(&self) -> Connected { + self.inner.connected() + } + } + + impl AsyncRead for Verbose { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut [u8] + ) -> Poll> { + match Pin::new(&mut self.inner).poll_read(cx, buf) { + Poll::Ready(Ok(n)) => { + log::trace!("{:08x} read: {:?}", self.id, Escape(&buf[..n])); + Poll::Ready(Ok(n)) + }, + Poll::Ready(Err(e)) => { + Poll::Ready(Err(e)) + }, + Poll::Pending => Poll::Pending, + } + } + } + + impl AsyncWrite 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)) => { + log::trace!("{:08x} write: {:?}", self.id, Escape(&buf[..n])); + Poll::Ready(Ok(n)) + }, + Poll::Ready(Err(e)) => { + Poll::Ready(Err(e)) + }, + Poll::Pending => Poll::Pending, + } + } + + 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) + } + } + + struct Escape<'a>(&'a [u8]); + + impl fmt::Debug for Escape<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "b\"")?; + for &c in self.0 { + // https://doc.rust-lang.org/reference.html#byte-escapes + if c == b'\n' { + write!(f, "\\n")?; + } else if c == b'\r' { + write!(f, "\\r")?; + } else if c == b'\t' { + write!(f, "\\t")?; + } else if c == b'\\' || c == b'"' { + write!(f, "\\{}", c as char)?; + } else if c == b'\0' { + write!(f, "\\0")?; + // ASCII printable + } else if c >= 0x20 && c < 0x7f { + write!(f, "{}", c as char)?; + } else { + write!(f, "\\x{:02x}", c)?; + } + } + write!(f, "\"")?; + Ok(()) + } } } diff --git a/src/lib.rs b/src/lib.rs index eb10cb3..60314aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -294,6 +294,7 @@ if_hyper! { pub mod redirect; #[cfg(feature = "__tls")] mod tls; + mod util; } if_wasm! { diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..43d809c --- /dev/null +++ b/src/util.rs @@ -0,0 +1,35 @@ +// xor-shift +pub(crate) fn fast_random() -> u64 { + use std::cell::Cell; + use std::collections::hash_map::RandomState; + use std::hash::{BuildHasher, Hasher}; + use std::num::Wrapping; + + thread_local! { + static RNG: Cell> = Cell::new(Wrapping(seed())); + } + + fn seed() -> u64 { + let seed = RandomState::new(); + + let mut out = 0; + let mut cnt = 0; + while out == 0 { + cnt += 1; + let mut hasher = seed.build_hasher(); + hasher.write_usize(cnt); + out = hasher.finish(); + } + out + } + + RNG.with(|rng| { + let mut n = rng.get(); + debug_assert_ne!(n.0, 0); + n ^= n >> 12; + n ^= n << 25; + n ^= n >> 27; + rng.set(n); + n.0.wrapping_mul(0x2545_f491_4f6c_dd1d) + }) +}