diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e7735bc --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,44 @@ +on: [push, pull_request] + +name: Cargo Build + +jobs: + build: + name: Build + strategy: + fail-fast: false + matrix: + config: + - node: ubuntu-latest + - node: windows-latest + - node: macos-latest + runs-on: ${{ matrix.config.node }} + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + + - uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-all-features + - uses: actions-rs/cargo@v1 + with: + command: build-all-features + + - uses: actions-rs/cargo@v1 + with: + command: build-all-features + + - uses: actions-rs/cargo@v1 + with: + command: build + args: --examples + + - uses: actions-rs/cargo@v1 + with: + command: build + args: --benches --features __bench \ No newline at end of file diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml index 4e40f56..e704f34 100644 --- a/.github/workflows/linting.yaml +++ b/.github/workflows/linting.yaml @@ -1,6 +1,6 @@ on: [push, pull_request] -name: CI +name: Cargo Check & Clippy jobs: check: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5025f0a..b69acf4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,6 @@ on: [push, pull_request] -name: CI +name: Cargo Test jobs: test: diff --git a/Cargo.toml b/Cargo.toml index 2fe551f..5abff50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,10 @@ edition = "2018" include = ["Cargo.toml", "LICENSE", "src/**/*"] +[[bench]] +name="internal" +harness = false + [dependencies] hyper = { version = "0.14.18", features = ["client"] } lazy_static = "1.4.0" @@ -41,3 +45,8 @@ hyper-trust-dns = { version = "0.4.2", features = [ rand = "0.8.5" tungstenite = "0.17" url = "2.2" +criterion = "0.3.5" + +[features] + +__bench=[] \ No newline at end of file diff --git a/benches/internal.rs b/benches/internal.rs new file mode 100644 index 0000000..fcd25df --- /dev/null +++ b/benches/internal.rs @@ -0,0 +1,227 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use hyper::client::connect::dns::GaiResolver; +use hyper::client::HttpConnector; +use hyper::header::HeaderName; +use hyper::Uri; +use hyper::{HeaderMap, Request, Response}; +use hyper_reverse_proxy::benches as internal_benches; +use hyper_reverse_proxy::ReverseProxy; +use rand::distributions::Alphanumeric; +use rand::prelude::*; +use std::net::Ipv4Addr; +use std::str::FromStr; +use test_context::AsyncTestContext; +use tokio::runtime::Runtime; +use tokiotest_httpserver::HttpTestContext; + +lazy_static::lazy_static! { + static ref PROXY_CLIENT: ReverseProxy> = { + ReverseProxy::new( + hyper::Client::new(), + ) + }; +} + +fn create_proxied_response(b: &mut Criterion) { + let headers_map = build_headers(); + + b.bench_function("create proxied response", |t| { + t.iter(|| { + let mut response = Response::builder().status(200); + + *response.headers_mut().unwrap() = headers_map.clone(); + + internal_benches::create_proxied_response(black_box(response.body(()).unwrap())); + }) + }); +} + +fn generate_string() -> String { + let take = rand::thread_rng().gen::().into(); + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(take) + .map(char::from) + .collect() +} + +fn build_headers() -> HeaderMap { + let mut headers_map: HeaderMap = (&*internal_benches::hop_headers()) + .iter() + .map(|el: &'static HeaderName| (el.clone(), generate_string().parse().unwrap())) + .collect(); + + for _i in 0..20 { + 'inserted: loop { + if let Ok(value) = + hyper::header::HeaderName::from_str(&generate_string().to_lowercase()) + { + headers_map.insert(value, generate_string().parse().unwrap()); + + break 'inserted; + } + } + } + + headers_map +} + +fn proxy_call(b: &mut Criterion) { + let rt = Runtime::new().unwrap(); + + let uri = Uri::from_static("http://0.0.0.0:8080/me?hello=world"); + + let http_context: HttpTestContext = rt.block_on(async { AsyncTestContext::setup().await }); + + let forward_url = &format!("http://0.0.0.0:{}", http_context.port); + + let headers_map = build_headers(); + + let client_ip = std::net::IpAddr::from(Ipv4Addr::from_str("0.0.0.0").unwrap()); + + b.bench_function("proxy call", |c| { + c.iter(|| { + rt.block_on(async { + let mut request = Request::builder().uri(uri.clone()); + + *request.headers_mut().unwrap() = headers_map.clone(); + + black_box(&PROXY_CLIENT) + .call( + black_box(client_ip), + black_box(forward_url), + black_box(request.body(hyper::Body::from("")).unwrap()), + ) + .await + .unwrap(); + }) + }) + }); +} + +fn forward_url_with_str_ending_slash(b: &mut Criterion) { + let uri = Uri::from_static("https://0.0.0.0:8080/me"); + let port = rand::thread_rng().gen::(); + let forward_url = &format!("https://0.0.0.0:{}/", port); + + b.bench_function("forward url with str ending slash", |b| { + b.iter(|| { + let request = Request::builder().uri(uri.clone()).body(()); + + internal_benches::forward_uri(forward_url, &request.unwrap()); + }) + }); +} + +fn forward_url_with_str_ending_slash_and_query(b: &mut Criterion) { + let uri = Uri::from_static("https://0.0.0.0:8080/me?hello=world"); + let port = rand::thread_rng().gen::(); + let forward_url = &format!("https://0.0.0.0:{}/", port); + + b.bench_function("forward url with str ending slash and query", |t| { + t.iter(|| { + let request = Request::builder().uri(uri.clone()).body(()); + + internal_benches::forward_uri(forward_url, &request.unwrap()); + }) + }); +} + +fn forward_url_no_ending_slash(b: &mut Criterion) { + let uri = Uri::from_static("https://0.0.0.0:8080/me"); + let port = rand::thread_rng().gen::(); + let forward_url = &format!("https://0.0.0.0:{}", port); + + b.bench_function("forward url no ending slash", |t| { + t.iter(|| { + let request = Request::builder().uri(uri.clone()).body(()); + + internal_benches::forward_uri(forward_url, &request.unwrap()); + }) + }); +} + +fn forward_url_with_query(b: &mut Criterion) { + let uri = Uri::from_static("https://0.0.0.0:8080/me?hello=world"); + let port = rand::thread_rng().gen::(); + let forward_url = &format!("https://0.0.0.0:{}", port); + + b.bench_function("forward_url_with_query", |t| { + t.iter(|| { + let request = Request::builder().uri(uri.clone()).body(()); + + internal_benches::forward_uri(forward_url, &request.unwrap()); + }) + }); +} + +fn create_proxied_request_forwarded_for_occupied(b: &mut Criterion) { + let uri = Uri::from_static("https://0.0.0.0:8080/me?hello=world"); + let port = rand::thread_rng().gen::(); + let forward_url = &format!("https://0.0.0.0:{}", port); + + let mut headers_map = build_headers(); + + headers_map.insert( + HeaderName::from_static("x-forwarded-for"), + "0.0.0.0".parse().unwrap(), + ); + + let client_ip = std::net::IpAddr::from(Ipv4Addr::from_str("0.0.0.0").unwrap()); + + b.bench_function("create proxied request forwarded for occupied", |t| { + t.iter(|| { + let mut request = Request::builder().uri(uri.clone()); + + *request.headers_mut().unwrap() = headers_map.clone(); + + internal_benches::create_proxied_request( + client_ip, + forward_url, + request.body(()).unwrap(), + None, + ); + }) + }); +} + +fn create_proxied_request_forwarded_for_vacant(b: &mut Criterion) { + let uri = Uri::from_static("https://0.0.0.0:8080/me?hello=world"); + let port = rand::thread_rng().gen::(); + let forward_url = &format!("https://0.0.0.0:{}", port); + + let headers_map = build_headers(); + + let client_ip = std::net::IpAddr::from(Ipv4Addr::from_str("0.0.0.0").unwrap()); + + b.bench_function("create proxied request forwarded for vacant", |t| { + t.iter(|| { + let mut request = Request::builder().uri(uri.clone()); + + *request.headers_mut().unwrap() = headers_map.clone(); + + internal_benches::create_proxied_request( + client_ip, + forward_url, + request.body(()).unwrap(), + None, + ); + }) + }); +} + +criterion_group!(external_api, proxy_call); +criterion_group!(responses, create_proxied_response); +criterion_group!( + url_parsing, + forward_url_with_query, + forward_url_no_ending_slash, + forward_url_with_str_ending_slash_and_query, + forward_url_with_str_ending_slash +); +criterion_group!( + requests, + create_proxied_request_forwarded_for_vacant, + create_proxied_request_forwarded_for_occupied +); +criterion_main!(external_api, responses, url_parsing, requests); diff --git a/src/lib.rs b/src/lib.rs index c2edf7c..51e8add 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,14 +108,9 @@ //! } //! //! ``` -#![cfg_attr(all(not(stable), test), feature(test))] - #[macro_use] extern crate tracing; -#[cfg(all(not(stable), test))] -extern crate test; - use hyper::header::{HeaderMap, HeaderName, HeaderValue, HOST}; use hyper::http::header::{InvalidHeaderValue, ToStrError}; use hyper::http::uri::InvalidUri; @@ -129,15 +124,17 @@ lazy_static! { static ref TE_HEADER: HeaderName = HeaderName::from_static("te"); static ref CONNECTION_HEADER: HeaderName = HeaderName::from_static("connection"); static ref UPGRADE_HEADER: HeaderName = HeaderName::from_static("upgrade"); + static ref TRAILER_HEADER: HeaderName = HeaderName::from_static("trailer"); + static ref TRAILERS_HEADER: HeaderName = HeaderName::from_static("trailers"); // A list of the headers, using hypers actual HeaderName comparison static ref HOP_HEADERS: [HeaderName; 9] = [ CONNECTION_HEADER.clone(), TE_HEADER.clone(), + TRAILER_HEADER.clone(), HeaderName::from_static("keep-alive"), HeaderName::from_static("proxy-connection"), HeaderName::from_static("proxy-authenticate"), HeaderName::from_static("proxy-authorization"), - HeaderName::from_static("trailer"), HeaderName::from_static("transfer-encoding"), HeaderName::from_static("upgrade"), ]; @@ -186,6 +183,7 @@ fn remove_hop_headers(headers: &mut HeaderMap) { } fn get_upgrade_type(headers: &HeaderMap) -> Option { + #[allow(clippy::blocks_in_if_conditions)] if headers .get(&*CONNECTION_HEADER) .map(|value| { @@ -193,7 +191,7 @@ fn get_upgrade_type(headers: &HeaderMap) -> Option { .to_str() .unwrap() .split(',') - .any(|e| e.trim().to_lowercase() == "upgrade") + .any(|e| e.trim() == *UPGRADE_HEADER) }) .unwrap_or(false) { @@ -327,7 +325,7 @@ fn create_proxied_request( .to_str() .unwrap() .split(',') - .any(|e| e.to_lowercase() == "trailers") + .any(|e| e.trim() == *TRAILERS_HEADER) }) .unwrap_or(false); @@ -473,191 +471,26 @@ impl Reverse } } -#[cfg(all(not(stable), test))] -mod tests { - use hyper::header::HeaderName; - use hyper::{Client, Uri}; - use hyper::{HeaderMap, Request, Response}; - use rand::distributions::Alphanumeric; - use rand::prelude::*; - use std::net::Ipv4Addr; - use std::str::FromStr; - use test::Bencher; - use test_context::AsyncTestContext; - use tokiotest_httpserver::HttpTestContext; - - fn generate_string() -> String { - let take = rand::thread_rng().gen::().into(); - rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(take) - .map(char::from) - .collect() - } - - fn build_headers() -> HeaderMap { - let mut headers_map: HeaderMap = (&*super::HOP_HEADERS) - .iter() - .map(|el: &'static HeaderName| (el.clone(), generate_string().parse().unwrap())) - .collect(); - - for _i in 0..20 { - 'inserted: loop { - if let Ok(value) = - hyper::header::HeaderName::from_str(&generate_string().to_lowercase()) - { - headers_map.insert(value, generate_string().parse().unwrap()); - - break 'inserted; - } - } - } - - headers_map - } - - #[bench] - fn proxy_call(b: &mut Bencher) { - use tokio::runtime::Runtime; - let rt = Runtime::new().unwrap(); - - let uri = Uri::from_static("http://0.0.0.0:8080/me?hello=world"); - - let http_context: HttpTestContext = rt.block_on(async { AsyncTestContext::setup().await }); - - let forward_url = &format!("http://0.0.0.0:{}", http_context.port); - - let headers_map = build_headers(); - - let client_ip = std::net::IpAddr::from(Ipv4Addr::from_str("0.0.0.0").unwrap()); - - let client = Client::new(); - - b.iter(|| { - rt.block_on(async { - let mut request = Request::builder().uri(uri.clone()); - - *request.headers_mut().unwrap() = headers_map.clone(); - - super::call( - client_ip, - forward_url, - request.body(hyper::Body::from("")).unwrap(), - &client, - ) - .await - .unwrap(); - }) - }); +#[cfg(feature = "__bench")] +pub mod benches { + pub fn hop_headers() -> &'static [crate::HeaderName] { + &*super::HOP_HEADERS } - #[bench] - fn create_proxied_response(b: &mut Bencher) { - let headers_map = build_headers(); - - b.iter(|| { - let mut response = Response::builder().status(200); - - *response.headers_mut().unwrap() = headers_map.clone(); - - super::create_proxied_response(response.body(()).unwrap()); - }); + pub fn create_proxied_response(response: crate::Response) { + super::create_proxied_response(response); } - #[bench] - fn forward_url_with_str_ending_slash(b: &mut Bencher) { - let uri = Uri::from_static("http://0.0.0.0:8080/me"); - let port = rand::thread_rng().gen::(); - let forward_url = &format!("http://0.0.0.0:{}/", port); - - b.iter(|| { - let request = Request::builder().uri(uri.clone()).body(()); - - super::forward_uri(forward_url, &request.unwrap()); - }); + pub fn forward_uri(forward_url: &str, req: &crate::Request) { + super::forward_uri(forward_url, req); } - #[bench] - fn forward_url_with_str_ending_slash_and_query(b: &mut Bencher) { - let uri = Uri::from_static("http://0.0.0.0:8080/me?hello=world"); - let port = rand::thread_rng().gen::(); - let forward_url = &format!("http://0.0.0.0:{}/", port); - - b.iter(|| { - let request = Request::builder().uri(uri.clone()).body(()); - - super::forward_uri(forward_url, &request.unwrap()); - }); - } - - #[bench] - fn forward_url_no_ending_slash(b: &mut Bencher) { - let uri = Uri::from_static("http://0.0.0.0:8080/me"); - let port = rand::thread_rng().gen::(); - let forward_url = &format!("http://0.0.0.0:{}", port); - - b.iter(|| { - let request = Request::builder().uri(uri.clone()).body(()); - - super::forward_uri(forward_url, &request.unwrap()); - }); - } - - #[bench] - fn forward_url_with_query(b: &mut Bencher) { - let uri = Uri::from_static("http://0.0.0.0:8080/me?hello=world"); - let port = rand::thread_rng().gen::(); - let forward_url = &format!("http://0.0.0.0:{}", port); - - b.iter(|| { - let request = Request::builder().uri(uri.clone()).body(()); - - super::forward_uri(forward_url, &request.unwrap()); - }); - } - - #[bench] - fn create_proxied_request_forwarded_for_occupied(b: &mut Bencher) { - let uri = Uri::from_static("http://0.0.0.0:8080/me?hello=world"); - let port = rand::thread_rng().gen::(); - let forward_url = &format!("http://0.0.0.0:{}", port); - - let mut headers_map = build_headers(); - - headers_map.insert( - HeaderName::from_static("x-forwarded-for"), - "0.0.0.0".parse().unwrap(), - ); - - let client_ip = std::net::IpAddr::from(Ipv4Addr::from_str("0.0.0.0").unwrap()); - - b.iter(|| { - let mut request = Request::builder().uri(uri.clone()); - - *request.headers_mut().unwrap() = headers_map.clone(); - - super::create_proxied_request(client_ip, forward_url, request.body(()).unwrap(), None) - .unwrap(); - }); - } - - #[bench] - fn create_proxied_request_forwarded_for_vacant(b: &mut Bencher) { - let uri = Uri::from_static("http://0.0.0.0:8080/me?hello=world"); - let port = rand::thread_rng().gen::(); - let forward_url = &format!("http://0.0.0.0:{}", port); - - let headers_map = build_headers(); - - let client_ip = std::net::IpAddr::from(Ipv4Addr::from_str("0.0.0.0").unwrap()); - - b.iter(|| { - let mut request = Request::builder().uri(uri.clone()); - - *request.headers_mut().unwrap() = headers_map.clone(); - - super::create_proxied_request(client_ip, forward_url, request.body(()).unwrap(), None) - .unwrap(); - }); + pub fn create_proxied_request( + client_ip: crate::IpAddr, + forward_url: &str, + request: crate::Request, + upgrade_type: Option<&String>, + ) { + super::create_proxied_request(client_ip, forward_url, request, upgrade_type).unwrap(); } }