Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(s2n-tls-hyper): Add localhost http tests #4838

Merged
merged 10 commits into from
Nov 18, 2024
1 change: 1 addition & 0 deletions bindings/rust/s2n-tls-hyper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
85 changes: 85 additions & 0 deletions bindings/rust/s2n-tls-hyper/tests/common/echo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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<hyper::body::Incoming>,
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
Ok(Response::new(req.into_body().boxed()))
}

async fn serve_echo<B>(
tcp_listener: TcpListener,
builder: B,
) -> Result<(), Box<dyn Error + Send + Sync>>
where
B: Builder,
<B as 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:
// https://github.com/aws/s2n-tls/issues/4855
//
// Also, it's possible that a NotConnected error could occur during some operation other
// than a shutdown. Ideally, these NotConnected errors wouldn't be ignored. However, it's
// not currently possible to distinguish between shutdown vs non-shutdown errors:
// https://github.com/aws/s2n-tls/issues/4856
if let Some(hyper_err) = err.downcast_ref::<hyper::Error>() {
if let Some(source) = hyper_err.source() {
if let Some(io_err) = source.downcast_ref::<tokio::io::Error>() {
if io_err.kind() == tokio::io::ErrorKind::NotConnected {
return Ok(());
}
}
}
}

return Err(err);
}

Ok(())
}

pub async fn make_echo_request<B, F, Fut>(
server_builder: B,
send_client_request: F,
) -> Result<(), Box<dyn Error + Send + Sync>>
where
B: Builder + Send + Sync + 'static,
<B as Builder>::Output: Unpin + Send + Sync + 'static,
F: FnOnce(u16) -> Fut,
Fut: Future<Output = Result<(), Box<dyn Error + Send + Sync>>> + 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(())
}
27 changes: 27 additions & 0 deletions bindings/rust/s2n-tls-hyper/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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"), "/../certs/cert.pem"));
pub static KEY_PEM: &[u8] =
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../certs/key.pem"));

pub fn config() -> Result<config::Builder, Error> {
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
}
}
140 changes: 140 additions & 0 deletions bindings/rust/s2n-tls-hyper/tests/http.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Error + Send + Sync>> {
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<Bytes>> =
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<dyn Error + Send + Sync>> {
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<Bytes>> =
Client::builder(TokioExecutor::new()).build(connector);
let request: Request<Full<Bytes>> = 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<dyn Error + Send + Sync>> {
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<Bytes>> = Client::builder(TokioExecutor::new()).build(connector);
let request: Request<Full<Bytes>> = 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<dyn Error + Send + Sync>> {
struct TestClientHelloHandler {
expected_server_name: &'static str,
}
impl ClientHelloCallback for TestClientHelloHandler {
fn on_client_hello(
&self,
connection: &mut Connection,
) -> Result<Option<Pin<Box<dyn ConnectionFuture>>>, 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<Bytes>> =
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(())
}
Loading