mirror of https://github.com/smithy-lang/smithy-rs
Add TLS support to Pokemon server (#1634)
* Add TLS support to Pokemon server * Add missing copyright header to TLS module * Handle connection errors * Move TLS example to own binary * Update comments according to reviews * Move rewrite base url middleware to its own function
This commit is contained in:
parent
e185990a0e
commit
86539f2b08
|
@ -5,6 +5,11 @@ edition = "2021"
|
|||
publish = false
|
||||
authors = ["Smithy-rs Server Team <smithy-rs-server@amazon.com>"]
|
||||
description = "A smithy Rust service to retrieve information about Pokémon."
|
||||
default-run = "pokemon-service"
|
||||
|
||||
[[bin]]
|
||||
name = "pokemon-service-tls"
|
||||
path = "src/bin/pokemon-service-tls.rs"
|
||||
|
||||
[dependencies]
|
||||
async-stream = "0.3"
|
||||
|
@ -17,6 +22,12 @@ tower-http = { version = "0.3", features = ["trace"] }
|
|||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# These dependencies are only required for `pokemon-service-tls`.
|
||||
tls-listener = { version = "0.5.1", features = ["rustls", "hyper-h2"] }
|
||||
tokio-rustls = "0.23.4"
|
||||
rustls-pemfile = "1.0.1"
|
||||
futures-util = "0.3"
|
||||
|
||||
# Local paths
|
||||
aws-smithy-http-server = { path = "../../" }
|
||||
pokemon-service-server-sdk = { path = "../pokemon-service-server-sdk/" }
|
||||
|
@ -27,6 +38,10 @@ home = "0.5"
|
|||
serial_test = "0.7.0"
|
||||
wrk-api-bench = "0.0.7"
|
||||
|
||||
# These dependencies are only required for testing `pokemon-service-tls`.
|
||||
hyper-rustls = { version = "0.23.0", features = ["http2"] }
|
||||
|
||||
# Local paths
|
||||
aws-smithy-client = { path = "../../../aws-smithy-client/", features = ["rustls"] }
|
||||
aws-smithy-client = { path = "../../../aws-smithy-client/", features = ["rustls"] }
|
||||
aws-smithy-http = { path = "../../../aws-smithy-http/" }
|
||||
pokemon-service-client = { path = "../pokemon-service-client/" }
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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;
|
||||
use std::future;
|
||||
use std::io::BufReader;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use aws_smithy_http_server::{AddExtensionLayer, Router};
|
||||
use clap::Parser;
|
||||
use futures_util::stream::StreamExt;
|
||||
use pokemon_service::{
|
||||
capture_pokemon, empty_operation, get_pokemon_species, get_server_statistics, get_storage, health_check_operation,
|
||||
setup_tracing, State,
|
||||
};
|
||||
use pokemon_service_server_sdk::operation_registry::OperationRegistryBuilder;
|
||||
use tokio_rustls::{
|
||||
rustls::{Certificate, PrivateKey, ServerConfig},
|
||||
TlsAcceptor,
|
||||
};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
struct Args {
|
||||
/// Hyper server bind address.
|
||||
#[clap(short, long, action, default_value = "127.0.0.1")]
|
||||
address: String,
|
||||
/// Hyper server bind port.
|
||||
#[clap(short, long, action, default_value = "13734")]
|
||||
port: u16,
|
||||
/// Hyper server TLS certificate path. Must be a PEM file.
|
||||
#[clap(long, default_value = "")]
|
||||
tls_cert_path: String,
|
||||
/// Hyper server TLS private key path. Must be a PEM file.
|
||||
#[clap(long, default_value = "")]
|
||||
tls_key_path: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() {
|
||||
let args = Args::parse();
|
||||
setup_tracing();
|
||||
let app: Router = OperationRegistryBuilder::default()
|
||||
// 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_operation(capture_pokemon)
|
||||
.empty_operation(empty_operation)
|
||||
.health_check_operation(health_check_operation)
|
||||
.build()
|
||||
.expect("Unable to build operation registry")
|
||||
// Convert it into a router that will route requests to the matching operation
|
||||
// implementation.
|
||||
.into();
|
||||
|
||||
// Setup shared state and middlewares.
|
||||
let shared_state = Arc::new(State::default());
|
||||
let app = app.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(AddExtensionLayer::new(shared_state)),
|
||||
);
|
||||
|
||||
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"),
|
||||
)
|
||||
.filter(|conn| {
|
||||
if let Err(err) = conn {
|
||||
eprintln!("connection error: {:?}", err);
|
||||
future::ready(false)
|
||||
} else {
|
||||
future::ready(true)
|
||||
}
|
||||
});
|
||||
let server = hyper::Server::builder(hyper::server::accept::from_stream(listener)).serve(app.into_make_service());
|
||||
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<Certificate> {
|
||||
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")
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// This program is exported as a binary named `pokemon_service`.
|
||||
// This program is exported as a binary named `pokemon-service`.
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use aws_smithy_http_server::{AddExtensionLayer, Router};
|
||||
|
|
|
@ -3,20 +3,67 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::process::{Child, Command};
|
||||
use std::time::Duration;
|
||||
|
||||
use assert_cmd::prelude::*;
|
||||
use aws_smithy_client::{erase::DynConnector, hyper_ext::Adapter};
|
||||
use aws_smithy_http::operation::Request;
|
||||
use pokemon_service_client::{Builder, Client, Config};
|
||||
use std::process::Command;
|
||||
use tokio::time;
|
||||
|
||||
const TEST_KEY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/testdata/localhost.key");
|
||||
const TEST_CERT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/testdata/localhost.crt");
|
||||
|
||||
enum PokemonServiceVariant {
|
||||
Http,
|
||||
Https,
|
||||
}
|
||||
|
||||
impl PokemonServiceVariant {
|
||||
async fn run_process(&self) -> Child {
|
||||
let process = match self {
|
||||
PokemonServiceVariant::Http => Command::cargo_bin("pokemon-service").unwrap().spawn().unwrap(),
|
||||
PokemonServiceVariant::Https => Command::cargo_bin("pokemon-service-tls")
|
||||
.unwrap()
|
||||
.args(["--tls-cert-path", TEST_CERT, "--tls-key-path", TEST_KEY])
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
};
|
||||
|
||||
// Give PokémonService some time to start up.
|
||||
time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
process
|
||||
}
|
||||
|
||||
fn base_url(&self) -> String {
|
||||
match self {
|
||||
PokemonServiceVariant::Http => "http://localhost:13734".to_string(),
|
||||
PokemonServiceVariant::Https => "https://localhost:13734".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PokemonService {
|
||||
child_process: std::process::Child,
|
||||
child_process: Child,
|
||||
}
|
||||
|
||||
impl PokemonService {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn run() -> Self {
|
||||
let process = Command::cargo_bin("pokemon-service").unwrap().spawn().unwrap();
|
||||
pub(crate) async fn run() -> Self {
|
||||
Self {
|
||||
child_process: PokemonServiceVariant::Http.run_process().await,
|
||||
}
|
||||
}
|
||||
|
||||
Self { child_process: process }
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn run_https() -> Self {
|
||||
Self {
|
||||
child_process: PokemonServiceVariant::Https.run_process().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,15 +80,53 @@ pub fn client() -> Client<
|
|||
aws_smithy_client::erase::DynConnector,
|
||||
aws_smithy_client::erase::DynMiddleware<aws_smithy_client::erase::DynConnector>,
|
||||
> {
|
||||
let base_url = PokemonServiceVariant::Http.base_url();
|
||||
let raw_client = Builder::new()
|
||||
.rustls()
|
||||
.middleware_fn(|mut req| {
|
||||
let http_req = req.http_mut();
|
||||
let uri = format!("http://localhost:13734{}", http_req.uri().path());
|
||||
*http_req.uri_mut() = uri.parse().unwrap();
|
||||
req
|
||||
})
|
||||
.middleware_fn(rewrite_base_url(base_url))
|
||||
.build_dyn();
|
||||
let config = Config::builder().build();
|
||||
Client::with_config(raw_client, config)
|
||||
}
|
||||
|
||||
// Returns a client that only talks through https and http2 connections.
|
||||
// It is useful in testing whether our server can talk to http2.
|
||||
#[allow(dead_code)]
|
||||
pub fn client_http2_only() -> Client<
|
||||
aws_smithy_client::erase::DynConnector,
|
||||
aws_smithy_client::erase::DynMiddleware<aws_smithy_client::erase::DynConnector>,
|
||||
> {
|
||||
// Create custom cert store and add our test certificate to prevent unknown cert issues.
|
||||
let mut reader = BufReader::new(File::open(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 base_url = PokemonServiceVariant::Https.base_url();
|
||||
let raw_client = Builder::new()
|
||||
.connector(DynConnector::new(Adapter::builder().build(connector)))
|
||||
.middleware_fn(rewrite_base_url(base_url))
|
||||
.build_dyn();
|
||||
let config = Config::builder().build();
|
||||
Client::with_config(raw_client, config)
|
||||
}
|
||||
|
||||
fn rewrite_base_url(base_url: String) -> impl Fn(Request) -> Request + Clone {
|
||||
move |mut req| {
|
||||
let http_req = req.http_mut();
|
||||
let uri = format!("{base_url}{}", http_req.uri().path());
|
||||
*http_req.uri_mut() = uri.parse().unwrap();
|
||||
req
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,7 @@
|
|||
// These tests only have access to your crate's public API.
|
||||
// See: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::helpers::{client, PokemonService};
|
||||
use crate::helpers::{client, client_http2_only, PokemonService};
|
||||
|
||||
use async_stream::stream;
|
||||
use pokemon_service_client::{
|
||||
|
@ -22,7 +20,6 @@ use pokemon_service_client::{
|
|||
};
|
||||
use rand::Rng;
|
||||
use serial_test::serial;
|
||||
use tokio::time;
|
||||
|
||||
mod helpers;
|
||||
|
||||
|
@ -48,19 +45,23 @@ fn get_pokemon_to_capture() -> String {
|
|||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_health_check_operation() {
|
||||
let _program = PokemonService::run();
|
||||
// Give PokémonService some time to start up.
|
||||
time::sleep(Duration::from_millis(500)).await;
|
||||
let _program = PokemonService::run().await;
|
||||
|
||||
let _health_check = client().health_check_operation().send().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_health_check_operation_http2() {
|
||||
// Make sure our server can serve http2
|
||||
let _program = PokemonService::run_https().await;
|
||||
let _health_check = client_http2_only().health_check_operation().send().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn simple_integration_test() {
|
||||
let _program = PokemonService::run();
|
||||
// Give PokémonService some time to start up.
|
||||
time::sleep(Duration::from_millis(500)).await;
|
||||
let _program = PokemonService::run().await;
|
||||
|
||||
let service_statistics_out = client().get_server_statistics().send().await.unwrap();
|
||||
assert_eq!(0, service_statistics_out.calls_count.unwrap());
|
||||
|
@ -112,9 +113,7 @@ async fn simple_integration_test() {
|
|||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn event_stream_test() {
|
||||
let _program = PokemonService::run();
|
||||
// Give PokémonService some time to start up.
|
||||
time::sleep(Duration::from_millis(500)).await;
|
||||
let _program = PokemonService::run().await;
|
||||
|
||||
let mut team = vec![];
|
||||
let input_stream = stream! {
|
||||
|
|
30
rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/testdata/localhost.crt
vendored
Normal file
30
rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/testdata/localhost.crt
vendored
Normal file
|
@ -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-----
|
52
rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/testdata/localhost.key
vendored
Normal file
52
rust-runtime/aws-smithy-http-server/examples/pokemon-service/tests/testdata/localhost.key
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
M8nPgza9amAhHxA8xPQgBs3l8d6k3w==
|
||||
-----END PRIVATE KEY-----
|
Loading…
Reference in New Issue