Unverified Commit b60b30bb authored by Russell Cohen's avatar Russell Cohen Committed by GitHub
Browse files

Initial implementation of aws-hyper (#201)

* Initial implementation of aws-hyper

Although this is expected to evolve, this initial implementation of aws-hyper supports endpoint, signing & response parsing middleware and is sufficient to drive basic AWS services.

A `TestConnection` helper is also provided. This enables end-to-end testing of clients by mocking out the connection with a vector of request/response pairs.

* Update test

* Add more docs, rename default to https

* Add rt tokio feature

* Fix hyper features
parent 213b00f4
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -127,6 +127,7 @@ pub fn set_endpoint_resolver(config: &mut PropertyBag, provider: AwsEndpointReso
/// 3. Apply the endpoint to the URI in the request
/// 4. Set the `SigningRegion` and `SigningService` in the property bag to drive downstream
/// signing middleware.
#[derive(Clone)]
pub struct AwsEndpointStage;

#[derive(Debug)]
@@ -183,7 +184,7 @@ mod test {
    use smithy_http::middleware::MapRequest;
    use smithy_http::operation;

    use crate::{AwsEndpointStage, DefaultAwsEndpointResolver, set_endpoint_resolver};
    use crate::{set_endpoint_resolver, AwsEndpointStage, DefaultAwsEndpointResolver};

    #[test]
    fn default_endpoint_updates_request() {
+25 −0
Original line number Diff line number Diff line
[package]
name = "aws-hyper"
version = "0.1.0"
authors = ["Russell Cohen <rcoh@amazon.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
hyper = { version = "0.14.2", features = ["client", "http1", "http2", "tcp", "runtime"] }
tower = { version = "0.4.2", features = ["util"] }
hyper-tls = "0.5.0"
aws-auth = { path = "../aws-auth" }
aws-sig-auth = { path = "../aws-sig-auth" }
aws-endpoint = { path = "../aws-endpoint" }
http = "0.2.3"
bytes = "1"
http-body = "0.4.0"
smithy-http = { path = "../../../rust-runtime/smithy-http" }
smithy-http-tower = { path = "../../../rust-runtime/smithy-http-tower" }

[dev-dependencies]
tokio = { version = "1", features = ["full"] }
tower-test = "0.4.0"
aws-types = { path = "../aws-types" }
+111 −0
Original line number Diff line number Diff line
pub mod test_connection;

use aws_endpoint::AwsEndpointStage;
use aws_sig_auth::middleware::SigV4SigningStage;
use aws_sig_auth::signer::SigV4Signer;
use hyper::client::HttpConnector;
use hyper::Client as HyperClient;
use hyper_tls::HttpsConnector;
use smithy_http::body::SdkBody;
use smithy_http::operation::Operation;
use smithy_http::response::ParseHttpResponse;
use smithy_http_tower::dispatch::DispatchLayer;
use smithy_http_tower::map_request::MapRequestLayer;
use smithy_http_tower::parse_response::ParseResponseLayer;
use std::error::Error;
use tower::{Service, ServiceBuilder, ServiceExt};

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

pub type SdkError<E> = smithy_http::result::SdkError<E, hyper::Body>;
pub type SdkSuccess<T> = smithy_http::result::SdkSuccess<T, hyper::Body>;

/// AWS Service Client
///
/// Hyper-based AWS Service Client. Most customers will want to construct a client with
/// [`Client::https()`](Client::https). For testing & other more advanced use cases, a custom
/// connector may be used via [`Client::new(connector)`](Client::new).
///
/// The internal connector must implement the following trait bound to be used to dispatch requests:
/// ```rust,ignore
///    S: Service<http::Request<SdkBody>, Response = http::Response<hyper::Body>>
///        + Send
///        + Clone
///        + 'static,
///    S::Error: Into<BoxError> + Send + Sync + 'static,
///    S::Future: Send + 'static,
/// ```

pub struct Client<S> {
    inner: S,
}

impl<S> Client<S> {
    /// Construct a new `Client` with a custom connector
    pub fn new(connector: S) -> Self {
        Client { inner: connector }
    }
}

impl Client<hyper::Client<HttpsConnector<HttpConnector>, SdkBody>> {
    /// Construct an `https` based client
    pub fn https() -> Self {
        let https = HttpsConnector::new();
        let client = HyperClient::builder().build::<_, SdkBody>(https);
        Client { inner: client }
    }
}

impl<S> Client<S>
where
    S: Service<http::Request<SdkBody>, Response = http::Response<hyper::Body>>
        + Send
        + Clone
        + 'static,
    S::Error: Into<BoxError> + Send + Sync + 'static,
    S::Future: Send + 'static,
{
    /// Dispatch this request to the network
    ///
    /// For ergonomics, this does not include the raw response for successful responses. To
    /// access the raw response use `call_raw`.
    pub async fn call<O, T, E, Retry>(&self, input: Operation<O, Retry>) -> Result<T, SdkError<E>>
    where
        O: ParseHttpResponse<hyper::Body, Output = Result<T, E>> + Send + Clone + 'static,
    {
        self.call_raw(input).await.map(|res| res.parsed)
    }

    /// Dispatch this request to the network
    ///
    /// The returned result contains the raw HTTP response which can be useful for debugging or implementing
    /// unsupported features.
    pub async fn call_raw<O, R, E, Retry>(
        &self,
        input: Operation<O, Retry>,
    ) -> Result<SdkSuccess<R>, SdkError<E>>
    where
        O: ParseHttpResponse<hyper::Body, Output = Result<R, E>> + Send + Clone + 'static,
    {
        let signer = MapRequestLayer::for_mapper(SigV4SigningStage::new(SigV4Signer::new()));
        let endpoint_resolver = MapRequestLayer::for_mapper(AwsEndpointStage);
        let inner = self.inner.clone();
        let mut svc = ServiceBuilder::new()
            .layer(ParseResponseLayer::<O, Retry>::new())
            .layer(endpoint_resolver)
            .layer(signer)
            .layer(DispatchLayer::new())
            .service(inner);
        svc.ready_and().await?.call(input).await
    }
}

#[cfg(test)]
mod tests {
    use crate::Client;

    #[test]
    fn construct_default_client() {
        let _ = Client::https();
    }
}
+104 −0
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

use http::Request;
use smithy_http::body::SdkBody;
use std::future::Ready;
use std::ops::Deref;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use tower::BoxError;

type ConnectVec<B> = Vec<(http::Request<SdkBody>, http::Response<B>)>;

pub struct ValidateRequest {
    pub expected: http::Request<SdkBody>,
    pub actual: http::Request<SdkBody>,
}

/// TestConnection for use with a [`aws_hyper::Client`](aws_hyper::Client)
///
/// A basic test connection. It will:
/// - Response to requests with a preloaded series of responses
/// - Record requests for future examination
///
/// For more complex use cases, see [Tower Test](https://docs.rs/tower-test/0.4.0/tower_test/)
/// Usage example:
/// ```rust
/// use aws_hyper::test_connection::TestConnection;
/// use smithy_http::body::SdkBody;
/// let events = vec![(
///    http::Request::new(SdkBody::from("request body")),
///    http::Response::builder()
///        .status(200)
///        .body("response body")
///        .unwrap(),
/// )];
/// let conn = TestConnection::new(events);
/// let client = aws_hyper::Client::new(conn);
/// ```
#[derive(Clone)]
pub struct TestConnection<B> {
    data: Arc<Mutex<ConnectVec<B>>>,
    requests: Arc<Mutex<Vec<ValidateRequest>>>,
}

impl<B> TestConnection<B> {
    pub fn new(mut data: ConnectVec<B>) -> Self {
        data.reverse();
        TestConnection {
            data: Arc::new(Mutex::new(data)),
            requests: Default::default(),
        }
    }

    pub fn requests(&self) -> impl Deref<Target = Vec<ValidateRequest>> + '_ {
        self.requests.lock().unwrap()
    }
}

impl<B: Into<hyper::Body>> tower::Service<http::Request<SdkBody>> for TestConnection<B> {
    type Response = http::Response<hyper::Body>;
    type Error = BoxError;
    type Future = Ready<Result<Self::Response, Self::Error>>;

    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }

    fn call(&mut self, actual: Request<SdkBody>) -> Self::Future {
        // todo: validate request
        if let Some((expected, resp)) = self.data.lock().unwrap().pop() {
            self.requests
                .lock()
                .unwrap()
                .push(ValidateRequest { actual, expected });
            std::future::ready(Ok(resp.map(|body| body.into())))
        } else {
            std::future::ready(Err("No more data".into()))
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::test_connection::TestConnection;
    use smithy_http::body::SdkBody;
    use tower::BoxError;

    /// Validate that the `TestConnection` meets the required trait bounds to be used with a aws-hyper service
    #[test]
    fn meets_trait_bounds() {
        fn check() -> impl tower::Service<
            http::Request<SdkBody>,
            Response = http::Response<hyper::Body>,
            Error = BoxError,
            Future = impl Send,
        > + Clone {
            TestConnection::<String>::new(vec![])
        }
        let _ = check();
    }
}
+86 −0
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

use aws_auth::Credentials;
use aws_endpoint::{set_endpoint_resolver, DefaultAwsEndpointResolver};
use aws_hyper::test_connection::{TestConnection, ValidateRequest};
use aws_hyper::Client;
use aws_sig_auth::signer::OperationSigningConfig;
use aws_types::region::Region;
use bytes::Bytes;
use http::header::AUTHORIZATION;
use http::{Response, Uri};
use smithy_http::body::SdkBody;
use smithy_http::operation;
use smithy_http::operation::Operation;
use smithy_http::response::ParseHttpResponse;
use std::convert::Infallible;
use std::sync::Arc;
use std::time::{Duration, UNIX_EPOCH};

#[derive(Clone)]
struct TestOperationParser;

impl<B> ParseHttpResponse<B> for TestOperationParser
where
    B: http_body::Body,
{
    type Output = Result<String, String>;

    fn parse_unloaded(&self, _response: &mut Response<B>) -> Option<Self::Output> {
        Some(Ok("Hello!".to_string()))
    }

    fn parse_loaded(&self, _response: &Response<Bytes>) -> Self::Output {
        Ok("Hello!".to_string())
    }
}

fn test_operation() -> Operation<TestOperationParser, ()> {
    let req = operation::Request::new(http::Request::new(SdkBody::from("request body")))
        .augment(|req, mut conf| {
            set_endpoint_resolver(
                &mut conf,
                Arc::new(DefaultAwsEndpointResolver::for_service("test-service")),
            );
            aws_auth::set_provider(
                &mut conf,
                Arc::new(Credentials::from_keys("access_key", "secret_key", None)),
            );
            conf.insert(Region::new("test-region"));
            conf.insert(OperationSigningConfig::default_config());
            conf.insert(UNIX_EPOCH + Duration::from_secs(1613414417));
            Result::<_, Infallible>::Ok(req)
        })
        .unwrap();
    Operation::new(req, TestOperationParser)
}

#[tokio::test]
async fn e2e_test() {
    let expected_req = http::Request::builder()
        .header(AUTHORIZATION, "AWS4-HMAC-SHA256 Credential=access_key/20210215/test-region/test-service/aws4_request, SignedHeaders=, Signature=e8a49c07c540558c4b53a5dcc61cbfb27003381fd8437fca0b3dddcdc703ec44")
        .header("x-amz-date", "20210215T184017Z")
        .uri(Uri::from_static("https://test-region.test-service.amazonaws.com/"))
        .body(SdkBody::from("request body")).unwrap();
    let events = vec![(
        expected_req,
        http::Response::builder()
            .status(200)
            .body("response body")
            .unwrap(),
    )];
    let conn = TestConnection::new(events);
    let client = Client::new(conn.clone());
    let resp = client.call(test_operation()).await;
    let resp = resp.expect("successful operation");
    assert_eq!(resp, "Hello!");

    assert_eq!(conn.requests().len(), 1);
    let ValidateRequest { expected, actual } = &conn.requests()[0];
    assert_eq!(actual.headers(), expected.headers());
    assert_eq!(actual.body().bytes(), expected.body().bytes());
    assert_eq!(actual.uri(), expected.uri());
}
Loading