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

feat(s2n-tls-hyper): Allow plain HTTP connections #4978

Merged
merged 4 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions bindings/rust/s2n-tls-hyper/src/connector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use tower_service::Service;
pub struct HttpsConnector<Http, ConnBuilder = Config> {
http: Http,
conn_builder: ConnBuilder,
plaintext_http: bool,
}

impl<ConnBuilder> HttpsConnector<HttpConnector, ConnBuilder>
Expand Down Expand Up @@ -101,7 +102,11 @@ where
/// configured on `conn_builder` with APIs like
/// `s2n_tls::config::Builder::set_application_protocol_preference()` will be ignored.
pub fn builder_with_http(http: Http, conn_builder: ConnBuilder) -> Builder<Http, ConnBuilder> {
Builder { http, conn_builder }
Builder {
http,
conn_builder,
plaintext_http: false,
}
}
}

Expand All @@ -110,14 +115,23 @@ where
pub struct Builder<Http, ConnBuilder> {
http: Http,
conn_builder: ConnBuilder,
plaintext_http: bool,
}

impl<Http, ConnBuilder> Builder<Http, ConnBuilder> {
/// If enabled, allows communication with plaintext HTTP endpoints in addition to secure HTTPS
/// endpoints (default: false).
pub fn with_plaintext_http(&mut self, enabled: bool) -> &mut Self {
self.plaintext_http = enabled;
self
}

/// Builds a new `HttpsConnector`.
pub fn build(self) -> HttpsConnector<Http, ConnBuilder> {
HttpsConnector {
http: self.http,
conn_builder: self.conn_builder,
plaintext_http: self.plaintext_http,
}
}
}
Expand Down Expand Up @@ -155,10 +169,18 @@ where
}

fn call(&mut self, req: Uri) -> Self::Future {
// Currently, the only supported stream type is TLS. If the application attempts to
// negotiate HTTP over plain TCP, return an error.
if req.scheme() == Some(&http::uri::Scheme::HTTP) {
return Box::pin(async move { Err(Error::InvalidScheme) });
match req.scheme() {
Some(scheme) if scheme == &http::uri::Scheme::HTTPS => (),
Some(scheme) if scheme == &http::uri::Scheme::HTTP && self.plaintext_http => {
let call = self.http.call(req);
return Box::pin(async move {
let tcp = call.await.map_err(|e| Error::HttpError(e.into()))?;
Ok(MaybeHttpsStream::Http(tcp))
});
}
_ => {
return Box::pin(async move { Err(Error::InvalidScheme) });
}
}

// Attempt to negotiate HTTP/2 by including it in the ALPN extension. Other supported HTTP
Expand Down Expand Up @@ -235,15 +257,16 @@ mod tests {
}

#[tokio::test]
async fn test_unsecure_http() -> Result<(), Box<dyn StdError>> {
async fn test_invalid_scheme() -> Result<(), Box<dyn StdError>> {
let connector = HttpsConnector::new(Config::default());
let client: Client<_, Empty<Bytes>> =
Client::builder(TokioExecutor::new()).build(connector);

let uri = Uri::from_str("http://www.amazon.com")?;
// Attempt to make a request with an arbitrary invalid scheme.
let uri = Uri::from_str("notascheme://www.amazon.com")?;
let error = client.get(uri).await.unwrap_err();

// Ensure that an InvalidScheme error is returned when HTTP over TCP is attempted.
// Ensure that an InvalidScheme error is returned.
let error = error.source().unwrap().downcast_ref::<Error>().unwrap();
assert!(matches!(error, Error::InvalidScheme));

Expand All @@ -252,4 +275,13 @@ mod tests {

Ok(())
}

#[tokio::test]
async fn default_builder() -> Result<(), Box<dyn StdError>> {
// Ensure that plaintext HTTP is disabled by default.
let connector = HttpsConnector::builder(Config::default()).build();
assert!(!connector.plaintext_http);

Ok(())
}
}
19 changes: 11 additions & 8 deletions bindings/rust/s2n-tls-hyper/src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@ use std::{
};

/// `MaybeHttpsStream` is a wrapper over a hyper TCP stream, `Transport`, allowing for TLS to be
/// negotiated over the TCP stream.
///
/// While not currently implemented, the `MaybeHttpsStream` enum will provide an `Http` type
/// corresponding to the plain TCP stream, allowing for HTTP to be negotiated in addition to HTTPS
/// when the HTTP scheme is used.
/// negotiated over the TCP stream via the `Https` type. The `Http` type bypasses TLS to optionally
/// allow for communication with HTTP endpoints over plain TCP.
///
/// This struct is used to implement `tower_service::Service` for `HttpsConnector`, and shouldn't
/// need to be used directly.
Expand All @@ -38,6 +35,7 @@ where
// traits to tokio's. This allows the `Read` and `Write` implementations for `MaybeHttpsStream`
// to simply call the `TokioIo` `poll` functions.
Https(TokioIo<TlsStream<TokioIo<Transport>, Builder::Output>>),
Http(Transport),
}

impl<Transport, Builder> HyperConnection for MaybeHttpsStream<Transport, Builder>
Expand All @@ -48,7 +46,7 @@ where
{
fn connected(&self) -> Connected {
match self {
MaybeHttpsStream::Https(stream) => {
Self::Https(stream) => {
let connected = stream.inner().get_ref().connected();
let conn = stream.inner().as_ref();
match conn.application_protocol() {
Expand All @@ -57,6 +55,7 @@ where
_ => connected,
}
}
Self::Http(stream) => stream.connected(),
}
}
}
Expand All @@ -74,6 +73,7 @@ where
) -> Poll<Result<(), Error>> {
match Pin::get_mut(self) {
Self::Https(stream) => Pin::new(stream).poll_read(cx, buf),
Self::Http(stream) => Pin::new(stream).poll_read(cx, buf),
}
}
}
Expand All @@ -91,18 +91,21 @@ where
) -> Poll<Result<usize, Error>> {
match Pin::get_mut(self) {
Self::Https(stream) => Pin::new(stream).poll_write(cx, buf),
Self::Http(stream) => Pin::new(stream).poll_write(cx, buf),
}
}

fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
match Pin::get_mut(self) {
MaybeHttpsStream::Https(stream) => Pin::new(stream).poll_flush(cx),
Self::Https(stream) => Pin::new(stream).poll_flush(cx),
Self::Http(stream) => Pin::new(stream).poll_flush(cx),
}
}

fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
match Pin::get_mut(self) {
MaybeHttpsStream::Https(stream) => Pin::new(stream).poll_shutdown(cx),
Self::Https(stream) => Pin::new(stream).poll_shutdown(cx),
Self::Http(stream) => Pin::new(stream).poll_shutdown(cx),
}
}
}
2 changes: 1 addition & 1 deletion bindings/rust/s2n-tls-hyper/tests/common/echo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use s2n_tls_tokio::TlsAcceptor;
use std::{error::Error, future::Future};
use tokio::net::TcpListener;

async fn echo(
pub async fn echo(
req: Request<hyper::body::Incoming>,
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
Ok(Response::new(req.into_body().boxed()))
Expand Down
75 changes: 71 additions & 4 deletions bindings/rust/s2n-tls-hyper/tests/http.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use crate::common::InsecureAcceptAllCertificatesHandler;
use crate::common::{echo::echo, InsecureAcceptAllCertificatesHandler};
use bytes::Bytes;
use common::echo::serve_echo;
use http::{Method, Request, Uri, Version};
use http_body_util::{BodyExt, Empty, Full};
use hyper_util::{client::legacy::Client, rt::TokioExecutor};
use hyper::service::service_fn;
use hyper_util::{
client::legacy::Client,
rt::{TokioExecutor, TokioIo},
};
use s2n_tls::{
callbacks::{ClientHelloCallback, ConnectionFuture},
config,
connection::Connection,
security::DEFAULT_TLS13,
};
use s2n_tls_hyper::connector::HttpsConnector;
use s2n_tls_hyper::{connector::HttpsConnector, error};
use std::{error::Error, pin::Pin, str::FromStr};
use tokio::{net::TcpListener, task::JoinHandle};
use tokio::{
net::TcpListener,
task::{JoinHandle, JoinSet},
};

pub mod common;

Expand Down Expand Up @@ -330,3 +337,63 @@ async fn config_alpn_ignored() -> Result<(), Box<dyn Error + Send + Sync>> {

Ok(())
}

#[tokio::test]
async fn plaintext_http() -> Result<(), Box<dyn Error + Send + Sync>> {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;

let mut tasks: JoinSet<Result<(), Box<dyn Error + Send + Sync>>> = JoinSet::new();
tasks.spawn(async move {
// Listen for HTTP requests on a plain TCP stream.
let (tcp_stream, _) = listener.accept().await.unwrap();
let server = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new());
server
.serve_connection(TokioIo::new(tcp_stream), service_fn(echo))
.await?;

Ok(())
});

tasks.spawn(async move {
for enable_plaintext_http in [false, true] {
let connector = {
let config = common::config()?.build()?;
let mut builder = HttpsConnector::builder(config);
builder.with_plaintext_http(enable_plaintext_http);
builder.build()
};

let client: Client<_, Empty<Bytes>> =
Client::builder(TokioExecutor::new()).build(connector);
let uri = Uri::from_str(format!("http://127.0.0.1:{}", addr.port()).as_str())?;
let response = client.get(uri).await;

if enable_plaintext_http {
// If plaintext HTTP is enabled, the request should succeed.
let response = response.unwrap();
assert_eq!(response.status(), 200);
} else {
// If plaintext HTTP is disabled, the request should error.
let error = response.unwrap_err();

// Ensure an InvalidScheme error is produced.
let error = error
.source()
.unwrap()
.downcast_ref::<error::Error>()
.unwrap();
assert!(matches!(error, error::Error::InvalidScheme));
assert!(!error.to_string().is_empty());
}
}

Ok(())
});

while let Some(res) = tasks.join_next().await {
res.unwrap()?;
}

Ok(())
}
Loading