diff --git a/bindings/rust/s2n-tls-hyper/Cargo.toml b/bindings/rust/s2n-tls-hyper/Cargo.toml index 35b484d604b..835735f0dff 100644 --- a/bindings/rust/s2n-tls-hyper/Cargo.toml +++ b/bindings/rust/s2n-tls-hyper/Cargo.toml @@ -23,4 +23,5 @@ http = { version= "1" } [dev-dependencies] tokio = { version = "1", features = ["macros", "test-util"] } http-body-util = "0.1" +hyper-util = { version = "0.1", features = ["server"] } bytes = "1" diff --git a/bindings/rust/s2n-tls-hyper/tests/common/certs/cert.pem b/bindings/rust/s2n-tls-hyper/tests/common/certs/cert.pem new file mode 100644 index 00000000000..d0698c6532e --- /dev/null +++ b/bindings/rust/s2n-tls-hyper/tests/common/certs/cert.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICLTCCAdOgAwIBAgIUEck98wr+WHfwlEQZ7FynMbi/XZ8wCgYIKoZIzj0EAwIw +azELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkFaMQ4wDAYDVQQHDAVUZW1wZTEPMA0G +A1UECgwGQW1hem9uMRowGAYDVQQLDBFBbWF6b25XZWJTZXJ2aWNlczESMBAGA1UE +AwwJbG9jYWxob3N0MCAXDTI0MDkyNjIyMTc1MVoYDzIxMjQwOTAyMjIxNzUxWjBr +MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQVoxDjAMBgNVBAcMBVRlbXBlMQ8wDQYD +VQQKDAZBbWF6b24xGjAYBgNVBAsMEUFtYXpvbldlYlNlcnZpY2VzMRIwEAYDVQQD +DAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATJ3u7mGyF7tZV6 +5po7ZYrbvcNEnBTjRhUqpHuh0CDTFjqzld5uNl5SLMzzgNYVhnpUI0oZl26APPnu +RUyHk1z0o1MwUTAdBgNVHQ4EFgQUizNu1CP7DugWrbpDNpe53rhnTwswHwYDVR0j +BBgwFoAUizNu1CP7DugWrbpDNpe53rhnTwswDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAgNIADBFAiEA/OQhpG1qNsxPG0PpBTPxUhjRdGJqEtpKAoZQVPujGKAC +IETtPrOuddbfbGtaIWNbxTPCzHInidCPl4VWZwmMCq0d +-----END CERTIFICATE----- diff --git a/bindings/rust/s2n-tls-hyper/tests/common/certs/generate.sh b/bindings/rust/s2n-tls-hyper/tests/common/certs/generate.sh new file mode 100755 index 00000000000..c7e5c8fe84c --- /dev/null +++ b/bindings/rust/s2n-tls-hyper/tests/common/certs/generate.sh @@ -0,0 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +echo "generating ec key and cert" +openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -keyout key.pem -out cert.pem -sha256 -days 36500 -nodes -subj "/C=US/ST=AZ/L=Tempe/O=Amazon/OU=AmazonWebServices/CN=localhost" diff --git a/bindings/rust/s2n-tls-hyper/tests/common/certs/key.pem b/bindings/rust/s2n-tls-hyper/tests/common/certs/key.pem new file mode 100644 index 00000000000..8af5c2255e8 --- /dev/null +++ b/bindings/rust/s2n-tls-hyper/tests/common/certs/key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8KTDTTja1jS2raS8 +y3vIyWoL2nDrlNUWr9MAAIiRkeehRANCAATJ3u7mGyF7tZV65po7ZYrbvcNEnBTj +RhUqpHuh0CDTFjqzld5uNl5SLMzzgNYVhnpUI0oZl26APPnuRUyHk1z0 +-----END PRIVATE KEY----- diff --git a/bindings/rust/s2n-tls-hyper/tests/common/echo.rs b/bindings/rust/s2n-tls-hyper/tests/common/echo.rs new file mode 100644 index 00000000000..4c5607816ac --- /dev/null +++ b/bindings/rust/s2n-tls-hyper/tests/common/echo.rs @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use bytes::Bytes; +use http::{Request, Response}; +use http_body_util::{combinators::BoxBody, BodyExt}; +use hyper::service::service_fn; +use hyper_util::rt::{TokioExecutor, TokioIo}; +use s2n_tls::connection::Builder; +use s2n_tls_tokio::TlsAcceptor; +use std::{error::Error, future::Future}; +use tokio::net::TcpListener; + +async fn echo( + req: Request, +) -> Result>, hyper::Error> { + Ok(Response::new(req.into_body().boxed())) +} + +async fn serve_echo( + tcp_listener: TcpListener, + builder: B, +) -> Result<(), Box> +where + B: Builder, + ::Output: Unpin + Send + Sync + 'static, +{ + let (tcp_stream, _) = tcp_listener.accept().await?; + let acceptor = TlsAcceptor::new(builder); + let tls_stream = acceptor.accept(tcp_stream).await?; + let io = TokioIo::new(tls_stream); + + let server = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()); + if let Err(err) = server.serve_connection(io, service_fn(echo)).await { + // The hyper client doesn't gracefully terminate by waiting for the server's shutdown. + // Instead, the client sends its shutdown and then immediately closes the socket. This can + // cause a NotConnected error to be emitted when the server attempts to send its shutdown. + // + // For now, NotConnected errors are ignored. After the hyper client can be configured to + // gracefully shutdown, this exception can be removed. + if let Some(hyper_err) = err.downcast_ref::() { + if let Some(source) = hyper_err.source() { + if let Some(io_err) = source.downcast_ref::() { + if io_err.kind() == std::io::ErrorKind::NotConnected { + return Ok(()); + } + } + } + } + + return Err(err); + } + + Ok(()) +} + +pub async fn make_echo_request( + server_builder: B, + send_client_request: F, +) -> Result<(), Box> +where + B: Builder + Send + Sync + 'static, + ::Output: Unpin + Send + Sync + 'static, + F: FnOnce(u16) -> Fut, + Fut: Future>> + Send + 'static, +{ + let listener = TcpListener::bind("127.0.0.1:0").await?; + let addr = listener.local_addr()?; + + let mut tasks = tokio::task::JoinSet::new(); + tasks.spawn(serve_echo(listener, server_builder)); + tasks.spawn(send_client_request(addr.port())); + + while let Some(res) = tasks.join_next().await { + res.unwrap()?; + } + + Ok(()) +} diff --git a/bindings/rust/s2n-tls-hyper/tests/common/mod.rs b/bindings/rust/s2n-tls-hyper/tests/common/mod.rs new file mode 100644 index 00000000000..018e84a5a97 --- /dev/null +++ b/bindings/rust/s2n-tls-hyper/tests/common/mod.rs @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use s2n_tls::{callbacks::VerifyHostNameCallback, config, error::Error, security::DEFAULT_TLS13}; + +pub mod echo; + +/// NOTE: this certificate and key are used for testing purposes only! +pub static CERT_PEM: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/common/certs/cert.pem" +)); +pub static KEY_PEM: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/common/certs/key.pem" +)); + +pub fn config() -> Result { + let mut builder = config::Config::builder(); + builder.set_security_policy(&DEFAULT_TLS13)?; + builder.trust_pem(CERT_PEM)?; + builder.load_pem(CERT_PEM, KEY_PEM)?; + Ok(builder) +} + +pub struct InsecureAcceptAllCertificatesHandler {} +impl VerifyHostNameCallback for InsecureAcceptAllCertificatesHandler { + fn verify_host_name(&self, _host_name: &str) -> bool { + true + } +} diff --git a/bindings/rust/s2n-tls-hyper/tests/http.rs b/bindings/rust/s2n-tls-hyper/tests/http.rs new file mode 100644 index 00000000000..0a5469b45de --- /dev/null +++ b/bindings/rust/s2n-tls-hyper/tests/http.rs @@ -0,0 +1,140 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use crate::common::InsecureAcceptAllCertificatesHandler; +use bytes::Bytes; +use http::{Method, Request, Uri}; +use http_body_util::{BodyExt, Empty, Full}; +use hyper_util::{client::legacy::Client, rt::TokioExecutor}; +use s2n_tls::{ + callbacks::{ClientHelloCallback, ConnectionFuture}, + connection::Connection, +}; +use s2n_tls_hyper::connector::HttpsConnector; +use std::{error::Error, pin::Pin, str::FromStr}; + +pub mod common; + +const TEST_DATA: &[u8] = "hello world".as_bytes(); + +// The maximum TLS record payload is 2^14 bytes. +// Send more to ensure multiple records. +const LARGE_TEST_DATA: &[u8] = &[5; (1 << 15)]; + +#[tokio::test] +async fn test_get_request() -> Result<(), Box> { + let config = common::config()?.build()?; + common::echo::make_echo_request(config.clone(), |port| async move { + let connector = HttpsConnector::new(config.clone()); + 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); + + Ok(()) + }) + .await?; + + Ok(()) +} + +#[tokio::test] +async fn test_http_methods() -> Result<(), Box> { + let methods = [Method::GET, Method::POST, Method::PUT, Method::DELETE]; + for method in methods { + let config = common::config()?.build()?; + common::echo::make_echo_request(config.clone(), |port| async move { + let connector = HttpsConnector::new(config.clone()); + let client: Client<_, Full> = + Client::builder(TokioExecutor::new()).build(connector); + let request: Request> = Request::builder() + .method(method) + .uri(Uri::from_str( + format!("https://localhost:{}", port).as_str(), + )?) + .body(Full::from(TEST_DATA))?; + + let response = client.request(request).await?; + assert_eq!(response.status(), 200); + + let body = response.into_body().collect().await?.to_bytes(); + assert_eq!(body.to_vec().as_slice(), TEST_DATA); + + Ok(()) + }) + .await?; + } + + Ok(()) +} + +#[tokio::test] +async fn test_large_request() -> Result<(), Box> { + let config = common::config()?.build()?; + common::echo::make_echo_request(config.clone(), |port| async move { + let connector = HttpsConnector::new(config.clone()); + let client: Client<_, Full> = Client::builder(TokioExecutor::new()).build(connector); + let request: Request> = Request::builder() + .method(Method::POST) + .uri(Uri::from_str( + format!("https://localhost:{}", port).as_str(), + )?) + .body(Full::from(LARGE_TEST_DATA))?; + + let response = client.request(request).await?; + assert_eq!(response.status(), 200); + + let body = response.into_body().collect().await?.to_bytes(); + assert_eq!(body.to_vec().as_slice(), LARGE_TEST_DATA); + + Ok(()) + }) + .await?; + + Ok(()) +} + +#[tokio::test] +async fn test_sni() -> Result<(), Box> { + struct TestClientHelloHandler { + expected_server_name: &'static str, + } + impl ClientHelloCallback for TestClientHelloHandler { + fn on_client_hello( + &self, + connection: &mut Connection, + ) -> Result>>, s2n_tls::error::Error> { + let server_name = connection.server_name().unwrap(); + assert_eq!(server_name, self.expected_server_name); + Ok(None) + } + } + + for hostname in ["localhost", "127.0.0.1"] { + let mut config = common::config()?; + config.set_client_hello_callback(TestClientHelloHandler { + // Ensure that the HttpsConnector correctly sets the SNI according to the hostname in + // the URI. + expected_server_name: hostname, + })?; + config.set_verify_host_callback(InsecureAcceptAllCertificatesHandler {})?; + let config = config.build()?; + + common::echo::make_echo_request(config.clone(), |port| async move { + let connector = HttpsConnector::new(config.clone()); + let client: Client<_, Empty> = + Client::builder(TokioExecutor::new()).build(connector); + + let uri = Uri::from_str(format!("https://{}:{}", hostname, port).as_str())?; + let response = client.get(uri).await?; + assert_eq!(response.status(), 200); + + Ok(()) + }) + .await?; + } + + Ok(()) +} diff --git a/bindings/rust/s2n-tls-hyper/tests/web_client.rs b/bindings/rust/s2n-tls-hyper/tests/web_client.rs deleted file mode 100644 index 41a72970ed3..00000000000 --- a/bindings/rust/s2n-tls-hyper/tests/web_client.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use bytes::Bytes; -use http::{status, Uri}; -use http_body_util::{BodyExt, Empty}; -use hyper_util::{client::legacy::Client, rt::TokioExecutor}; -use s2n_tls::config::Config; -use s2n_tls_hyper::connector::HttpsConnector; -use std::{error::Error, str::FromStr}; - -#[tokio::test] -async fn test_get_request() -> Result<(), Box> { - let connector = HttpsConnector::new(Config::default()); - let client: Client<_, Empty> = Client::builder(TokioExecutor::new()).build(connector); - - let uri = Uri::from_str("https://www.amazon.com")?; - let response = client.get(uri).await?; - assert_eq!(response.status(), status::StatusCode::OK); - - let body = response.into_body().collect().await?.to_bytes(); - assert!(!body.is_empty()); - - Ok(()) -}