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(bindings): Add hyper compatibility crate #4617

Merged
merged 13 commits into from
Aug 1, 2024
1 change: 1 addition & 0 deletions bindings/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"s2n-tls",
"s2n-tls-sys",
"s2n-tls-tokio",
"s2n-tls-hyper",
]
# generate can't be included in the workspace because of a bootstrapping problem
# s2n-tls-sys/Cargo.toml (part of the workspace) is generated by
Expand Down
25 changes: 25 additions & 0 deletions bindings/rust/s2n-tls-hyper/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "s2n-tls-hyper"
description = "A compatbility crate allowing s2n-tls to be used with the hyper HTTP library"
version = "0.0.1"
authors = ["AWS s2n"]
edition = "2021"
rust-version = "1.63.0"
repository = "https://github.com/aws/s2n-tls"
license = "Apache-2.0"

[features]
default = []

[dependencies]
s2n-tls = { version = "=0.2.7", path = "../s2n-tls" }
s2n-tls-tokio = { version = "=0.2.7", path = "../s2n-tls-tokio" }
goatgoose marked this conversation as resolved.
Show resolved Hide resolved
hyper = { version = "1" }
hyper-util = { version = "0.1", features = ["client-legacy", "tokio", "http1"] }
tower-service = { version = "0.3" }
http = { version= "1" }

[dev-dependencies]
tokio = { version = "1", features = ["macros", "test-util"] }
http-body-util = "0.1"
bytes = "1"
3 changes: 3 additions & 0 deletions bindings/rust/s2n-tls-hyper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
`s2n-tls-hyper` provides compatibility structs for [hyper](https://hyper.rs/), allowing s2n-tls to be used as the underlying TLS implementation with hyper clients.

This crate is currently under development and is extremely unstable.
goatgoose marked this conversation as resolved.
Show resolved Hide resolved
180 changes: 180 additions & 0 deletions bindings/rust/s2n-tls-hyper/src/connector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use crate::stream::MaybeHttpsStream;
use http::uri::Uri;
use hyper::rt::{Read, Write};
use hyper_util::{
client::legacy::connect::{Connection, HttpConnector},
rt::TokioIo,
};
use s2n_tls::{config::Config, connection};
use s2n_tls_tokio::TlsConnector;
use std::{
fmt,
fmt::{Debug, Formatter},
future::Future,
pin::Pin,
task::{Context, Poll},
};
use tower_service::Service;

type BoxError = Box<dyn std::error::Error + Send + Sync>;

#[derive(Clone)]
pub struct HttpsConnector<T, B = Config>
where
B: connection::Builder,
<B as connection::Builder>::Output: Unpin,
{
http: T,
conn_builder: B,
}

impl<B> HttpsConnector<HttpConnector, B>
where
B: connection::Builder,
<B as connection::Builder>::Output: Unpin,
{
/// Creates a new `Builder` used to create an `HttpsConnector`.
///
/// This builder is created using the default hyper `HttpConnector`. To use a custom HTTP
/// connector, use `HttpsConnector::builder_with_http()`.
pub fn builder(conn_builder: B) -> Builder<HttpConnector, B> {
let mut http = HttpConnector::new();
http.enforce_http(false);

Builder::new(Self { http, conn_builder })
}
}

impl<T, B> HttpsConnector<T, B>
where
B: connection::Builder,
<B as connection::Builder>::Output: Unpin,
{
/// Creates a new `Builder` used to create an `HttpsConnector`.
pub fn builder_with_http(http: T, conn_builder: B) -> Builder<T, B> {
Builder::new(Self { http, conn_builder })
}
}

impl<T, B> Service<Uri> for HttpsConnector<T, B>
where
T: Service<Uri>,
T::Response: Read + Write + Connection + Unpin + Send + 'static,
T::Future: Send + 'static,
T::Error: Into<BoxError>,
B: connection::Builder + Send + Sync + 'static,
<B as connection::Builder>::Output: Unpin + Send,
{
type Response = MaybeHttpsStream<T::Response, B>;
type Error = BoxError;
type Future =
Pin<Box<dyn Future<Output = Result<MaybeHttpsStream<T::Response, B>, BoxError>> + Send>>;

fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match self.http.poll_ready(cx) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
Poll::Ready(Err(e)) => Poll::Ready(Err(e.into())),
Poll::Pending => Poll::Pending,
}
}

fn call(&mut self, req: Uri) -> Self::Future {
if req.scheme() == Some(&http::uri::Scheme::HTTP) {
return Box::pin(async move { Err(UnsupportedScheme.into()) });
}

let builder = self.conn_builder.clone();
let host = req.host().unwrap_or("").to_owned();
let call = self.http.call(req);
Box::pin(async move {
let tcp = call.await.map_err(Into::into)?;
let tcp = TokioIo::new(tcp);

let connector = TlsConnector::new(builder);
let tls = connector.connect(&host, tcp).await?;

Ok(MaybeHttpsStream::Https(TokioIo::new(tls)))
})
}
}

pub struct Builder<T, B>
where
B: connection::Builder,
<B as connection::Builder>::Output: Unpin,
{
connector: HttpsConnector<T, B>,
}

impl<T, B> Builder<T, B>
where
goatgoose marked this conversation as resolved.
Show resolved Hide resolved
B: connection::Builder,
<B as connection::Builder>::Output: Unpin,
{
pub fn new(connector: HttpsConnector<T, B>) -> Self {
Self { connector }
}

pub fn build(self) -> HttpsConnector<T, B> {
self.connector
}
}

#[derive(Debug)]
struct UnsupportedScheme;
goatgoose marked this conversation as resolved.
Show resolved Hide resolved

impl fmt::Display for UnsupportedScheme {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("The provided URI scheme is not supported")
}
}

impl std::error::Error for UnsupportedScheme {}

#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use http::status;
use http_body_util::{BodyExt, Empty};
use hyper_util::{client::legacy::Client, rt::TokioExecutor};
use std::{error::Error, str::FromStr};

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

let uri = Uri::from_str("https://www.amazon.com")?;
goatgoose marked this conversation as resolved.
Show resolved Hide resolved
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(())
}

#[tokio::test]
async fn test_unsecure_http() -> Result<(), BoxError> {
goatgoose marked this conversation as resolved.
Show resolved Hide resolved
let connector = HttpsConnector::builder(Config::default()).build();
let client: Client<_, Empty<Bytes>> =
Client::builder(TokioExecutor::new()).build(connector);

let uri = Uri::from_str("http://www.amazon.com")?;
let error = client.get(uri).await.unwrap_err();

// Ensure that an UnsupportedScheme error is returned when HTTP over TCP is attempted.
let _ = error
.source()
.unwrap()
.downcast_ref::<UnsupportedScheme>()
.unwrap();

Ok(())
}
}
5 changes: 5 additions & 0 deletions bindings/rust/s2n-tls-hyper/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

goatgoose marked this conversation as resolved.
Show resolved Hide resolved
pub mod connector;
mod stream;
83 changes: 83 additions & 0 deletions bindings/rust/s2n-tls-hyper/src/stream.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use hyper::rt::{Read, ReadBufCursor, Write};
use hyper_util::{
client::legacy::connect::{Connected, Connection},
rt::TokioIo,
};
use s2n_tls::connection::Builder;
use s2n_tls_tokio::TlsStream;
use std::{
io::Error,
pin::Pin,
task::{Context, Poll},
};

pub enum MaybeHttpsStream<T, B>
where
T: Read + Write + Connection + Unpin,
B: Builder,
<B as Builder>::Output: Unpin,
{
Https(TokioIo<TlsStream<TokioIo<T>, B::Output>>),
}

impl<T, B> Connection for MaybeHttpsStream<T, B>
goatgoose marked this conversation as resolved.
Show resolved Hide resolved
where
goatgoose marked this conversation as resolved.
Show resolved Hide resolved
T: Read + Write + Connection + Unpin,
B: Builder,
<B as Builder>::Output: Unpin,
{
fn connected(&self) -> Connected {
match self {
MaybeHttpsStream::Https(stream) => stream.inner().get_ref().connected(),
}
}
}

impl<T, B> Read for MaybeHttpsStream<T, B>
where
T: Read + Write + Connection + Unpin,
B: Builder,
<B as Builder>::Output: Unpin,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: ReadBufCursor<'_>,
) -> Poll<Result<(), Error>> {
match Pin::get_mut(self) {
Self::Https(stream) => Pin::new(stream).poll_read(cx, buf),
}
}
}

impl<T, B> Write for MaybeHttpsStream<T, B>
where
T: Read + Write + Connection + Unpin,
B: Builder,
<B as Builder>::Output: Unpin,
{
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, Error>> {
match Pin::get_mut(self) {
Self::Https(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),
}
}

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),
}
}
}
Loading