diff --git a/.changelog/1762114944.md b/.changelog/1762114944.md new file mode 100644 index 00000000000..a69ccd4997f --- /dev/null +++ b/.changelog/1762114944.md @@ -0,0 +1,14 @@ +--- +applies_to: +- server +authors: +- drganjoo +references: +- smithy-rs#3362 +breaking: false +new_feature: true +bug_fix: false +--- +Add http-1x examples demonstrating server SDK usage with both http@0.2.x and http@1.x + + Adds legacy examples for http@0 and updated examples for http@1 with hyper 1.x compatibility. diff --git a/examples/legacy/.gitignore b/examples/legacy/.gitignore new file mode 100644 index 00000000000..aaf1fa215c8 --- /dev/null +++ b/examples/legacy/.gitignore @@ -0,0 +1,3 @@ +pokemon-service-client/ +pokemon-service-server-sdk/ +Cargo.lock diff --git a/examples/legacy/Cargo.toml b/examples/legacy/Cargo.toml new file mode 100644 index 00000000000..a374adf6f0e --- /dev/null +++ b/examples/legacy/Cargo.toml @@ -0,0 +1,15 @@ +# Without this configuration, the workspace will be read from `rust-runtime`, causing the build to fail. +[workspace] +resolver = "2" +members = [ + "pokemon-service-common", + "pokemon-service", + "pokemon-service-tls", + "pokemon-service-lambda", + "pokemon-service-server-sdk", + "pokemon-service-client", + "pokemon-service-client-usage", +] + +[profile.release] +lto = true diff --git a/examples/legacy/Makefile b/examples/legacy/Makefile new file mode 100644 index 00000000000..adaf8d22edd --- /dev/null +++ b/examples/legacy/Makefile @@ -0,0 +1,44 @@ +SRC_DIR := $(shell git rev-parse --show-toplevel) +CUR_DIR := $(shell pwd) +GRADLE := $(SRC_DIR)/gradlew +SERVER_SDK_DST := $(CUR_DIR)/pokemon-service-server-sdk +CLIENT_SDK_DST := $(CUR_DIR)/pokemon-service-client +SERVER_SDK_SRC := $(SRC_DIR)/codegen-server-test/build/smithyprojections/codegen-server-test/pokemon-service-server-sdk-http0x/rust-server-codegen +CLIENT_SDK_SRC := $(SRC_DIR)/codegen-client-test/build/smithyprojections/codegen-client-test/pokemon-service-client-http0x/rust-client-codegen + +all: codegen + +codegen: + $(GRADLE) --project-dir $(SRC_DIR) -P modules='pokemon-service-server-sdk-http0x,pokemon-service-client-http0x' :codegen-client-test:assemble :codegen-server-test:assemble + mkdir -p $(SERVER_SDK_DST) $(CLIENT_SDK_DST) + cp -av $(SERVER_SDK_SRC)/* $(SERVER_SDK_DST)/ + cp -av $(CLIENT_SDK_SRC)/* $(CLIENT_SDK_DST)/ + +build: codegen + cargo build + +run: codegen + cargo run + +clippy: codegen + cargo clippy + +test: codegen + cargo test + +doc-open: codegen + cargo doc --no-deps --open + +clean: + cargo clean || echo "Unable to run cargo clean" + +lambda_watch: + cargo lambda watch + +lambda_invoke: + cargo lambda invoke pokemon-service-lambda --data-file pokemon-service/tests/fixtures/example-apigw-request.json + +distclean: clean + rm -rf $(SERVER_SDK_DST) $(CLIENT_SDK_DST) Cargo.lock + +.PHONY: all diff --git a/examples/legacy/README.md b/examples/legacy/README.md new file mode 100644 index 00000000000..2e7140b8fb2 --- /dev/null +++ b/examples/legacy/README.md @@ -0,0 +1,179 @@ +# Legacy HTTP 0.x Examples + +This directory contains examples for Smithy-rs using HTTP 0.x (hyper 0.14, http 0.2). These examples use the legacy HTTP stack with `aws-smithy-legacy-http` and `aws-smithy-legacy-http-server`. + +For HTTP 1.x examples (hyper 1.x, http 1.x), see the parent [examples](../) directory. + +## Building + +### 1. Generate the SDKs + +From this directory, run: + +```bash +make codegen +``` + +This will generate: +- `pokemon-service-server-sdk-http0x` - Server SDK using HTTP 0.x +- `pokemon-service-client-http0x` - Client SDK using HTTP 0.x + +The generated SDKs are copied to: +- `pokemon-service-server-sdk/` +- `pokemon-service-client/` + +### 2. Build all examples + +```bash +cargo build +``` + +Or to check without building artifacts: + +```bash +cargo check +``` + +## Running the Examples + +### Start the Pokemon Service + +In one terminal, start the server: + +```bash +cargo run --bin pokemon-service +``` + +The server will start on `http://localhost:13734` + +### Run Client Examples + +In another terminal, from the `pokemon-service-client-usage/` directory: + +```bash +cd pokemon-service-client-usage +cargo run --example simple-client +``` + +#### Available Client Examples + +| Example | Description | +|---------|-------------| +| `simple-client` | Basic client usage - creates a client and calls an operation | +| `endpoint-resolver` | Custom endpoint resolver configuration | +| `handling-errors` | Sending input parameters and handling errors | +| `custom-header` | Adding custom headers to requests | +| `custom-header-using-interceptor` | Accessing operation name in an interceptor | +| `response-header-interceptor` | Getting operation name and accessing response before deserialization | +| `use-config-bag` | Using the property bag to pass data across interceptors | +| `retry-customize` | Customizing retry settings | +| `timeout-config` | Configuring timeouts | +| `mock-request` | Using custom HttpConnector for mock responses | +| `trace-serialize` | Tracing request/response during serialization | +| `client-connector` | Changing TLS configuration | + +To list all available examples: + +```bash +cd pokemon-service-client-usage +cargo run --example +``` + +### Other Services + +#### Pokemon Service with TLS + +```bash +cargo run --bin pokemon-service-tls +``` + +#### Pokemon Service on AWS Lambda + +```bash +cargo run --bin pokemon-service-lambda +``` + +## Project Structure + +``` +legacy/ +├── pokemon-service/ # Main HTTP service implementation +├── pokemon-service-tls/ # TLS-enabled service +├── pokemon-service-lambda/ # AWS Lambda service +├── pokemon-service-common/ # Shared service logic +├── pokemon-service-client-usage/ # Client usage examples +├── pokemon-service-server-sdk/ # Generated server SDK (HTTP 0.x) +└── pokemon-service-client/ # Generated client SDK (HTTP 0.x) +``` + +## Key Dependencies (HTTP 0.x) + +- `hyper = "0.14"` +- `http = "0.2"` +- `aws-smithy-legacy-http` +- `aws-smithy-legacy-http-server` + +## Regenerating SDKs + +If you need to regenerate the SDKs from scratch: + +```bash +rm -rf pokemon-service-server-sdk pokemon-service-client +make codegen +``` + +## Testing + +Run all tests: + +```bash +cargo test +``` + +Run tests for a specific package: + +```bash +cargo test -p pokemon-service +``` + +## Troubleshooting + +### Port Already in Use + +If port 13734 is already in use, you can specify a different port: + +```bash +cargo run --bin pokemon-service -- --port 8080 +``` + +Then update the client examples to use the new port by setting the environment variable: + +```bash +POKEMON_SERVICE_URL=http://localhost:8080 cargo run --example simple-client +``` + +### SDK Generation Issues + +If the generated SDKs have issues, try cleaning and regenerating: + +```bash +# Clean generated SDKs +rm -rf pokemon-service-server-sdk pokemon-service-client + +# Clean gradle cache +cd ../.. +./gradlew clean + +# Regenerate +cd examples/legacy +make codegen +``` + +## Migration to HTTP 1.x + +For new projects, we recommend using the HTTP 1.x examples in the parent [examples](../) directory. These legacy examples are maintained for backward compatibility and for projects that need to use the HTTP 0.x stack. + +The main differences: +- HTTP 1.x uses `hyper 1.x`, `http 1.x` +- HTTP 1.x uses `aws-smithy-http`, `aws-smithy-http-server` (not legacy versions) +- HTTP 1.x has better performance and modern async runtime support diff --git a/examples/legacy/pokemon-service-client-usage/Cargo.toml b/examples/legacy/pokemon-service-client-usage/Cargo.toml new file mode 100644 index 00000000000..fe942c0e9fd --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "pokemon-service-client-usage" +version = "0.1.0" +edition = "2021" +publish = false + +[features] + + +[dependencies] +# The generated client utilizes types defined in other crates, such as `aws_smithy_types` +# and `aws_smithy_http`. However, most of these types are re-exported by the generated client, +# eliminating the need to directly depend on the crates that provide them. In rare instances, +# you may still need to include one of these crates as a dependency. Examples that require this +# are specifically noted in comments above the corresponding dependency in this file. +pokemon-service-client = { path = "../pokemon-service-client/", package = "pokemon-service-client-http0x", features = ["behavior-version-latest"] } + +# Required for getting the operation name from the `Metadata`. +aws-smithy-legacy-http = { path = "../../../rust-runtime/aws-smithy-legacy-http/" } + +# Required for `Storable` and `StoreReplace` in `response-header-interceptor` example. +aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types/" } + +# Required for `HyperClientBuilder` in `client-connector` example. +aws-smithy-runtime = { path = "../../../rust-runtime/aws-smithy-runtime/", features=["test-util"] } + +# Required for `Metadata` in `custom-header-using-interceptor` example. +aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api/", features=["client"] } + + +hyper = { version = "0.14.25", features = ["client", "full"] } +tokio = {version = "1.26.0", features=["full"]} +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +rustls = "0.21.8" +hyper-rustls = "0.24.1" +http = "0.2.9" +uuid = {version="1.4.1", features = ["v4"]} +thiserror = "1.0.49" diff --git a/examples/legacy/pokemon-service-client-usage/README.md b/examples/legacy/pokemon-service-client-usage/README.md new file mode 100644 index 00000000000..08021c37e82 --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/README.md @@ -0,0 +1,49 @@ +# smithy-rs Client Examples + +This package contains some examples on how to use the Smithy Client to communicate +with a Smithy-based service. + +## Pre-requisites + +1. Build the `pokemon-service-client` and `pokemon-service` by invoking `make` in the + [examples](https://github.com/smithy-lang/smithy-rs/tree/main/examples) folder. + +```console +make +``` + +2. Run the Pokemon service locally by issuing the following command from the + [examples](https://github.com/smithy-lang/smithy-rs/tree/main/examples) folder. This + will launch the Smithy-Rs based service on TCP port 13734. + +```console +cargo run --bin pokemon-service +``` + +## Running the examples + +You can view a list of examples by running `cargo run --example` from the +[pokemon-service-client-usage](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage) +folder. To run an example, pass its name to the `cargo run --example` command, e.g.: + +```console +cargo run --example simple-client +``` + +## List of examples + +| Rust Example | Description | +|--------------------------------|-------------------------------------------------------------------------| +| simple-client | Creates a Smithy Client and calls an operation on it. | +| endpoint-resolver | How to set a custom endpoint resolver. | +| handling-errors | How to send an input parameter to an operation, and to handle errors. | +| custom-header | How to add headers to a request. | +| custom-header-using-interceptor| How to access operation name being called in an interceptor. | +| response-header-interceptor | How to get operation name and access response before it is deserialized.| +| use-config-bag | How to use the property bag to pass data across interceptors. | +| retries-customize | Customize retry settings. | +| retries-disable | How to disable retries. | +| timeout-config | How to configure timeouts. | +| mock-request | Use a custom HttpConnector / Client to generate mock responses. | +| trace-serialize | Trace request and response as they are serialized / deserialized. | +| client-connector | Shows how to change TLS related configuration. | diff --git a/examples/legacy/pokemon-service-client-usage/examples/client-connector.rs b/examples/legacy/pokemon-service-client-usage/examples/client-connector.rs new file mode 100644 index 00000000000..b1a7ecef846 --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/examples/client-connector.rs @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how to set connector settings. For example, how to set +/// trusted root certificates to use for HTTPs communication. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example client-connector`. +/// +use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder; +use hyper_rustls::ConfigBuilderExt; +use pokemon_service_client::Client as PokemonClient; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon +/// service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + let tls_config = rustls::ClientConfig::builder() + .with_safe_defaults() + // `with_native_roots()`: Load platform trusted root certificates. + // `with_webpki_roots()`: Load Mozilla’s set of trusted roots. + .with_native_roots() + // To use client side certificates, you can use + // `.with_client_auth_cert(client_cert, client_key)` instead of `.with_no_client_auth()` + .with_no_client_auth(); + + let tls_connector = hyper_rustls::HttpsConnectorBuilder::new() + .with_tls_config(tls_config) + // This can be changed to `.https_only()` to ensure that the client always uses HTTPs + .https_or_http() + .enable_http1() + .enable_http2() + .build(); + + // Create a hyper-based HTTP client that uses this TLS connector. + let http_client = HyperClientBuilder::new().build(tls_connector); + + // Pass the smithy connector to the Client::ConfigBuilder + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .http_client(http_client) + .build(); + + // Instantiate a client by applying the configuration. + pokemon_service_client::Client::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + tracing::info!(?response, "Response from service") +} diff --git a/examples/legacy/pokemon-service-client-usage/examples/custom-header-using-interceptor.rs b/examples/legacy/pokemon-service-client-usage/examples/custom-header-using-interceptor.rs new file mode 100644 index 00000000000..00878beb5eb --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/examples/custom-header-using-interceptor.rs @@ -0,0 +1,157 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// In this example, a custom header `x-amzn-client-ttl-seconds` is set for all outgoing requests. +/// It serves as a demonstration of how an operation name can be retrieved and utilized within +/// the interceptor. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example custom-header-using-interceptor`. +/// +use std::{collections::HashMap, time::Duration}; + +use aws_smithy_runtime_api::client::orchestrator::Metadata; +use pokemon_service_client::config::{ConfigBag, Intercept}; +use pokemon_service_client::Client as PokemonClient; +use pokemon_service_client::{ + config::{interceptors::BeforeTransmitInterceptorContextMut, RuntimeComponents}, + error::BoxError, +}; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +// The `TtlHeaderInterceptor` keeps a map of operation specific value to send +// in the header for each Request. +#[derive(Debug)] +pub struct TtlHeaderInterceptor { + /// Default time-to-live for an operation. + default_ttl: hyper::http::HeaderValue, + /// Operation specific time-to-live. + operation_ttl: HashMap<&'static str, hyper::http::HeaderValue>, +} + +// Helper function to format duration as fractional seconds. +fn format_ttl_value(ttl: Duration) -> String { + format!("{:.2}", ttl.as_secs_f64()) +} + +impl TtlHeaderInterceptor { + fn new(default_ttl: Duration) -> Self { + let duration_str = format_ttl_value(default_ttl); + let default_ttl_value = hyper::http::HeaderValue::from_str(duration_str.as_str()) + .expect("could not create a header value for the default ttl"); + + Self { + default_ttl: default_ttl_value, + operation_ttl: Default::default(), + } + } + + /// Adds an operation name specific timeout value that needs to be set in the header. + fn add_operation_ttl(&mut self, operation_name: &'static str, ttl: Duration) { + let duration_str = format_ttl_value(ttl); + + self.operation_ttl.insert( + operation_name, + hyper::http::HeaderValue::from_str(duration_str.as_str()) + .expect("cannot create header value for the given ttl duration"), + ); + } +} + +/// Appends the header `x-amzn-client-ttl-seconds` using either the default time-to-live value +/// or an operation-specific value if it was set earlier using `add_operation_ttl`. +//impl aws_smithy_runtime_api::client::interceptors::Interceptor for TtlHeaderInterceptor { +impl Intercept for TtlHeaderInterceptor { + fn name(&self) -> &'static str { + "TtlHeaderInterceptor" + } + + /// Before the request is signed, add the header to the outgoing request. + fn modify_before_signing( + &self, + context: &mut BeforeTransmitInterceptorContextMut<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + // Metadata in the ConfigBag has the operation name. + let metadata = cfg.load::().expect("metadata should exist"); + let operation_name = metadata.name(); + + // Get operation specific or default HeaderValue to set for the header key. + let ttl = self + .operation_ttl + .get(operation_name) + .unwrap_or(&self.default_ttl); + + context + .request_mut() + .headers_mut() + .insert("x-amzn-client-ttl-seconds", ttl.clone()); + + tracing::info!("{operation_name} header set to {ttl:?}"); + + Ok(()) + } +} + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // By default set the value of all operations to 6 seconds. + const DEFAULT_TTL: Duration = Duration::from_secs(6); + + // Set up the interceptor to add an operation specific value of 3.5 seconds to be added + // for GetStorage operation. + let mut ttl_headers_interceptor = TtlHeaderInterceptor::new(DEFAULT_TTL); + ttl_headers_interceptor.add_operation_ttl("GetStorage", Duration::from_millis(3500)); + + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .interceptor(ttl_headers_interceptor) + .build(); + + pokemon_service_client::Client::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response for get_server_statistics()"); + + // Call the operation `get_storage` on the Pokémon service. The `TtlHeaderInterceptor` + // interceptor will add a specific header name / value pair for this operation. + let response = client + .get_storage() + .user("ash") + .passcode("pikachu123") + .send() + .await + .expect("operation failed"); + + // Print the response received from the service. + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/legacy/pokemon-service-client-usage/examples/custom-header.rs b/examples/legacy/pokemon-service-client-usage/examples/custom-header.rs new file mode 100644 index 00000000000..d84432f68c2 --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/examples/custom-header.rs @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how to create a `smithy-rs` client, and call an operation with custom +/// headers in the request. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example custom-header` +/// +use pokemon_service_client::Client as PokemonClient; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon +/// service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .build(); + + // Apply the configuration on the client, and return that. + pokemon_service_client::Client::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .customize() + .mutate_request(|req| { + // For demonstration purposes, add a header `x-ttl-seconds` to the outgoing request. + let headers = req.headers_mut(); + headers.insert( + hyper::header::HeaderName::from_static("x-ttl-seconds"), + hyper::header::HeaderValue::from(30), + ); + }) + .send() + .await + .expect("operation failed"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/legacy/pokemon-service-client-usage/examples/endpoint-resolver.rs b/examples/legacy/pokemon-service-client-usage/examples/endpoint-resolver.rs new file mode 100644 index 00000000000..565efb18acd --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/examples/endpoint-resolver.rs @@ -0,0 +1,102 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how a custom `ResolveEndpoint` can be implemented for resolving +/// endpoint of a request. Additionally, it shows how a header can be added using the endpoint +/// builder. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example endpoint-resolver`. +/// +use pokemon_service_client::config::endpoint::{Endpoint, EndpointFuture, Params, ResolveEndpoint}; +use pokemon_service_client::primitives::{DateTime, DateTimeFormat}; +use pokemon_service_client::Client as PokemonClient; +use pokemon_service_client_usage::setup_tracing_subscriber; + +use std::time::SystemTime; + +// This struct, provided as an example, constructs the URL that should be set on each request during initialization. +// It also implements the `ResolveEndpoint` trait, enabling it to be assigned as the endpoint_resolver in the `Config`. +#[derive(Debug)] +struct RegionalEndpoint { + url_to_use: String, +} + +impl RegionalEndpoint { + fn new(regional_url: &str, port: u16) -> Self { + let url_to_use = format!("{}:{}", regional_url, port); + RegionalEndpoint { url_to_use } + } +} + +impl ResolveEndpoint for RegionalEndpoint { + fn resolve_endpoint<'a>(&'a self, _params: &'a Params) -> EndpointFuture<'a> { + // Construct an endpoint using the Endpoint::Builder. Set the URL and, + // optionally, any headers to be sent with the request. For this example, + // we'll set the 'x-amz-date' header to the current date for all outgoing requests. + // `DateTime` can be used for formatting an RFC 3339 date time. + let now = SystemTime::now(); + let date_time = DateTime::from(now); + + let endpoint = Endpoint::builder() + .url(self.url_to_use.clone()) + .header( + "x-amz-date", + date_time + .fmt(DateTimeFormat::DateTimeWithOffset) + .expect("Could not create a date in UTC format"), + ) + .build(); + tracing::info!(?endpoint, "Resolving endpoint"); + EndpointFuture::ready(Ok(endpoint)) + } +} + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + const DEFAULT_PORT: u16 = 13734; + + // Use the environment variable `REGIONAL_URL` for the URL. + let resolver = RegionalEndpoint::new( + std::env::var("REGIONAL_URL") + .as_deref() + .unwrap_or("http://localhost"), + DEFAULT_PORT, + ); + + let config = pokemon_service_client::Config::builder() + .endpoint_resolver(resolver) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + tracing::info!(?response, "Response received"); +} diff --git a/examples/legacy/pokemon-service-client-usage/examples/handling-errors.rs b/examples/legacy/pokemon-service-client-usage/examples/handling-errors.rs new file mode 100644 index 00000000000..dfcebda6109 --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/examples/handling-errors.rs @@ -0,0 +1,130 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! This example demonstrates how to handle service generated errors. +//! +//! The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +//! Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +//! file for instructions on how to launch the service locally. +//! +//! The example can be run using `cargo run --example handling-errors`. + +use pokemon_service_client::error::DisplayErrorContext; +use pokemon_service_client::Client as PokemonClient; +use pokemon_service_client::{error::SdkError, operation::get_storage::GetStorageError}; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // The following example sends an incorrect passcode to the operation `get_storage`, + // which will return + // [StorageAccessNotAuthorized](https://github.com/smithy-lang/smithy-rs/blob/main/codegen-core/common-test-models/pokemon.smithy#L48) + let response_result = client + .get_storage() + .user("ash") + // Give a wrong password to generate a service error. + .passcode("pkachu123") + .send() + .await; + + // All errors are consolidated into an `SdkError` + match response_result { + Ok(response) => { + tracing::info!(?response, "Response from service") + } + Err(SdkError::ServiceError(se)) => { + // When an error response is received from the service, it is modeled + // as a `SdkError::ServiceError`. + match se.err() { + // Not authorized to access Pokémon storage. + GetStorageError::StorageAccessNotAuthorized(_) => { + tracing::error!("You do not have access to this resource."); + } + GetStorageError::ResourceNotFoundError(rnfe) => { + let message = rnfe.message(); + tracing::error!(error = %message, + "Given Pikachu does not exist on the server." + ) + } + GetStorageError::ValidationError(ve) => { + tracing::error!(error = %ve, "A required field has not been set."); + } + // The SdkError is marked as `#[non_exhaustive]`. Therefore, a catch-all pattern is required to handle + // potential future variants introduced in SdkError. + _ => { + tracing::error!(error = %DisplayErrorContext(se.err()), "Some other error has occurred on the server") + } + } + } + Err(SdkError::TimeoutError(_)) => { + tracing::error!("The request timed out and could not be completed"); + } + Err(SdkError::ResponseError(re)) => { + // Raw response received from the service can be retrieved using + // the `raw()` method. + tracing::error!( + "An unparsable response was received. Raw response: {:?}", + re.raw() + ); + } + Err(sdk_error) => { + // To retrieve the `source()` of an error within the following match statements, + // we work with the parent `SdkError` type, as individual variants don't directly provide it. + // Converting the parent error to its source transfers ownership of the variable. + match sdk_error { + SdkError::DispatchFailure(ref failure) => { + if failure.is_io() { + tracing::error!("An I/O error occurred"); + } else if failure.is_timeout() { + tracing::error!("Request timed out"); + } else if failure.is_user() { + tracing::error!("An invalid HTTP request has been provided"); + } else { + tracing::error!("Some other dispatch error occurred."); + }; + + if let Ok(source) = sdk_error.into_source() { + tracing::error!(%source, "Error source"); + } + } + SdkError::ConstructionFailure(_) => { + if let Ok(source) = sdk_error.into_source() { + tracing::error!(%source, "Request could not be constructed."); + } else { + tracing::error!("Request could not be constructed for unknown reasons"); + } + } + _ => { + tracing::error!("An unknown error has occurred"); + } + } + } + } +} diff --git a/examples/legacy/pokemon-service-client-usage/examples/mock-request.rs b/examples/legacy/pokemon-service-client-usage/examples/mock-request.rs new file mode 100644 index 00000000000..5afafdaee14 --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/examples/mock-request.rs @@ -0,0 +1,76 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how to use a mock connector with `capture_request`. This allows for +/// responding with a static `Response` while capturing the incoming request. The captured request +/// can later be asserted to verify that the correct headers and body were sent to the server. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example mock-request`. +/// +use aws_smithy_runtime::client::http::test_util::capture_request; +use pokemon_service_client::primitives::SdkBody; +use pokemon_service_client::Client as PokemonClient; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Build a response that should be sent when the operation is called. + let response = http::Response::builder() + .status(200) + .body(SdkBody::from(r#"{"calls_count":100}"#)) + .expect("response could not be constructed"); + + // Call `capture_request` to obtain a HTTP connector and a request receiver. + // The request receiver captures the incoming request, while the connector can be passed + // to `Config::builder().http_client`. + let (http_client, captured_request) = capture_request(Some(response)); + + // Pass the `http_client` connector to `Config::builder`. The connector won't send + // the request over the network; instead, it will return the static response provided + // during its initialization. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .http_client(http_client) + .build(); + + // Instantiate a client by applying the configuration. + let client = PokemonClient::from_conf(config); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .customize() + .mutate_request(|req| { + // For demonstration, send an extra header that can be verified to confirm + // that the client actually sends it. + let headers = req.headers_mut(); + headers.insert( + hyper::header::HeaderName::from_static("user-agent"), + hyper::header::HeaderName::from_static("sample-client"), + ); + }) + .send() + .await + .expect("operation failed"); + + // Print the response received from the service. + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); + + // The captured request can be verified to have certain headers. + let req = captured_request.expect_request(); + assert_eq!(req.headers().get("user-agent"), Some("sample-client")); + + // As an example, you can verify the URL matches. + assert_eq!(req.uri(), "http://localhost:13734/stats"); + + // You can convert the captured body into a &str and use assert! + // on it if you want to verify the contents of the request body. + // let str_body = std::str::from_utf8(req.body().bytes().unwrap()).unwrap(); +} diff --git a/examples/legacy/pokemon-service-client-usage/examples/response-header-interceptor.rs b/examples/legacy/pokemon-service-client-usage/examples/response-header-interceptor.rs new file mode 100644 index 00000000000..7f153873705 --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/examples/response-header-interceptor.rs @@ -0,0 +1,162 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +use aws_smithy_runtime_api::client::orchestrator::Metadata; +/// This example demonstrates how response headers can be examined before they are deserialized +/// into the output type. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example response-header-interceptor`. +/// +use aws_smithy_types::config_bag::{Storable, StoreReplace}; +use pokemon_service_client::{ + config::{ + interceptors::{ + BeforeDeserializationInterceptorContextRef, BeforeTransmitInterceptorContextMut, + }, + ConfigBag, Intercept, RuntimeComponents, + }, + error::BoxError, + Client as PokemonClient, +}; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; +use uuid::Uuid; + +#[derive(Debug, Clone)] +struct RequestId { + client_id: String, + server_id: Option, +} + +impl Storable for RequestId { + type Storer = StoreReplace; +} + +#[derive(Debug, thiserror::Error)] +enum RequestIdError { + /// Client side + #[error("Client side request ID has not been set")] + ClientRequestIdMissing(), +} + +#[derive(Debug, Default)] +pub struct ResponseHeaderLoggingInterceptor; + +impl ResponseHeaderLoggingInterceptor { + /// Creates a new `ResponseHeaderLoggingInterceptor` + pub fn new() -> Self { + Self::default() + } +} + +impl Intercept for ResponseHeaderLoggingInterceptor { + fn name(&self) -> &'static str { + "ResponseHeaderLoggingInterceptor" + } + + /// Before the request is signed, add the header to the outgoing request. + fn modify_before_signing( + &self, + context: &mut BeforeTransmitInterceptorContextMut<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + let client_id = Uuid::new_v4().to_string(); + + let request_id = hyper::header::HeaderValue::from_str(&client_id) + .expect("failed to construct a header value from UUID"); + context + .request_mut() + .headers_mut() + .insert("x-amzn-requestid", request_id); + + cfg.interceptor_state().store_put(RequestId { + client_id, + server_id: None, + }); + + Ok(()) + } + + fn read_before_deserialization( + &self, + context: &BeforeDeserializationInterceptorContextRef<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + // `Metadata` in the `ConfigBag` has the operation name in it. + let metadata = cfg.load::().expect("metadata should exist"); + let operation_name = metadata.name().to_string(); + + // Get the server side request ID and set it in the RequestID data type + // that is in the ConfigBag. This way any other interceptor that requires the mapping + // can easily find it from the bag. + let response = context.response(); + let header_received = response + .headers() + .iter() + .find(|(header_name, _)| *header_name == "x-request-id"); + + if let Some((_, server_id)) = header_received { + let request_details = cfg + .get_mut::() + .ok_or_else(|| Box::new(RequestIdError::ClientRequestIdMissing()))?; + + tracing::info!(operation = %operation_name, + "RequestID Mapping: {} = {server_id}", + request_details.client_id, + ); + + request_details.server_id = Some(server_id.into()); + } else { + tracing::info!(operation = %operation_name, "Server RequestID missing in response"); + } + + Ok(()) + } +} + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon +/// service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .interceptor(ResponseHeaderLoggingInterceptor) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + // If you need to access the `RequestIdError` raised by the interceptor, + // you can convert `SdkError::DispatchFailure` to a `ConnectorError` + // and then use `downcast_ref` on its source to get a `RequestIdError`. + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/legacy/pokemon-service-client-usage/examples/retry-classifier.rs b/examples/legacy/pokemon-service-client-usage/examples/retry-classifier.rs new file mode 100644 index 00000000000..b5754a5fba1 --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/examples/retry-classifier.rs @@ -0,0 +1,112 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how a custom RetryClassifier can be written to decide +/// which error conditions should be retried. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example retry-classifier`. +/// +use http::StatusCode; +use pokemon_service_client::{ + config::{ + interceptors::InterceptorContext, + retry::{ClassifyRetry, RetryAction, RetryConfig}, + }, + operation::get_server_statistics::GetServerStatisticsError, +}; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; +use std::time::Duration; + +use pokemon_service_client::Client as PokemonClient; + +#[derive(Debug)] +struct SampleRetryClassifier; + +// By default, the generated client uses the `aws_http::retry::AwsResponseRetryClassifier` +// to determine whether an error should be retried. To use a custom retry classifier, +// implement the `ClassifyRetry` trait and pass it to the retry_classifier method +// of the `Config::builder`. +impl ClassifyRetry for SampleRetryClassifier { + fn name(&self) -> &'static str { + "SampleRetryClassifier" + } + + // For this example, the classifier should retry in case the error is GetServerStatisticsError + // and the status code is 503. + fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction { + // Get the output or error that has been deserialized from the response. + let output_or_error = ctx.output_or_error(); + + let error = match output_or_error { + Some(Ok(_)) | None => return RetryAction::NoActionIndicated, + Some(Err(err)) => err, + }; + + // Retry in case the error returned is GetServerStatisticsError and StatusCode is 503. + if let Some(_err) = error + .as_operation_error() + .and_then(|err| err.downcast_ref::()) + { + if let Some(response) = ctx.response() { + if response.status() == StatusCode::SERVICE_UNAVAILABLE.into() { + return RetryAction::server_error(); + } + } + } + + // Let other classifiers run and decide if the request should be retried. + // Returning RetryAction::RetryForbidden will forbid any retries. + RetryAction::NoActionIndicated + } +} + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // By default the Smithy client uses RetryConfig::standard() strategy, with 3 retries, and + // an initial exponential back off of 1 second. To turn it off use RetryConfig::disabled(). + let retry_config = RetryConfig::standard() + .with_initial_backoff(Duration::from_secs(3)) + .with_max_attempts(5); + + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .retry_config(retry_config) + // Add the retry classifier. + .retry_classifier(SampleRetryClassifier {}) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/legacy/pokemon-service-client-usage/examples/retry-customize.rs b/examples/legacy/pokemon-service-client-usage/examples/retry-customize.rs new file mode 100644 index 00000000000..13c67af4655 --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/examples/retry-customize.rs @@ -0,0 +1,60 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how to customize retry settings on a Smithy client. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example retry-customize`. +/// +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; +use std::time::Duration; + +use pokemon_service_client::{config::retry::RetryConfig, Client as PokemonClient}; + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // By default the Smithy client uses `RetryConfig::standard()` strategy, with 3 retries, and + // an initial exponential back off of 1 second. To turn it off use `RetryConfig::disabled()`. + let retry_config = RetryConfig::standard() + .with_initial_backoff(Duration::from_secs(3)) + .with_max_attempts(5); + + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .retry_config(retry_config) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/legacy/pokemon-service-client-usage/examples/simple-client.rs b/examples/legacy/pokemon-service-client-usage/examples/simple-client.rs new file mode 100644 index 00000000000..dbc7303386a --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/examples/simple-client.rs @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how to create a `smithy-rs` Client and call an +/// [operation](https://smithy.io/2.0/spec/idl.html?highlight=operation#operation-shape). +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example simple-client`. +/// +use pokemon_service_client::Client as PokemonClient; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon +/// service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // The generated client contains a type `config::Builder` for constructing a `Config` instance. + // This enables configuration of endpoint resolvers, timeouts, retries, etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .build(); + + // Instantiate a client by applying the configuration. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + // Print the response received from the service. + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/legacy/pokemon-service-client-usage/examples/timeout-config.rs b/examples/legacy/pokemon-service-client-usage/examples/timeout-config.rs new file mode 100644 index 00000000000..717961fb96a --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/examples/timeout-config.rs @@ -0,0 +1,64 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how to create a `smithy-rs` Client and set connection +/// and operation related timeouts on the client. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example timeout-config` +/// +use std::time::Duration; + +use pokemon_service_client::{config::timeout::TimeoutConfig, Client as PokemonClient}; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // Different type of timeouts can be set on the client. These are: + // operation_attempt_timeout - If retries are enabled, this represents the timeout + // for each individual operation attempt. + // operation_timeout - Overall timeout for the operation to complete. + // connect timeout - The amount of time allowed for a connection to be established. + let timeout_config = TimeoutConfig::builder() + .operation_attempt_timeout(Duration::from_secs(1)) + .operation_timeout(Duration::from_secs(5)) + .connect_timeout(Duration::from_millis(500)) + .build(); + + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .timeout_config(timeout_config) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("Pokemon service does not seem to be running on localhost:13734"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/legacy/pokemon-service-client-usage/examples/trace-serialize.rs b/examples/legacy/pokemon-service-client-usage/examples/trace-serialize.rs new file mode 100644 index 00000000000..c5dc8f206f1 --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/examples/trace-serialize.rs @@ -0,0 +1,126 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +use aws_smithy_runtime::client::http::connection_poisoning::CaptureSmithyConnection; +/// This example demonstrates how an interceptor can be written to trace what is being +/// serialized / deserialized on the wire. +/// +/// Please beware that this may log sensitive information! This example is meant for pedagogical +/// purposes and may be useful in debugging scenarios. Please don't use this as-is in production. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example trace-serialize`. +/// +use http::StatusCode; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; +use std::str; + +use pokemon_service_client::{ + config::{ + interceptors::{ + BeforeDeserializationInterceptorContextRef, BeforeTransmitInterceptorContextRef, + }, + ConfigBag, Intercept, RuntimeComponents, + }, + error::BoxError, + Client as PokemonClient, +}; + +/// An example interceptor that logs the request and response as they're sent and received. +#[derive(Debug, Default)] +pub struct WireFormatInterceptor; + +impl Intercept for WireFormatInterceptor { + fn name(&self) -> &'static str { + "WireFormatInterceptor" + } + + // Called after the operation input has been serialized but before it's dispatched over the wire. + fn read_after_serialization( + &self, + context: &BeforeTransmitInterceptorContextRef<'_>, + _runtime_components: &RuntimeComponents, + _cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + // Get the request type from the context. + let request = context.request(); + // Print the request to the debug tracing log. + tracing::debug!(?request); + + Ok(()) + } + + // Called after the operation's response has been received but before it's deserialized into the + // operation's output type. + fn read_before_deserialization( + &self, + context: &BeforeDeserializationInterceptorContextRef<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + // Get the response type from the context. + let response = context.response(); + // Print the response. + if response.status().as_u16() == StatusCode::OK.as_u16() { + tracing::info!(?response, "Response received:"); + } else { + tracing::error!(?response); + } + + // Print the connection information + let captured_connection = cfg.load::().cloned(); + if let Some(captured_connection) = captured_connection.and_then(|conn| conn.get()) { + tracing::info!( + remote_addr = ?captured_connection.remote_addr(), + local_addr = ?captured_connection.local_addr(), + "Captured connection info" + ); + } else { + tracing::warn!("Connection info is missing!"); + } + + Ok(()) + } +} + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .interceptor(WireFormatInterceptor {}) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("operation failed"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/legacy/pokemon-service-client-usage/examples/use-config-bag.rs b/examples/legacy/pokemon-service-client-usage/examples/use-config-bag.rs new file mode 100644 index 00000000000..9452051a6da --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/examples/use-config-bag.rs @@ -0,0 +1,140 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/// This example demonstrates how different interceptor can use a property bag to pass +/// state from one interceptor to the next. +/// +/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. +/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) +/// file for instructions on how to launch the service locally. +/// +/// The example can be run using `cargo run --example use-config-bag`. +/// +use aws_smithy_types::config_bag::{Storable, StoreReplace}; +use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; +use std::time::Instant; + +use pokemon_service_client::{ + config::{ + interceptors::{ + BeforeDeserializationInterceptorContextRef, FinalizerInterceptorContextRef, + }, + ConfigBag, Intercept, RuntimeComponents, + }, + error::BoxError, + Client as PokemonClient, +}; + +#[derive(Debug)] +struct RequestTimestamp(Instant); + +impl Storable for RequestTimestamp { + type Storer = StoreReplace; +} + +#[derive(Debug, Default)] +pub struct SetTimeInterceptor; + +/// Note: This is merely an example demonstrating how state can +/// be shared between two different interceptors. In a practical +/// scenario, there wouldn't be a need to write two interceptors +/// merely to display the duration from the start of the lifecycle +/// to the receipt of the response. This task can be accomplished +/// within a single interceptor by overriding both +/// read_before_execution and read_before_deserialization. +impl Intercept for SetTimeInterceptor { + fn name(&self) -> &'static str { + "SetTimeInterceptor" + } + + fn read_before_execution( + &self, + _context: &pokemon_service_client::config::interceptors::BeforeSerializationInterceptorContextRef<'_>, + cfg: &mut aws_smithy_types::config_bag::ConfigBag, + ) -> Result<(), pokemon_service_client::error::BoxError> { + cfg.interceptor_state() + .store_put(RequestTimestamp(Instant::now())); + Ok(()) + } +} + +#[derive(Debug, Default)] +pub struct GetTimeInterceptor; + +impl Intercept for GetTimeInterceptor { + fn name(&self) -> &'static str { + "GetTimeInterceptor" + } + + fn read_before_deserialization( + &self, + _context: &BeforeDeserializationInterceptorContextRef<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + let stop_watch = cfg + .load::() + .expect("StopWatch not found in the ConfigBag"); + + let time_taken = stop_watch.0.elapsed(); + tracing::info!(time_taken = %time_taken.as_micros(), "Microseconds:"); + + Ok(()) + } + + fn read_after_execution( + &self, + _context: &FinalizerInterceptorContextRef<'_>, + _runtime_components: &RuntimeComponents, + cfg: &mut ConfigBag, + ) -> Result<(), pokemon_service_client::error::BoxError> { + let timestamp = cfg + .load::() + .expect("RequestTimeStamp not found in the ConfigBag"); + + let time_taken = timestamp.0.elapsed(); + tracing::info!(time_taken = %time_taken.as_micros(), "Microseconds:"); + + Ok(()) + } +} + +/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// let client = create_client(); +/// ``` +fn create_client() -> PokemonClient { + // The generated client has a type `Config::Builder` that can be used to build a `Config`, which + // allows configuring endpoint-resolver, timeouts, retries etc. + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .interceptor(SetTimeInterceptor) + .interceptor(GetTimeInterceptor) + .build(); + + // Apply the configuration on the client, and return that. + PokemonClient::from_conf(config) +} + +#[tokio::main] +async fn main() { + setup_tracing_subscriber(); + + // Create a configured `smithy-rs` client. + let client = create_client(); + + // Call an operation `get_server_statistics` on the Pokémon service. + let response = client + .get_server_statistics() + .send() + .await + .expect("Pokemon service does not seem to be running on localhost:13734"); + + tracing::info!(%POKEMON_SERVICE_URL, ?response, "Response received"); +} diff --git a/examples/legacy/pokemon-service-client-usage/src/lib.rs b/examples/legacy/pokemon-service-client-usage/src/lib.rs new file mode 100644 index 00000000000..6612ddb4c4c --- /dev/null +++ b/examples/legacy/pokemon-service-client-usage/src/lib.rs @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +pub static POKEMON_SERVICE_URL: &str = "http://localhost:13734"; + +/// Sets up the tracing subscriber to print `tracing::info!` and `tracing::error!` messages on the console. +pub fn setup_tracing_subscriber() { + // Add a tracing subscriber that uses the environment variable RUST_LOG + // to figure out which log level should be emitted. By default use `tracing::info!` + // as the logging level. + let filter = tracing_subscriber::EnvFilter::builder() + .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) + .from_env_lossy(); + + tracing_subscriber::fmt::fmt() + .with_env_filter(filter) + .init(); +} diff --git a/examples/legacy/pokemon-service-common/Cargo.toml b/examples/legacy/pokemon-service-common/Cargo.toml new file mode 100644 index 00000000000..de77646d3d0 --- /dev/null +++ b/examples/legacy/pokemon-service-common/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "pokemon-service-common" +version = "0.1.0" +edition = "2021" +publish = false +authors = ["Smithy-rs Server Team "] +description = "A smithy Rust service to retrieve information about Pokémon." + +[dependencies] +async-stream = "0.3" +http = "0.2.9" +rand = "0.8" +tracing = "0.1" +tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"] } +tokio = { version = "1", default-features = false, features = ["time"] } +tower = "0.4" + +# Local paths +aws-smithy-runtime = { path = "../../../rust-runtime/aws-smithy-runtime", features = ["client", "connector-hyper-0-14-x", "tls-rustls"] } +aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api", features = ["client"] } +pokemon-service-client = { path = "../pokemon-service-client/", package = "pokemon-service-client-http0x", features = [ + "behavior-version-latest", +] } +pokemon-service-server-sdk = { path = "../pokemon-service-server-sdk", package = "pokemon-service-server-sdk-http0x" } + +[dev-dependencies] +aws-smithy-runtime = { path = "../../../rust-runtime/aws-smithy-runtime", features = ["test-util"] } diff --git a/examples/legacy/pokemon-service-common/src/lib.rs b/examples/legacy/pokemon-service-common/src/lib.rs new file mode 100644 index 00000000000..9fc8c004577 --- /dev/null +++ b/examples/legacy/pokemon-service-common/src/lib.rs @@ -0,0 +1,378 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Pokémon Service +//! +//! This crate implements the Pokémon Service. +#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)] +use std::{ + collections::HashMap, + convert::TryInto, + process::Child, + sync::{atomic::AtomicUsize, Arc}, +}; + +use async_stream::stream; +use aws_smithy_runtime::client::http::hyper_014::HyperConnector; +use aws_smithy_runtime_api::client::http::HttpConnector; +use http::Uri; +use pokemon_service_server_sdk::{ + error, input, model, + model::CapturingPayload, + output, + server::Extension, + types::{Blob, ByteStream, SdkBody}, +}; +use rand::{seq::SliceRandom, Rng}; +use tracing_subscriber::{prelude::*, EnvFilter}; + +const PIKACHU_ENGLISH_FLAVOR_TEXT: &str = + "When several of these Pokémon gather, their electricity could build and cause lightning storms."; +const PIKACHU_SPANISH_FLAVOR_TEXT: &str = + "Cuando varios de estos Pokémon se juntan, su energía puede causar fuertes tormentas."; +const PIKACHU_ITALIAN_FLAVOR_TEXT: &str = + "Quando vari Pokémon di questo tipo si radunano, la loro energia può causare forti tempeste."; +const PIKACHU_JAPANESE_FLAVOR_TEXT: &str = + "ほっぺたの りょうがわに ちいさい でんきぶくろを もつ。ピンチのときに ほうでんする。"; + +/// Kills [`Child`] process when dropped. +#[derive(Debug)] +#[must_use] +pub struct ChildDrop(pub Child); + +impl Drop for ChildDrop { + fn drop(&mut self) { + self.0.kill().expect("failed to kill process") + } +} + +/// Setup `tracing::subscriber` to read the log level from RUST_LOG environment variable. +pub fn setup_tracing() { + let format = tracing_subscriber::fmt::layer().json(); + let filter = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + tracing_subscriber::registry() + .with(format) + .with(filter) + .init(); +} + +/// Structure holding the translations for a Pokémon description. +#[derive(Debug)] +struct PokemonTranslations { + en: String, + es: String, + it: String, + jp: String, +} + +/// PokémonService shared state. +/// +/// Some applications may want to manage state between handlers. Imagine having a database connection pool +/// that can be shared between different handlers and operation implementations. +/// State management can be expressed in a struct where the attributes hold the shared entities. +/// +/// **NOTE: It is up to the implementation of the state structure to handle concurrency by protecting** +/// **its attributes using synchronization mechanisms.** +/// +/// The framework stores the `Arc` inside an `http::Extensions` and conveniently passes it to +/// the operation's implementation, making it able to handle operations with two different async signatures: +/// * `FnOnce(InputType) -> Future` +/// * `FnOnce(InputType, Extension>) -> Future` +/// +/// Wrapping the service with a [`tower::Layer`] will allow to have operations' signatures with and without shared state: +/// +/// ```compile_fail +/// use std::sync::Arc; +/// use aws_smithy_http_server::{AddExtensionLayer, Extension, Router}; +/// use tower::ServiceBuilder; +/// use tokio::sync::RwLock; +/// +/// // Shared state, +/// #[derive(Debug, State)] +/// pub struct State { +/// pub count: RwLock +/// } +/// +/// // Operation implementation with shared state. +/// async fn operation_with_state(input: Input, state: Extension>) -> Output { +/// let mut count = state.0.write().await; +/// *count += 1; +/// Ok(Output::new()) +/// } +/// +/// // Operation implementation without shared state. +/// async fn operation_without_state(input: Input) -> Output { +/// Ok(Output::new()) +/// } +/// +/// let app: Router = OperationRegistryBuilder::default() +/// .operation_with_state(operation_with_state) +/// .operation_without_state(operation_without_state) +/// .build() +/// .unwrap() +/// .into(); +/// let shared_state = Arc::new(State::default()); +/// let app = app.layer(ServiceBuilder::new().layer(AddExtensionLayer::new(shared_state))); +/// let server = hyper::Server::bind(&"0.0.0.0:13734".parse().unwrap()).serve(app.into_make_service()); +/// ... +/// ``` +/// +/// Without the middleware layer, the framework will require operations' signatures without +/// the shared state. +/// +/// [`middleware`]: [`aws_smithy_http_server::AddExtensionLayer`] +#[derive(Debug)] +pub struct State { + pokemons_translations: HashMap, + call_count: AtomicUsize, +} + +impl Default for State { + fn default() -> Self { + let mut pokemons_translations = HashMap::new(); + pokemons_translations.insert( + String::from("pikachu"), + PokemonTranslations { + en: String::from(PIKACHU_ENGLISH_FLAVOR_TEXT), + es: String::from(PIKACHU_SPANISH_FLAVOR_TEXT), + it: String::from(PIKACHU_ITALIAN_FLAVOR_TEXT), + jp: String::from(PIKACHU_JAPANESE_FLAVOR_TEXT), + }, + ); + Self { + pokemons_translations, + call_count: Default::default(), + } + } +} + +/// Retrieves information about a Pokémon species. +pub async fn get_pokemon_species( + input: input::GetPokemonSpeciesInput, + state: Extension>, +) -> Result { + state + .0 + .call_count + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + // We only support retrieving information about Pikachu. + let pokemon = state.0.pokemons_translations.get(&input.name); + match pokemon.as_ref() { + Some(pokemon) => { + tracing::debug!("Requested Pokémon is {}", input.name); + let flavor_text_entries = vec![ + model::FlavorText { + flavor_text: pokemon.en.to_owned(), + language: model::Language::English, + }, + model::FlavorText { + flavor_text: pokemon.es.to_owned(), + language: model::Language::Spanish, + }, + model::FlavorText { + flavor_text: pokemon.it.to_owned(), + language: model::Language::Italian, + }, + model::FlavorText { + flavor_text: pokemon.jp.to_owned(), + language: model::Language::Japanese, + }, + ]; + let output = output::GetPokemonSpeciesOutput { + name: String::from("pikachu"), + flavor_text_entries, + }; + Ok(output) + } + None => { + tracing::error!("Requested Pokémon {} not available", input.name); + Err(error::GetPokemonSpeciesError::ResourceNotFoundException( + error::ResourceNotFoundException { + message: String::from("Requested Pokémon not available"), + }, + )) + } + } +} + +/// Retrieves the user's storage. +pub async fn get_storage( + input: input::GetStorageInput, + _state: Extension>, +) -> Result { + tracing::debug!("attempting to authenticate storage user"); + + // We currently only support Ash and he has nothing stored + if !(input.user == "ash" && input.passcode == "pikachu123") { + tracing::debug!("authentication failed"); + return Err(error::GetStorageError::StorageAccessNotAuthorized( + error::StorageAccessNotAuthorized {}, + )); + } + Ok(output::GetStorageOutput { collection: vec![] }) +} + +/// Calculates and reports metrics about this server instance. +pub async fn get_server_statistics( + _input: input::GetServerStatisticsInput, + state: Extension>, +) -> output::GetServerStatisticsOutput { + // Read the current calls count. + let counter = state.0.call_count.load(std::sync::atomic::Ordering::SeqCst); + let calls_count = counter + .try_into() + .map_err(|e| { + tracing::error!("Unable to convert u64 to i64: {}", e); + }) + .unwrap_or(0); + tracing::debug!("This instance served {} requests", counter); + output::GetServerStatisticsOutput { calls_count } +} + +/// Attempts to capture a Pokémon. +pub async fn capture_pokemon( + mut input: input::CapturePokemonInput, +) -> Result { + if input.region != "Kanto" { + return Err(error::CapturePokemonError::UnsupportedRegionError( + error::UnsupportedRegionError { + region: input.region, + }, + )); + } + let output_stream = stream! { + loop { + use std::time::Duration; + match input.events.recv().await { + Ok(maybe_event) => match maybe_event { + Some(event) => { + let capturing_event = event.as_event(); + if let Ok(attempt) = capturing_event { + let payload = attempt.payload.clone().unwrap_or_else(|| CapturingPayload::builder().build()); + let pokeball = payload.pokeball().unwrap_or(""); + if ! matches!(pokeball, "Master Ball" | "Great Ball" | "Fast Ball") { + yield Err( + crate::error::CapturePokemonEventsError::InvalidPokeballError( + crate::error::InvalidPokeballError { + pokeball: pokeball.to_owned() + } + ) + ); + } else { + let captured = match pokeball { + "Master Ball" => true, + "Great Ball" => rand::thread_rng().gen_range(0..100) > 33, + "Fast Ball" => rand::thread_rng().gen_range(0..100) > 66, + _ => unreachable!("invalid pokeball"), + }; + // Only support Kanto + tokio::time::sleep(Duration::from_millis(1000)).await; + // Will it capture the Pokémon? + if captured { + let shiny = rand::thread_rng().gen_range(0..4096) == 0; + let pokemon = payload + .name() + .unwrap_or("") + .to_string(); + let pokedex: Vec = (0..255).collect(); + yield Ok(crate::model::CapturePokemonEvents::Event( + crate::model::CaptureEvent { + name: Some(pokemon), + shiny: Some(shiny), + pokedex_update: Some(Blob::new(pokedex)), + captured: Some(true), + } + )); + } + } + } + } + None => break, + }, + Err(e) => println!("{e:?}"), + } + } + }; + Ok(output::CapturePokemonOutput::builder() + .events(output_stream.into()) + .build() + .unwrap()) +} + +/// Empty operation used to benchmark the service. +pub async fn do_nothing(_input: input::DoNothingInput) -> output::DoNothingOutput { + output::DoNothingOutput {} +} + +/// Operation used to show the service is running. +pub async fn check_health(_input: input::CheckHealthInput) -> output::CheckHealthOutput { + output::CheckHealthOutput {} +} + +const RADIO_STREAMS: [&str; 2] = [ + "https://ia800107.us.archive.org/33/items/299SoundEffectCollection/102%20Palette%20Town%20Theme.mp3", + "https://ia600408.us.archive.org/29/items/PocketMonstersGreenBetaLavenderTownMusicwwwFlvtoCom/Pocket%20Monsters%20Green%20Beta-%20Lavender%20Town%20Music-%5Bwww_flvto_com%5D.mp3", +]; + +/// Streams a random Pokémon song. +pub async fn stream_pokemon_radio( + _input: input::StreamPokemonRadioInput, +) -> output::StreamPokemonRadioOutput { + let radio_stream_url = RADIO_STREAMS + .choose(&mut rand::thread_rng()) + .expect("`RADIO_STREAMS` is empty") + .parse::() + .expect("Invalid url in `RADIO_STREAMS`"); + + let connector = HyperConnector::builder().build_https(); + let result = connector + .call( + http::Request::builder() + .uri(radio_stream_url) + .body(SdkBody::empty()) + .unwrap() + .try_into() + .unwrap(), + ) + .await + .unwrap(); + + output::StreamPokemonRadioOutput { + data: ByteStream::new(result.into_body()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn get_pokemon_species_pikachu_spanish_flavor_text() { + let input = input::GetPokemonSpeciesInput { + name: String::from("pikachu"), + }; + + let state = Arc::new(State::default()); + + let actual_spanish_flavor_text = get_pokemon_species(input, Extension(state.clone())) + .await + .unwrap() + .flavor_text_entries + .into_iter() + .find(|flavor_text| flavor_text.language == model::Language::Spanish) + .unwrap(); + + assert_eq!( + PIKACHU_SPANISH_FLAVOR_TEXT, + actual_spanish_flavor_text.flavor_text() + ); + + let input = input::GetServerStatisticsInput {}; + let stats = get_server_statistics(input, Extension(state.clone())).await; + assert_eq!(1, stats.calls_count); + } +} diff --git a/examples/legacy/pokemon-service-common/tests/plugins_execution_order.rs b/examples/legacy/pokemon-service-common/tests/plugins_execution_order.rs new file mode 100644 index 00000000000..ee0fbad3526 --- /dev/null +++ b/examples/legacy/pokemon-service-common/tests/plugins_execution_order.rs @@ -0,0 +1,121 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::{ + ops::Deref, + sync::Arc, + sync::Mutex, + task::{Context, Poll}, +}; + +use pokemon_service_server_sdk::{ + server::plugin::{HttpMarker, HttpPlugins, Plugin}, + PokemonService, PokemonServiceConfig, +}; +use tower::{Layer, Service}; + +use aws_smithy_runtime::client::http::test_util::capture_request; +use pokemon_service_client::{Client, Config}; +use pokemon_service_common::do_nothing; + +#[tokio::test] +async fn plugin_layers_are_executed_in_registration_order() { + // Each plugin layer will push its name into this vector when it gets invoked. + // We can then check the vector content to verify the invocation order + let output = Arc::new(Mutex::new(Vec::new())); + + let http_plugins = HttpPlugins::new() + .push(SentinelPlugin::new("first", output.clone())) + .push(SentinelPlugin::new("second", output.clone())); + let config = PokemonServiceConfig::builder() + .http_plugin(http_plugins) + .build(); + let mut app = PokemonService::builder(config) + .do_nothing(do_nothing) + .build_unchecked(); + + let request = { + let (http_client, rcvr) = capture_request(None); + let config = Config::builder() + .http_client(http_client) + .endpoint_url("http://localhost:1234") + .build(); + Client::from_conf(config).do_nothing().send().await.unwrap(); + rcvr.expect_request() + }; + + app.call(request.try_into().unwrap()).await.unwrap(); + + let output_guard = output.lock().unwrap(); + assert_eq!(output_guard.deref(), &vec!["first", "second"]); +} + +struct SentinelPlugin { + name: &'static str, + output: Arc>>, +} + +impl SentinelPlugin { + pub fn new(name: &'static str, output: Arc>>) -> Self { + Self { name, output } + } +} + +impl Plugin for SentinelPlugin { + type Output = SentinelService; + + fn apply(&self, inner: T) -> Self::Output { + SentinelService { + inner, + name: self.name, + output: self.output.clone(), + } + } +} + +impl HttpMarker for SentinelPlugin {} + +#[derive(Clone, Debug)] +pub struct SentinelService { + inner: S, + output: Arc>>, + name: &'static str, +} + +impl Service for SentinelService +where + S: Service, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: R) -> Self::Future { + self.output.lock().unwrap().push(self.name); + self.inner.call(req) + } +} + +#[derive(Debug)] +pub struct SentinelLayer { + name: &'static str, + output: Arc>>, +} + +impl Layer for SentinelLayer { + type Service = SentinelService; + + fn layer(&self, service: S) -> Self::Service { + SentinelService { + inner: service, + output: self.output.clone(), + name: self.name, + } + } +} diff --git a/examples/legacy/pokemon-service-lambda/Cargo.toml b/examples/legacy/pokemon-service-lambda/Cargo.toml new file mode 100644 index 00000000000..5046d855438 --- /dev/null +++ b/examples/legacy/pokemon-service-lambda/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "pokemon-service-lambda" +version = "0.1.0" +edition = "2021" +publish = false +authors = ["Smithy-rs Server Team "] +description = "A smithy Rust service to retrieve information about Pokémon via Lambda." + +[dependencies] +async-stream = "0.3.4" +clap = { version = "4.1.11", features = ["derive"] } +hyper = {version = "0.14.26", features = ["server"] } +tokio = "1.26.0" +tracing = "0.1" + +# `aws-smithy-legacy-http-server` is only guaranteed to be compatible with this +# version of `lambda_http`, or semver-compatible versions of this version. +# Depending on other versions of `lambda_http` may not work. +lambda_http = "0.8.0" + +# Local paths +pokemon-service-server-sdk = { path = "../pokemon-service-server-sdk/", package = "pokemon-service-server-sdk-http0x", features = ["aws-lambda"] } +pokemon-service-common = { path = "../pokemon-service-common/" } diff --git a/examples/legacy/pokemon-service-lambda/src/lib.rs b/examples/legacy/pokemon-service-lambda/src/lib.rs new file mode 100644 index 00000000000..81f7c97cb20 --- /dev/null +++ b/examples/legacy/pokemon-service-lambda/src/lib.rs @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::sync::Arc; + +use pokemon_service_common::State; +use pokemon_service_server_sdk::{ + error::{GetStorageError, StorageAccessNotAuthorized}, + input::GetStorageInput, + output::GetStorageOutput, + server::{request::lambda::Context, Extension}, +}; + +/// Retrieves the user's storage and logs the lambda request ID. +pub async fn get_storage_lambda( + input: GetStorageInput, + _state: Extension>, + context: Context, +) -> Result { + tracing::debug!(request_id = %context.request_id, "attempting to authenticate storage user"); + + // We currently only support Ash and he has nothing stored + if !(input.user == "ash" && input.passcode == "pikachu123") { + tracing::debug!("authentication failed"); + return Err(GetStorageError::StorageAccessNotAuthorized( + StorageAccessNotAuthorized {}, + )); + } + Ok(GetStorageOutput { collection: vec![] }) +} diff --git a/examples/legacy/pokemon-service-lambda/src/main.rs b/examples/legacy/pokemon-service-lambda/src/main.rs new file mode 100644 index 00000000000..93de83b2ede --- /dev/null +++ b/examples/legacy/pokemon-service-lambda/src/main.rs @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::sync::Arc; + +use pokemon_service_common::{ + capture_pokemon, check_health, do_nothing, get_pokemon_species, get_server_statistics, + setup_tracing, stream_pokemon_radio, State, +}; +use pokemon_service_lambda::get_storage_lambda; +use pokemon_service_server_sdk::{ + server::{routing::LambdaHandler, AddExtensionLayer}, + PokemonService, PokemonServiceConfig, +}; + +#[tokio::main] +pub async fn main() { + setup_tracing(); + + let config = PokemonServiceConfig::builder() + // Set up shared state and middlewares. + .layer(AddExtensionLayer::new(Arc::new(State::default()))) + .build(); + let app = PokemonService::builder(config) + // Build a registry containing implementations to all the operations in the service. These + // are async functions or async closures that take as input the operation's input and + // return the operation's output. + .get_pokemon_species(get_pokemon_species) + .get_storage(get_storage_lambda) + .get_server_statistics(get_server_statistics) + .capture_pokemon(capture_pokemon) + .do_nothing(do_nothing) + .check_health(check_health) + .stream_pokemon_radio(stream_pokemon_radio) + .build() + .expect("failed to build an instance of PokemonService"); + + let handler = LambdaHandler::new(app); + let lambda = lambda_http::run(handler); + + if let Err(err) = lambda.await { + eprintln!("lambda error: {err}"); + } +} diff --git a/examples/legacy/pokemon-service-lambda/tests/fixtures/example-apigw-request.json b/examples/legacy/pokemon-service-lambda/tests/fixtures/example-apigw-request.json new file mode 100644 index 00000000000..9ba7dcf8bd1 --- /dev/null +++ b/examples/legacy/pokemon-service-lambda/tests/fixtures/example-apigw-request.json @@ -0,0 +1,89 @@ +{ + "body": null, + "headers": { + "Accept": "application/json", + "Accept-Encoding": "gzip, deflate", + "cache-control": "no-cache", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Content-Type": "application/json", + "headerName": "headerValue", + "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com", + "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", + "User-Agent": "PostmanRuntime/2.4.5", + "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", + "X-Forwarded-For": "54.240.196.186, 54.182.214.83", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "httpMethod": "GET", + "isBase64Encoded": false, + "multiValueHeaders": { + "Accept": ["application/json"], + "Accept-Encoding": ["gzip, deflate"], + "cache-control": ["no-cache"], + "CloudFront-Forwarded-Proto": ["https"], + "CloudFront-Is-Desktop-Viewer": ["true"], + "CloudFront-Is-Mobile-Viewer": ["false"], + "CloudFront-Is-SmartTV-Viewer": ["false"], + "CloudFront-Is-Tablet-Viewer": ["false"], + "CloudFront-Viewer-Country": ["US"], + "Content-Type": ["application/json"], + "headerName": ["headerValue"], + "Host": ["gy415nuibc.execute-api.us-east-1.amazonaws.com"], + "Postman-Token": ["9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f"], + "User-Agent": ["PostmanRuntime/2.4.5"], + "Via": ["1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)"], + "X-Amz-Cf-Id": ["pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A=="], + "X-Forwarded-For": ["54.240.196.186, 54.182.214.83"], + "X-Forwarded-Port": ["443"], + "X-Forwarded-Proto": ["https"] + }, + "multiValueQueryStringParameters": { + "key": ["value"] + }, + "path": "/stats", + "pathParameters": null, + "queryStringParameters": { + "key": "value" + }, + "requestContext": { + "accountId": "xxxxx", + "apiId": "xxxxx", + "domainName": "testPrefix.testDomainName", + "domainPrefix": "testPrefix", + "extendedRequestId": "NvWWKEZbliAFliA=", + "httpMethod": "GET", + "identity": { + "accessKey": "xxxxx", + "accountId": "xxxxx", + "apiKey": "test-invoke-api-key", + "apiKeyId": "test-invoke-api-key-id", + "caller": "xxxxx:xxxxx", + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "principalOrgId": null, + "sourceIp": "test-invoke-source-ip", + "user": "xxxxx:xxxxx", + "userAgent": "aws-internal/3 aws-sdk-java/1.12.154 Linux/5.4.156-94.273.amzn2int.x86_64 OpenJDK_64-Bit_Server_VM/25.322-b06 java/1.8.0_322 vendor/Oracle_Corporation cfg/retry-mode/standard", + "userArn": "arn:aws:sts::xxxxx:assumed-role/xxxxx/xxxxx" + }, + "path": "/stats", + "protocol": "HTTP/1.1", + "requestId": "e5488776-afe4-4e5e-92b1-37bd23f234d6", + "requestTime": "18/Feb/2022:13:23:12 +0000", + "requestTimeEpoch": 1645190592806, + "resourceId": "ddw8yd", + "resourcePath": "/stats", + "stage": "test-invoke-stage" + }, + "resource": "/stats", + "stageVariables": null +} diff --git a/examples/legacy/pokemon-service-tls/Cargo.toml b/examples/legacy/pokemon-service-tls/Cargo.toml new file mode 100644 index 00000000000..0c3e5648699 --- /dev/null +++ b/examples/legacy/pokemon-service-tls/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "pokemon-service-tls" +version = "0.1.0" +edition = "2021" +publish = false +authors = ["Smithy-rs Server Team "] +description = "A smithy Rust service to retrieve information about Pokémon." + +[dependencies] +clap = { version = "4.1.11", features = ["derive"] } +hyper = { version = "0.14.26", features = ["server"] } +tokio = "1.26.0" +tracing = "0.1" + +# These dependencies are only required for the `pokemon-service-tls` program. + +# Latest version supporting hyper 0.x +tls-listener = { version = "0.8", features = ["rustls", "hyper-h2"] } +tokio-rustls = "0.24" +rustls-pemfile = "1" +futures-util = { version = "0.3.29", default-features = false } + +# Local paths +pokemon-service-server-sdk = { path = "../pokemon-service-server-sdk/", package = "pokemon-service-server-sdk-http0x" } +pokemon-service-common = { path = "../pokemon-service-common/" } + +[dev-dependencies] +assert_cmd = "2.0" +serial_test = "3.1.1" + +# These dependencies are only required for testing the `pokemon-service-tls` program. +hyper-rustls = { version = "0.24", features = ["http2"] } +hyper-tls = { version = "0.5" } + +# Local paths +aws-smithy-legacy-http = { path = "../../../rust-runtime/aws-smithy-legacy-http/" } +aws-smithy-runtime = { path = "../../../rust-runtime/aws-smithy-runtime", features = ["client", "connector-hyper-0-14-x"] } +aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types/" } +pokemon-service-client = { path = "../pokemon-service-client/", package = "pokemon-service-client-http0x", features = [ + "behavior-version-latest", +] } diff --git a/examples/legacy/pokemon-service-tls/src/lib.rs b/examples/legacy/pokemon-service-tls/src/lib.rs new file mode 100644 index 00000000000..d5006504e1d --- /dev/null +++ b/examples/legacy/pokemon-service-tls/src/lib.rs @@ -0,0 +1,13 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Defaults shared between `main.rs` and `/tests`. +pub const DEFAULT_TEST_KEY: &str = + concat!(env!("CARGO_MANIFEST_DIR"), "/tests/testdata/localhost.key"); +pub const DEFAULT_TEST_CERT: &str = + concat!(env!("CARGO_MANIFEST_DIR"), "/tests/testdata/localhost.crt"); +pub const DEFAULT_ADDRESS: &str = "127.0.0.1"; +pub const DEFAULT_PORT: u16 = 13734; +pub const DEFAULT_DOMAIN: &str = "localhost"; diff --git a/examples/legacy/pokemon-service-tls/src/main.rs b/examples/legacy/pokemon-service-tls/src/main.rs new file mode 100644 index 00000000000..b077c5ca37f --- /dev/null +++ b/examples/legacy/pokemon-service-tls/src/main.rs @@ -0,0 +1,191 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// This program is exported as a binary named `pokemon-service-tls`. +// It uses `tls-listener`, `tokio-rustls` (and `rustls-pemfile` to parse PEM files) +// to serve TLS connections. It also enables h2 ALPN protocol, +// without this clients by default don't upgrade to http2. +// +// You can use `mkcert` (https://github.com/FiloSottile/mkcert) to create certificates for testing: +// `$ mkcert localhost` +// it should create `./localhost.pem` and `./localhost-key.pem`, +// then you can run TLS server via: +// `$ cargo run --bin pokemon-service-tls -- --tls-cert-path ./localhost.pem --tls-key-path ./localhost-key.pem` +// and test it: +// ```bash +// $ curl -k -D- -H "Accept: application/json" https://localhost:13734/pokemon-species/pikachu +// HTTP/2 200 +// # ... +// ``` +// note that by default created certificates will be unknown and you should use `-k|--insecure` +// flag while making requests with cURL or you can run `mkcert -install` to trust certificates created by `mkcert`. + +use std::{fs::File, future, io::BufReader, net::SocketAddr, sync::Arc}; + +use clap::Parser; +use futures_util::stream::StreamExt; +use tokio_rustls::{ + rustls::{Certificate, PrivateKey, ServerConfig}, + TlsAcceptor, +}; + +use pokemon_service_common::{ + capture_pokemon, check_health, get_pokemon_species, get_server_statistics, get_storage, + setup_tracing, stream_pokemon_radio, State, +}; +use pokemon_service_server_sdk::{ + input, output, + server::{request::connect_info::ConnectInfo, routing::Connected, AddExtensionLayer}, + PokemonService, PokemonServiceConfig, +}; +use pokemon_service_tls::{DEFAULT_ADDRESS, DEFAULT_PORT, DEFAULT_TEST_CERT, DEFAULT_TEST_KEY}; + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + /// Hyper server bind address. + #[clap(short, long, action, default_value = DEFAULT_ADDRESS)] + address: String, + /// Hyper server bind port. + #[clap(short, long, action, default_value_t = DEFAULT_PORT)] + port: u16, + /// Hyper server TLS certificate path. Must be a PEM file. + #[clap(long, default_value = DEFAULT_TEST_CERT)] + tls_cert_path: String, + /// Hyper server TLS private key path. Must be a PEM file. + #[clap(long, default_value = DEFAULT_TEST_KEY)] + tls_key_path: String, +} + +/// Information derived from the TLS connection. +#[derive(Debug, Clone)] +pub struct TlsConnectInfo { + /// The remote peer address of this connection. + pub socket_addr: SocketAddr, + + /// The set of TLS certificates presented by the peer in this connection. + pub certs: Option>>, +} + +impl Connected<&tokio_rustls::server::TlsStream> + for TlsConnectInfo +{ + fn connect_info( + target: &tokio_rustls::server::TlsStream, + ) -> Self { + let (addr_stream, session) = target.get_ref(); + let socket_addr = addr_stream.remote_addr(); + + let certs = session + .peer_certificates() + .map(|certs| Arc::new(certs.to_vec())); + + TlsConnectInfo { socket_addr, certs } + } +} + +/// Empty operation used to showcase how we can get access to information derived from the TLS +/// connection in. +pub async fn do_nothing_with_tls_connect_info( + _input: input::DoNothingInput, + ConnectInfo(tls_connect_info): ConnectInfo, +) -> output::DoNothingOutput { + // Logging these might pose a security concern! You probably don't want to do this in + // production. + tracing::debug!(?tls_connect_info.certs, "peer TLS certificates"); + + output::DoNothingOutput {} +} + +#[tokio::main] +pub async fn main() { + let args = Args::parse(); + setup_tracing(); + + let config = PokemonServiceConfig::builder() + // Set up shared state and middlewares. + .layer(AddExtensionLayer::new(Arc::new(State::default()))) + .build(); + let app = PokemonService::builder(config) + // Build a registry containing implementations to all the operations in the service. These + // are async functions or async closures that take as input the operation's input and + // return the operation's output. + .get_pokemon_species(get_pokemon_species) + .get_storage(get_storage) + .get_server_statistics(get_server_statistics) + .capture_pokemon(capture_pokemon) + .do_nothing(do_nothing_with_tls_connect_info) + .check_health(check_health) + .stream_pokemon_radio(stream_pokemon_radio) + .build() + .expect("failed to build an instance of PokemonService"); + + let addr: SocketAddr = format!("{}:{}", args.address, args.port) + .parse() + .expect("unable to parse the server bind address and port"); + + let acceptor = acceptor(&args.tls_cert_path, &args.tls_key_path); + let listener = tls_listener::TlsListener::new( + acceptor, + hyper::server::conn::AddrIncoming::bind(&addr).expect("could not bind"), + ) + .connections() + .filter(|conn| { + if let Err(err) = conn { + eprintln!("connection error: {err:?}"); + future::ready(false) + } else { + future::ready(true) + } + }); + // Using `into_make_service_with_connect_info`, rather than `into_make_service`, to adjoin the `TlsConnectInfo` + // connection info. + let make_app = app.into_make_service_with_connect_info::(); + let server = + hyper::Server::builder(hyper::server::accept::from_stream(listener)).serve(make_app); + if let Err(err) = server.await { + eprintln!("server error: {err}"); + } +} + +// Returns a `TlsAcceptor` that can be used to create `TlsListener` +// which then can be used with Hyper. +pub fn acceptor(cert_path: &str, key_path: &str) -> TlsAcceptor { + let certs = load_certs(cert_path); + let key = load_key(key_path); + let mut server_config = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(certs, key) + .expect("could not create server config"); + + // If we don't state we are accepting "h2", clients by default don't negotiate way up to http2. + server_config.alpn_protocols = vec!["h2".into(), "http/1.1".into()]; + + TlsAcceptor::from(Arc::new(server_config)) +} + +fn load_certs(path: &str) -> Vec { + let mut reader = BufReader::new(File::open(path).expect("could not open certificate")); + rustls_pemfile::certs(&mut reader) + .expect("could not parse certificate") + .into_iter() + .map(Certificate) + .collect() +} + +fn load_key(path: &str) -> PrivateKey { + let mut reader = BufReader::new(File::open(path).expect("could not open private key")); + loop { + match rustls_pemfile::read_one(&mut reader).expect("could not parse private key") { + Some(rustls_pemfile::Item::RSAKey(key)) => return PrivateKey(key), + Some(rustls_pemfile::Item::PKCS8Key(key)) => return PrivateKey(key), + Some(rustls_pemfile::Item::ECKey(key)) => return PrivateKey(key), + None => break, + _ => {} + } + } + panic!("invalid private key") +} diff --git a/examples/legacy/pokemon-service-tls/tests/common/mod.rs b/examples/legacy/pokemon-service-tls/tests/common/mod.rs new file mode 100644 index 00000000000..8954365a205 --- /dev/null +++ b/examples/legacy/pokemon-service-tls/tests/common/mod.rs @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::{fs::File, io::BufReader, process::Command, time::Duration}; + +use assert_cmd::prelude::*; +use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder; +use tokio::time::sleep; + +use pokemon_service_client::{Client, Config}; +use pokemon_service_common::ChildDrop; +use pokemon_service_tls::{DEFAULT_DOMAIN, DEFAULT_PORT, DEFAULT_TEST_CERT}; + +pub async fn run_server() -> ChildDrop { + let crate_name = std::env::var("CARGO_PKG_NAME").unwrap(); + let child = Command::cargo_bin(crate_name).unwrap().spawn().unwrap(); + + sleep(Duration::from_millis(500)).await; + + ChildDrop(child) +} + +// Returns a client that only talks through https and http2 connections. +// It is useful in testing whether our server can talk to http2. +pub fn client_http2_only() -> Client { + // Create custom cert store and add our test certificate to prevent unknown cert issues. + let mut reader = + BufReader::new(File::open(DEFAULT_TEST_CERT).expect("could not open certificate")); + let certs = rustls_pemfile::certs(&mut reader).expect("could not parse certificate"); + let mut roots = tokio_rustls::rustls::RootCertStore::empty(); + roots.add_parsable_certificates(&certs); + + let connector = hyper_rustls::HttpsConnectorBuilder::new() + .with_tls_config( + tokio_rustls::rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(roots) + .with_no_client_auth(), + ) + .https_only() + .enable_http2() + .build(); + + let config = Config::builder() + .http_client(HyperClientBuilder::new().build(connector)) + .endpoint_url(format!("https://{DEFAULT_DOMAIN}:{DEFAULT_PORT}")) + .build(); + Client::from_conf(config) +} + +/// A `hyper` connector that uses the `native-tls` crate for TLS. To use this in a Smithy client, +/// wrap with a [`HyperClientBuilder`]. +pub type NativeTlsConnector = hyper_tls::HttpsConnector; + +fn native_tls_connector() -> NativeTlsConnector { + let cert = hyper_tls::native_tls::Certificate::from_pem( + std::fs::read_to_string(DEFAULT_TEST_CERT) + .expect("could not open certificate") + .as_bytes(), + ) + .expect("could not parse certificate"); + + let tls_connector = hyper_tls::native_tls::TlsConnector::builder() + .min_protocol_version(Some(hyper_tls::native_tls::Protocol::Tlsv12)) + .add_root_certificate(cert) + .build() + .unwrap_or_else(|e| panic!("error while creating TLS connector: {}", e)); + let mut http_connector = hyper::client::HttpConnector::new(); + http_connector.enforce_http(false); + hyper_tls::HttpsConnector::from((http_connector, tls_connector.into())) +} + +pub fn native_tls_client() -> Client { + let config = Config::builder() + .http_client(HyperClientBuilder::new().build(native_tls_connector())) + .endpoint_url(format!("https://{DEFAULT_DOMAIN}:{DEFAULT_PORT}")) + .build(); + Client::from_conf(config) +} diff --git a/examples/legacy/pokemon-service-tls/tests/custom_connectors.rs b/examples/legacy/pokemon-service-tls/tests/custom_connectors.rs new file mode 100644 index 00000000000..a65881afe10 --- /dev/null +++ b/examples/legacy/pokemon-service-tls/tests/custom_connectors.rs @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +pub mod common; + +use serial_test::serial; + +// This test invokes an operation with a client that can only send HTTP2 requests and whose TLS +// implementation is backed by `rustls`. +#[tokio::test] +#[serial] +async fn test_do_nothing_http2_rustls_connector() { + let _child = common::run_server().await; + let client = common::client_http2_only(); + + let _check_health = client.do_nothing().send().await.unwrap(); +} + +// This test invokes an operation with a client whose TLS implementation is backed by `native_tls`. +#[tokio::test] +#[serial] +async fn test_do_nothing_native_tls_connector() { + let _child = common::run_server().await; + let client = common::native_tls_client(); + + let _check_health = client.do_nothing().send().await.unwrap(); +} diff --git a/examples/legacy/pokemon-service-tls/tests/testdata/localhost.crt b/examples/legacy/pokemon-service-tls/tests/testdata/localhost.crt new file mode 100644 index 00000000000..eecdc6dc8c2 --- /dev/null +++ b/examples/legacy/pokemon-service-tls/tests/testdata/localhost.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFGTCCAwGgAwIBAgIUN/FD3OayKwJt9hXNKo4JKxqFSK4wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDgxNzE1MjQzMFoXDTMyMDgx +NDE1MjQzMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAulMGcyA69ioNMT8Kz0CdP2QP5elLNnltBykoqoJwbvKS +94+l5XA//29M4NpLphHcDxNXx3qB318bixUIPBtu66OiIsTGX8yrYPA4IO3Xt5/2 +wp2z1lNLouyW1+gPaPjKzcrjnHmqHS90CFDQqxdv9I0rIFIQ+U5hm5T9Hjr5xs36 +43l2FXAjeigoEuwtVBDt44yhEyeLSDwFJES3sH73AvpruMdxGv2KDVN4whuajWll +RLTqpqBvVSM6JbaV/VD2simpZeolSl8yKIenM2PWPdLIHSMEBg6IaYgpSpzoyvmh +089peAaiJfVrN53QjqDVyaN5os9ST03ZEzXQUI38lpvWGmV9Tcs5WfidLA1EbPjv +yE1zBbZh0SrP/+EALwkoIRslI8DXvz/9U5Cq7q9U4OHjWB+yjE5/BX6o6hfrqfJ1 +Ldg2fTp/TYEudmefM8eRzx6sdYtTPZBrSpkRgvmxd+6k3QUtsAQhtBTMpvJpWsgs +sD7Uo6G2JRag53oT/2cxG03Qy5HqySZUK1bpFW03W5FL3Pq6AkpGy1hnSxlifkHp +si61dbjCV5uRdxRCLyH9fD3HImecet+vnuZlvsP0MAzh0vbli/dcFZ7xUoSqFWnj +egnPohdOmF6C8kXvWBt51N4jjW+eLxPAr9H0mJtdIvEHWBNNW9iitzGz5Gw0g4sC +AwEAAaNjMGEwHQYDVR0OBBYEFEoLkB78Z6jgPPmOyf0XnWo/LjA9MB8GA1UdIwQY +MBaAFEoLkB78Z6jgPPmOyf0XnWo/LjA9MBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAJ +BgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQC17OljBEEVYefzk2mwg20AXDtL +PUJ46hLrUM7BcNBjd8AbtrLH/pdCRCexnv7tzYbwMhDNdqHiIcXDHEMNP3gXryB2 +ckU5ms/LzfKADM2/hrDZiR03XYSL4thjFkQNVfYnk9k7LTv9pKW0b+J2OrMun7+w +bdXcNw+igvnYiBgNJRo0IC9O5nejqLGWwBfveAJPetxjy6PvBkLqgIw2glivmTrh +Kdoq/I2/ZcxT0GyhEVIHP9W8Hh5goNm+RbsB/hDYhK+5s2+rL1lwJrwhNBrHhG1u +CtYmd2rD0J/mGf1cAw7t+hmwW0O7J9BVZw4YL/m4vDAsTO4zaeoAvDwsgQwPzPF1 +rmRtV+7jJHyIP/b021XIdIZU5KsXCCA3+B31mHJF1GLreG7WI+wClRsiNSbP7Zuw +OnUOTDZc77Y4oaDKl0UL8tz1GNwX5G9U5h+FciTPKCtg1gGiqSkB/3BOON2WaVOb +6Di9iAoH+dIjvWR/7ez7DAk/ITpGvBXS5RqaIXfB9pSJlVYsGp03ikgng1eJdXy4 +57XZnd47upHH88NTvIH9G/iOXQQCzF3MQXOqrJ/gem3ICeelvOoyNseHLvi8ZEqa +s693CJWaQAK/jD1mhka7yQzmb/Y1I53crc2UqSxX4FqFYP8xymza4Cg/E6pPJerG +LE/drJtbrIHTUlJB2Q== +-----END CERTIFICATE----- diff --git a/examples/legacy/pokemon-service-tls/tests/testdata/localhost.key b/examples/legacy/pokemon-service-tls/tests/testdata/localhost.key new file mode 100644 index 00000000000..6a8cc7f9bea --- /dev/null +++ b/examples/legacy/pokemon-service-tls/tests/testdata/localhost.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC6UwZzIDr2Kg0x +PwrPQJ0/ZA/l6Us2eW0HKSiqgnBu8pL3j6XlcD//b0zg2kumEdwPE1fHeoHfXxuL +FQg8G27ro6IixMZfzKtg8Dgg7de3n/bCnbPWU0ui7JbX6A9o+MrNyuOceaodL3QI +UNCrF2/0jSsgUhD5TmGblP0eOvnGzfrjeXYVcCN6KCgS7C1UEO3jjKETJ4tIPAUk +RLewfvcC+mu4x3Ea/YoNU3jCG5qNaWVEtOqmoG9VIzoltpX9UPayKall6iVKXzIo +h6czY9Y90sgdIwQGDohpiClKnOjK+aHTz2l4BqIl9Ws3ndCOoNXJo3miz1JPTdkT +NdBQjfyWm9YaZX1NyzlZ+J0sDURs+O/ITXMFtmHRKs//4QAvCSghGyUjwNe/P/1T +kKrur1Tg4eNYH7KMTn8FfqjqF+up8nUt2DZ9On9NgS52Z58zx5HPHqx1i1M9kGtK +mRGC+bF37qTdBS2wBCG0FMym8mlayCywPtSjobYlFqDnehP/ZzEbTdDLkerJJlQr +VukVbTdbkUvc+roCSkbLWGdLGWJ+QemyLrV1uMJXm5F3FEIvIf18PcciZ5x636+e +5mW+w/QwDOHS9uWL91wVnvFShKoVaeN6Cc+iF06YXoLyRe9YG3nU3iONb54vE8Cv +0fSYm10i8QdYE01b2KK3MbPkbDSDiwIDAQABAoICAAvSJaLF2jJw44pgILGaZ1Tf +ZnTPKBLqLDpxPYpny8tLf3sjoBeeLKk/ffChWNL4khiwwPe/tB/1muaS1zASYNH5 +UoQt2L9jhEHvq5fx5FGFiAm700OB4Fa9939LfTgghKP+vxGtKazqrEwKGIWqRH45 +kJFfM4LQRWKyAUcFiyrg5DhspcsMD2wkwmTE8Bvua7FCjvDgqDZVJycFvGOprRvW +wwvON2+fbek/hktGULgFBkQ6zXefI8ESgudj80Bxfl06RcGDU99T38zwzPD2i1/m +ZgTB38j562Sf8K1c/BXt4CWdzz1VVRHfGptvheJD85xJz0yUJk7atllrfMOyO7fp +4nj6M4EGZGfqqM6CFULkspVSoza/nLN3sOkcZqG+EJ9x6bo/MfUudJ50+cq2BhlQ +jM43j+wtm9DYPnJNXIC5FCze41N5MSDfK9h2oC16E6H6/VG9Y+AMMVrEDvsXXuOi +I0G8rcVanBdS3+nmmbTt4n0EVBLujB/ZJ/Qhsz/7QEeWn/xQNT4i00yRGG1mYJG0 +Ps0cy6t6jVrRoZmf7aYcUat97vHEP/ddo2V6ANRiZR3wVjhhoX1lVC8T0llzjxr4 +FEIDDuS+fnFqK1uHGBxS4lPHy/57gpdpYskoQtykpXURh4k39Fc28mzxKsrBhX6V +qY07bpgMNqYPC7SpkzO1AoIBAQDxEsGrZl0gNPhkXUwRSFvQxQDh0jqZAnEHdqOA +nO49z7ym7e/LELtq7y/HP9sZxoVsAcOryGL0qUpFrQozXMnSzWwqkxwOIABpQ4gq +mSJIZAUFVnV7m5h5xdln2jJ+xhvKv2vnXyuP3wRkiKrQPMqe6jE93cJb4YcMTK2V +xgxcUTZjT5LoMUCZguT1LCT/xR66epfombhGEweeTHJKEwPbwq1HbOECsB8vjZ8G +nwlm/Dt1fJXIo/+dvnfM+v79ebxKzC3t900Nj2eSCsX0bIU76zc1dqj+V/PD4+6h +NojOFrAusVaaOj5ssTTzebBqsmHiOs1a4YR5MOYidPpqvZ+9AoIBAQDF3HHwiiUp +zit5oIUkz1EkN7+jgEfrLNd9Kt3kNz3rTwXWoCE8tE4ctxBdn61RD3CHaT6PThNg +6naENyTFcjrP4F0O0K76ErlYxNSoV7w/OyrRmRu21U4gTF9nWidxOSTOo1qGJdKI +baAk4tSFsjsdysx9xcLueqDQdGOobzeSBr6tJSq8cvEvW39E6cNHDxVk5CEg0Ffq +7XA8+l+LfoP+6YL2du5jEe0K+/dTt2vYch8/9DloRezga21kV7Jea68Mqcxb5xsB +Coh5pe3OipUtaAWe6G+J1pRuz9OldacI36VuHQa/YBI7Ws7dt3IhPQoHnh0qujYp +iasxJQLH5ODnAoIBAEYBE1pJfGt41lSWvxsZrwfd3Va2LKv4CIiJTAtyBsDOTVMQ +Lx0Bu9reoDo08dP3URE/JeoBY7L2Ygn/qMGnhTgAzRND6tazNkta//SWyVzKJqcZ +Jz6AvXNHH83Hj/g+YR2sHpJukYDS2zyybx/PN2uUSD5V4jW6NPQ+Y/3lJ/u63ZdT +KS7h9oddek0zx366aCTwqqIx2VAIAKNYQav+/5TWYGkoVeLo7/VoI7DRh/Ju9nk0 +d25vKTBOeg19KYTD0AjMZ939fVOdvA6tsDQ9OydeM4cD8SkCs1fEHayU4H8wGXNF +rgdVOIFpqB23zaH+MOx39OAaMtTafUmuPHW4oOUCggEAe/jm70cvh+UlOl0Ib4ry +lVXU3nYXGdSL5GJCi6bNRi3KQ7MrgCSdOMK/H1pYNw0MfdvElffejn/56FfA03IC +RZOX2xuINyoaNfOGJ0Bps9i3uIJNah52iCgyMsi7I+chF9QkeR8jrdW6XMI/VNHa +1ozl2fxaaiAtuM7kTnn5AKb3O/eoslD2q6yRrrUlZNWfmwqRc0T3gTxqcdqSmQ2Z +WNQo+ZKFRU/LDXHYgvzPNtwylljIy3vcsrS84v1LxnuEP9P4NrE0K0/VORttSFdu +pvehZfLPSDdJ47CWNPrlwNqYhcjsHGbupX/9U9CIUykyqpk4PzhTjW0z9WPyPRs8 +iwKCAQEAsQRYdefBm/lYil70rlHvgxOvoCf8wTy6kiUTHFMZWUcbPB9+5C8HRRVu +kg+QTFn502H6gZhs3VkzpE4y1tClOe0s0HAfdBNfjP1Kk8i54hYOUzu0RAlOg4t+ +DcUBSmeXgXbYtzKLb2WqifTjOtuBYD515vOtcIM/19EaAMeccH0yWcvWDwFJu0jN +6DXUPTwIetMnmui5X1oFVgu9XDdXmhC7mFvMtaADHhh37hNqDlKDYpHQMMEJT/cT +WJvTCDK6nLkAYltPwehV74v2BEVknk0GHP1IcCLOjv6v3c1kt0TPZtnUr8pIfZGi +M8nPgza9amAhHxA8xPQgBs3l8d6k3w== +-----END PRIVATE KEY----- diff --git a/examples/legacy/pokemon-service/Cargo.toml b/examples/legacy/pokemon-service/Cargo.toml new file mode 100644 index 00000000000..eeda5a93860 --- /dev/null +++ b/examples/legacy/pokemon-service/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "pokemon-service" +version = "0.1.0" +edition = "2021" +publish = false +authors = ["Smithy-rs Server Team "] +description = "A smithy Rust service to retrieve information about Pokémon." + +[dependencies] +clap = { version = "4", features = ["derive"] } +http = "0.2" +hyper = { version = "0.14.26", features = ["server"] } +tokio = "1.26.0" +tower = "0.4" +tracing = "0.1" + +# Local paths +pokemon-service-server-sdk = { path = "../pokemon-service-server-sdk/", package = "pokemon-service-server-sdk-http0x", features = ["request-id"]} +pokemon-service-common = { path = "../pokemon-service-common/" } + +[dev-dependencies] +assert_cmd = "2.0" +async-stream = "0.3" +rand = "0.8.5" +serial_test = "3.1.1" + +# We use hyper client in tests +hyper = { version = "0.14.26", features = ["server", "client"] } + +# This dependency is only required for testing the `pokemon-service-tls` program. +hyper-rustls = { version = "0.24", features = ["http2"] } + +# Local paths +aws-smithy-legacy-http = { path = "../../../rust-runtime/aws-smithy-legacy-http/" } +pokemon-service-client = { path = "../pokemon-service-client/", package = "pokemon-service-client-http0x", features = [ + "behavior-version-latest", +] } diff --git a/examples/legacy/pokemon-service/src/authz.rs b/examples/legacy/pokemon-service/src/authz.rs new file mode 100644 index 00000000000..51a1c82e526 --- /dev/null +++ b/examples/legacy/pokemon-service/src/authz.rs @@ -0,0 +1,219 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! This file showcases a rather minimal model plugin that is agnostic over the operation that it +//! is applied to. +//! +//! It is interesting because it is not trivial to figure out how to write one. As the +//! documentation for [`aws_smithy_http_server::plugin::ModelMarker`] calls out, most model +//! plugins' implementation are _operation-specific_, which are simpler. + +use std::{marker::PhantomData, pin::Pin}; + +use pokemon_service_server_sdk::server::{ + body::BoxBody, + operation::OperationShape, + plugin::{ModelMarker, Plugin}, + response::IntoResponse, +}; +use tower::Service; + +pub struct AuthorizationPlugin { + // Private so that users are forced to use the `new` constructor. + _private: (), +} + +impl AuthorizationPlugin { + pub fn new() -> Self { + Self { _private: () } + } +} + +/// `T` is the inner service this plugin is applied to. +/// See the documentation for [`Plugin`] for details. +impl Plugin for AuthorizationPlugin { + type Output = AuthorizeService; + + fn apply(&self, input: T) -> Self::Output { + AuthorizeService { + inner: input, + authorizer: Authorizer::new(), + } + } +} + +impl ModelMarker for AuthorizationPlugin {} + +pub struct AuthorizeService { + inner: S, + authorizer: Authorizer, +} + +/// We manually implement `Clone` instead of adding `#[derive(Clone)]` because we don't require +/// `Op` to be cloneable. +impl Clone for AuthorizeService +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + authorizer: self.authorizer.clone(), + } + } +} + +/// The error returned by [`AuthorizeService`]. +pub enum AuthorizeServiceError { + /// Authorization was successful, but the inner service yielded an error. + InnerServiceError(E), + /// Authorization was not successful. + AuthorizeError { message: String }, +} + +// Only the _outermost_ model plugin needs to apply a `Service` whose error type implements +// `IntoResponse` for the protocol the service uses (this requirement comes from the `Service` +// implementation of [`aws_smithy_http_server::operation::Upgrade`]). So if the model plugin is +// meant to be applied in any position, and to any Smithy service, one should implement +// `IntoResponse` for all protocols. +// +// Having model plugins apply a `Service` that has a `Service::Response` type or a `Service::Error` +// type that is different from those returned by the inner service hence diminishes the reusability +// of the plugin because it makes the plugin less composable. Most plugins should instead work with +// the inner service's types, and _at most_ require that those be `Op::Input` and `Op::Error`, for +// maximum composability: +// +// ``` +// ... +// where +// S: Service<(Op::Input, ($($var,)*)), Error = Op::Error> +// ... +// { +// type Response = S::Response; +// type Error = S::Error; +// type Future = Pin> + Send>>; +// } +// +// ``` +// +// This plugin still exemplifies how changing a type can be done to make it more interesting. + +impl IntoResponse

for AuthorizeServiceError +where + E: IntoResponse

, +{ + fn into_response(self) -> http::Response { + match self { + AuthorizeServiceError::InnerServiceError(e) => e.into_response(), + AuthorizeServiceError::AuthorizeError { message } => http::Response::builder() + .status(http::StatusCode::UNAUTHORIZED) + .body(pokemon_service_server_sdk::server::body::to_boxed(message)) + .expect("attempted to build an invalid HTTP response; please file a bug report"), + } + } +} + +macro_rules! impl_service { + ($($var:ident),*) => { + impl Service<(Op::Input, ($($var,)*))> for AuthorizeService + where + S: Service<(Op::Input, ($($var,)*)), Error = Op::Error> + Clone + Send + 'static, + S::Future: Send, + Op: OperationShape + Send + Sync + 'static, + Op::Input: Send + Sync + 'static, + $($var: Send + 'static,)* + { + type Response = S::Response; + type Error = AuthorizeServiceError; + type Future = + Pin> + Send>>; + + fn poll_ready( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.inner + .poll_ready(cx) + .map_err(|e| Self::Error::InnerServiceError(e)) + } + + fn call(&mut self, req: (Op::Input, ($($var,)*))) -> Self::Future { + let (input, exts) = req; + + // Replacing the service is necessary to avoid readiness problems. + // https://docs.rs/tower/latest/tower/trait.Service.html#be-careful-when-cloning-inner-services + let service = self.inner.clone(); + let mut service = std::mem::replace(&mut self.inner, service); + + let authorizer = self.authorizer.clone(); + + let fut = async move { + let is_authorized = authorizer.authorize(&input).await; + if !is_authorized { + return Err(Self::Error::AuthorizeError { + message: "Not authorized!".to_owned(), + }); + } + + service + .call((input, exts)) + .await + .map_err(|e| Self::Error::InnerServiceError(e)) + }; + Box::pin(fut) + } + } + }; +} + +struct Authorizer { + operation: PhantomData, +} + +/// We manually implement `Clone` instead of adding `#[derive(Clone)]` because we don't require +/// `Op` to be cloneable. +impl Clone for Authorizer { + fn clone(&self) -> Self { + Self { + operation: PhantomData, + } + } +} + +impl Authorizer { + fn new() -> Self { + Self { + operation: PhantomData, + } + } + + async fn authorize(&self, _input: &Op::Input) -> bool + where + Op: OperationShape, + { + // We'd perform the actual authorization here. + // We would likely need to add bounds on `Op::Input`, `Op::Error`, if we wanted to do + // anything useful. + true + } +} + +// If we want our plugin to be as reusable as possible, the service it applies should work with +// inner services (i.e. operation handlers) that take a variable number of parameters. A Rust macro +// is helpful in providing those implementations concisely. +// Each handler function registered must accept the operation's input type (if there is one). +// Additionally, it can take up to 7 different parameters, each of which must implement the +// `FromParts` trait. To ensure that this `AuthorizeService` works with any of those inner +// services, we must implement it to handle up to +// 7 different types. Therefore, we invoke the `impl_service` macro 8 times. + +impl_service!(); +impl_service!(T1); +impl_service!(T1, T2); +impl_service!(T1, T2, T3); +impl_service!(T1, T2, T3, T4); +impl_service!(T1, T2, T3, T4, T5); +impl_service!(T1, T2, T3, T4, T5, T6); +impl_service!(T1, T2, T3, T4, T5, T6, T7); diff --git a/examples/legacy/pokemon-service/src/lib.rs b/examples/legacy/pokemon-service/src/lib.rs new file mode 100644 index 00000000000..95341abd9fe --- /dev/null +++ b/examples/legacy/pokemon-service/src/lib.rs @@ -0,0 +1,59 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::net::{IpAddr, SocketAddr}; + +use pokemon_service_server_sdk::{ + error::{GetStorageError, StorageAccessNotAuthorized}, + input::{DoNothingInput, GetStorageInput}, + output::{DoNothingOutput, GetStorageOutput}, + server::request::{connect_info::ConnectInfo, request_id::ServerRequestId}, +}; + +// Defaults shared between `main.rs` and `/tests`. +pub const DEFAULT_ADDRESS: &str = "127.0.0.1"; +pub const DEFAULT_PORT: u16 = 13734; + +/// Logs the request IDs to `DoNothing` operation. +pub async fn do_nothing_but_log_request_ids( + _input: DoNothingInput, + request_id: ServerRequestId, +) -> DoNothingOutput { + tracing::debug!(%request_id, "do nothing"); + DoNothingOutput {} +} + +/// Retrieves the user's storage. No authentication required for locals. +pub async fn get_storage_with_local_approved( + input: GetStorageInput, + connect_info: ConnectInfo, +) -> Result { + tracing::debug!("attempting to authenticate storage user"); + + if !(input.user == "ash" && input.passcode == "pikachu123") { + tracing::debug!("authentication failed"); + return Err(GetStorageError::StorageAccessNotAuthorized( + StorageAccessNotAuthorized {}, + )); + } + + // We support trainers in our local gym + let local = connect_info.0.ip() == "127.0.0.1".parse::().unwrap(); + if local { + tracing::info!("welcome back"); + return Ok(GetStorageOutput { + collection: vec![ + String::from("bulbasaur"), + String::from("charmander"), + String::from("squirtle"), + String::from("pikachu"), + ], + }); + } + + Ok(GetStorageOutput { + collection: vec![String::from("pikachu")], + }) +} diff --git a/examples/legacy/pokemon-service/src/main.rs b/examples/legacy/pokemon-service/src/main.rs new file mode 100644 index 00000000000..16045350405 --- /dev/null +++ b/examples/legacy/pokemon-service/src/main.rs @@ -0,0 +1,115 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +mod authz; +mod plugin; + +use std::{net::SocketAddr, sync::Arc}; + +use clap::Parser; +use pokemon_service_server_sdk::server::{ + extension::OperationExtensionExt, + instrumentation::InstrumentExt, + layer::alb_health_check::AlbHealthCheckLayer, + plugin::{HttpPlugins, ModelPlugins, Scoped}, + request::request_id::ServerRequestIdProviderLayer, + AddExtensionLayer, +}; + +use hyper::StatusCode; +use plugin::PrintExt; + +use pokemon_service::{ + do_nothing_but_log_request_ids, get_storage_with_local_approved, DEFAULT_ADDRESS, DEFAULT_PORT, +}; +use pokemon_service_common::{ + capture_pokemon, check_health, get_pokemon_species, get_server_statistics, setup_tracing, + stream_pokemon_radio, State, +}; +use pokemon_service_server_sdk::{scope, PokemonService, PokemonServiceConfig}; + +use crate::authz::AuthorizationPlugin; + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Args { + /// Hyper server bind address. + #[clap(short, long, action, default_value = DEFAULT_ADDRESS)] + address: String, + /// Hyper server bind port. + #[clap(short, long, action, default_value_t = DEFAULT_PORT)] + port: u16, +} + +#[tokio::main] +pub async fn main() { + let args = Args::parse(); + setup_tracing(); + + scope! { + /// A scope containing `GetPokemonSpecies` and `GetStorage`. + struct PrintScope { + includes: [GetPokemonSpecies, GetStorage] + } + } + + // Scope the `PrintPlugin`, defined in `plugin.rs`, to `PrintScope`. + let print_plugin = Scoped::new::(HttpPlugins::new().print()); + + let http_plugins = HttpPlugins::new() + // Apply the scoped `PrintPlugin` + .push(print_plugin) + // Apply the `OperationExtensionPlugin` defined in `aws_smithy_http_server::extension`. This allows other + // plugins or tests to access a `aws_smithy_http_server::extension::OperationExtension` from + // `Response::extensions`, or infer routing failure when it's missing. + .insert_operation_extension() + // Adds `tracing` spans and events to the request lifecycle. + .instrument(); + + let authz_plugin = AuthorizationPlugin::new(); + let model_plugins = ModelPlugins::new().push(authz_plugin); + + let config = PokemonServiceConfig::builder() + // Set up shared state and middlewares. + .layer(AddExtensionLayer::new(Arc::new(State::default()))) + // Handle `/ping` health check requests. + .layer(AlbHealthCheckLayer::from_handler("/ping", |_req| async { + StatusCode::OK + })) + // Add server request IDs. + .layer(ServerRequestIdProviderLayer::new()) + .http_plugin(http_plugins) + .model_plugin(model_plugins) + .build(); + + let app = PokemonService::builder(config) + // Build a registry containing implementations to all the operations in the service. These + // are async functions or async closures that take as input the operation's input and + // return the operation's output. + .get_pokemon_species(get_pokemon_species) + .get_storage(get_storage_with_local_approved) + .get_server_statistics(get_server_statistics) + .capture_pokemon(capture_pokemon) + .do_nothing(do_nothing_but_log_request_ids) + .check_health(check_health) + .stream_pokemon_radio(stream_pokemon_radio) + .build() + .expect("failed to build an instance of PokemonService"); + + // Using `into_make_service_with_connect_info`, rather than `into_make_service`, to adjoin the `SocketAddr` + // connection info. + let make_app = app.into_make_service_with_connect_info::(); + + // Bind the application to a socket. + let bind: SocketAddr = format!("{}:{}", args.address, args.port) + .parse() + .expect("unable to parse the server bind address and port"); + let server = hyper::Server::bind(&bind).serve(make_app); + + // Run forever-ish... + if let Err(err) = server.await { + eprintln!("server error: {err}"); + } +} diff --git a/examples/legacy/pokemon-service/src/plugin.rs b/examples/legacy/pokemon-service/src/plugin.rs new file mode 100644 index 00000000000..a1eec0d6a69 --- /dev/null +++ b/examples/legacy/pokemon-service/src/plugin.rs @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Provides an example [`Plugin`] implementation - [`PrintPlugin`]. + +use pokemon_service_server_sdk::server::{ + operation::OperationShape, + plugin::{HttpMarker, HttpPlugins, Plugin, PluginStack}, + service::ServiceShape, + shape_id::ShapeId, +}; +use tower::Service; + +use std::task::{Context, Poll}; + +/// A [`Service`] that prints a given string. +#[derive(Clone, Debug)] +pub struct PrintService { + inner: S, + operation_id: ShapeId, + service_id: ShapeId, +} + +impl Service for PrintService +where + S: Service, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: R) -> Self::Future { + println!( + "Hi {} in {}", + self.operation_id.absolute(), + self.service_id.absolute() + ); + self.inner.call(req) + } +} +/// A [`Plugin`] for a service builder to add a [`PrintLayer`] over operations. +#[derive(Debug)] +pub struct PrintPlugin; + +impl Plugin for PrintPlugin +where + Ser: ServiceShape, + Op: OperationShape, +{ + type Output = PrintService; + + fn apply(&self, inner: T) -> Self::Output { + PrintService { + inner, + operation_id: Op::ID, + service_id: Ser::ID, + } + } +} + +impl HttpMarker for PrintPlugin {} + +/// This provides a [`print`](PrintExt::print) method on [`HttpPlugins`]. +pub trait PrintExt { + /// Causes all operations to print the operation name when called. + /// + /// This works by applying the [`PrintPlugin`]. + fn print(self) -> HttpPlugins>; +} + +impl PrintExt for HttpPlugins { + fn print(self) -> HttpPlugins> { + self.push(PrintPlugin) + } +} diff --git a/examples/legacy/pokemon-service/tests/common/mod.rs b/examples/legacy/pokemon-service/tests/common/mod.rs new file mode 100644 index 00000000000..2d58f4a975a --- /dev/null +++ b/examples/legacy/pokemon-service/tests/common/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::{process::Command, time::Duration}; + +use assert_cmd::prelude::*; +use tokio::time::sleep; + +use pokemon_service::{DEFAULT_ADDRESS, DEFAULT_PORT}; +use pokemon_service_client::{Client, Config}; +use pokemon_service_common::ChildDrop; + +pub async fn run_server() -> ChildDrop { + let crate_name = std::env::var("CARGO_PKG_NAME").unwrap(); + let child = Command::cargo_bin(crate_name).unwrap().spawn().unwrap(); + + sleep(Duration::from_millis(500)).await; + + ChildDrop(child) +} + +pub fn base_url() -> String { + format!("http://{DEFAULT_ADDRESS}:{DEFAULT_PORT}") +} + +pub fn client() -> Client { + let config = Config::builder() + .endpoint_url(format!("http://{DEFAULT_ADDRESS}:{DEFAULT_PORT}")) + .build(); + Client::from_conf(config) +} diff --git a/examples/legacy/pokemon-service/tests/event_streaming.rs b/examples/legacy/pokemon-service/tests/event_streaming.rs new file mode 100644 index 00000000000..664827620bb --- /dev/null +++ b/examples/legacy/pokemon-service/tests/event_streaming.rs @@ -0,0 +1,162 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +pub mod common; + +use async_stream::stream; +use rand::Rng; +use serial_test::serial; + +use pokemon_service_client::types::{ + error::{AttemptCapturingPokemonEventError, MasterBallUnsuccessful}, + AttemptCapturingPokemonEvent, CapturingEvent, CapturingPayload, +}; + +fn get_pokemon_to_capture() -> String { + let pokemons = vec!["Charizard", "Pikachu", "Regieleki"]; + pokemons[rand::thread_rng().gen_range(0..pokemons.len())].to_string() +} + +fn get_pokeball() -> String { + let random = rand::thread_rng().gen_range(0..100); + let pokeball = if random < 5 { + "Master Ball" + } else if random < 30 { + "Great Ball" + } else if random < 80 { + "Fast Ball" + } else { + "Smithy Ball" + }; + pokeball.to_string() +} + +#[tokio::test] +#[serial] +async fn event_stream_test() { + let _child = common::run_server().await; + let client = common::client(); + + let mut team = vec![]; + let input_stream = stream! { + // Always Pikachu + yield Ok(AttemptCapturingPokemonEvent::Event( + CapturingEvent::builder() + .payload(CapturingPayload::builder() + .name("Pikachu") + .pokeball("Master Ball") + .build()) + .build() + )); + yield Ok(AttemptCapturingPokemonEvent::Event( + CapturingEvent::builder() + .payload(CapturingPayload::builder() + .name("Regieleki") + .pokeball("Fast Ball") + .build()) + .build() + )); + yield Err(AttemptCapturingPokemonEventError::MasterBallUnsuccessful(MasterBallUnsuccessful::builder().build())); + // The next event should not happen + yield Ok(AttemptCapturingPokemonEvent::Event( + CapturingEvent::builder() + .payload(CapturingPayload::builder() + .name("Charizard") + .pokeball("Great Ball") + .build()) + .build() + )); + }; + + // Throw many! + let mut output = common::client() + .capture_pokemon() + .region("Kanto") + .events(input_stream.into()) + .send() + .await + .unwrap(); + loop { + match output.events.recv().await { + Ok(Some(capture)) => { + let pokemon = capture.as_event().unwrap().name.as_ref().unwrap().clone(); + let pokedex = capture + .as_event() + .unwrap() + .pokedex_update + .as_ref() + .unwrap() + .clone(); + let shiny = if *capture.as_event().unwrap().shiny.as_ref().unwrap() { + "" + } else { + "not " + }; + let expected_pokedex: Vec = (0..255).collect(); + println!("captured {} ({}shiny)", pokemon, shiny); + if expected_pokedex == pokedex.into_inner() { + println!("pokedex updated") + } + team.push(pokemon); + } + Err(e) => { + println!("error from the server: {:?}", e); + break; + } + Ok(None) => break, + } + } + + while team.len() < 6 { + let pokeball = get_pokeball(); + let pokemon = get_pokemon_to_capture(); + let input_stream = stream! { + yield Ok(AttemptCapturingPokemonEvent::Event( + CapturingEvent::builder() + .payload(CapturingPayload::builder() + .name(pokemon) + .pokeball(pokeball) + .build()) + .build() + )) + }; + let mut output = client + .capture_pokemon() + .region("Kanto") + .events(input_stream.into()) + .send() + .await + .unwrap(); + match output.events.recv().await { + Ok(Some(capture)) => { + let pokemon = capture.as_event().unwrap().name.as_ref().unwrap().clone(); + let pokedex = capture + .as_event() + .unwrap() + .pokedex_update + .as_ref() + .unwrap() + .clone(); + let shiny = if *capture.as_event().unwrap().shiny.as_ref().unwrap() { + "" + } else { + "not " + }; + let expected_pokedex: Vec = (0..255).collect(); + println!("captured {} ({}shiny)", pokemon, shiny); + if expected_pokedex == pokedex.into_inner() { + println!("pokedex updated") + } + team.push(pokemon); + } + Err(e) => { + println!("error from the server: {:?}", e); + break; + } + Ok(None) => {} + } + } + println!("Team: {:?}", team); +} diff --git a/examples/legacy/pokemon-service/tests/simple.rs b/examples/legacy/pokemon-service/tests/simple.rs new file mode 100644 index 00000000000..215ab25f9fe --- /dev/null +++ b/examples/legacy/pokemon-service/tests/simple.rs @@ -0,0 +1,128 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use bytes; +use http_body_util; +use hyper_util::{client::legacy::Client, rt::TokioExecutor}; +use pokemon_service_client::{ + error::{DisplayErrorContext, SdkError}, + operation::get_storage::GetStorageError, + types::error::StorageAccessNotAuthorized, +}; +use serial_test::serial; + +pub mod common; + +#[tokio::test] +#[serial] +async fn simple_integration_test() { + let _child = common::run_server().await; + let client = common::client(); + + let service_statistics_out = client.get_server_statistics().send().await.unwrap(); + assert_eq!(0, service_statistics_out.calls_count); + + let pokemon_species_output = client + .get_pokemon_species() + .name("pikachu") + .send() + .await + .unwrap(); + assert_eq!("pikachu", pokemon_species_output.name()); + + let service_statistics_out = client.get_server_statistics().send().await.unwrap(); + assert_eq!(1, service_statistics_out.calls_count); + + let storage_err = client + .get_storage() + .user("ash") + .passcode("pikachu321") + .send() + .await; + let has_not_authorized_error = if let Err(SdkError::ServiceError(context)) = storage_err { + matches!( + context.err(), + GetStorageError::StorageAccessNotAuthorized(StorageAccessNotAuthorized { .. }), + ) + } else { + false + }; + assert!(has_not_authorized_error, "expected NotAuthorized error"); + + let storage_out = client + .get_storage() + .user("ash") + .passcode("pikachu123") + .send() + .await + .unwrap(); + assert_eq!( + vec![ + "bulbasaur".to_string(), + "charmander".to_string(), + "squirtle".to_string(), + "pikachu".to_string() + ], + storage_out.collection + ); + + let pokemon_species_error = client + .get_pokemon_species() + .name("some_pokémon") + .send() + .await + .unwrap_err(); + let message = DisplayErrorContext(pokemon_species_error).to_string(); + let expected = + r#"ResourceNotFoundError [ResourceNotFoundException]: Requested Pokémon not available"#; + assert!( + message.contains(expected), + "expected '{message}' to contain '{expected}'" + ); + + let service_statistics_out = client.get_server_statistics().send().await.unwrap(); + assert_eq!(2, service_statistics_out.calls_count); + + let hyper_client = Client::builder(TokioExecutor::new()).build_http(); + let health_check_url = format!("{}/ping", common::base_url()); + let health_check_url = hyper::Uri::try_from(health_check_url).unwrap(); + let request = hyper::Request::builder() + .uri(health_check_url) + .body(http_body_util::Empty::::new()) + .unwrap(); + let result = hyper_client.request(request).await.unwrap(); + + assert_eq!(result.status(), 200); +} + +#[tokio::test] +#[serial] +async fn health_check() { + let _child = common::run_server().await; + + use pokemon_service::{DEFAULT_ADDRESS, DEFAULT_PORT}; + let url = format!("http://{DEFAULT_ADDRESS}:{DEFAULT_PORT}/ping"); + let uri = url.parse::().expect("invalid URL"); + + // Since the `/ping` route is not modeled in Smithy, we use a regular + // Hyper HTTP client to make a request to it. + let request = hyper::Request::builder() + .uri(uri) + .body(http_body_util::Empty::::new()) + .expect("failed to build request"); + + let client = Client::builder(TokioExecutor::new()).build_http(); + let response = client + .request(request) + .await + .expect("failed to get response"); + + assert_eq!(response.status(), hyper::StatusCode::OK); + let body = http_body_util::BodyExt::collect(response.into_body()) + .await + .expect("failed to read response body") + .to_bytes(); + assert!(body.is_empty()); +} diff --git a/examples/pokemon-service-client-usage/Cargo.toml b/examples/pokemon-service-client-usage/Cargo.toml index 290c8d80cb5..3bbefe25d05 100644 --- a/examples/pokemon-service-client-usage/Cargo.toml +++ b/examples/pokemon-service-client-usage/Cargo.toml @@ -15,25 +15,26 @@ publish = false # are specifically noted in comments above the corresponding dependency in this file. pokemon-service-client = { path = "../pokemon-service-client/", features = ["behavior-version-latest"] } -# Required for getting the operation name from the `Metadata`. -aws-smithy-http = { path = "../../rust-runtime/aws-smithy-http/" } - # Required for `Storable` and `StoreReplace` in `response-header-interceptor` example. aws-smithy-types = { path = "../../rust-runtime/aws-smithy-types/" } -# Required for `HyperClientBuilder` in `client-connector` example. +# Required for `Builder` in `client-connector` example. +aws-smithy-http-client = { path = "../../rust-runtime/aws-smithy-http-client/", features=["default-client", "rustls-aws-lc"] } + +# Required for test utilities in examples. aws-smithy-runtime = { path = "../../rust-runtime/aws-smithy-runtime/", features=["test-util"] } # Required for `Metadata` in `custom-header-using-interceptor` example. aws-smithy-runtime-api = { path = "../../rust-runtime/aws-smithy-runtime-api/", features=["client"] } -hyper = { version = "0.14.25", features = ["client", "full"] } +hyper = { version = "1", features = ["client", "http1", "http2"] } +hyper-util = { version = "0.1", features = ["client", "client-legacy", "http1", "http2"] } tokio = {version = "1.26.0", features=["full"]} tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -rustls = "0.21.8" -hyper-rustls = "0.24.1" -http = "0.2.9" +rustls = "0.23" +hyper-rustls = "0.27" +http = "1" uuid = {version="1.4.1", features = ["v4"]} thiserror = "1.0.49" diff --git a/examples/pokemon-service-client-usage/examples/client-connector.rs b/examples/pokemon-service-client-usage/examples/client-connector.rs index b1a7ecef846..13f2e82540b 100644 --- a/examples/pokemon-service-client-usage/examples/client-connector.rs +++ b/examples/pokemon-service-client-usage/examples/client-connector.rs @@ -11,8 +11,7 @@ /// /// The example can be run using `cargo run --example client-connector`. /// -use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder; -use hyper_rustls::ConfigBuilderExt; +use aws_smithy_http_client::{Builder, tls}; use pokemon_service_client::Client as PokemonClient; use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; @@ -26,25 +25,19 @@ use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL /// let client = create_client(); /// ``` fn create_client() -> PokemonClient { - let tls_config = rustls::ClientConfig::builder() - .with_safe_defaults() - // `with_native_roots()`: Load platform trusted root certificates. - // `with_webpki_roots()`: Load Mozilla’s set of trusted roots. - .with_native_roots() - // To use client side certificates, you can use - // `.with_client_auth_cert(client_cert, client_key)` instead of `.with_no_client_auth()` - .with_no_client_auth(); + // Create a TLS context that loads platform trusted root certificates. + // The TrustStore::default() enables native roots by default. + let tls_context = tls::TlsContext::builder() + .with_trust_store(tls::TrustStore::default()) + .build() + .expect("failed to build TLS context"); - let tls_connector = hyper_rustls::HttpsConnectorBuilder::new() - .with_tls_config(tls_config) - // This can be changed to `.https_only()` to ensure that the client always uses HTTPs - .https_or_http() - .enable_http1() - .enable_http2() - .build(); - - // Create a hyper-based HTTP client that uses this TLS connector. - let http_client = HyperClientBuilder::new().build(tls_connector); + // Create an HTTP client using rustls with AWS-LC crypto provider. + // To use client side certificates, you would need to customize the TLS config further. + let http_client = Builder::new() + .tls_provider(tls::Provider::Rustls(tls::rustls_provider::CryptoMode::AwsLc)) + .tls_context(tls_context) + .build_https(); // Pass the smithy connector to the Client::ConfigBuilder let config = pokemon_service_client::Config::builder() diff --git a/examples/pokemon-service-client-usage/examples/mock-request.rs b/examples/pokemon-service-client-usage/examples/mock-request.rs index 5afafdaee14..eb7436c112a 100644 --- a/examples/pokemon-service-client-usage/examples/mock-request.rs +++ b/examples/pokemon-service-client-usage/examples/mock-request.rs @@ -12,7 +12,7 @@ /// /// The example can be run using `cargo run --example mock-request`. /// -use aws_smithy_runtime::client::http::test_util::capture_request; +use aws_smithy_http_client::test_util::capture_request; use pokemon_service_client::primitives::SdkBody; use pokemon_service_client::Client as PokemonClient; use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; diff --git a/examples/pokemon-service-common/Cargo.toml b/examples/pokemon-service-common/Cargo.toml index 04cf0e8c1fd..8de6ca648d0 100644 --- a/examples/pokemon-service-common/Cargo.toml +++ b/examples/pokemon-service-common/Cargo.toml @@ -8,7 +8,7 @@ description = "A smithy Rust service to retrieve information about Pokémon." [dependencies] async-stream = "0.3" -http = "0.2.9" +http = "1" rand = "0.8" tracing = "0.1" tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"] } @@ -16,7 +16,8 @@ tokio = { version = "1", default-features = false, features = ["time"] } tower = "0.4" # Local paths -aws-smithy-runtime = { path = "../../rust-runtime/aws-smithy-runtime", features = ["client", "connector-hyper-0-14-x", "tls-rustls"] } +aws-smithy-http-client = { path = "../../rust-runtime/aws-smithy-http-client", features = ["rustls-aws-lc"] } +aws-smithy-runtime = { path = "../../rust-runtime/aws-smithy-runtime", features = ["client", "default-https-client"] } aws-smithy-runtime-api = { path = "../../rust-runtime/aws-smithy-runtime-api", features = ["client"] } pokemon-service-client = { path = "../pokemon-service-client/", features = [ "behavior-version-latest", diff --git a/examples/pokemon-service-common/src/lib.rs b/examples/pokemon-service-common/src/lib.rs index 16c25e02f55..ef231fd8bff 100644 --- a/examples/pokemon-service-common/src/lib.rs +++ b/examples/pokemon-service-common/src/lib.rs @@ -15,7 +15,7 @@ use std::{ }; use async_stream::stream; -use aws_smithy_runtime::client::http::hyper_014::HyperConnector; +use aws_smithy_http_client::{Connector, tls}; use aws_smithy_runtime_api::client::http::HttpConnector; use http::Uri; use pokemon_service_server_sdk::{ @@ -293,7 +293,7 @@ pub async fn capture_pokemon( } None => break, }, - Err(e) => println!("{:?}", e), + Err(e) => println!("{e:?}"), } } }; @@ -328,7 +328,9 @@ pub async fn stream_pokemon_radio( .parse::() .expect("Invalid url in `RADIO_STREAMS`"); - let connector = HyperConnector::builder().build_https(); + let connector = Connector::builder() + .tls_provider(tls::Provider::Rustls(tls::rustls_provider::CryptoMode::AwsLc)) + .build(); let result = connector .call( http::Request::builder() diff --git a/examples/pokemon-service-common/tests/plugins_execution_order.rs b/examples/pokemon-service-common/tests/plugins_execution_order.rs index 62fb47ed033..ee0fbad3526 100644 --- a/examples/pokemon-service-common/tests/plugins_execution_order.rs +++ b/examples/pokemon-service-common/tests/plugins_execution_order.rs @@ -46,7 +46,7 @@ async fn plugin_layers_are_executed_in_registration_order() { rcvr.expect_request() }; - app.call(request.try_into_http02x().unwrap()).await.unwrap(); + app.call(request.try_into().unwrap()).await.unwrap(); let output_guard = output.lock().unwrap(); assert_eq!(output_guard.deref(), &vec!["first", "second"]); diff --git a/examples/pokemon-service-lambda/Cargo.toml b/examples/pokemon-service-lambda/Cargo.toml index 21318a47525..96d64cab499 100644 --- a/examples/pokemon-service-lambda/Cargo.toml +++ b/examples/pokemon-service-lambda/Cargo.toml @@ -9,14 +9,12 @@ description = "A smithy Rust service to retrieve information about Pokémon via [dependencies] async-stream = "0.3.4" clap = { version = "4.1.11", features = ["derive"] } -hyper = {version = "0.14.26", features = ["server"] } +http = "1" +hyper = {version = "1", features = ["server"] } tokio = "1.26.0" tracing = "0.1" -# `aws-smithy-http-server` is only guaranteed to be compatible with this -# version of `lambda_http`, or semver-compatible versions of this version. -# Depending on other versions of `lambda_http` may not work. -lambda_http = "0.8.0" +lambda_http = "0.17" # Local paths pokemon-service-server-sdk = { path = "../pokemon-service-server-sdk/", features = ["aws-lambda"] } diff --git a/examples/pokemon-service-lambda/src/main.rs b/examples/pokemon-service-lambda/src/main.rs index a5c914534b9..93de83b2ede 100644 --- a/examples/pokemon-service-lambda/src/main.rs +++ b/examples/pokemon-service-lambda/src/main.rs @@ -41,6 +41,6 @@ pub async fn main() { let lambda = lambda_http::run(handler); if let Err(err) = lambda.await { - eprintln!("lambda error: {}", err); + eprintln!("lambda error: {err}"); } } diff --git a/examples/pokemon-service-tls/Cargo.toml b/examples/pokemon-service-tls/Cargo.toml index 81c746e0411..a6eef43df5b 100644 --- a/examples/pokemon-service-tls/Cargo.toml +++ b/examples/pokemon-service-tls/Cargo.toml @@ -8,16 +8,17 @@ description = "A smithy Rust service to retrieve information about Pokémon." [dependencies] clap = { version = "4.1.11", features = ["derive"] } -hyper = { version = "0.14.26", features = ["server"] } -tokio = "1.26.0" +http = "1" +hyper = { version = "1", features = ["server"] } +hyper-util = { version = "0.1", features = ["tokio", "server", "server-auto", "service"] } +tokio = { version = "1.26.0", features = ["rt-multi-thread", "macros"] } tracing = "0.1" +tower = "0.4" # These dependencies are only required for the `pokemon-service-tls` program. - -# Latest version supporting hyper 0.x -tls-listener = { version = "0.8", features = ["rustls", "hyper-h2"] } -tokio-rustls = "0.24" -rustls-pemfile = "1" +tokio-rustls = "0.26" +rustls = "0.23" +rustls-pemfile = "2" futures-util = { version = "0.3.29", default-features = false } # Local paths @@ -29,12 +30,11 @@ assert_cmd = "2.0" serial_test = "3.1.1" # These dependencies are only required for testing the `pokemon-service-tls` program. -hyper-rustls = { version = "0.24", features = ["http2"] } -hyper-tls = { version = "0.5" } +hyper-rustls = { version = "0.27", features = ["http2"] } # Local paths -aws-smithy-http = { path = "../../rust-runtime/aws-smithy-http/" } -aws-smithy-runtime = { path = "../../rust-runtime/aws-smithy-runtime", features = ["client", "connector-hyper-0-14-x"] } +aws-smithy-http-client = { path = "../../rust-runtime/aws-smithy-http-client", features = ["default-client", "rustls-aws-lc"] } +aws-smithy-runtime = { path = "../../rust-runtime/aws-smithy-runtime", features = ["client", "default-https-client"] } aws-smithy-types = { path = "../../rust-runtime/aws-smithy-types/" } pokemon-service-client = { path = "../pokemon-service-client/", features = [ "behavior-version-latest", diff --git a/examples/pokemon-service-tls/src/main.rs b/examples/pokemon-service-tls/src/main.rs index 19d05119dc5..2a95d1fe53e 100644 --- a/examples/pokemon-service-tls/src/main.rs +++ b/examples/pokemon-service-tls/src/main.rs @@ -22,12 +22,20 @@ // note that by default created certificates will be unknown and you should use `-k|--insecure` // flag while making requests with cURL or you can run `mkcert -install` to trust certificates created by `mkcert`. -use std::{fs::File, future, io::BufReader, net::SocketAddr, sync::Arc}; +use std::{ + fs::File, + io::{self, BufReader}, + net::SocketAddr, + sync::Arc, +}; use clap::Parser; -use futures_util::stream::StreamExt; +use tokio::net::TcpListener; use tokio_rustls::{ - rustls::{Certificate, PrivateKey, ServerConfig}, + rustls::{ + pki_types::{CertificateDer, PrivateKeyDer}, + ServerConfig, + }, TlsAcceptor, }; @@ -37,10 +45,16 @@ use pokemon_service_common::{ }; use pokemon_service_server_sdk::{ input, output, - server::{request::connect_info::ConnectInfo, routing::Connected, AddExtensionLayer}, + server::{ + request::connect_info::ConnectInfo, + routing::Connected, + serve::{serve, Listener}, + AddExtensionLayer, + }, PokemonService, PokemonServiceConfig, }; use pokemon_service_tls::{DEFAULT_ADDRESS, DEFAULT_PORT, DEFAULT_TEST_CERT, DEFAULT_TEST_KEY}; +use tokio::net::TcpStream; #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] @@ -59,6 +73,48 @@ struct Args { tls_key_path: String, } +/// A TLS listener that wraps TcpListener and TlsAcceptor +pub struct TlsListener { + tcp_listener: TcpListener, + tls_acceptor: TlsAcceptor, +} + +impl TlsListener { + pub fn new(tcp_listener: TcpListener, tls_acceptor: TlsAcceptor) -> Self { + Self { + tcp_listener, + tls_acceptor, + } + } +} + +impl Listener for TlsListener { + type Io = tokio_rustls::server::TlsStream; + type Addr = SocketAddr; + + async fn accept(&mut self) -> (Self::Io, Self::Addr) { + loop { + match self.tcp_listener.accept().await { + Ok((tcp_stream, remote_addr)) => match self.tls_acceptor.accept(tcp_stream).await { + Ok(tls_stream) => return (tls_stream, remote_addr), + Err(err) => { + eprintln!("TLS handshake failed: {err}"); + continue; + } + }, + Err(err) => { + eprintln!("Failed to accept TCP connection: {err}"); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + } + } + } + + fn local_addr(&self) -> io::Result { + self.tcp_listener.local_addr() + } +} + /// Information derived from the TLS connection. #[derive(Debug, Clone)] pub struct TlsConnectInfo { @@ -66,19 +122,21 @@ pub struct TlsConnectInfo { pub socket_addr: SocketAddr, /// The set of TLS certificates presented by the peer in this connection. - pub certs: Option>>, + pub certs: Option>>>, } -impl Connected<&tokio_rustls::server::TlsStream> +impl<'a> Connected> for TlsConnectInfo { fn connect_info( - target: &tokio_rustls::server::TlsStream, + target: pokemon_service_server_sdk::server::serve::IncomingStream<'a, TlsListener>, ) -> Self { - let (addr_stream, session) = target.get_ref(); - let socket_addr = addr_stream.remote_addr(); + let tls_stream = target.io(); + let socket_addr = *target.remote_addr(); - let certs = session + let certs = tls_stream + .get_ref() + .1 .peer_certificates() .map(|certs| Arc::new(certs.to_vec())); @@ -126,27 +184,26 @@ pub async fn main() { .parse() .expect("unable to parse the server bind address and port"); - let acceptor = acceptor(&args.tls_cert_path, &args.tls_key_path); - let listener = tls_listener::TlsListener::new( - acceptor, - hyper::server::conn::AddrIncoming::bind(&addr).expect("could not bind"), - ) - .connections() - .filter(|conn| { - if let Err(err) = conn { - eprintln!("connection error: {:?}", err); - future::ready(false) - } else { - future::ready(true) - } - }); + let tls_acceptor = acceptor(&args.tls_cert_path, &args.tls_key_path); + let tcp_listener = TcpListener::bind(addr) + .await + .expect("failed to bind TCP listener"); + + // Get the actual bound address (important when port 0 is used for random port) + let actual_addr = tcp_listener.local_addr().expect("failed to get local address"); + + let tls_listener = TlsListener::new(tcp_listener, tls_acceptor); + + // Signal that the server is ready to accept connections, including the actual port + eprintln!("SERVER_READY:{}", actual_addr.port()); + // Using `into_make_service_with_connect_info`, rather than `into_make_service`, to adjoin the `TlsConnectInfo` // connection info. let make_app = app.into_make_service_with_connect_info::(); - let server = - hyper::Server::builder(hyper::server::accept::from_stream(listener)).serve(make_app); - if let Err(err) = server.await { - eprintln!("server error: {}", err); + + // Run the server using the serve function + if let Err(err) = serve(tls_listener, make_app).await { + eprintln!("server error: {err}"); } } @@ -156,7 +213,6 @@ pub fn acceptor(cert_path: &str, key_path: &str) -> TlsAcceptor { let certs = load_certs(cert_path); let key = load_key(key_path); let mut server_config = ServerConfig::builder() - .with_safe_defaults() .with_no_client_auth() .with_single_cert(certs, key) .expect("could not create server config"); @@ -167,22 +223,20 @@ pub fn acceptor(cert_path: &str, key_path: &str) -> TlsAcceptor { TlsAcceptor::from(Arc::new(server_config)) } -fn load_certs(path: &str) -> Vec { +fn load_certs(path: &str) -> Vec> { let mut reader = BufReader::new(File::open(path).expect("could not open certificate")); rustls_pemfile::certs(&mut reader) + .collect::, _>>() .expect("could not parse certificate") - .into_iter() - .map(Certificate) - .collect() } -fn load_key(path: &str) -> PrivateKey { +fn load_key(path: &str) -> PrivateKeyDer<'static> { let mut reader = BufReader::new(File::open(path).expect("could not open private key")); loop { match rustls_pemfile::read_one(&mut reader).expect("could not parse private key") { - Some(rustls_pemfile::Item::RSAKey(key)) => return PrivateKey(key), - Some(rustls_pemfile::Item::PKCS8Key(key)) => return PrivateKey(key), - Some(rustls_pemfile::Item::ECKey(key)) => return PrivateKey(key), + Some(rustls_pemfile::Item::Pkcs1Key(key)) => return key.into(), + Some(rustls_pemfile::Item::Pkcs8Key(key)) => return key.into(), + Some(rustls_pemfile::Item::Sec1Key(key)) => return key.into(), None => break, _ => {} } diff --git a/examples/pokemon-service-tls/tests/common/mod.rs b/examples/pokemon-service-tls/tests/common/mod.rs index 8954365a205..2cce4127e7d 100644 --- a/examples/pokemon-service-tls/tests/common/mod.rs +++ b/examples/pokemon-service-tls/tests/common/mod.rs @@ -3,79 +3,85 @@ * SPDX-License-Identifier: Apache-2.0 */ -use std::{fs::File, io::BufReader, process::Command, time::Duration}; +use std::{ + io::{BufRead, BufReader}, + process::{Command, Stdio}, + time::Duration, +}; -use assert_cmd::prelude::*; -use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder; -use tokio::time::sleep; +use aws_smithy_http_client::{Builder, tls}; +use tokio::time::timeout; use pokemon_service_client::{Client, Config}; use pokemon_service_common::ChildDrop; -use pokemon_service_tls::{DEFAULT_DOMAIN, DEFAULT_PORT, DEFAULT_TEST_CERT}; +use pokemon_service_tls::{DEFAULT_DOMAIN, DEFAULT_TEST_CERT}; -pub async fn run_server() -> ChildDrop { - let crate_name = std::env::var("CARGO_PKG_NAME").unwrap(); - let child = Command::cargo_bin(crate_name).unwrap().spawn().unwrap(); +pub struct ServerHandle { + pub child: ChildDrop, + pub port: u16, +} + +pub async fn run_server() -> ServerHandle { + let mut child = Command::new(assert_cmd::cargo::cargo_bin!("pokemon-service-tls")) + .args(["--port", "0"]) // Use port 0 for random available port + .stderr(Stdio::piped()) + .spawn() + .unwrap(); - sleep(Duration::from_millis(500)).await; + // Wait for the server to signal it's ready by reading stderr + let stderr = child.stderr.take().unwrap(); + let ready_signal = tokio::task::spawn_blocking(move || { + let reader = BufReader::new(stderr); + for line in reader.lines() { + if let Ok(line) = line { + if let Some(port_str) = line.strip_prefix("SERVER_READY:") { + if let Ok(port) = port_str.parse::() { + return Some(port); + } + } + } + } + None + }); - ChildDrop(child) + // Wait for the ready signal with a timeout + let port = match timeout(Duration::from_secs(5), ready_signal).await { + Ok(Ok(Some(port))) => port, + _ => { + panic!("Server did not become ready within 5 seconds"); + } + }; + + ServerHandle { + child: ChildDrop(child), + port, + } } // Returns a client that only talks through https and http2 connections. // It is useful in testing whether our server can talk to http2. -pub fn client_http2_only() -> Client { +pub fn client_http2_only(port: u16) -> Client { // Create custom cert store and add our test certificate to prevent unknown cert issues. - let mut reader = - BufReader::new(File::open(DEFAULT_TEST_CERT).expect("could not open certificate")); - let certs = rustls_pemfile::certs(&mut reader).expect("could not parse certificate"); - let mut roots = tokio_rustls::rustls::RootCertStore::empty(); - roots.add_parsable_certificates(&certs); - - let connector = hyper_rustls::HttpsConnectorBuilder::new() - .with_tls_config( - tokio_rustls::rustls::ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(roots) - .with_no_client_auth(), - ) - .https_only() - .enable_http2() - .build(); - - let config = Config::builder() - .http_client(HyperClientBuilder::new().build(connector)) - .endpoint_url(format!("https://{DEFAULT_DOMAIN}:{DEFAULT_PORT}")) - .build(); - Client::from_conf(config) -} - -/// A `hyper` connector that uses the `native-tls` crate for TLS. To use this in a Smithy client, -/// wrap with a [`HyperClientBuilder`]. -pub type NativeTlsConnector = hyper_tls::HttpsConnector; + let cert_pem = std::fs::read(DEFAULT_TEST_CERT).expect("could not open certificate"); -fn native_tls_connector() -> NativeTlsConnector { - let cert = hyper_tls::native_tls::Certificate::from_pem( - std::fs::read_to_string(DEFAULT_TEST_CERT) - .expect("could not open certificate") - .as_bytes(), - ) - .expect("could not parse certificate"); + let trust_store = tls::TrustStore::empty() + .with_native_roots(false) + .with_pem_certificate(cert_pem); - let tls_connector = hyper_tls::native_tls::TlsConnector::builder() - .min_protocol_version(Some(hyper_tls::native_tls::Protocol::Tlsv12)) - .add_root_certificate(cert) + let tls_context = tls::TlsContext::builder() + .with_trust_store(trust_store) .build() - .unwrap_or_else(|e| panic!("error while creating TLS connector: {}", e)); - let mut http_connector = hyper::client::HttpConnector::new(); - http_connector.enforce_http(false); - hyper_tls::HttpsConnector::from((http_connector, tls_connector.into())) -} + .expect("failed to build TLS context"); + + let http_client = Builder::new() + .tls_provider(tls::Provider::Rustls(tls::rustls_provider::CryptoMode::AwsLc)) + .tls_context(tls_context) + .build_https(); -pub fn native_tls_client() -> Client { let config = Config::builder() - .http_client(HyperClientBuilder::new().build(native_tls_connector())) - .endpoint_url(format!("https://{DEFAULT_DOMAIN}:{DEFAULT_PORT}")) + .http_client(http_client) + .endpoint_url(format!("https://{DEFAULT_DOMAIN}:{port}")) .build(); Client::from_conf(config) } + diff --git a/examples/pokemon-service-tls/tests/custom_connectors.rs b/examples/pokemon-service-tls/tests/custom_connectors.rs index a65881afe10..e8b8006ef0b 100644 --- a/examples/pokemon-service-tls/tests/custom_connectors.rs +++ b/examples/pokemon-service-tls/tests/custom_connectors.rs @@ -5,25 +5,12 @@ pub mod common; -use serial_test::serial; - // This test invokes an operation with a client that can only send HTTP2 requests and whose TLS // implementation is backed by `rustls`. #[tokio::test] -#[serial] async fn test_do_nothing_http2_rustls_connector() { - let _child = common::run_server().await; - let client = common::client_http2_only(); - - let _check_health = client.do_nothing().send().await.unwrap(); -} - -// This test invokes an operation with a client whose TLS implementation is backed by `native_tls`. -#[tokio::test] -#[serial] -async fn test_do_nothing_native_tls_connector() { - let _child = common::run_server().await; - let client = common::native_tls_client(); + let server = common::run_server().await; + let client = common::client_http2_only(server.port); let _check_health = client.do_nothing().send().await.unwrap(); } diff --git a/examples/pokemon-service/Cargo.toml b/examples/pokemon-service/Cargo.toml index 5afe717c6ba..e8b9fc26e0b 100644 --- a/examples/pokemon-service/Cargo.toml +++ b/examples/pokemon-service/Cargo.toml @@ -8,9 +8,10 @@ description = "A smithy Rust service to retrieve information about Pokémon." [dependencies] clap = { version = "4", features = ["derive"] } -http = "0.2" -hyper = { version = "0.14.26", features = ["server"] } -tokio = "1.26.0" +http = "1" +hyper = { version = "1", features = ["server"] } +hyper-util = { version = "0.1", features = ["tokio", "server", "server-auto", "service"] } +tokio = { version = "1.26.0", features = ["rt-multi-thread", "macros"] } tower = "0.4" tracing = "0.1" @@ -21,17 +22,20 @@ pokemon-service-common = { path = "../pokemon-service-common/" } [dev-dependencies] assert_cmd = "2.0" async-stream = "0.3" +bytes = "1" +http-body-util = "0.1" rand = "0.8.5" serial_test = "3.1.1" # We use hyper client in tests -hyper = { version = "0.14.26", features = ["server", "client"] } +hyper = { version = "1", features = ["server", "client"] } +hyper-util = { version = "0.1", features = ["client", "client-legacy", "http1", "http2"] } # This dependency is only required for testing the `pokemon-service-tls` program. -hyper-rustls = { version = "0.24", features = ["http2"] } +hyper-rustls = { version = "0.27", features = ["http2"] } # Local paths -aws-smithy-http = { path = "../../rust-runtime/aws-smithy-http/" } +aws-smithy-runtime = { path = "../../rust-runtime/aws-smithy-runtime" } pokemon-service-client = { path = "../pokemon-service-client/", features = [ "behavior-version-latest", ] } diff --git a/examples/pokemon-service/src/main.rs b/examples/pokemon-service/src/main.rs index 4420553a4d1..6cc453a61bb 100644 --- a/examples/pokemon-service/src/main.rs +++ b/examples/pokemon-service/src/main.rs @@ -15,10 +15,11 @@ use pokemon_service_server_sdk::server::{ layer::alb_health_check::AlbHealthCheckLayer, plugin::{HttpPlugins, ModelPlugins, Scoped}, request::request_id::ServerRequestIdProviderLayer, - AddExtensionLayer, + serve, AddExtensionLayer, }; +use tokio::net::TcpListener; -use hyper::StatusCode; +use http::StatusCode; use plugin::PrintExt; use pokemon_service::{ @@ -106,10 +107,18 @@ pub async fn main() { let bind: SocketAddr = format!("{}:{}", args.address, args.port) .parse() .expect("unable to parse the server bind address and port"); - let server = hyper::Server::bind(&bind).serve(make_app); + let listener = TcpListener::bind(bind) + .await + .expect("failed to bind TCP listener"); + + // Get the actual bound address (important when port 0 is used for random port) + let actual_addr = listener.local_addr().expect("failed to get local address"); + + // Signal that the server is ready to accept connections, including the actual port + eprintln!("SERVER_READY:{}", actual_addr.port()); // Run forever-ish... - if let Err(err) = server.await { - eprintln!("server error: {}", err); + if let Err(err) = serve(listener, make_app).await { + eprintln!("server error: {err}"); } } diff --git a/examples/pokemon-service/tests/common/mod.rs b/examples/pokemon-service/tests/common/mod.rs index 2d58f4a975a..d8a13280a27 100644 --- a/examples/pokemon-service/tests/common/mod.rs +++ b/examples/pokemon-service/tests/common/mod.rs @@ -3,31 +3,67 @@ * SPDX-License-Identifier: Apache-2.0 */ -use std::{process::Command, time::Duration}; +use std::{ + io::{BufRead, BufReader}, + process::{Command, Stdio}, + time::Duration, +}; -use assert_cmd::prelude::*; -use tokio::time::sleep; +use tokio::time::timeout; -use pokemon_service::{DEFAULT_ADDRESS, DEFAULT_PORT}; +use pokemon_service::DEFAULT_ADDRESS; use pokemon_service_client::{Client, Config}; use pokemon_service_common::ChildDrop; -pub async fn run_server() -> ChildDrop { - let crate_name = std::env::var("CARGO_PKG_NAME").unwrap(); - let child = Command::cargo_bin(crate_name).unwrap().spawn().unwrap(); +pub struct ServerHandle { + pub child: ChildDrop, + pub port: u16, +} + +pub async fn run_server() -> ServerHandle { + let mut child = Command::new(assert_cmd::cargo::cargo_bin!("pokemon-service")) + .args(["--port", "0"]) // Use port 0 for random available port + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + + // Wait for the server to signal it's ready by reading stderr + let stderr = child.stderr.take().unwrap(); + let ready_signal = tokio::task::spawn_blocking(move || { + let reader = BufReader::new(stderr); + for line in reader.lines() { + if let Ok(line) = line { + if let Some(port_str) = line.strip_prefix("SERVER_READY:") { + if let Ok(port) = port_str.parse::() { + return Some(port); + } + } + } + } + None + }); - sleep(Duration::from_millis(500)).await; + // Wait for the ready signal with a timeout + let port = match timeout(Duration::from_secs(5), ready_signal).await { + Ok(Ok(Some(port))) => port, + _ => { + panic!("Server did not become ready within 5 seconds"); + } + }; - ChildDrop(child) + ServerHandle { + child: ChildDrop(child), + port, + } } -pub fn base_url() -> String { - format!("http://{DEFAULT_ADDRESS}:{DEFAULT_PORT}") +pub fn base_url(port: u16) -> String { + format!("http://{DEFAULT_ADDRESS}:{port}") } -pub fn client() -> Client { +pub fn client(port: u16) -> Client { let config = Config::builder() - .endpoint_url(format!("http://{DEFAULT_ADDRESS}:{DEFAULT_PORT}")) + .endpoint_url(format!("http://{DEFAULT_ADDRESS}:{port}")) .build(); Client::from_conf(config) } diff --git a/examples/pokemon-service/tests/event_streaming.rs b/examples/pokemon-service/tests/event_streaming.rs index 664827620bb..dd68eefed18 100644 --- a/examples/pokemon-service/tests/event_streaming.rs +++ b/examples/pokemon-service/tests/event_streaming.rs @@ -7,7 +7,6 @@ pub mod common; use async_stream::stream; use rand::Rng; -use serial_test::serial; use pokemon_service_client::types::{ error::{AttemptCapturingPokemonEventError, MasterBallUnsuccessful}, @@ -34,10 +33,9 @@ fn get_pokeball() -> String { } #[tokio::test] -#[serial] async fn event_stream_test() { - let _child = common::run_server().await; - let client = common::client(); + let server = common::run_server().await; + let client = common::client(server.port); let mut team = vec![]; let input_stream = stream! { @@ -71,7 +69,7 @@ async fn event_stream_test() { }; // Throw many! - let mut output = common::client() + let mut output = common::client(server.port) .capture_pokemon() .region("Kanto") .events(input_stream.into()) diff --git a/examples/pokemon-service/tests/simple.rs b/examples/pokemon-service/tests/simple.rs index dc9f88d0b53..2b24af7eddc 100644 --- a/examples/pokemon-service/tests/simple.rs +++ b/examples/pokemon-service/tests/simple.rs @@ -3,20 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ +use bytes; +use http_body_util; +use hyper_util::{client::legacy::Client, rt::TokioExecutor}; use pokemon_service_client::{ error::{DisplayErrorContext, SdkError}, operation::get_storage::GetStorageError, types::error::StorageAccessNotAuthorized, }; -use serial_test::serial; pub mod common; #[tokio::test] -#[serial] async fn simple_integration_test() { - let _child = common::run_server().await; - let client = common::client(); + let server = common::run_server().await; + let client = common::client(server.port); let service_statistics_out = client.get_server_statistics().send().await.unwrap(); assert_eq!(0, service_statistics_out.calls_count); @@ -82,38 +83,42 @@ async fn simple_integration_test() { let service_statistics_out = client.get_server_statistics().send().await.unwrap(); assert_eq!(2, service_statistics_out.calls_count); - let hyper_client = hyper::Client::new(); - let health_check_url = format!("{}/ping", common::base_url()); + let hyper_client = Client::builder(TokioExecutor::new()).build_http(); + let health_check_url = format!("{}/ping", common::base_url(server.port)); let health_check_url = hyper::Uri::try_from(health_check_url).unwrap(); - let result = hyper_client.get(health_check_url).await.unwrap(); + let request = hyper::Request::builder() + .uri(health_check_url) + .body(http_body_util::Empty::::new()) + .unwrap(); + let result = hyper_client.request(request).await.unwrap(); assert_eq!(result.status(), 200); } #[tokio::test] -#[serial] async fn health_check() { - let _child = common::run_server().await; + let server = common::run_server().await; - use pokemon_service::{DEFAULT_ADDRESS, DEFAULT_PORT}; - let url = format!("http://{DEFAULT_ADDRESS}:{DEFAULT_PORT}/ping"); + let url = common::base_url(server.port) + "/ping"; let uri = url.parse::().expect("invalid URL"); // Since the `/ping` route is not modeled in Smithy, we use a regular // Hyper HTTP client to make a request to it. let request = hyper::Request::builder() .uri(uri) - .body(hyper::Body::empty()) + .body(http_body_util::Empty::::new()) .expect("failed to build request"); - let response = hyper::Client::new() + let client = Client::builder(TokioExecutor::new()).build_http(); + let response = client .request(request) .await .expect("failed to get response"); assert_eq!(response.status(), hyper::StatusCode::OK); - let body = hyper::body::to_bytes(response.into_body()) + let body = http_body_util::BodyExt::collect(response.into_body()) .await - .expect("failed to read response body"); + .expect("failed to read response body") + .to_bytes(); assert!(body.is_empty()); } diff --git a/examples/upgrade-http1x.md b/examples/upgrade-http1x.md new file mode 100644 index 00000000000..4b7281e629c --- /dev/null +++ b/examples/upgrade-http1x.md @@ -0,0 +1,340 @@ +# Upgrading from http@0.2/hyper@0.14 to http@1.x/hyper@1.x + +This guide provides a comprehensive walkthrough for upgrading your smithy-rs server applications from http@0.2.x and hyper@0.14 to http@1.x and hyper@1.x. + +## Table of Contents + +- [Overview](#overview) +- [Why Upgrade?](#why-upgrade) +- [Before You Begin](#before-you-begin) +- [Dependency Updates](#dependency-updates) +- [Server-Side Changes](#server-side-changes) +- [Client-Side Changes](#client-side-changes) +- [Test Infrastructure Updates](#test-infrastructure-updates) +- [Common Migration Patterns](#common-migration-patterns) +- [Troubleshooting](#troubleshooting) +- [Migration Checklist](#migration-checklist) + +## Overview + +The http and hyper crates have released major version updates (http@1.x and hyper@1.x) with significant API improvements and breaking changes. This guide helps you migrate your smithy-rs server applications to these new versions. + +**Key Changes:** +- http: 0.2.x → 1.x +- hyper: 0.14.x → 1.x +- New hyper-util crate for additional utilities + +## Why Upgrade? + +- **Improved API**: More ergonomic and safer APIs in both http and hyper +- **Active Support**: Future updates and bug fixes will target 1.x versions +- **Ecosystem Alignment**: New libraries are targeting http@1.x and hyper@1.x +- **Security Updates**: Continued security patches for 1.x line + +## Before You Begin + +**Important Considerations:** + +1. **Breaking Changes**: This is a major version upgrade with breaking API changes +2. **Testing Required**: Thoroughly test your application after migration +3. **Gradual Migration**: Consider migrating one service at a time +4. **Legacy Examples**: The `examples/legacy/` directory contains fully working http@0.2 examples for reference + +## Dependency Updates + +### Cargo.toml Changes + +#### Server Dependencies + +**Before (http@0.2/hyper@0.14):** +```toml +[dependencies] +http = "0.2" +hyper = { version = "0.14.26", features = ["server"] } +tokio = "1.26.0" +tower = "0.4" +``` + +**After (http@1.x/hyper@1.x):** +```toml +[dependencies] +http = "1" +hyper = { version = "1", features = ["server"] } +hyper-util = { version = "0.1", features = ["tokio", "server", "server-auto", "service"] } +tokio = { version = "1.26.0", features = ["rt-multi-thread", "macros"] } +tower = "0.4" +``` + +**Key Changes:** +- `http`: `0.2` → `1` +- `hyper`: `0.14.26` → `1` +- **New**: `hyper-util` crate for server and client utilities +- **New**: `bytes` and `http-body-util` for body handling +- `hyper-rustls`: `0.24` → `0.27` + +## Server-Side Changes + +### 1. Server Initialization + +**Before (hyper@0.14):** +```rust +#[tokio::main] +pub async fn main() { + // ... setup config and app ... + + let make_app = app.into_make_service_with_connect_info::(); + + let bind: SocketAddr = format!("{}:{}", args.address, args.port) + .parse() + .expect("unable to parse the server bind address and port"); + + let server = hyper::Server::bind(&bind).serve(make_app); + + if let Err(err) = server.await { + eprintln!("server error: {err}"); + } +} +``` + +**After (hyper@1.x):** +```rust +#[tokio::main] +pub async fn main() { + // ... setup config and app ... + + let make_app = app.into_make_service_with_connect_info::(); + + let bind: SocketAddr = format!("{}:{}", args.address, args.port) + .parse() + .expect("unable to parse the server bind address and port"); + + let listener = TcpListener::bind(bind) + .await + .expect("failed to bind TCP listener"); + + // Optional: Get the actual bound address (useful for port 0) + let actual_addr = listener.local_addr().expect("failed to get local address"); + eprintln!("Server listening on {}", actual_addr); + + if let Err(err) = serve(listener, make_app).await { + eprintln!("server error: {err}"); + } +} +``` + +**Key Changes:** +1. Replace `hyper::Server::bind(&bind)` with `TcpListener::bind(bind).await` +2. Use the `serve()` helper function instead of `.serve(make_app)` +3. Can get actual bound address with `.local_addr()` (useful for testing with port 0) + +### 2. Service Building + +The service building API remains the same: + +```rust +let app = PokemonService::builder(config) + .get_pokemon_species(get_pokemon_species) + .get_storage(get_storage_with_local_approved) + .get_server_statistics(get_server_statistics) + .capture_pokemon(capture_pokemon) + .do_nothing(do_nothing_but_log_request_ids) + .check_health(check_health) + .build() + .expect("failed to build an instance of PokemonService"); + +let make_app = app.into_make_service_with_connect_info::(); +``` + +No changes needed here! The service builder API is stable. + +## Client-Side Changes + +### HTTP Client Connector Setup + +**Before (hyper@0.14 with hyper-rustls):** +```rust +use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder; +use hyper_rustls::ConfigBuilderExt; + +fn create_client() -> PokemonClient { + let tls_config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_native_roots() + .with_no_client_auth(); + + let tls_connector = hyper_rustls::HttpsConnectorBuilder::new() + .with_tls_config(tls_config) + .https_or_http() + .enable_http1() + .enable_http2() + .build(); + + let http_client = HyperClientBuilder::new().build(tls_connector); + + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .http_client(http_client) + .build(); + + pokemon_service_client::Client::from_conf(config) +} +``` + +**After (http@1.x with aws-smithy-http-client):** +```rust +use aws_smithy_http_client::{Builder, tls}; + +fn create_client() -> PokemonClient { + // Create a TLS context with platform trusted root certificates + let tls_context = tls::TlsContext::builder() + .with_trust_store(tls::TrustStore::default()) + .build() + .expect("failed to build TLS context"); + + // Create an HTTP client using rustls with AWS-LC crypto provider + let http_client = Builder::new() + .tls_provider(tls::Provider::Rustls(tls::rustls_provider::CryptoMode::AwsLc)) + .tls_context(tls_context) + .build_https(); + + let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .http_client(http_client) + .build(); + + pokemon_service_client::Client::from_conf(config) +} +``` + +**Key Changes:** +1. Replace `HyperClientBuilder` with `aws_smithy_http_client::Builder` +2. Use `tls::TlsContext` and `tls::TrustStore` instead of direct rustls config +3. Specify TLS provider explicitly (Rustls with AWS-LC or Ring crypto) +4. Use `.build_https()` instead of passing a connector + +### Using Default Client + +If you don't need custom TLS configuration, you can use the default client: + +```rust +// Default client with system trust store +let config = pokemon_service_client::Config::builder() + .endpoint_url(POKEMON_SERVICE_URL) + .build(); + +let client = pokemon_service_client::Client::from_conf(config); +``` + +## Common Migration Patterns + +### 1. Response Building + +**Before:** +```rust +use hyper::{Body, Response, StatusCode}; + +let response = Response::builder() + .status(StatusCode::OK) + .body(Body::from("Hello")) + .unwrap(); +``` + +**After:** +```rust +use aws_smithy_http_server::http::{Response, StatusCode}; +use http_body_util::Full; +use bytes::Bytes; + +let response = Response::builder() + .status(StatusCode::OK) + .body(Full::new(Bytes::from("Hello"))) + .unwrap(); +``` + +### 2. Middleware and Layers + +Tower layers continue to work the same way: + +```rust +// No changes needed for tower layers +let config = PokemonServiceConfig::builder() + .layer(AddExtensionLayer::new(Arc::new(State::default()))) + .layer(AlbHealthCheckLayer::from_handler("/ping", |_req| async { + StatusCode::OK + })) + .layer(ServerRequestIdProviderLayer::new()) + .build(); +``` + +## Troubleshooting + +### Common Errors and Solutions + +#### 1. "no method named `serve` found for struct `hyper::Server`" + +**Error:** +``` +error[E0599]: no method named `serve` found for struct `hyper::Server` in the current scope +``` + +**Solution:** +Use `TcpListener` with the `serve()` function instead: +```rust +let listener = TcpListener::bind(bind).await?; +serve(listener, make_app).await?; +``` + +#### 2. "the trait `tower::Service` is not implemented" + +**Error:** +``` +error[E0277]: the trait `tower::Service>` is not implemented for `...` +``` + +**Solution:** +Make sure you're using `hyper-util` with the right features: +```toml +hyper-util = { version = "0.1", features = ["tokio", "server", "server-auto", "service"] } +``` + +#### 3. "cannot find `HyperClientBuilder` in module" + +**Error:** +``` +error[E0432]: unresolved import `aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder` +``` + +**Solution:** +Use the new client builder: +```rust +use aws_smithy_http_client::Builder; + +let http_client = Builder::new() + .build_https(); +``` + +#### 4. Type mismatch with `Body` or `Bytes` + +**Error:** +``` +error[E0308]: mismatched types +expected struct `hyper::body::Incoming` +found struct `http_body_util::combinators::boxed::UnsyncBoxBody<...>` +``` + +**Solution:** +Add `http-body-util` and `bytes` dependencies: +```toml +bytes = "1" +http-body-util = "0.1" +``` + +Then use the appropriate body types from `http-body-util`. + +### Getting Help + +- **Examples**: Check the `examples/` directory for working http@1.x code +- **Legacy Examples**: Check `examples/legacy/` for http@0.2 reference +- **Documentation**: https://docs.rs/aws-smithy-http-server/ +- **GitHub Issues**: https://github.com/smithy-lang/smithy-rs/issues +