Skip to content

Commit 9d2d088

Browse files
authored
Python: TLS Support for servers (#2002)
* Add `PyTlsConfig` struct * Support TLS server * Add TLS support to Pokemon service * Make sure to create `tokio::net::TcpListener` in a Tokio context * Fix doc link * Add missing imports in tests * Add `tls::Listener` * Reload TLS config periodically * Add context to `TODO` * Return `&'static str` from `base_url()` * Flatten `match` in `tls::Listener` * Propogate listener errors but ignore handshake errors in `tls::Listener` * Add tests to `tls::Listener` * Add test to `tls::Listener` to make sure we are propogating listener errors * Use `PathBuf` instead of plain `String`s for paths
1 parent b2b8e7e commit 9d2d088

File tree

13 files changed

+821
-55
lines changed

13 files changed

+821
-55
lines changed

codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,17 +264,18 @@ class PythonApplicationGenerator(
264264
Ok(())
265265
}
266266
/// Main entrypoint: start the server on multiple workers.
267-
##[pyo3(text_signature = "(${'$'}self, address, port, backlog, workers)")]
267+
##[pyo3(text_signature = "(${'$'}self, address, port, backlog, workers, tls)")]
268268
pub fn run(
269269
&mut self,
270270
py: #{pyo3}::Python,
271271
address: Option<String>,
272272
port: Option<i32>,
273273
backlog: Option<i32>,
274274
workers: Option<usize>,
275+
tls: Option<#{SmithyPython}::tls::PyTlsConfig>,
275276
) -> #{pyo3}::PyResult<()> {
276277
use #{SmithyPython}::PyApp;
277-
self.run_server(py, address, port, backlog, workers)
278+
self.run_server(py, address, port, backlog, workers, tls)
278279
}
279280
/// Lambda entrypoint: start the server on Lambda.
280281
##[cfg(feature = "aws-lambda")]
@@ -287,17 +288,18 @@ class PythonApplicationGenerator(
287288
self.run_lambda_handler(py)
288289
}
289290
/// Build the service and start a single worker.
290-
##[pyo3(text_signature = "(${'$'}self, socket, worker_number)")]
291+
##[pyo3(text_signature = "(${'$'}self, socket, worker_number, tls)")]
291292
pub fn start_worker(
292293
&mut self,
293294
py: pyo3::Python,
294295
socket: &pyo3::PyCell<#{SmithyPython}::PySocket>,
295296
worker_number: isize,
297+
tls: Option<#{SmithyPython}::tls::PyTlsConfig>,
296298
) -> pyo3::PyResult<()> {
297299
use #{SmithyPython}::PyApp;
298300
let event_loop = self.configure_python_event_loop(py)?;
299301
let service = self.build_and_configure_service(py, event_loop)?;
300-
self.start_hyper_worker(py, socket, event_loop, service, worker_number)
302+
self.start_hyper_worker(py, socket, event_loop, service, worker_number, tls)
301303
}
302304
""",
303305
*codegenScope,

codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerModuleGenerator.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class PythonServerModuleGenerator(
4848
renderPySocketType()
4949
renderPyLogging()
5050
renderPyMiddlewareTypes()
51+
renderPyTlsTypes()
5152
renderPyApplicationType()
5253
}
5354
}
@@ -162,6 +163,22 @@ class PythonServerModuleGenerator(
162163
)
163164
}
164165

166+
private fun RustWriter.renderPyTlsTypes() {
167+
rustTemplate(
168+
"""
169+
let tls = #{pyo3}::types::PyModule::new(py, "tls")?;
170+
tls.add_class::<#{SmithyPython}::tls::PyTlsConfig>()?;
171+
pyo3::py_run!(
172+
py,
173+
tls,
174+
"import sys; sys.modules['$libName.tls'] = tls"
175+
);
176+
m.add_submodule(tls)?;
177+
""",
178+
*codegenScope,
179+
)
180+
}
181+
165182
// Render Python application type.
166183
private fun RustWriter.renderPyApplicationType() {
167184
rustTemplate(

rust-runtime/aws-smithy-http-server-python/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ futures = "0.3"
2626
http = "0.2"
2727
hyper = { version = "0.14.20", features = ["server", "http1", "http2", "tcp", "stream"] }
2828
lambda_http = { version = "0.7.1", optional = true }
29+
tls-listener = { version = "0.5.1", features = ["rustls", "hyper-h2"] }
30+
rustls-pemfile = "1.0.1"
31+
tokio-rustls = "0.23.4"
2932
num_cpus = "1.13.1"
3033
parking_lot = "0.12.1"
3134
pin-project-lite = "0.2"
@@ -47,6 +50,8 @@ futures-util = "0.3"
4750
tower-test = "0.4"
4851
tokio-test = "0.4"
4952
pyo3-asyncio = { version = "0.17.0", features = ["testing", "attributes", "tokio-runtime"] }
53+
rcgen = "0.10.0"
54+
hyper-rustls = { version = "0.23.1", features = ["http2"] }
5055

5156
[[test]]
5257
name = "middleware_tests"

rust-runtime/aws-smithy-http-server-python/examples/pokemon-service-test/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ description = "Run tests against the Python server implementation"
99
[dev-dependencies]
1010
command-group = "1.0"
1111
tokio = { version = "1.20.1", features = ["full"] }
12+
serial_test = "0.9.0"
13+
rustls-pemfile = "1.0.1"
14+
tokio-rustls = "0.23.4"
15+
hyper-rustls = { version = "0.23.0", features = ["http2"] }
1216

1317
# Local paths
1418
aws-smithy-client = { path = "../../../aws-smithy-client/", features = ["rustls"] }
19+
aws-smithy-http = { path = "../../../aws-smithy-http/" }
1520
aws-smithy-types = { path = "../../../aws-smithy-types/" }
1621
pokemon-service-client = { path = "../pokemon-service-client/" }

rust-runtime/aws-smithy-http-server-python/examples/pokemon-service-test/tests/helpers.rs

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,61 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
use std::fs::File;
7+
use std::io::BufReader;
8+
use std::process::Command;
9+
use std::time::Duration;
10+
11+
use aws_smithy_client::{erase::DynConnector, hyper_ext::Adapter};
12+
use aws_smithy_http::operation::Request;
613
use command_group::{CommandGroup, GroupChild};
714
use pokemon_service_client::{Builder, Client, Config};
8-
use std::{process::Command, thread, time::Duration};
15+
use tokio::time;
16+
17+
const TEST_KEY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/testdata/localhost.key");
18+
const TEST_CERT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/testdata/localhost.crt");
19+
20+
pub type PokemonClient = Client<
21+
aws_smithy_client::erase::DynConnector,
22+
aws_smithy_client::erase::DynMiddleware<aws_smithy_client::erase::DynConnector>,
23+
>;
24+
25+
enum PokemonServiceVariant {
26+
Http,
27+
Http2,
28+
}
29+
30+
impl PokemonServiceVariant {
31+
async fn run_process(&self) -> GroupChild {
32+
let mut args = vec!["../pokemon_service.py".to_string()];
33+
34+
match self {
35+
PokemonServiceVariant::Http => {}
36+
PokemonServiceVariant::Http2 => {
37+
args.push("--enable-tls".to_string());
38+
args.push(format!("--tls-key-path={TEST_KEY}"));
39+
args.push(format!("--tls-cert-path={TEST_CERT}"));
40+
}
41+
}
42+
43+
let process = Command::new("python3")
44+
.args(args)
45+
.group_spawn()
46+
.expect("failed to spawn the Pokémon Service program");
47+
48+
// The Python interpreter takes a little to startup.
49+
time::sleep(Duration::from_secs(2)).await;
50+
51+
process
52+
}
53+
54+
fn base_url(&self) -> &'static str {
55+
match self {
56+
PokemonServiceVariant::Http => "http://localhost:13734",
57+
PokemonServiceVariant::Http2 => "https://localhost:13734",
58+
}
59+
}
60+
}
961

1062
pub(crate) struct PokemonService {
1163
// We need to ensure all processes forked by the Python interpreter
@@ -16,15 +68,16 @@ pub(crate) struct PokemonService {
1668

1769
impl PokemonService {
1870
#[allow(dead_code)]
19-
pub(crate) fn run() -> Self {
20-
let process = Command::new("python3")
21-
.arg("../pokemon_service.py")
22-
.group_spawn()
23-
.expect("failed to spawn the Pokémon Service program");
24-
// The Python interpreter takes a little to startup.
25-
thread::sleep(Duration::from_secs(2));
71+
pub(crate) async fn run() -> Self {
2672
Self {
27-
child_process: process,
73+
child_process: PokemonServiceVariant::Http.run_process().await,
74+
}
75+
}
76+
77+
#[allow(dead_code)]
78+
pub(crate) async fn run_http2() -> Self {
79+
Self {
80+
child_process: PokemonServiceVariant::Http2.run_process().await,
2881
}
2982
}
3083
}
@@ -39,19 +92,49 @@ impl Drop for PokemonService {
3992
}
4093

4194
#[allow(dead_code)]
42-
pub fn client() -> Client<
43-
aws_smithy_client::erase::DynConnector,
44-
aws_smithy_client::erase::DynMiddleware<aws_smithy_client::erase::DynConnector>,
45-
> {
95+
pub fn client() -> PokemonClient {
96+
let base_url = PokemonServiceVariant::Http.base_url();
4697
let raw_client = Builder::new()
4798
.rustls_connector(Default::default())
48-
.middleware_fn(|mut req| {
49-
let http_req = req.http_mut();
50-
let uri = format!("http://localhost:13734{}", http_req.uri().path());
51-
*http_req.uri_mut() = uri.parse().unwrap();
52-
req
53-
})
99+
.middleware_fn(rewrite_base_url(base_url))
100+
.build_dyn();
101+
let config = Config::builder().build();
102+
Client::with_config(raw_client, config)
103+
}
104+
105+
#[allow(dead_code)]
106+
pub fn http2_client() -> PokemonClient {
107+
// Create custom cert store and add our test certificate to prevent unknown cert issues.
108+
let mut reader = BufReader::new(File::open(TEST_CERT).expect("could not open certificate"));
109+
let certs = rustls_pemfile::certs(&mut reader).expect("could not parse certificate");
110+
let mut roots = tokio_rustls::rustls::RootCertStore::empty();
111+
roots.add_parsable_certificates(&certs);
112+
113+
let connector = hyper_rustls::HttpsConnectorBuilder::new()
114+
.with_tls_config(
115+
tokio_rustls::rustls::ClientConfig::builder()
116+
.with_safe_defaults()
117+
.with_root_certificates(roots)
118+
.with_no_client_auth(),
119+
)
120+
.https_only()
121+
.enable_http2()
122+
.build();
123+
124+
let base_url = PokemonServiceVariant::Http2.base_url();
125+
let raw_client = Builder::new()
126+
.connector(DynConnector::new(Adapter::builder().build(connector)))
127+
.middleware_fn(rewrite_base_url(base_url))
54128
.build_dyn();
55129
let config = Config::builder().build();
56130
Client::with_config(raw_client, config)
57131
}
132+
133+
fn rewrite_base_url(base_url: &'static str) -> impl Fn(Request) -> Request + Clone {
134+
move |mut req| {
135+
let http_req = req.http_mut();
136+
let uri = format!("{base_url}{}", http_req.uri().path());
137+
*http_req.uri_mut() = uri.parse().unwrap();
138+
req
139+
}
140+
}

rust-runtime/aws-smithy-http-server-python/examples/pokemon-service-test/tests/simple_integration_test.rs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,43 @@
77
// These tests only have access to your crate's public API.
88
// See: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests
99

10-
use std::time::Duration;
11-
12-
use crate::helpers::{client, PokemonService};
1310
use aws_smithy_types::error::display::DisplayErrorContext;
14-
use tokio::time;
11+
use serial_test::serial;
12+
13+
use crate::helpers::{client, http2_client, PokemonClient, PokemonService};
1514

1615
mod helpers;
1716

1817
#[tokio::test]
18+
#[serial]
1919
async fn simple_integration_test() {
20-
let _program = PokemonService::run();
21-
// Give PokemonSérvice some time to start up.
22-
time::sleep(Duration::from_millis(50)).await;
20+
let _program = PokemonService::run().await;
21+
simple_integration_test_with_client(client()).await;
22+
}
23+
24+
#[tokio::test]
25+
#[serial]
26+
async fn simple_integration_test_http2() {
27+
let _program = PokemonService::run_http2().await;
28+
simple_integration_test_with_client(http2_client()).await;
29+
}
2330

24-
let service_statistics_out = client().get_server_statistics().send().await.unwrap();
31+
async fn simple_integration_test_with_client(client: PokemonClient) {
32+
let service_statistics_out = client.get_server_statistics().send().await.unwrap();
2533
assert_eq!(0, service_statistics_out.calls_count.unwrap());
2634

27-
let pokemon_species_output = client()
35+
let pokemon_species_output = client
2836
.get_pokemon_species()
2937
.name("pikachu")
3038
.send()
3139
.await
3240
.unwrap();
3341
assert_eq!("pikachu", pokemon_species_output.name().unwrap());
3442

35-
let service_statistics_out = client().get_server_statistics().send().await.unwrap();
43+
let service_statistics_out = client.get_server_statistics().send().await.unwrap();
3644
assert_eq!(1, service_statistics_out.calls_count.unwrap());
3745

38-
let pokemon_species_error = client()
46+
let pokemon_species_error = client
3947
.get_pokemon_species()
4048
.name("some_pokémon")
4149
.send()
@@ -49,8 +57,8 @@ async fn simple_integration_test() {
4957
"expected '{message}' to contain '{expected}'"
5058
);
5159

52-
let service_statistics_out = client().get_server_statistics().send().await.unwrap();
60+
let service_statistics_out = client.get_server_statistics().send().await.unwrap();
5361
assert_eq!(2, service_statistics_out.calls_count.unwrap());
5462

55-
let _health_check = client().check_health().send().await.unwrap();
63+
let _health_check = client.check_health().send().await.unwrap();
5664
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIFGTCCAwGgAwIBAgIUN/FD3OayKwJt9hXNKo4JKxqFSK4wDQYJKoZIhvcNAQEL
3+
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDgxNzE1MjQzMFoXDTMyMDgx
4+
NDE1MjQzMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
5+
AAOCAg8AMIICCgKCAgEAulMGcyA69ioNMT8Kz0CdP2QP5elLNnltBykoqoJwbvKS
6+
94+l5XA//29M4NpLphHcDxNXx3qB318bixUIPBtu66OiIsTGX8yrYPA4IO3Xt5/2
7+
wp2z1lNLouyW1+gPaPjKzcrjnHmqHS90CFDQqxdv9I0rIFIQ+U5hm5T9Hjr5xs36
8+
43l2FXAjeigoEuwtVBDt44yhEyeLSDwFJES3sH73AvpruMdxGv2KDVN4whuajWll
9+
RLTqpqBvVSM6JbaV/VD2simpZeolSl8yKIenM2PWPdLIHSMEBg6IaYgpSpzoyvmh
10+
089peAaiJfVrN53QjqDVyaN5os9ST03ZEzXQUI38lpvWGmV9Tcs5WfidLA1EbPjv
11+
yE1zBbZh0SrP/+EALwkoIRslI8DXvz/9U5Cq7q9U4OHjWB+yjE5/BX6o6hfrqfJ1
12+
Ldg2fTp/TYEudmefM8eRzx6sdYtTPZBrSpkRgvmxd+6k3QUtsAQhtBTMpvJpWsgs
13+
sD7Uo6G2JRag53oT/2cxG03Qy5HqySZUK1bpFW03W5FL3Pq6AkpGy1hnSxlifkHp
14+
si61dbjCV5uRdxRCLyH9fD3HImecet+vnuZlvsP0MAzh0vbli/dcFZ7xUoSqFWnj
15+
egnPohdOmF6C8kXvWBt51N4jjW+eLxPAr9H0mJtdIvEHWBNNW9iitzGz5Gw0g4sC
16+
AwEAAaNjMGEwHQYDVR0OBBYEFEoLkB78Z6jgPPmOyf0XnWo/LjA9MB8GA1UdIwQY
17+
MBaAFEoLkB78Z6jgPPmOyf0XnWo/LjA9MBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAJ
18+
BgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQC17OljBEEVYefzk2mwg20AXDtL
19+
PUJ46hLrUM7BcNBjd8AbtrLH/pdCRCexnv7tzYbwMhDNdqHiIcXDHEMNP3gXryB2
20+
ckU5ms/LzfKADM2/hrDZiR03XYSL4thjFkQNVfYnk9k7LTv9pKW0b+J2OrMun7+w
21+
bdXcNw+igvnYiBgNJRo0IC9O5nejqLGWwBfveAJPetxjy6PvBkLqgIw2glivmTrh
22+
Kdoq/I2/ZcxT0GyhEVIHP9W8Hh5goNm+RbsB/hDYhK+5s2+rL1lwJrwhNBrHhG1u
23+
CtYmd2rD0J/mGf1cAw7t+hmwW0O7J9BVZw4YL/m4vDAsTO4zaeoAvDwsgQwPzPF1
24+
rmRtV+7jJHyIP/b021XIdIZU5KsXCCA3+B31mHJF1GLreG7WI+wClRsiNSbP7Zuw
25+
OnUOTDZc77Y4oaDKl0UL8tz1GNwX5G9U5h+FciTPKCtg1gGiqSkB/3BOON2WaVOb
26+
6Di9iAoH+dIjvWR/7ez7DAk/ITpGvBXS5RqaIXfB9pSJlVYsGp03ikgng1eJdXy4
27+
57XZnd47upHH88NTvIH9G/iOXQQCzF3MQXOqrJ/gem3ICeelvOoyNseHLvi8ZEqa
28+
s693CJWaQAK/jD1mhka7yQzmb/Y1I53crc2UqSxX4FqFYP8xymza4Cg/E6pPJerG
29+
LE/drJtbrIHTUlJB2Q==
30+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)