Skip to content

Commit

Permalink
feat(bindings): Add hyper compatibility crate
Browse files Browse the repository at this point in the history
Co-authored-by: James Mayclin <maycj@amazon.com>
  • Loading branch information
goatgoose and jmayclin committed Jun 19, 2024
1 parent c8a0444 commit 977ca2b
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 0 deletions.
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" }
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.
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
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;

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")?;
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> {
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

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

0 comments on commit 977ca2b

Please sign in to comment.