diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml new file mode 100644 index 0000000..3c9cf5e --- /dev/null +++ b/.github/workflows/checks.yaml @@ -0,0 +1,38 @@ +name: Checks + +on: + push: + branches: + - main + pull_request: ~ + +jobs: + test: + name: Test + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install latest stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + components: rustfmt,clippy + + - name: Run rustfmt + run: cargo fmt --all --check + + - name: Run clippy + uses: giraffate/clippy-action@v1 + with: + reporter: 'github-pr-check' + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run tests default + run: cargo test + - name: Run tests rustls-tls-webpki-roots + run: cargo test --no-default-features --features rustls-tls-webpki-roots + - name: Run tests native-tls-vendored + run: cargo test --no-default-features --features native-tls-vendored + - name: Run tests native-tls + run: cargo test --no-default-features --features native-tls \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..31756a2 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,20 @@ +name: Release +on: + push: + tags: + - "*.*.*" + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - uses: katyo/publish-crates@v2 + with: + registry-token: ${{ secrets.CRATES_IO_SECRET }} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e72721b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: rust -cache: cargo -sudo: false - -rust: - - nightly - - beta - - stable - -before_script: - - export PATH=$HOME/.cargo/bin:$HOME/.local/bin:$PATH - - if [[ $(rustup show active-toolchain) == stable* ]]; then rustup component add rustfmt; fi; - -script: - - if [[ $(rustup show active-toolchain) == stable* ]]; then cargo fmt -- --check; fi; - - cargo test --features tls - - cargo test --features rustls --no-default-features - - cargo test --features rustls-webpki --no-default-features - - cargo test --no-default-features diff --git a/Cargo.toml b/Cargo.toml index 9f8601f..30e8512 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,49 +1,53 @@ [package] -name = "hyper-proxy" -version = "0.9.0" -authors = ["Johann Tuffe "] +name = "rigetti-hyper-proxy" +version = "1.1.1" +authors = ["MetalBear Tech LTD "] description = "A proxy connector for Hyper-based applications" - -documentation = "https://docs.rs/hyper-proxy" -repository = "https://github.com/tafia/hyper-proxy" - +documentation = "https://docs.rs/rigetti-hyper-proxy" +repository = "https://github.com/metalbear-co/hyper-http-proxy" readme = "README.md" keywords = ["hyper", "proxy", "tokio", "ssl"] categories = ["web-programming::http-client", "asynchronous", "authentication"] license = "MIT" -edition = "2018" +edition = "2021" +rust-version = "1.70.0" [dependencies] -tokio = { version = "1", features = ["io-std", "io-util"] } -hyper = { version = "0.14", features = ["client"] } +tokio = { version = "1.47", features = ["io-std", "io-util"] } +hyper = { version = "1", features = ["client"] } +hyper-util = { version = "0.1", features = ["client", "client-legacy", "tokio"] } tower-service = "0.3" -http = "0.2" +http = "1" futures-util = { version = "0.3", default-features = false } -bytes = "1.0" -hyper-tls = { version = "0.5.0", optional = true } -tokio-native-tls = { version = "0.3.0", optional = true } +bytes = "1.11" +pin-project-lite = "0.2" +hyper-tls = { version = "0.6", optional = true } +tokio-native-tls = { version = "0.3", optional = true } native-tls = { version = "0.2", optional = true } -openssl = { version = "0.10", optional = true } -tokio-openssl = { version = "0.6", optional = true } -tokio-rustls = { version = "0.22", optional = true } -hyper-rustls = { version = "0.22", optional = true } +tokio-rustls = { version = "0.26", optional = true, default-features = false} +hyper-rustls = { version = "0.27", optional = true, default-features = false } -webpki = { version = "0.21", optional = true } -rustls-native-certs = { version = "0.5.0", optional = true } -webpki-roots = { version = "0.21.0", optional = true } -headers = "0.3" +webpki-roots = { version = "1.0", optional = true } +headers = "0.4" [dev-dependencies] -tokio = { version = "1.0", features = ["full"] } -hyper = { version = "0.14", features = ["client", "http1", "tcp"] } +tokio = { version = "1.47", features = ["full"] } +hyper = { version = "1.9", features = ["client", "http1"] } +hyper-util = { version = "0.1", features = ["client", "client-legacy", "http1", "tokio"] } +http-body-util = "0.1" +futures = "0.3" [features] -openssl-tls = ["openssl", "tokio-openssl"] -tls = ["tokio-native-tls", "hyper-tls", "native-tls"] -# note that `rustls-base` is not a valid feature on its own - it will configure rustls without root -# certificates! -rustls-base = ["tokio-rustls", "hyper-rustls", "webpki"] -rustls = ["rustls-base", "rustls-native-certs", "hyper-rustls/native-tokio"] -rustls-webpki = ["rustls-base", "webpki-roots", "hyper-rustls/webpki-tokio"] -default = ["tls"] +default = ["default-tls"] +default-tls = ["rustls-tls-native-roots"] +native-tls = ["dep:native-tls", "tokio-native-tls", "hyper-tls", "__tls"] +native-tls-vendored = ["native-tls", "tokio-native-tls?/vendored"] +rustls-tls-webpki-roots = ["dep:webpki-roots", "__rustls", "hyper-rustls/webpki-roots"] +rustls-tls-native-roots = ["__rustls", "hyper-rustls/rustls-native-certs"] + +__tls = [] + +# Enables common rustls code. +# Equivalent to rustls-tls-manual-roots but shorter :) +__rustls = ["dep:hyper-rustls", "dep:tokio-rustls", "__tls"] diff --git a/Changelog.md b/Changelog.md deleted file mode 100644 index de8070e..0000000 --- a/Changelog.md +++ /dev/null @@ -1,50 +0,0 @@ -> Legend: - - feat: A new feature - - fix: A bug fix - - docs: Documentation only changes - - style: White-space, formatting, missing semi-colons, etc - - refactor: A code change that neither fixes a bug nor adds a feature - - perf: A code change that improves performance - - test: Adding missing tests - - chore: Changes to the build process or auxiliary tools/libraries/documentation - -## 0.9.0 -- feat: upgrade to tokio 1.0 -- feat: add tokio-openssl support - -## 0.8.0 -- feat: add rustls-webpki feature (see #16) -- feat: add ability to force use CONNECT method for HTTP/2.0 - -## 0.7.0 -- fix: plain http connection not proxied - -## 0.6.0 -- feat: upgrade to hyper 0.13 and tokio 0.2 - -## 0.5.1 -- feat: add rustls feature - -## 0.5.0 -- feat: upgrade to hyper 0.12 - -## 0.4.1 -- feat: make TLS support configurable - -## 0.4.0 -- feat: split Proxy into Proxy and ProxyConnector allowing to handle a list of proxies -- doc: add a set_proxy expression for http requests -- doc: fix some wrong comments -- perf: avoid one clone - -## 0.3.0 -- refactor: add a match_fn macro in tunnel -- fix: add missing '\' in connect message -- feat: do not use connect for pure http request. Else provide headers to update the primary request with. -- feat: have Custom intercept be an opaque struct using `Arc` to be Send + Sync + Clone - -## 0.2.0 -- feat: Add Intercept::None to never intercept any connection -- fix: Add Send + Sync constraints on Intercept::Custom function (breaking) -- feat: Make Intercept::matches function public -- feat: Add several function to get/modify internal states diff --git a/LICENSE-MIT.md b/LICENSE-MIT.md index 47d7815..5e82b80 100644 --- a/LICENSE-MIT.md +++ b/LICENSE-MIT.md @@ -1,6 +1,8 @@ The MIT License (MIT) Copyright (c) 2017 Johann Tuffe +Copyright (c) 2024 Natsuki Ikeguchi +Copyright (c) 2024 MetalBear Tech LTD Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c9afabe..118a2b9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# hyper-proxy +# rigetti-hyper-proxy -[![Travis Build Status](https://travis-ci.org/tafia/hyper-proxy.svg?branch=master)](https://travis-ci.org/tafia/hyper-proxy) -[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) -[![crates.io](http://meritbadge.herokuapp.com/hyper-proxy)](https://crates.io/crates/hyper-proxy) +[![Checks](https://github.com/metalbear-co/hyper-http-proxy/actions/workflows/checks.yaml/badge.svg)](https://github.com/metalbear-co/hyper-http-proxy/actions/workflows/checks.yaml) +[![MIT licensed](https://img.shields.io/github/license/metalbear-co/hyper-http-proxy)](./LICENSE-MIT.md) +[![crates.io](https://img.shields.io/crates/v/rigetti-hyper-proxy)](https://crates.io/crates/rigetti-hyper-proxy) A proxy connector for [hyper][1] based applications. @@ -10,70 +10,86 @@ A proxy connector for [hyper][1] based applications. ## Example -```rust,no_run -use hyper::{Client, Request, Uri}; -use hyper::client::HttpConnector; -use futures::{TryFutureExt, TryStreamExt}; -use hyper_proxy::{Proxy, ProxyConnector, Intercept}; -use headers::Authorization; +```rust use std::error::Error; +use bytes::Bytes; +use headers::Authorization; +use http_body_util::{BodyExt, Empty}; +use hyper::{Request, Uri}; +use rigetti_hyper_proxy::{Proxy, ProxyConnector, Intercept}; +use hyper_util::client::legacy::Client; +use hyper_util::client::legacy::connect::HttpConnector; +use hyper_util::rt::TokioExecutor; + #[tokio::main] async fn main() -> Result<(), Box> { - let proxy = { - let proxy_uri = "http://my-proxy:8080".parse().unwrap(); - let mut proxy = Proxy::new(Intercept::All, proxy_uri); - proxy.set_authorization(Authorization::basic("John Doe", "Agent1234")); - let connector = HttpConnector::new(); - let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap(); - proxy_connector - }; - - // Connecting to http will trigger regular GETs and POSTs. - // We need to manually append the relevant headers to the request - let uri: Uri = "http://my-remote-website.com".parse().unwrap(); - let mut req = Request::get(uri.clone()).body(hyper::Body::empty()).unwrap(); - - if let Some(headers) = proxy.http_headers(&uri) { - req.headers_mut().extend(headers.clone().into_iter()); - } - - let client = Client::builder().build(proxy); - let fut_http = client.request(req) - .and_then(|res| res.into_body().map_ok(|x|x.to_vec()).try_concat()) - .map_ok(move |body| ::std::str::from_utf8(&body).unwrap().to_string()); - - // Connecting to an https uri is straightforward (uses 'CONNECT' method underneath) - let uri = "https://my-remote-websitei-secured.com".parse().unwrap(); - let fut_https = client.get(uri) - .and_then(|res| res.into_body().map_ok(|x|x.to_vec()).try_concat()) - .map_ok(move |body| ::std::str::from_utf8(&body).unwrap().to_string()); - - let (http_res, https_res) = futures::future::join(fut_http, fut_https).await; - let (_, _) = (http_res?, https_res?); - - Ok(()) + let proxy = { + let proxy_uri = "http://my-proxy:8080".parse().unwrap(); + let mut proxy = Proxy::new(Intercept::All, proxy_uri); + proxy.set_authorization(Authorization::basic("John Doe", "Agent1234")); + let connector = HttpConnector::new(); + let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap(); + proxy_connector + }; + + // Connecting to http will trigger regular GETs and POSTs. + // We need to manually append the relevant headers to the request + let uri: Uri = "http://my-remote-website.com".parse().unwrap(); + let mut req = Request::get(uri.clone()).body(Empty::::new()).unwrap(); + + if let Some(headers) = proxy.http_headers(&uri) { + req.headers_mut().extend(headers.clone().into_iter()); + } + + let client = Client::builder(TokioExecutor::new()).build(proxy); + let fut_http = async { + let res = client.request(req).await?; + let body = res.into_body().collect().await?.to_bytes(); + + Ok::<_, Box>(String::from_utf8(body.to_vec()).unwrap()) + }; + + // Connecting to an https uri is straightforward (uses 'CONNECT' method underneath) + let uri = "https://my-remote-websitei-secured.com".parse().unwrap(); + let fut_https = async { + let res = client.get(uri).await?; + let body = res.into_body().collect().await?.to_bytes(); + + Ok::<_, Box>(String::from_utf8(body.to_vec()).unwrap()) + }; + + let (http_res, https_res) = futures::future::join(fut_http, fut_https).await; + let (_, _) = (http_res?, https_res?); + + Ok(()) } ``` ## Features -`hyper-proxy` exposes three main Cargo features, to configure which TLS implementation it uses to +`rigetti-hyper-proxy` exposes Cargo features, to configure which TLS implementation it uses to connect to a proxy. It can also be configured without TLS support, by compiling without default features entirely. The supported list of configurations is: +native-tls = ["dep:native-tls", "tokio-native-tls", "hyper-tls", "__tls"] +native-tls-vendored = ["native-tls", "tokio-native-tls?/vendored"] +rustls-tls-manual-roots = ["__rustls"] +rustls-tls-webpki-roots = ["dep:webpki-roots", "__rustls"] +rustls-tls-native-roots = ["dep:rustls-native-certs", "__rustls", "hyper-rustls/rustls-native-certs"] 1. No TLS support (`default-features = false`) -2. TLS support via `native-tls` to link against the operating system's native TLS implementation - (default) -3. TLS support via `rustls` (`default-features = false, features = ["rustls"]`) +2. TLS support via `native-tls` to link against the operating system's native TLS implementation (`default-features = false, features = ["native-tls"]`) +3. TLS support via `rustls` using native certificates (default). 4. TLS support via `rustls`, using a statically-compiled set of CA certificates to bypass the - operating system's default store (`default-features = false, features = ["rustls-webpki"]`) + operating system's default store (`default-features = false, features = ["rustls-tls-webpki-roots"]`) ## Credits +This was forked from https://github.com/siketyan/hyper-http-proxy that originally forked from https://github.com/tafia/hyper-proxy + + Large part of the code comes from [reqwest][2]. The core part as just been extracted and slightly enhanced. - Main changes are: - support for authentication - add non secured tunneling @@ -81,4 +97,4 @@ The core part as just been extracted and slightly enhanced. [1]: https://crates.io/crates/hyper [2]: https://github.com/seanmonstar/reqwest -[3]: https://docs.rs/hyper-proxy +[3]: https://docs.rs/rigetti-hyper-proxy diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..f9cc328 --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,61 @@ +use std::error::Error; + +use bytes::Bytes; +use headers::Authorization; +use http_body_util::{BodyExt, Empty}; +use hyper::{Request, Uri}; +use rigetti_hyper_proxy::{Intercept, Proxy, ProxyConnector}; +use hyper_util::client::legacy::connect::HttpConnector; +use hyper_util::client::legacy::Client; +use hyper_util::rt::TokioExecutor; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let proxy = { + let proxy_uri = "http://my-proxy:8080".parse().unwrap(); + let mut proxy = Proxy::new(Intercept::All, proxy_uri); + proxy.set_authorization(Authorization::basic("John Doe", "Agent1234")); + let connector = HttpConnector::new(); + + #[cfg(not(any(feature = "tls", feature = "rustls-base", feature = "openssl-tls")))] + let proxy_connector = ProxyConnector::from_proxy_unsecured(connector, proxy); + + #[cfg(any(feature = "tls", feature = "rustls-base", feature = "openssl"))] + let proxy_connector = ProxyConnector::from_proxy(connector, proxy).unwrap(); + + proxy_connector + }; + + // Connecting to http will trigger regular GETs and POSTs. + // We need to manually append the relevant headers to the request + let uri: Uri = "http://my-remote-website.com".parse().unwrap(); + let mut req = Request::get(uri.clone()) + .body(Empty::::new()) + .unwrap(); + + if let Some(headers) = proxy.http_headers(&uri) { + req.headers_mut().extend(headers.clone().into_iter()); + } + + let client = Client::builder(TokioExecutor::new()).build(proxy); + let fut_http = async { + let res = client.request(req).await?; + let body = res.into_body().collect().await?.to_bytes(); + + Ok::<_, Box>(String::from_utf8(body.to_vec()).unwrap()) + }; + + // Connecting to an https uri is straightforward (uses 'CONNECT' method underneath) + let uri = "https://my-remote-websitei-secured.com".parse().unwrap(); + let fut_https = async { + let res = client.get(uri).await?; + let body = res.into_body().collect().await?.to_bytes(); + + Ok::<_, Box>(String::from_utf8(body.to_vec()).unwrap()) + }; + + let (http_res, https_res) = futures::future::join(fut_http, fut_https).await; + let (_, _) = (http_res?, https_res?); + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index e75c830..65f0f28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,17 +2,21 @@ //! //! # Example //! ```rust,no_run -//! use hyper::{Client, Request, Uri, body::HttpBody}; -//! use hyper::client::HttpConnector; +//! use hyper::{Request, Uri, body::Body}; +//! use hyper_util::client::legacy::Client; +//! use hyper_util::client::legacy::connect::HttpConnector; +//! use hyper_util::rt::TokioExecutor; +//! use bytes::Bytes; //! use futures_util::{TryFutureExt, TryStreamExt}; -//! use hyper_proxy::{Proxy, ProxyConnector, Intercept}; +//! use http_body_util::{BodyExt, Empty}; +//! use hyper_http_proxy::{Proxy, ProxyConnector, Intercept}; //! use headers::Authorization; //! use std::error::Error; //! use tokio::io::{stdout, AsyncWriteExt as _}; //! //! #[tokio::main] //! async fn main() -> Result<(), Box> { -//! let proxy = { +//! let proxy = { //! let proxy_uri = "http://my-proxy:8080".parse().unwrap(); //! let mut proxy = Proxy::new(Intercept::All, proxy_uri); //! proxy.set_authorization(Authorization::basic("John Doe", "Agent1234")); @@ -27,25 +31,25 @@ //! // Connecting to http will trigger regular GETs and POSTs. //! // We need to manually append the relevant headers to the request //! let uri: Uri = "http://my-remote-website.com".parse().unwrap(); -//! let mut req = Request::get(uri.clone()).body(hyper::Body::empty()).unwrap(); +//! let mut req = Request::get(uri.clone()).body(Empty::::new()).unwrap(); //! //! if let Some(headers) = proxy.http_headers(&uri) { //! req.headers_mut().extend(headers.clone().into_iter()); //! } //! -//! let client = Client::builder().build(proxy); +//! let client = Client::builder(TokioExecutor::new()).build(proxy); //! let mut resp = client.request(req).await?; //! println!("Response: {}", resp.status()); -//! while let Some(chunk) = resp.body_mut().data().await { -//! stdout().write_all(&chunk?).await?; +//! while let Some(chunk) = resp.body_mut().collect().await.ok().map(|c| c.to_bytes()) { +//! stdout().write_all(&chunk).await?; //! } //! //! // Connecting to an https uri is straightforward (uses 'CONNECT' method underneath) //! let uri = "https://my-remote-websitei-secured.com".parse().unwrap(); //! let mut resp = client.get(uri).await?; //! println!("Response: {}", resp.status()); -//! while let Some(chunk) = resp.body_mut().data().await { -//! stdout().write_all(&chunk?).await?; +//! while let Some(chunk) = resp.body_mut().collect().await.ok().map(|c| c.to_bytes()) { +//! stdout().write_all(&chunk).await?; //! } //! //! Ok(()) @@ -54,13 +58,10 @@ #![allow(missing_docs)] +mod rt; mod stream; mod tunnel; -use http::header::{HeaderMap, HeaderName, HeaderValue}; -use hyper::{service::Service, Uri}; - -use futures_util::future::TryFutureExt; use std::{fmt, io, sync::Arc}; use std::{ future::Future, @@ -68,24 +69,29 @@ use std::{ task::{Context, Poll}, }; +use futures_util::future::TryFutureExt; +use headers::{authorization::Credentials, Authorization, HeaderMapExt, ProxyAuthorization}; +use http::header::{HeaderMap, HeaderName, HeaderValue}; +use hyper::rt::{Read, Write}; +use hyper::Uri; +use tower_service::Service; + pub use stream::ProxyStream; -use tokio::io::{AsyncRead, AsyncWrite}; -#[cfg(feature = "tls")] +#[cfg(all(not(feature = "__rustls"), feature = "native-tls"))] use native_tls::TlsConnector as NativeTlsConnector; -#[cfg(feature = "tls")] +#[cfg(all(not(feature = "__rustls"), feature = "native-tls"))] use tokio_native_tls::TlsConnector; -#[cfg(feature = "rustls-base")] + +#[cfg(feature = "__rustls")] +use hyper_rustls::ConfigBuilderExt; + +#[cfg(feature = "__rustls")] use tokio_rustls::TlsConnector; -use headers::{authorization::Credentials, Authorization, HeaderMapExt, ProxyAuthorization}; -#[cfg(feature = "openssl-tls")] -use openssl::ssl::{SslConnector as OpenSslConnector, SslMethod}; -#[cfg(feature = "openssl-tls")] -use tokio_openssl::SslStream; -#[cfg(feature = "rustls-base")] -use webpki::DNSNameRef; +#[cfg(feature = "__rustls")] +use tokio_rustls::rustls::pki_types::ServerName; type BoxError = Box; @@ -133,9 +139,12 @@ pub(crate) fn io_err>>(e: E) -> io::Error::new(io::ErrorKind::Other, e) } +pub type CustomProxyCallback = + dyn Fn(Option<&str>, Option<&str>, Option) -> bool + Send + Sync; + /// A Custom struct to proxy custom uris #[derive(Clone)] -pub struct Custom(Arc, Option<&str>, Option) -> bool + Send + Sync>); +pub struct Custom(Arc); impl fmt::Debug for Custom { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { @@ -184,12 +193,18 @@ pub struct Proxy { impl Proxy { /// Create a new `Proxy` pub fn new>(intercept: I, uri: Uri) -> Proxy { - Proxy { + let mut proxy = Proxy { intercept: intercept.into(), - uri: uri, + uri: uri.clone(), headers: HeaderMap::new(), force_connect: false, + }; + + if let Some((user, pass)) = extract_user_pass(&uri) { + proxy.set_authorization(Authorization::basic(user, pass)); } + + proxy } /// Set `Proxy` authorization @@ -241,16 +256,13 @@ pub struct ProxyConnector { proxies: Vec, connector: C, - #[cfg(feature = "tls")] + #[cfg(all(not(feature = "__rustls"), feature = "native-tls"))] tls: Option, - #[cfg(feature = "rustls-base")] + #[cfg(feature = "__rustls")] tls: Option, - #[cfg(feature = "openssl-tls")] - tls: Option, - - #[cfg(not(any(feature = "tls", feature = "rustls-base", feature = "openssl-tls")))] + #[cfg(not(feature = "__tls"))] tls: Option<()>, } @@ -272,7 +284,7 @@ impl fmt::Debug for ProxyConnector { impl ProxyConnector { /// Create a new secured Proxies - #[cfg(feature = "tls")] + #[cfg(all(not(feature = "__rustls"), feature = "native-tls"))] pub fn new(connector: C) -> Result { let tls = NativeTlsConnector::builder() .build() @@ -286,43 +298,22 @@ impl ProxyConnector { } /// Create a new secured Proxies - #[cfg(feature = "rustls-base")] + #[cfg(feature = "__rustls")] pub fn new(connector: C) -> Result { - let mut config = tokio_rustls::rustls::ClientConfig::new(); + let config = tokio_rustls::rustls::ClientConfig::builder(); - #[cfg(feature = "rustls")] - { - config.root_store = - rustls_native_certs::load_native_certs().map_err(|(_store, io)| io)?; - } + #[cfg(feature = "rustls-tls-native-roots")] + let config = config.with_native_roots()?; - #[cfg(feature = "rustls-webpki")] - { - config - .root_store - .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); - } + #[cfg(feature = "rustls-tls-webpki-roots")] + let config = config.with_webpki_roots(); - let cfg = Arc::new(config); + let cfg = Arc::new(config.with_no_client_auth()); let tls = TlsConnector::from(cfg); Ok(ProxyConnector { proxies: Vec::new(), - connector: connector, - tls: Some(tls), - }) - } - - #[allow(missing_docs)] - #[cfg(feature = "openssl-tls")] - pub fn new(connector: C) -> Result { - let builder = OpenSslConnector::builder(SslMethod::tls()) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - let tls = builder.build(); - - Ok(ProxyConnector { - proxies: Vec::new(), - connector: connector, + connector, tls: Some(tls), }) } @@ -331,13 +322,13 @@ impl ProxyConnector { pub fn unsecured(connector: C) -> Self { ProxyConnector { proxies: Vec::new(), - connector: connector, + connector, tls: None, } } /// Create a proxy connector and attach a particular proxy - #[cfg(any(feature = "tls", feature = "rustls-base", feature = "openssl-tls"))] + #[cfg(feature = "__tls")] pub fn from_proxy(connector: C, proxy: Proxy) -> Result { let mut c = ProxyConnector::new(connector)?; c.proxies.push(proxy); @@ -354,30 +345,24 @@ impl ProxyConnector { /// Change proxy connector pub fn with_connector(self, connector: CC) -> ProxyConnector { ProxyConnector { - connector: connector, + connector, proxies: self.proxies, tls: self.tls, } } /// Set or unset tls when tunneling - #[cfg(any(feature = "tls"))] + #[cfg(all(not(feature = "__rustls"), feature = "native-tls"))] pub fn set_tls(&mut self, tls: Option) { self.tls = tls; } /// Set or unset tls when tunneling - #[cfg(any(feature = "rustls-base"))] + #[cfg(feature = "__rustls")] pub fn set_tls(&mut self, tls: Option) { self.tls = tls; } - /// Set or unset tls when tunneling - #[cfg(any(feature = "openssl-tls"))] - pub fn set_tls(&mut self, tls: Option) { - self.tls = tls; - } - /// Get the current proxies pub fn proxies(&self) -> &[Proxy] { &self.proxies @@ -422,7 +407,7 @@ macro_rules! mtry { impl Service for ProxyConnector where C: Service, - C::Response: AsyncRead + AsyncWrite + Send + Unpin + 'static, + C::Response: Read + Write + Send + Unpin + 'static, C::Future: Send + 'static, C::Error: Into, { @@ -442,7 +427,14 @@ where if let (Some(p), Some(host)) = (self.match_proxy(&uri), uri.host()) { if uri.scheme() == Some(&http::uri::Scheme::HTTPS) || p.force_connect { let host = host.to_owned(); - let port = uri.port_u16().unwrap_or(if uri.scheme() == Some(&http::uri::Scheme::HTTP) { 80 } else { 443 }); + let port = + uri.port_u16() + .unwrap_or(if uri.scheme() == Some(&http::uri::Scheme::HTTP) { + 80 + } else { + 443 + }); + let tunnel = tunnel::new(&host, port, &p.headers); let connection = proxy_dst(&uri, &p.uri).map(|proxy_url| self.connector.call(proxy_url)); @@ -453,48 +445,39 @@ where }; Box::pin(async move { + // this hack will gone once `try_blocks` will eventually stabilized + #[allow(clippy::never_loop)] loop { - // this hack will gone once `try_blocks` will eventually stabilized let proxy_stream = mtry!(mtry!(connection).await.map_err(io_err)); let tunnel_stream = mtry!(tunnel.with_stream(proxy_stream).await); break match tls { - #[cfg(feature = "tls")] - Some(tls) => { - let tls = TlsConnector::from(tls); - let secure_stream = - mtry!(tls.connect(&host, tunnel_stream).await.map_err(io_err)); - - Ok(ProxyStream::Secured(secure_stream)) - } - - #[cfg(feature = "rustls-base")] + #[cfg(all(not(feature = "__rustls"), feature = "native-tls"))] Some(tls) => { - let dnsref = - mtry!(DNSNameRef::try_from_ascii_str(&host).map_err(io_err)); + use hyper_util::rt::TokioIo; let tls = TlsConnector::from(tls); - let secure_stream = - mtry!(tls.connect(dnsref, tunnel_stream).await.map_err(io_err)); + let secure_stream = mtry!(tls + .connect(&host, TokioIo::new(tunnel_stream)) + .await + .map_err(io_err)); - Ok(ProxyStream::Secured(secure_stream)) + Ok(ProxyStream::Secured(Box::new(TokioIo::new(secure_stream)))) } - #[cfg(feature = "openssl-tls")] + #[cfg(feature = "__rustls")] Some(tls) => { - let config = tls.configure().map_err(io_err)?; - let ssl = config.into_ssl(&host).map_err(io_err)?; - - let mut stream = mtry!(SslStream::new(ssl, tunnel_stream)); - mtry!(Pin::new(&mut stream).connect().await.map_err(io_err)); - - Ok(ProxyStream::Secured(stream)) + use hyper_util::rt::TokioIo; + let server_name = + mtry!(ServerName::try_from(host.to_string()).map_err(io_err)); + let secure_stream = mtry!(tls + .connect(server_name, TokioIo::new(tunnel_stream)) + .await + .map_err(io_err)); + + Ok(ProxyStream::Secured(Box::new(TokioIo::new(secure_stream)))) } - #[cfg(not(any( - feature = "tls", - feature = "rustls-base", - feature = "openssl-tls" - )))] + #[cfg(not(feature = "__tls",))] Some(_) => panic!("hyper-proxy was not built with TLS support"), None => Ok(ProxyStream::Regular(tunnel_stream)), @@ -536,7 +519,53 @@ fn proxy_dst(dst: &Uri, proxy: &Uri) -> io::Result { .ok_or_else(|| io_err(format!("proxy uri missing host: {}", proxy)))? .clone(), ) - .path_and_query(dst.path_and_query().unwrap().clone()) + .path_and_query( + dst.path_and_query() + .ok_or_else(|| io_err(format!("dst uri missing path: {}", proxy)))? + .clone(), + ) .build() .map_err(|err| io_err(format!("other error: {}", err))) } + +/// Extracts the username and password from the URI +fn extract_user_pass(uri: &Uri) -> Option<(&str, &str)> { + let authority = uri.authority()?.as_str(); + let (userinfo, _) = authority.rsplit_once('@')?; + let mut parts = userinfo.splitn(2, ':'); + let username = parts.next()?; + let password = parts.next()?; + Some((username, password)) +} + +#[cfg(test)] +mod tests { + use http::Uri; + + use crate::{Intercept, Proxy}; + + #[test] + fn test_new_proxy_with_authorization() { + let proxy = Proxy::new( + Intercept::All, + Uri::from_static("https://bob:secret@my-proxy:8080"), + ); + + assert_eq!( + proxy + .headers() + .get("authorization") + .unwrap() + .to_str() + .unwrap(), + "Basic Ym9iOnNlY3JldA==" + ); + } + + #[test] + fn test_new_proxy_without_authorization() { + let proxy = Proxy::new(Intercept::All, Uri::from_static("https://my-proxy:8080")); + + assert_eq!(proxy.headers().get("authorization"), None); + } +} diff --git a/src/rt.rs b/src/rt.rs new file mode 100644 index 0000000..d153efb --- /dev/null +++ b/src/rt.rs @@ -0,0 +1,140 @@ +use std::future::Future; +use std::marker::PhantomPinned; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use bytes::{Buf, BufMut}; +use futures_util::ready; +use hyper::rt; +use pin_project_lite::pin_project; + +pin_project! { + #[derive(Debug)] + #[must_use = "futures do nothing unless you `.await` or poll them"] + pub struct ReadBuf<'a, R: ?Sized, B: ?Sized> { + reader: &'a mut R, + buf: &'a mut B, + #[pin] + _pin: PhantomPinned, + } +} + +impl Future for ReadBuf<'_, R, B> +where + R: rt::Read + Unpin + ?Sized, + B: BufMut + ?Sized, +{ + type Output = std::io::Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + use hyper::rt::{Read as _, ReadBuf}; + use std::mem::MaybeUninit; + + let me = self.project(); + + if !me.buf.has_remaining_mut() { + return Poll::Ready(Ok(0)); + } + + let n = { + let dst = me.buf.chunk_mut(); + let dst = unsafe { &mut *(dst as *mut _ as *mut [MaybeUninit]) }; + let mut buf = ReadBuf::uninit(dst); + let ptr = buf.filled().as_ptr(); + ready!(Pin::new(me.reader).poll_read(cx, buf.unfilled())?); + + // Ensure the pointer does not change from under us + assert_eq!(ptr, buf.filled().as_ptr()); + buf.filled().len() + }; + + // Safety: This is guaranteed to be the number of initialized (and read) + // bytes due to the invariants provided by `ReadBuf::filled`. + unsafe { + me.buf.advance_mut(n); + } + + Poll::Ready(Ok(n)) + } +} + +pub(crate) fn read_buf<'a, R, B>(reader: &'a mut R, buf: &'a mut B) -> ReadBuf<'a, R, B> +where + R: rt::Read + Unpin + ?Sized, + B: BufMut + ?Sized, +{ + ReadBuf { + reader, + buf, + _pin: PhantomPinned, + } +} + +pin_project! { + #[derive(Debug)] + #[must_use = "futures do nothing unless you `.await` or poll them"] + pub struct WriteBuf<'a, W, B> { + writer: &'a mut W, + buf: &'a mut B, + #[pin] + _pin: PhantomPinned, + } +} + +impl Future for WriteBuf<'_, W, B> +where + W: rt::Write + Unpin, + B: Buf, +{ + type Output = std::io::Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + use rt::Write as _; + + let me = self.project(); + + if !me.buf.has_remaining() { + return Poll::Ready(Ok(0)); + } + + let n = ready!(Pin::new(me.writer).poll_write(cx, me.buf.chunk()))?; + me.buf.advance(n); + Poll::Ready(Ok(n)) + } +} + +pub(crate) fn write_buf<'a, W, B>(writer: &'a mut W, buf: &'a mut B) -> WriteBuf<'a, W, B> +where + W: rt::Write + Unpin, + B: Buf, +{ + WriteBuf { + writer, + buf, + _pin: PhantomPinned, + } +} + +pub(crate) trait ReadExt: rt::Read { + fn read_buf<'a, B>(&'a mut self, buf: &'a mut B) -> ReadBuf<'a, Self, B> + where + Self: Unpin, + B: BufMut + ?Sized, + { + read_buf(self, buf) + } +} + +impl ReadExt for T where T: rt::Read {} + +pub(crate) trait WriteExt: rt::Write { + fn write_buf<'a, B>(&'a mut self, src: &'a mut B) -> WriteBuf<'a, Self, B> + where + Self: Sized + Unpin, + B: Buf, + { + write_buf(self, src) + } +} + +impl WriteExt for T where T: rt::Write {} diff --git a/src/stream.rs b/src/stream.rs index 4f45be6..5e7fe84 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,31 +1,31 @@ use std::io; use std::pin::Pin; use std::task::{Context, Poll}; -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -#[cfg(feature = "rustls-base")] -use tokio_rustls::client::TlsStream as RustlsStream; +use hyper::rt::{Read, ReadBufCursor, Write}; +use hyper_util::client::legacy::connect::{Connected, Connection}; -#[cfg(feature = "tls")] -use tokio_native_tls::TlsStream; +#[cfg(feature = "__tls")] +use hyper_util::rt::TokioIo; -#[cfg(feature = "openssl-tls")] -use tokio_openssl::SslStream as OpenSslStream; +#[cfg(feature = "__rustls")] +use tokio_rustls::client::TlsStream as RustlsStream; -use hyper::client::connect::{Connected, Connection}; +#[cfg(all(not(feature = "__rustls"), feature = "native-tls"))] +use tokio_native_tls::TlsStream as TokioNativeTlsStream; -#[cfg(feature = "rustls-base")] -pub type TlsStream = RustlsStream; +#[cfg(feature = "__rustls")] +pub type TlsStream = TokioIo>>; -#[cfg(feature = "openssl-tls")] -pub type TlsStream = OpenSslStream; +#[cfg(all(not(feature = "__rustls"), feature = "native-tls"))] +pub type TlsStream = TokioIo>>; /// A Proxy Stream wrapper pub enum ProxyStream { NoProxy(R), Regular(R), - #[cfg(any(feature = "tls", feature = "rustls-base", feature = "openssl-tls"))] - Secured(TlsStream), + #[cfg(feature = "__tls")] + Secured(Box>), } macro_rules! match_fn_pinned { @@ -33,7 +33,7 @@ macro_rules! match_fn_pinned { match $self.get_mut() { ProxyStream::NoProxy(s) => Pin::new(s).$fn($ctx, $buf), ProxyStream::Regular(s) => Pin::new(s).$fn($ctx, $buf), - #[cfg(any(feature = "tls", feature = "rustls-base", feature = "openssl-tls"))] + #[cfg(feature = "__tls")] ProxyStream::Secured(s) => Pin::new(s).$fn($ctx, $buf), } }; @@ -42,23 +42,23 @@ macro_rules! match_fn_pinned { match $self.get_mut() { ProxyStream::NoProxy(s) => Pin::new(s).$fn($ctx), ProxyStream::Regular(s) => Pin::new(s).$fn($ctx), - #[cfg(any(feature = "tls", feature = "rustls-base", feature = "openssl-tls"))] + #[cfg(feature = "__tls")] ProxyStream::Secured(s) => Pin::new(s).$fn($ctx), } }; } -impl AsyncRead for ProxyStream { +impl Read for ProxyStream { fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, + buf: ReadBufCursor<'_>, ) -> Poll> { match_fn_pinned!(self, poll_read, cx, buf) } } -impl AsyncWrite for ProxyStream { +impl Write for ProxyStream { fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, @@ -79,7 +79,7 @@ impl AsyncWrite for ProxyStream { match self { ProxyStream::NoProxy(s) => s.is_write_vectored(), ProxyStream::Regular(s) => s.is_write_vectored(), - #[cfg(any(feature = "tls", feature = "rustls-base", feature = "openssl-tls"))] + #[cfg(feature = "__tls")] ProxyStream::Secured(s) => s.is_write_vectored(), } } @@ -93,20 +93,24 @@ impl AsyncWrite for ProxyStream { } } -impl Connection for ProxyStream { +impl Connection for ProxyStream { fn connected(&self) -> Connected { match self { ProxyStream::NoProxy(s) => s.connected(), ProxyStream::Regular(s) => s.connected().proxy(true), - #[cfg(feature = "tls")] - ProxyStream::Secured(s) => s.get_ref().get_ref().get_ref().connected().proxy(true), - - #[cfg(feature = "rustls-base")] - ProxyStream::Secured(s) => s.get_ref().0.connected().proxy(true), - - #[cfg(feature = "openssl-tls")] - ProxyStream::Secured(s) => s.get_ref().connected().proxy(true), + #[cfg(all(not(feature = "__rustls"), feature = "native-tls"))] + ProxyStream::Secured(s) => s + .inner() + .get_ref() + .get_ref() + .get_ref() + .inner() + .connected() + .proxy(true), + + #[cfg(feature = "__rustls")] + ProxyStream::Secured(s) => s.inner().get_ref().0.inner().connected().proxy(true), } } } diff --git a/src/tunnel.rs b/src/tunnel.rs index 40535e9..736f9a0 100644 --- a/src/tunnel.rs +++ b/src/tunnel.rs @@ -1,12 +1,15 @@ -use crate::io_err; -use bytes::{buf::Buf, BytesMut}; -use http::HeaderMap; use std::fmt::{self, Display, Formatter}; use std::future::Future; use std::io; use std::pin::Pin; use std::task::{Context, Poll}; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +use bytes::{buf::Buf, BytesMut}; +use http::HeaderMap; +use hyper::rt::{Read, Write}; + +use crate::io_err; +use crate::rt::{ReadExt as _, WriteExt as _}; macro_rules! try_ready { ($x:expr) => { @@ -76,7 +79,7 @@ pub(crate) fn new(host: &str, port: u16, headers: &HeaderMap) -> TunnelConnect { } } -impl Future for Tunnel { +impl Future for Tunnel { type Output = Result; fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { @@ -131,6 +134,7 @@ impl Future for Tunnel { mod tests { use super::{HeaderMap, Tunnel}; use futures_util::future::TryFutureExt; + use hyper_util::rt::TokioIo; use std::io::{Read, Write}; use std::net::TcpListener; use std::thread; @@ -141,7 +145,7 @@ mod tests { super::new(&host, port, &HeaderMap::new()).with_stream(conn) } - #[cfg_attr(rustfmt, rustfmt_skip)] + #[rustfmt::skip] macro_rules! mock_tunnel { () => {{ mock_tunnel!( @@ -184,7 +188,7 @@ mod tests { let work = TcpStream::connect(&addr); let host = addr.ip().to_string(); let port = addr.port(); - let work = work.and_then(|tcp| tunnel(tcp, host, port)); + let work = work.and_then(|tcp| tunnel(TokioIo::new(tcp), host, port)); core.block_on(work).unwrap(); } @@ -197,7 +201,7 @@ mod tests { let work = TcpStream::connect(&addr); let host = addr.ip().to_string(); let port = addr.port(); - let work = work.and_then(|tcp| tunnel(tcp, host, port)); + let work = work.and_then(|tcp| tunnel(TokioIo::new(tcp), host, port)); core.block_on(work).unwrap_err(); } @@ -210,7 +214,7 @@ mod tests { let work = TcpStream::connect(&addr); let host = addr.ip().to_string(); let port = addr.port(); - let work = work.and_then(|tcp| tunnel(tcp, host, port)); + let work = work.and_then(|tcp| tunnel(TokioIo::new(tcp), host, port)); core.block_on(work).unwrap_err(); } diff --git a/upstream_commit_recommendations.txt b/upstream_commit_recommendations.txt new file mode 100644 index 0000000..7865b65 --- /dev/null +++ b/upstream_commit_recommendations.txt @@ -0,0 +1,43 @@ +Here's my assessment of all commits in `main..`, grouped by fork: + +--- + +## hyper-http-proxy fork + +| Commit | Message | Recommendation | +|--------|---------|----------------| +| `tlnmuprv` | update dependencies (tokio-rustls 0.25→0.26, hyper-rustls 0.26→0.27) | **KEEP** — but superseded by later commits; only needed if taking partial chain | +| `xlwskxwp` | Refactor (new feature naming, drop openssl, rename webpki→rustls-webpki) | **KEEP** — major structural improvement; drops openssl-tls support (intentional in this fork) | +| `lotvxkwy` | rename to hyper-http-proxy (package name + 1.0.0) | **KEEP with modification** — take the Cargo.toml/README/LICENSE changes, but you'll want to decide on your own version numbering and crate name | +| `okyzxmwl` | hygiene (remove rustls-tls/rustls-tls-manual-roots aliases, update README) | **KEEP** | +| `rpvqwonu` | lint (fix CI `--features` flags, add `CustomProxyCallback` type alias, minor lints) | **KEEP** | +| `xlmqyxqu` | release (add release.yaml CI workflow) | **DISCARD** — MetalBear's CI setup, not yours | +| `pmxowuno` | fix test (remove rustls-tls-manual-roots from CI since feature was removed) | **KEEP** | +| `rvlmmzus` | `..` (1.0.0 tag; likely just Cargo.lock update) | **DISCARD** — noise commit | +| `tzpxxmtk` | call set_authorization when creating a Proxy (auto-extract credentials from URI) | **KEEP** — useful feature with tests | +| `qsxzsozm` | Update lib.rs (refactor extract_user_pass: rsplit_once, avoid allocation) | **KEEP** | +| `pspprnvv` | add doc (docstring for `extract_user_pass`) | **KEEP** | +| `ztllkrlr` | cargo fmt --all | **KEEP** | +| `spnpoosu` | 1.1.0 version bump | **KEEP** (re-version as appropriate) | +| `ozuxnknk` | remove unused rustls-native-certs direct dep (let hyper-rustls handle it) | **KEEP** | +| `uqkvyvqt` | Bump 1.1.0→1.1.1, fix missing trailing newline in Cargo.toml | **KEEP** (re-version as appropriate) | + +--- + +## hyper-proxy2 fork + +| Commit | Message | Recommendation | +|--------|---------|----------------| +| `ovssllkp` | feat: Upgrade to http/hyper 1.0 | **DISCARD** — foundation for both forks; hyper-http-proxy's chain is a superset of this work | +| `mvrkoylw` | ci: Add GitHub Actions workflow | **DISCARD** — superseded by hyper-http-proxy's CI (which has better feature matrix) | +| `vmzzswov` | fix: Use from_proxy_unsecured in examples for no-tls/native-tls | **DISCARD** — the example and feature names changed in hyper-http-proxy refactor | +| `zxrptwwl` | chore: Fix doc URL, bump MSRV to 1.70 | **DISCARD** — doc URL is for siketyan's fork; MSRV bump is worth keeping conceptually but re-apply manually | +| `qupmtylz` | ci: Remove Travis CI | **DISCARD** — already absent in hyper-http-proxy chain | +| `qrqymtru` | Do not crash if path is missing (`.unwrap()` → `.ok_or_else()`) | **KEEP** — genuine bug fix not present in hyper-http-proxy chain; should be cherry-picked | +| `rsrxxons` | Bump tokio-rustls 0.25→0.26, hyper-rustls 0.26→0.27, webpki-roots 0.26→1.0.3 | **DISCARD** — tokio-rustls/hyper-rustls bump is in hyper-http-proxy chain; webpki-roots bump to 1.0.3 may conflict with hyper-http-proxy's 0.26 | + +--- + +**Summary**: Take the full hyper-http-proxy chain (minus `xlmqyxqu` release.yaml and `rvlmmzus` noise), plus cherry-pick `qrqymtru` from hyper-proxy2 for the panic fix. The hyper-proxy2 branch is otherwise entirely superseded. + +One thing to decide before you start: do you want to keep the crate name as `hyper-proxy` or adopt `hyper-http-proxy`? That affects how you handle `lotvxkwy`.