diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 05e73b0..1ac7d25 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -21,6 +21,7 @@ jobs:
- stable
- beta
- nightly
+ - "1.75.0"
steps:
- uses: actions/checkout@v4
diff --git a/Cargo.toml b/Cargo.toml
index 2908623..d86456d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,6 +2,7 @@
name = "hudsucker"
version = "0.21.0"
edition = "2021"
+rust-version = "1.75.0"
description = "MITM HTTP/S proxy"
documentation = "https://docs.rs/hudsucker"
readme = "README.md"
@@ -18,44 +19,45 @@ rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
async-compression = { version = "0.4.0", features = ["tokio", "brotli", "gzip", "zlib", "zstd"], optional = true }
-async-trait = "0.1.67"
bstr = "1.0.0"
-bytes = "1.0.0"
futures = "0.3.11"
-http = "0.2.0"
-hyper = { version = "0.14.15", features = ["client", "http1", "server", "tcp"] }
-hyper-rustls = { version = "0.24.0", default-features = false, features = ["http1", "logging", "tls12", "webpki-tokio"], optional = true }
-hyper-tls = { version = "0.5.0", optional = true }
-hyper-tungstenite = "0.11.1"
+http = "1.1.0"
+http-body-util = "0.1.0"
+hyper = "1.1.0"
+hyper-rustls = { version = "0.26.0", default-features = false, features = ["http1", "logging", "ring", "tls12", "webpki-tokio"], optional = true }
+hyper-tls = { version = "0.6.0", optional = true }
+hyper-tungstenite = "0.13.0"
+hyper-util = { version="0.1.3", features = ["client-legacy", "server", "http1"] }
moka = { version = "0.12.0", features = ["future"], optional = true }
-openssl = { version = "0.10.39", optional = true }
+openssl = { version = "0.10.46", optional = true }
rand = { version = "0.8.0", optional = true }
rcgen = { version = "0.12.0", features = ["x509-parser"], optional = true }
thiserror = "1.0.30"
time = { version = "0.3.20", optional = true }
-tokio = { version = "1.24.2", features = ["rt"] }
-tokio-rustls = "0.24.0"
-tokio-tungstenite = "0.20.0"
-tokio-util = { version = "0.7.0", features = ["io"], optional = true }
-tracing = { version = "0.1.23", features = ["log"] }
+tokio = { version = "1.24.2", features = ["macros", "rt"] }
+tokio-graceful = "0.1.6"
+tokio-rustls = "0.25.0"
+tokio-tungstenite = "0.21.0"
+tokio-util = { version = "0.7.1", features = ["io"], optional = true }
+tracing = { version = "0.1.35", features = ["log"] }
[dev-dependencies]
async-http-proxy = { version = "1.2.5", features = ["runtime-tokio"] }
criterion = { version = "0.5.0", features = ["async_tokio"] }
reqwest = "0.11.10"
-rustls-native-certs = "0.6.2"
+rustls-native-certs = "0.7.0"
rustls-pemfile = "2.0.0"
-tls-listener = { version = "0.8.0", features = ["rustls", "hyper-h1", "hyper-h2"] }
+tls-listener = { version = "0.9.1", features = ["rustls"] }
tokio = { version = "1.24.2", features = ["full"] }
tokio-native-tls = "0.3.1"
-tracing-subscriber = "0.3.0"
+tracing-subscriber = "0.3.8"
x509-parser = "0.16.0"
[features]
-decoder = ["dep:async-compression", "dep:tokio-util", "hyper/stream", "tokio/io-util"]
+decoder = ["dep:async-compression", "dep:tokio-util", "tokio/io-util"]
default = ["decoder", "rcgen-ca", "rustls-client"]
full = ["decoder", "http2", "native-tls-client", "openssl-ca", "rcgen-ca", "rustls-client"]
-http2 = ["hyper/http2", "hyper-rustls?/http2"]
+http2 = ["hyper-util/http2", "hyper-rustls?/http2"]
native-tls-client = ["dep:hyper-tls", "tokio-tungstenite/native-tls"]
openssl-ca = ["dep:openssl", "dep:moka"]
rcgen-ca = ["dep:rcgen", "dep:moka", "dep:time", "dep:rand"]
diff --git a/benches/certificate_authorities.rs b/benches/certificate_authorities.rs
index b9692d8..475a464 100644
--- a/benches/certificate_authorities.rs
+++ b/benches/certificate_authorities.rs
@@ -3,7 +3,6 @@ use http::uri::Authority;
use hudsucker::{
certificate_authority::{CertificateAuthority, OpensslAuthority, RcgenAuthority},
openssl::{hash::MessageDigest, pkey::PKey, x509::X509},
- rustls,
};
use rustls_pemfile as pemfile;
@@ -16,21 +15,13 @@ fn runtime() -> tokio::runtime::Runtime {
fn build_rcgen_ca(cache_size: u64) -> RcgenAuthority {
let mut private_key_bytes: &[u8] = include_bytes!("../examples/ca/hudsucker.key");
let mut ca_cert_bytes: &[u8] = include_bytes!("../examples/ca/hudsucker.cer");
- let private_key = rustls::PrivateKey(
- pemfile::pkcs8_private_keys(&mut private_key_bytes)
- .next()
- .unwrap()
- .expect("Failed to parse private key")
- .secret_pkcs8_der()
- .to_vec(),
- );
- let ca_cert = rustls::Certificate(
- pemfile::certs(&mut ca_cert_bytes)
- .next()
- .unwrap()
- .expect("Failed to parse CA certificate")
- .to_vec(),
- );
+ let private_key = pemfile::private_key(&mut private_key_bytes)
+ .unwrap()
+ .expect("Failed to parse private key");
+ let ca_cert = pemfile::certs(&mut ca_cert_bytes)
+ .next()
+ .unwrap()
+ .expect("Failed to parse CA certificate");
RcgenAuthority::new(private_key, ca_cert, cache_size)
.expect("Failed to create Certificate Authority")
diff --git a/benches/decoder.rs b/benches/decoder.rs
index 56ce879..6c9644c 100644
--- a/benches/decoder.rs
+++ b/benches/decoder.rs
@@ -4,8 +4,9 @@ use hudsucker::{
decode_request, decode_response,
hyper::{
header::{CONTENT_ENCODING, CONTENT_LENGTH},
- Body, Request, Response,
+ Request, Response,
},
+ Body,
};
use tokio::io::BufReader;
use tokio_util::io::ReaderStream;
diff --git a/benches/proxy.rs b/benches/proxy.rs
index 468a6bf..39fd514 100644
--- a/benches/proxy.rs
+++ b/benches/proxy.rs
@@ -1,21 +1,21 @@
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
+use http_body_util::Empty;
use hudsucker::{
certificate_authority::{CertificateAuthority, RcgenAuthority},
- hyper::{
- client::connect::HttpConnector,
- service::{make_service_fn, service_fn},
- Body, Method, Request, Response, Server,
+ hyper::{body::Incoming, service::service_fn, Method, Request, Response},
+ hyper_util::client::legacy::{connect::HttpConnector, Client},
+ hyper_util::{
+ rt::{TokioExecutor, TokioIo},
+ server::conn::auto,
},
- rustls, Proxy,
+ Body, Proxy,
};
use reqwest::Certificate;
use rustls_pemfile as pemfile;
-use std::{
- convert::Infallible,
- net::{SocketAddr, TcpListener},
-};
+use std::{convert::Infallible, net::SocketAddr};
use tls_listener::TlsListener;
-use tokio::sync::oneshot::Sender;
+use tokio::{net::TcpListener, sync::oneshot::Sender};
+use tokio_graceful::Shutdown;
use tokio_native_tls::native_tls;
fn runtime() -> tokio::runtime::Runtime {
@@ -29,74 +29,109 @@ fn runtime() -> tokio::runtime::Runtime {
fn build_ca() -> RcgenAuthority {
let mut private_key_bytes: &[u8] = include_bytes!("../examples/ca/hudsucker.key");
let mut ca_cert_bytes: &[u8] = include_bytes!("../examples/ca/hudsucker.cer");
- let private_key = rustls::PrivateKey(
- pemfile::pkcs8_private_keys(&mut private_key_bytes)
- .next()
- .unwrap()
- .expect("Failed to parse private key")
- .secret_pkcs8_der()
- .to_vec(),
- );
- let ca_cert = rustls::Certificate(
- pemfile::certs(&mut ca_cert_bytes)
- .next()
- .unwrap()
- .expect("Failed to parse CA certificate")
- .to_vec(),
- );
+ let private_key = pemfile::private_key(&mut private_key_bytes)
+ .unwrap()
+ .expect("Failed to parse private key");
+ let ca_cert = pemfile::certs(&mut ca_cert_bytes)
+ .next()
+ .unwrap()
+ .expect("Failed to parse CA certificate");
RcgenAuthority::new(private_key, ca_cert, 1_000)
.expect("Failed to create Certificate Authority")
}
-async fn test_server(req: Request
) -> Result, Infallible> {
+async fn test_server(req: Request) -> Result, Infallible> {
match (req.method(), req.uri().path()) {
(&Method::GET, "/hello") => Ok(Response::new(Body::from("hello, world"))),
- _ => Ok(Response::new(Body::empty())),
+ _ => Ok(Response::new(Body::from(Empty::new()))),
}
}
-fn start_http_server() -> Result<(SocketAddr, Sender<()>), Box> {
- let make_svc = make_service_fn(|_| async { Ok::<_, Infallible>(service_fn(test_server)) });
-
- let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))?;
+pub async fn start_http_server() -> Result<(SocketAddr, Sender<()>), Box> {
+ let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).await?;
let addr = listener.local_addr()?;
-
let (tx, rx) = tokio::sync::oneshot::channel();
- tokio::spawn(
- Server::from_tcp(listener)?
- .serve(make_svc)
- .with_graceful_shutdown(async { rx.await.unwrap_or_default() }),
- );
+ tokio::spawn(async move {
+ let server = auto::Builder::new(TokioExecutor::new());
+ let shutdown = Shutdown::new(async { rx.await.unwrap_or_default() });
+ let guard = shutdown.guard_weak();
+
+ loop {
+ tokio::select! {
+ res = listener.accept() => {
+ let Ok((tcp, _)) = res else {
+ continue;
+ };
+
+ let server = server.clone();
+
+ shutdown.spawn_task(async move {
+ server
+ .serve_connection_with_upgrades(TokioIo::new(tcp), service_fn(test_server))
+ .await
+ .unwrap();
+ });
+ }
+ _ = guard.cancelled() => {
+ break;
+ }
+ }
+ }
+
+ shutdown.shutdown().await;
+ });
Ok((addr, tx))
}
-async fn start_https_server() -> Result<(SocketAddr, Sender<()>), Box> {
- let make_svc = make_service_fn(|_| async { Ok::<_, Infallible>(service_fn(test_server)) });
-
- let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))?;
- listener.set_nonblocking(true)?;
+pub async fn start_https_server(
+ ca: impl CertificateAuthority,
+) -> Result<(SocketAddr, Sender<()>), Box> {
+ let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).await?;
let addr = listener.local_addr()?;
- let acceptor: tokio_rustls::TlsAcceptor = build_ca()
- .gen_server_config(&format!("localhost:{}", addr.port()).parse().unwrap())
+ let acceptor: tokio_rustls::TlsAcceptor = ca
+ .gen_server_config(&"localhost".parse().unwrap())
.await
.into();
- let listener = TlsListener::new(acceptor, tokio::net::TcpListener::from_std(listener)?);
-
+ let mut listener = TlsListener::new(acceptor, listener);
let (tx, rx) = tokio::sync::oneshot::channel();
- tokio::spawn(
- Server::builder(listener)
- .serve(make_svc)
- .with_graceful_shutdown(async { rx.await.unwrap_or_default() }),
- );
+ tokio::spawn(async move {
+ let server = auto::Builder::new(TokioExecutor::new());
+ let shutdown = Shutdown::new(async { rx.await.unwrap_or_default() });
+ let guard = shutdown.guard_weak();
+
+ loop {
+ tokio::select! {
+ res = listener.accept() => {
+ let Ok((tcp, _)) = res else {
+ continue;
+ };
+
+ let server = server.clone();
+
+ shutdown.spawn_task(async move {
+ server
+ .serve_connection_with_upgrades(TokioIo::new(tcp), service_fn(test_server))
+ .await
+ .unwrap();
+ });
+ }
+ _ = guard.cancelled() => {
+ break;
+ }
+ }
+ }
+
+ shutdown.shutdown().await;
+ });
Ok((addr, tx))
}
-fn native_tls_client() -> hyper::client::Client> {
+fn native_tls_client() -> Client, Body> {
let mut http = HttpConnector::new();
http.enforce_http(false);
let ca_cert =
@@ -110,25 +145,25 @@ fn native_tls_client() -> hyper::client::Client = (http, tls).into();
- hyper::Client::builder().build(https)
+ Client::builder(TokioExecutor::new()).build(https)
}
-fn start_proxy(
+async fn start_proxy(
ca: impl CertificateAuthority,
) -> Result<(SocketAddr, Sender<()>), Box> {
- let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0)))?;
+ let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).await?;
let addr = listener.local_addr()?;
let (tx, rx) = tokio::sync::oneshot::channel();
-
let proxy = Proxy::builder()
.with_listener(listener)
.with_client(native_tls_client())
.with_ca(ca)
+ .with_graceful_shutdown(async {
+ rx.await.unwrap_or_default();
+ })
.build();
- tokio::spawn(proxy.start(async {
- rx.await.unwrap_or_default();
- }));
+ tokio::spawn(proxy.start());
Ok((addr, tx))
}
@@ -157,9 +192,9 @@ fn bench_local(c: &mut Criterion) {
let runtime = runtime();
let _guard = runtime.enter();
- let (proxy_addr, stop_proxy) = start_proxy(build_ca()).unwrap();
- let (http_addr, stop_http) = start_http_server().unwrap();
- let (https_addr, stop_https) = runtime.block_on(start_https_server()).unwrap();
+ let (proxy_addr, stop_proxy) = runtime.block_on(start_proxy(build_ca())).unwrap();
+ let (http_addr, stop_http) = runtime.block_on(start_http_server()).unwrap();
+ let (https_addr, stop_https) = runtime.block_on(start_https_server(build_ca())).unwrap();
let client = build_client();
let proxied_client = build_proxied_client(&proxy_addr.to_string());
@@ -212,7 +247,7 @@ fn bench_remote(c: &mut Criterion) {
let runtime = runtime();
let _guard = runtime.enter();
- let (proxy_addr, stop_proxy) = start_proxy(build_ca()).unwrap();
+ let (proxy_addr, stop_proxy) = runtime.block_on(start_proxy(build_ca())).unwrap();
let client = build_client();
let proxied_client = build_proxied_client(&proxy_addr.to_string());
diff --git a/examples/log.rs b/examples/log.rs
index f4c81ca..520a811 100644
--- a/examples/log.rs
+++ b/examples/log.rs
@@ -1,7 +1,6 @@
use hudsucker::{
- async_trait::async_trait,
certificate_authority::RcgenAuthority,
- hyper::{Body, Request, Response},
+ hyper::{Request, Response},
tokio_tungstenite::tungstenite::Message,
*,
};
@@ -18,7 +17,6 @@ async fn shutdown_signal() {
#[derive(Clone)]
struct LogHandler;
-#[async_trait]
impl HttpHandler for LogHandler {
async fn handle_request(
&mut self,
@@ -35,7 +33,6 @@ impl HttpHandler for LogHandler {
}
}
-#[async_trait]
impl WebSocketHandler for LogHandler {
async fn handle_message(&mut self, _ctx: &WebSocketContext, msg: Message) -> Option {
println!("{:?}", msg);
@@ -49,21 +46,13 @@ async fn main() {
let mut private_key_bytes: &[u8] = include_bytes!("ca/hudsucker.key");
let mut ca_cert_bytes: &[u8] = include_bytes!("ca/hudsucker.cer");
- let private_key = rustls::PrivateKey(
- pemfile::pkcs8_private_keys(&mut private_key_bytes)
- .next()
- .unwrap()
- .expect("Failed to parse private key")
- .secret_pkcs8_der()
- .to_vec(),
- );
- let ca_cert = rustls::Certificate(
- pemfile::certs(&mut ca_cert_bytes)
- .next()
- .unwrap()
- .expect("Failed to parse CA certificate")
- .to_vec(),
- );
+ let private_key = pemfile::private_key(&mut private_key_bytes)
+ .unwrap()
+ .expect("Failed to parse private key");
+ let ca_cert = pemfile::certs(&mut ca_cert_bytes)
+ .next()
+ .unwrap()
+ .expect("Failed to parse CA certificate");
let ca = RcgenAuthority::new(private_key, ca_cert, 1_000)
.expect("Failed to create Certificate Authority");
@@ -74,9 +63,10 @@ async fn main() {
.with_ca(ca)
.with_http_handler(LogHandler)
.with_websocket_handler(LogHandler)
+ .with_graceful_shutdown(shutdown_signal())
.build();
- if let Err(e) = proxy.start(shutdown_signal()).await {
+ if let Err(e) = proxy.start().await {
error!("{}", e);
}
}
diff --git a/examples/noop.rs b/examples/noop.rs
index 7041487..0471f3f 100644
--- a/examples/noop.rs
+++ b/examples/noop.rs
@@ -15,21 +15,13 @@ async fn main() {
let mut private_key_bytes: &[u8] = include_bytes!("ca/hudsucker.key");
let mut ca_cert_bytes: &[u8] = include_bytes!("ca/hudsucker.cer");
- let private_key = rustls::PrivateKey(
- pemfile::pkcs8_private_keys(&mut private_key_bytes)
- .next()
- .unwrap()
- .expect("Failed to parse private key")
- .secret_pkcs8_der()
- .to_vec(),
- );
- let ca_cert = rustls::Certificate(
- pemfile::certs(&mut ca_cert_bytes)
- .next()
- .unwrap()
- .expect("Failed to parse CA certificate")
- .to_vec(),
- );
+ let private_key = pemfile::private_key(&mut private_key_bytes)
+ .unwrap()
+ .expect("Failed to parse private key");
+ let ca_cert = pemfile::certs(&mut ca_cert_bytes)
+ .next()
+ .unwrap()
+ .expect("Failed to parse CA certificate");
let ca = RcgenAuthority::new(private_key, ca_cert, 1_000)
.expect("Failed to create Certificate Authority");
@@ -38,9 +30,10 @@ async fn main() {
.with_addr(SocketAddr::from(([127, 0, 0, 1], 3000)))
.with_rustls_client()
.with_ca(ca)
+ .with_graceful_shutdown(shutdown_signal())
.build();
- if let Err(e) = proxy.start(shutdown_signal()).await {
+ if let Err(e) = proxy.start().await {
error!("{}", e);
}
}
diff --git a/examples/openssl.rs b/examples/openssl.rs
index c7e9af8..86d34a4 100644
--- a/examples/openssl.rs
+++ b/examples/openssl.rs
@@ -1,7 +1,6 @@
use hudsucker::{
- async_trait::async_trait,
certificate_authority::OpensslAuthority,
- hyper::{Body, Request, Response},
+ hyper::{Request, Response},
openssl::{hash::MessageDigest, pkey::PKey, x509::X509},
tokio_tungstenite::tungstenite::Message,
*,
@@ -18,7 +17,6 @@ async fn shutdown_signal() {
#[derive(Clone)]
struct LogHandler;
-#[async_trait]
impl HttpHandler for LogHandler {
async fn handle_request(
&mut self,
@@ -35,7 +33,6 @@ impl HttpHandler for LogHandler {
}
}
-#[async_trait]
impl WebSocketHandler for LogHandler {
async fn handle_message(&mut self, _ctx: &WebSocketContext, msg: Message) -> Option {
println!("{:?}", msg);
@@ -60,9 +57,10 @@ async fn main() {
.with_rustls_client()
.with_ca(ca)
.with_http_handler(LogHandler)
+ .with_graceful_shutdown(shutdown_signal())
.build();
- if let Err(e) = proxy.start(shutdown_signal()).await {
+ if let Err(e) = proxy.start().await {
error!("{}", e);
}
}
diff --git a/src/body.rs b/src/body.rs
new file mode 100644
index 0000000..d11850e
--- /dev/null
+++ b/src/body.rs
@@ -0,0 +1,151 @@
+use crate::Error;
+use futures::{Stream, StreamExt};
+use http_body_util::{combinators::BoxBody, Collected, Empty, Full, StreamBody};
+use hyper::body::{Body as HyperBody, Bytes, Frame, Incoming, SizeHint};
+use std::pin::Pin;
+
+#[derive(Debug)]
+enum Internal {
+ BoxBody(BoxBody),
+ Collected(Collected),
+ Empty(Empty),
+ Full(Full),
+ Incoming(Incoming),
+ String(String),
+}
+
+#[derive(Debug)]
+pub struct Body {
+ inner: Internal,
+}
+
+impl Body {
+ pub fn wrap_stream(stream: S) -> Self
+ where
+ S: Stream- > + Send + Sync + 'static,
+ O: Into,
+ E: Into,
+ {
+ Self {
+ inner: Internal::BoxBody(BoxBody::new(StreamBody::new(
+ stream.map(|res| res.map(Into::into).map(Frame::data).map_err(Into::into)),
+ ))),
+ }
+ }
+}
+
+impl HyperBody for Body {
+ type Data = Bytes;
+ type Error = crate::Error;
+
+ fn poll_frame(
+ mut self: std::pin::Pin<&mut Self>,
+ cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll