-
Notifications
You must be signed in to change notification settings - Fork 707
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test(s2n-tls-hyper): Add localhost http tests (#4838)
- Loading branch information
Showing
4 changed files
with
253 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} |