From 552530e336ae72409463d8340d5e769cfe9011f6 Mon Sep 17 00:00:00 2001 From: Sam Clark <3758302+goatgoose@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:10:12 -0500 Subject: [PATCH] feat(s2n-tls-hyper): Add support for negotiating HTTP/2 --- .github/workflows/ci_rust.yml | 4 +- bindings/rust/s2n-tls-hyper/Cargo.toml | 13 +++++-- bindings/rust/s2n-tls-hyper/src/stream.rs | 11 +++++- bindings/rust/s2n-tls-hyper/tests/http.rs | 45 ++++++++++++++++++++++- 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci_rust.yml b/.github/workflows/ci_rust.yml index b53c1408da5..a726e612962 100644 --- a/.github/workflows/ci_rust.yml +++ b/.github/workflows/ci_rust.yml @@ -49,10 +49,10 @@ jobs: working-directory: ${{env.ROOT_PATH}} run: cargo test - - name: "Feature Tests: Fingerprint, kTLS, QUIC, and PQ" + - name: "Feature Tests: Fingerprint, kTLS, QUIC, PQ, and http2" working-directory: ${{env.ROOT_PATH}} # Test all features except for FIPS, which is tested separately. - run: cargo test --features unstable-fingerprint,unstable-ktls,quic,pq + run: cargo test --features unstable-fingerprint,unstable-ktls,quic,pq,http2 - name: "Feature Test: Renegotiate" working-directory: ${{env.ROOT_PATH}} diff --git a/bindings/rust/s2n-tls-hyper/Cargo.toml b/bindings/rust/s2n-tls-hyper/Cargo.toml index 8cdf850b3bd..865346a97e7 100644 --- a/bindings/rust/s2n-tls-hyper/Cargo.toml +++ b/bindings/rust/s2n-tls-hyper/Cargo.toml @@ -10,15 +10,22 @@ license = "Apache-2.0" publish = false [features] -default = [] +default = ["http1"] +http1 = ["hyper-util/http1"] +http2 = ["hyper-util/http2", "dep:hashbrown", "dep:tokio-util"] [dependencies] s2n-tls = { version = "=0.3.7", path = "../s2n-tls" } s2n-tls-tokio = { version = "=0.3.7", path = "../s2n-tls-tokio" } hyper = { version = "1" } -hyper-util = { version = "0.1", features = ["client-legacy", "tokio", "http1"] } +hyper-util = { version = "0.1", features = ["client-legacy", "tokio"] } tower-service = { version = "0.3" } -http = { version= "1" } +http = { version = "1" } + +# Newer versions require Rust 1.65, see https://github.com/aws/s2n-tls/issues/4242. +hashbrown = { version = "=0.15.0", optional = true } +# Newer versions require Rust 1.70, see https://github.com/aws/s2n-tls/issues/4395. +tokio-util = { version = "=0.7.11", optional = true } [dev-dependencies] tokio = { version = "1", features = ["macros", "test-util"] } diff --git a/bindings/rust/s2n-tls-hyper/src/stream.rs b/bindings/rust/s2n-tls-hyper/src/stream.rs index 61bc59fe682..76a7e7dda89 100644 --- a/bindings/rust/s2n-tls-hyper/src/stream.rs +++ b/bindings/rust/s2n-tls-hyper/src/stream.rs @@ -48,7 +48,16 @@ where { fn connected(&self) -> Connected { match self { - MaybeHttpsStream::Https(stream) => stream.inner().get_ref().connected(), + MaybeHttpsStream::Https(stream) => { + let connected = stream.inner().get_ref().connected(); + let conn = stream.inner().as_ref(); + match conn.application_protocol() { + // Inform hyper to use HTTP/2 when HTTP/2 is negotiated in the ALPN. + #[cfg(feature = "http2")] + Some(b"h2") => connected.negotiated_h2(), + _ => connected, + } + } } } } diff --git a/bindings/rust/s2n-tls-hyper/tests/http.rs b/bindings/rust/s2n-tls-hyper/tests/http.rs index 8a1a8a37243..2d6b0913e99 100644 --- a/bindings/rust/s2n-tls-hyper/tests/http.rs +++ b/bindings/rust/s2n-tls-hyper/tests/http.rs @@ -4,7 +4,7 @@ use crate::common::InsecureAcceptAllCertificatesHandler; use bytes::Bytes; use common::echo::serve_echo; -use http::{Method, Request, Uri}; +use http::{Method, Request, Uri, Version}; use http_body_util::{BodyExt, Empty, Full}; use hyper_util::{client::legacy::Client, rt::TokioExecutor}; use s2n_tls::{ @@ -215,3 +215,46 @@ async fn error_matching() -> Result<(), Box> { server_task.abort(); Ok(()) } + +#[tokio::test] +async fn http2() -> Result<(), Box> { + let server_config = { + let mut builder = common::config()?; + builder.set_application_protocol_preference(["h2"])?; + builder.build()? + }; + + for expected_http_version in [Version::HTTP_11, Version::HTTP_2] { + let client_config = { + let mut builder = common::config()?; + if expected_http_version == Version::HTTP_2 { + builder.set_application_protocol_preference(["h2"])?; + } + builder.build()? + }; + + common::echo::make_echo_request(server_config.clone(), |port| async move { + let connector = HttpsConnector::new(client_config); + let client: Client<_, Empty> = + Client::builder(TokioExecutor::new()).build(connector); + + let uri = Uri::from_str(format!("https://localhost:{}", port).as_str())?; + let response = client.get(uri).await?; + assert_eq!(response.status(), 200); + + // Ensure that HTTP/2 is negotiated when included in the ALPN. + #[cfg(feature = "http2")] + assert_eq!(response.version(), expected_http_version); + + // If the http2 feature isn't enabled, then HTTP/1 should be negotiated even if HTTP/2 + // was included in the ALPN. + #[cfg(not(feature = "http2"))] + assert_eq!(response.version(), Version::HTTP_11); + + Ok(()) + }) + .await?; + } + + Ok(()) +}