From 0fecc09c75219105149508b354144a0f7cc1b2e2 Mon Sep 17 00:00:00 2001 From: Hanson Char Date: Wed, 21 Sep 2022 15:19:30 -0700 Subject: [PATCH] Support full configuration of TCP keepalive probles, upgrading to v3.0.0 Use hyper stream branch 0.14.x Locally patched axum-server to make all TCP keepalive parameters available ref: git@github.com:hansonchar/axum-server.git branch: v0.4.3 commit: 839ada3 https://github.com/aws-samples/aws-kms-xks-proxy/pull/7 --- docker/README.md | 20 +- rpmspec/xks-proxy.spec | 4 +- xks-axum/Cargo.toml | 6 +- xks-axum/axum-server/.gitignore | 2 + xks-axum/axum-server/CHANGELOG.md | 115 ++++ xks-axum/axum-server/Cargo.toml | 67 ++ xks-axum/axum-server/LICENSE | 19 + xks-axum/axum-server/README.md | 58 ++ .../examples/configure_addr_incoming.rs | 29 + .../axum-server/examples/configure_http.rs | 26 + .../axum-server/examples/from_std_listener.rs | 19 + .../examples/from_std_listener_rustls.rs | 28 + .../axum-server/examples/graceful_shutdown.rs | 50 ++ xks-axum/axum-server/examples/hello_world.rs | 18 + .../axum-server/examples/http_and_https.rs | 52 ++ .../axum-server/examples/remote_address.rs | 21 + .../examples/remote_address_using_tower.rs | 27 + .../axum-server/examples/rustls_reload.rs | 52 ++ .../axum-server/examples/rustls_server.rs | 26 + .../axum-server/examples/rustls_session.rs | 80 +++ .../examples/self-signed-certs/cert.pem | 32 + .../examples/self-signed-certs/key.pem | 52 ++ .../self-signed-certs/reload/cert.pem | 32 + .../examples/self-signed-certs/reload/key.pem | 52 ++ xks-axum/axum-server/examples/shutdown.rs | 40 ++ xks-axum/axum-server/src/accept.rs | 42 ++ .../axum-server/src/addr_incoming_config.rs | 76 +++ xks-axum/axum-server/src/handle.rs | 127 ++++ xks-axum/axum-server/src/http_config.rs | 215 +++++++ xks-axum/axum-server/src/lib.rs | 112 ++++ xks-axum/axum-server/src/notify_once.rs | 28 + xks-axum/axum-server/src/server.rs | 407 ++++++++++++ xks-axum/axum-server/src/service.rs | 156 +++++ xks-axum/axum-server/src/tls_rustls/future.rs | 114 ++++ xks-axum/axum-server/src/tls_rustls/mod.rs | 578 ++++++++++++++++++ xks-axum/configuration/settings.toml | 10 +- xks-axum/configuration/settings_cloudhsm.toml | 10 +- xks-axum/configuration/settings_docker.toml | 10 +- xks-axum/configuration/settings_luna.toml | 10 +- .../configuration/settings_softhsmv2.toml | 10 +- .../configuration/settings_softhsmv2_osx.toml | 10 +- xks-axum/src/main.rs | 11 +- xks-axum/src/settings.rs | 24 +- 43 files changed, 2855 insertions(+), 22 deletions(-) create mode 100644 xks-axum/axum-server/.gitignore create mode 100644 xks-axum/axum-server/CHANGELOG.md create mode 100644 xks-axum/axum-server/Cargo.toml create mode 100644 xks-axum/axum-server/LICENSE create mode 100644 xks-axum/axum-server/README.md create mode 100644 xks-axum/axum-server/examples/configure_addr_incoming.rs create mode 100644 xks-axum/axum-server/examples/configure_http.rs create mode 100644 xks-axum/axum-server/examples/from_std_listener.rs create mode 100644 xks-axum/axum-server/examples/from_std_listener_rustls.rs create mode 100644 xks-axum/axum-server/examples/graceful_shutdown.rs create mode 100644 xks-axum/axum-server/examples/hello_world.rs create mode 100644 xks-axum/axum-server/examples/http_and_https.rs create mode 100644 xks-axum/axum-server/examples/remote_address.rs create mode 100644 xks-axum/axum-server/examples/remote_address_using_tower.rs create mode 100644 xks-axum/axum-server/examples/rustls_reload.rs create mode 100644 xks-axum/axum-server/examples/rustls_server.rs create mode 100644 xks-axum/axum-server/examples/rustls_session.rs create mode 100644 xks-axum/axum-server/examples/self-signed-certs/cert.pem create mode 100644 xks-axum/axum-server/examples/self-signed-certs/key.pem create mode 100644 xks-axum/axum-server/examples/self-signed-certs/reload/cert.pem create mode 100644 xks-axum/axum-server/examples/self-signed-certs/reload/key.pem create mode 100644 xks-axum/axum-server/examples/shutdown.rs create mode 100644 xks-axum/axum-server/src/accept.rs create mode 100644 xks-axum/axum-server/src/addr_incoming_config.rs create mode 100644 xks-axum/axum-server/src/handle.rs create mode 100644 xks-axum/axum-server/src/http_config.rs create mode 100644 xks-axum/axum-server/src/lib.rs create mode 100644 xks-axum/axum-server/src/notify_once.rs create mode 100644 xks-axum/axum-server/src/server.rs create mode 100644 xks-axum/axum-server/src/service.rs create mode 100644 xks-axum/axum-server/src/tls_rustls/future.rs create mode 100644 xks-axum/axum-server/src/tls_rustls/mod.rs diff --git a/docker/README.md b/docker/README.md index 7423962..ca6d78b 100644 --- a/docker/README.md +++ b/docker/README.md @@ -18,25 +18,25 @@ This is an outline of how to build a docker image for `xks-proxy`, including inf 1. Adjust [Dockerfile](Dockerfile) as needed. 1. Build a docker image for `xks-proxy`: - docker build -t xks-proxy:v2.0.1 . + docker build -t xks-proxy:v3.0.0 . 1. Save the image to a tar file, if it needs to be exported/shared: - docker save -o xks-proxy-docker-v2.0.1.tar xks-proxy:v2.0.1 -1. Compress `xks-proxy-docker-v2.0.1.tar` into `xks-proxy-docker-v2.0.1.tar.xz` if necessary: + docker save -o xks-proxy-docker-v3.0.0.tar xks-proxy:v3.0.0 +1. Compress `xks-proxy-docker-v3.0.0.tar` into `xks-proxy-docker-v3.0.0.tar.xz` if necessary: - xz -z -0 xks-proxy-docker-v2.0.1.tar + xz -z -0 xks-proxy-docker-v3.0.0.tar ## How to run `xks-proxy` in a docker container? -1. Decompress `xks-proxy-docker-v2.0.1.tar.xz` to `xks-proxy-docker-v2.0.1.tar` if necessary: +1. Decompress `xks-proxy-docker-v3.0.0.tar.xz` to `xks-proxy-docker-v3.0.0.tar` if necessary: - xz -d xks-proxy-docker-v2.0.1.tar.xz + xz -d xks-proxy-docker-v3.0.0.tar.xz 1. Load the docker image if necessary: - docker load -i xks-proxy-docker-v2.0.1.tar + docker load -i xks-proxy-docker-v3.0.0.tar 1. Run `xks-proxy` in a docker container exposing port `80` (of the container) as port `80` on the running host: - docker run --name xks-proxy -d -p 0.0.0.0:80:80 xks-proxy:v2.0.1 + docker run --name xks-proxy -d -p 0.0.0.0:80:80 xks-proxy:v3.0.0 1. Now you can access it at `http:///example/uri/path/prefix/kms/xks/v1` or whatever URI path you've configured in `settings.toml`. @@ -45,7 +45,7 @@ or whatever URI path you've configured in `settings.toml`. * Remove the `xks-proxy` docker image: - docker rmi xks-proxy:v2.0.1 + docker rmi xks-proxy:v3.0.0 * Exec into the `xks-proxy` docker container: docker exec -it xks-proxy bash @@ -57,7 +57,7 @@ or whatever URI path you've configured in `settings.toml`. docker container ls * Ping `xks-proxy` running in docker container - # should get back a "pong from xks-proxy v2.0.1" response + # should get back a "pong from xks-proxy v3.0.0" response curl http://localhost/ping * Follow the log of the running `xks-proxy` in the docker container diff --git a/rpmspec/xks-proxy.spec b/rpmspec/xks-proxy.spec index 569a203..9edc7ee 100644 --- a/rpmspec/xks-proxy.spec +++ b/rpmspec/xks-proxy.spec @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 Name: xks-proxy -Version: 2.0.1 +Version: 3.0.0 Release: 0%{?dist} Summary: AWS External Keystore (XKS) Proxy Service @@ -45,6 +45,8 @@ systemctl disable xks-proxy.service systemctl disable xks-proxy_cleanlogs.timer %changelog +* Sun Sep 21 2022 Hanson Char - 3.0.0 +- Support full configurable of TCP keepalive probes * Sun Sep 11 2022 Hanson Char - 2.0.1 - Support configurable interval to send TCP keepalive probes * Thu Sep 08 2022 Hanson Char - 2.0.0 diff --git a/xks-axum/Cargo.toml b/xks-axum/Cargo.toml index e014511..da98a6e 100644 --- a/xks-axum/Cargo.toml +++ b/xks-axum/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "xks-proxy" -version = "2.0.1" +version = "3.0.0" edition = "2018" publish = false @@ -65,6 +65,10 @@ serial_test_derive = "0.9" # https://github.com/mheese/rust-pkcs11/issues/50 pkcs11 = { path = "rust-pkcs11" } +# Patch to enable configuration of full TCP keepalive parameters +hyper = { git = "https://github.com/hyperium/hyper.git", branch = "0.14.x" } +axum-server = { path = "axum-server" } + [profile.dev] panic = "abort" diff --git a/xks-axum/axum-server/.gitignore b/xks-axum/axum-server/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/xks-axum/axum-server/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/xks-axum/axum-server/CHANGELOG.md b/xks-axum/axum-server/CHANGELOG.md new file mode 100644 index 0000000..711ba60 --- /dev/null +++ b/xks-axum/axum-server/CHANGELOG.md @@ -0,0 +1,115 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog], and this project adheres to +[Semantic Versioning]. + +# Unreleased + +None. + +# 0.4.2 (5. August 2022) + +- **added:** Added `Server::from_tcp`, `axum_server::from_tcp` and + `axum_server::from_tcp_rustls` methods to create `Server` from + `std::net::TcpListener`. + +# 0.4.1 (29. July 2022) + +- **added:** Added `map`, `get` and `get_mut` methods to access the acceptor + of `Server`. + +# 0.4.0 (18. April 2022) + +- Added TLS handshake timeout(10 seconds). +- In `RustlsConfig`: `from_pem` and `from_pem_file` methods now accept EC + keys. +- **added:** Added `AddrIncomingConfig` to allow configuration of + `hyper::server::conn::AddrIncoming`. +- **added:** Added `HttpConfig::http1_header_read_timeout`. +- **breaking:** Changed `Handle::listening` return type to + `Option`. If binding fails, `Option::None` will be returned. + +# 0.3.2 (17. November 2021) + +- **added:** Added `HttpConfig` to allow more configuration. + +# 0.3.1 (10. November 2021) + +- **fixed:** `tls-rustls` feature doesn't compile if `fs` feature in `tokio` + is not enabled. + +# 0.3.0 (10. November 2021) + +- **Total rewrite of source code.** +- **Major api changes:** + - **breaking:** Removed `bind_rustls`, `certificate`, `certificate_file`, + `loader`, `new`, `private_key`, `private_key_file`, `serve_and_record`, + `tls_config` methods from `Server`. + - **breaking:** Removed `tls` module. + - **breaking:** Removed `record` module and feature. + - **breaking:** Removed `Handle::listening_addrs` method. + - **breaking:** `Server::bind` method doesn't take `self` anymore and + creates an `Server`. + - **breaking:** `bind` method now takes a `SocketAddr`. + - **breaking:** `bind_rustls` method now takes a `SocketAddr` and an + `tls_rustls::RustlsConfig`. + - **breaking:** `Server::serve` method now takes a `MakeService`. + - **breaking:** `Handle::listening` method now returns `SocketAddr`. + - **added:** Added `Handle::connection_count` that can be used to get alive + connection count. + - **added:** Added `service` module. + - **added:** Added `service::MakeServiceRef` and `service::SendService` + traits aliases for convenience. + - **added:** Added `accept` module. + - **added:** Added `accept::Accept` trait that can be implemented to modify + io stream and service. + - **added:** Added `accept::DefaultAcceptor` struct that implements + `accept::Accept` to be used as a default 'Accept' for 'Server'. + - **added:** Added `Server::acceptor` method that can be used to provide a + custom `accept::Accept`. + - **added:** Added `tls_rustls` module. + - **added:** Added `tls_rustls::RustlsAcceptor` that can be used with + `Server::acceptor` to make a tls `Server`. + - **added:** Added `tls_rustls::RustlsConfig` to create rustls utilities and + to provide reload functionality. + - **added:** Added `tls_rustls::bind_rustls` which is same as `bind_rustls` + function. + +# 0.2.5 (5. October 2021) + +- Compile on rust `1.51`. + +# 0.2.4 (17. September 2021) + +- Reduced `futures-util` features to improve compile times. + +# 0.2.3 (14. September 2021) + +- Fixed `bind` and `bind_rustls` not working on some types. + +# 0.2.2 (6. September 2021) + +- Added uri `Scheme` in `Request` extensions. +- Fixed memory leak that happens as connections are accepted. + +# 0.2.1 (30. August 2021) + +- Fixed `serve_and_record` not recording independently for each connection. + +# 0.2.0 (29. August 2021) + +- Added `TlsLoader` to reload tls configuration. +- Added `Handle` to provide additional utilities for server. + +# 0.1.2 (24. August 2021) + +- Fixed an import issue when using `tls-rustls` feature. + +# 0.1.0 (23. August 2021) + +- Initial release. + +[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/ +[Semantic Versioning]: https://semver.org/spec/v2.0.0.html diff --git a/xks-axum/axum-server/Cargo.toml b/xks-axum/axum-server/Cargo.toml new file mode 100644 index 0000000..fcb9239 --- /dev/null +++ b/xks-axum/axum-server/Cargo.toml @@ -0,0 +1,67 @@ +[package] +authors = ["Programatik "] +categories = ["asynchronous", "network-programming", "web-programming"] +description = "High level server designed to be used with axum framework." +edition = "2018" +homepage = "https://github.com/programatik29/axum-server" +keywords = ["http", "https", "web", "server"] +license = "MIT" +name = "axum-server" +readme = "README.md" +repository = "https://github.com/programatik29/axum-server" +version = "0.4.2" + +[features] +default = [] +tls-rustls = ["arc-swap", "pin-project-lite", "rustls", "rustls-pemfile", "tokio/fs", "tokio/time", "tokio-rustls"] + +[dependencies] +bytes = "1" +futures-util = { version = "0.3", default-features = false, features = ["alloc"] } +http = "0.2" +http-body = "0.4" +hyper = { version = "0.14.16", features = ["http1", "http2", "server", "runtime"] } +tokio = { version = "1", features = ["macros", "net", "sync"] } +tower-service = "0.3" + +# optional dependencies +arc-swap = { version = "1", optional = true } +pin-project-lite = { version = "0.2", optional = true } +rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true } +rustls-pemfile = { version = "1", optional = true } +tokio-rustls = { version = "0.23", optional = true } + +[dev-dependencies] +axum = "0.5" +hyper = { version = "0.14", features = ["full"] } +tokio = { version = "1", features = ["full"] } +tower = { version = "0.4", features = ["util"] } +tower-http = { version = "0.3", features = ["add-extension"] } + +[package.metadata.docs.rs] +all-features = true +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples=examples"] +rustdoc-args = ["--cfg", "docsrs"] + +[[example]] +name = "from_std_listener_rustls" +required-features = ["tls-rustls"] + +[[example]] +name = "http_and_https" +required-features = ["tls-rustls"] + +[[example]] +name = "rustls_reload" +required-features = ["tls-rustls"] + +[[example]] +name = "rustls_server" +required-features = ["tls-rustls"] + +[[example]] +name = "rustls_session" +required-features = ["tls-rustls"] + +[patch.crates-io] +hyper = { git = "https://github.com/hyperium/hyper", branch = "0.14.x" } diff --git a/xks-axum/axum-server/LICENSE b/xks-axum/axum-server/LICENSE new file mode 100644 index 0000000..cd5a49f --- /dev/null +++ b/xks-axum/axum-server/LICENSE @@ -0,0 +1,19 @@ +Copyright 2021 Axum Server Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/xks-axum/axum-server/README.md b/xks-axum/axum-server/README.md new file mode 100644 index 0000000..1c9cd30 --- /dev/null +++ b/xks-axum/axum-server/README.md @@ -0,0 +1,58 @@ +[![License](https://img.shields.io/crates/l/axum-server)](https://choosealicense.com/licenses/mit/) +[![Crates.io](https://img.shields.io/crates/v/axum-server)](https://crates.io/crates/axum-server) +[![Docs - Master](https://img.shields.io/badge/docs-master-blue)](https://programatik29.github.io/axum-server/axum_server/) +[![Docs - Stable](https://img.shields.io/crates/v/axum-server?color=blue&label=docs)](https://docs.rs/axum-server/) + +# axum-server + +axum-server is a [hyper] server implementation designed to be used with [axum] framework. + +This project is maintained by community independently from [axum]. + +## Features + +- HTTP/1 and HTTP/2 +- HTTPS through [rustls]. +- High performance through [hyper]. +- Using [tower] make service API. +- Very good [axum] compatibility. Likely to work with future [axum] releases. + +## Usage Example + +A simple hello world application can be served like: + +```rust +use axum::{routing::get, Router}; +use std::net::SocketAddr; + +#[tokio::main] +async fn main() { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + println!("listening on {}", addr); + axum_server::bind(addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} +``` + +You can find more examples [here](/examples). + +## Minimum Supported Rust Version + +axum-server's MSRV is `1.49`. + +## Safety + +This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in 100% safe Rust. + +## License + +This project is licensed under the [MIT license](LICENSE). + +[axum]: https://crates.io/crates/axum +[hyper]: https://crates.io/crates/hyper +[rustls]: https://crates.io/crates/rustls +[tower]: https://crates.io/crates/tower diff --git a/xks-axum/axum-server/examples/configure_addr_incoming.rs b/xks-axum/axum-server/examples/configure_addr_incoming.rs new file mode 100644 index 0000000..f4287d3 --- /dev/null +++ b/xks-axum/axum-server/examples/configure_addr_incoming.rs @@ -0,0 +1,29 @@ +//! Run with `cargo run --example configure_http` command. +//! +//! To connect through browser, navigate to "http://localhost:3000" url. + +use axum::{routing::get, Router}; +use axum_server::AddrIncomingConfig; +use std::net::SocketAddr; +use std::time::Duration; + +#[tokio::main] +async fn main() { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let config = AddrIncomingConfig::new() + .tcp_nodelay(true) + .tcp_sleep_on_accept_errors(true) + .tcp_keepalive(Some(Duration::from_secs(32))) + .tcp_keepalive_interval(Some(Duration::from_secs(1))) + .tcp_keepalive_retries(Some(1)) + .build(); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + println!("listening on {}", addr); + axum_server::bind(addr) + .addr_incoming_config(config) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/xks-axum/axum-server/examples/configure_http.rs b/xks-axum/axum-server/examples/configure_http.rs new file mode 100644 index 0000000..bb1991b --- /dev/null +++ b/xks-axum/axum-server/examples/configure_http.rs @@ -0,0 +1,26 @@ +//! Run with `cargo run --example configure_http` command. +//! +//! To connect through browser, navigate to "http://localhost:3000" url. + +use axum::{routing::get, Router}; +use axum_server::HttpConfig; +use std::net::SocketAddr; + +#[tokio::main] +async fn main() { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let config = HttpConfig::new() + .http1_only(true) + .http2_only(false) + .max_buf_size(8192) + .build(); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + println!("listening on {}", addr); + axum_server::bind(addr) + .http_config(config) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/xks-axum/axum-server/examples/from_std_listener.rs b/xks-axum/axum-server/examples/from_std_listener.rs new file mode 100644 index 0000000..150b384 --- /dev/null +++ b/xks-axum/axum-server/examples/from_std_listener.rs @@ -0,0 +1,19 @@ +//! Run with `cargo run --example from_std_listener` command. +//! +//! To connect through browser, navigate to "http://localhost:3000" url. + +use axum::{routing::get, Router}; +use std::net::{SocketAddr, TcpListener}; + +#[tokio::main] +async fn main() { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + let listener = TcpListener::bind(addr).unwrap(); + println!("listening on {}", addr); + axum_server::from_tcp(listener) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/xks-axum/axum-server/examples/from_std_listener_rustls.rs b/xks-axum/axum-server/examples/from_std_listener_rustls.rs new file mode 100644 index 0000000..fa8d961 --- /dev/null +++ b/xks-axum/axum-server/examples/from_std_listener_rustls.rs @@ -0,0 +1,28 @@ +//! Run with `cargo run --all-features --example from_std_listener_rustls` +//! command. +//! +//! To connect through browser, navigate to "https://localhost:3000" url. + +use axum::{routing::get, Router}; +use axum_server::tls_rustls::RustlsConfig; +use std::net::{SocketAddr, TcpListener}; + +#[tokio::main] +async fn main() { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let config = RustlsConfig::from_pem_file( + "examples/self-signed-certs/cert.pem", + "examples/self-signed-certs/key.pem", + ) + .await + .unwrap(); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + let listener = TcpListener::bind(addr).unwrap(); + println!("listening on {}", addr); + axum_server::from_tcp_rustls(listener, config) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/xks-axum/axum-server/examples/graceful_shutdown.rs b/xks-axum/axum-server/examples/graceful_shutdown.rs new file mode 100644 index 0000000..07b9ca1 --- /dev/null +++ b/xks-axum/axum-server/examples/graceful_shutdown.rs @@ -0,0 +1,50 @@ +//! Run with `cargo run --example graceful_shutdown` command. +//! +//! To connect through browser, navigate to "http://localhost:3000" url. +//! +//! After 10 seconds: +//! - If there aren't any connections alive, server will shutdown. +//! - If there are connections alive, server will wait until deadline is elapsed. +//! - Deadline is 30 seconds. Server will shutdown anyways when deadline is elapsed. + +use axum::{routing::get, Router}; +use axum_server::Handle; +use std::{net::SocketAddr, time::Duration}; +use tokio::time::sleep; + +#[tokio::main] +async fn main() { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let handle = Handle::new(); + + // Spawn a task to gracefully shutdown server. + tokio::spawn(graceful_shutdown(handle.clone())); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + println!("listening on {}", addr); + axum_server::bind(addr) + .handle(handle) + .serve(app.into_make_service()) + .await + .unwrap(); + + println!("server is shut down"); +} + +async fn graceful_shutdown(handle: Handle) { + // Wait 10 seconds. + sleep(Duration::from_secs(10)).await; + + println!("sending graceful shutdown signal"); + + // Signal the server to shutdown using Handle. + handle.graceful_shutdown(Some(Duration::from_secs(30))); + + // Print alive connection count every second. + loop { + sleep(Duration::from_secs(1)).await; + + println!("alive connections: {}", handle.connection_count()); + } +} diff --git a/xks-axum/axum-server/examples/hello_world.rs b/xks-axum/axum-server/examples/hello_world.rs new file mode 100644 index 0000000..32d94c7 --- /dev/null +++ b/xks-axum/axum-server/examples/hello_world.rs @@ -0,0 +1,18 @@ +//! Run with `cargo run --example hello_world` command. +//! +//! To connect through browser, navigate to "http://localhost:3000" url. + +use axum::{routing::get, Router}; +use std::net::SocketAddr; + +#[tokio::main] +async fn main() { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + println!("listening on {}", addr); + axum_server::bind(addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/xks-axum/axum-server/examples/http_and_https.rs b/xks-axum/axum-server/examples/http_and_https.rs new file mode 100644 index 0000000..d320838 --- /dev/null +++ b/xks-axum/axum-server/examples/http_and_https.rs @@ -0,0 +1,52 @@ +//! Run with `cargo run --all-features --example http_and_https` command. +//! +//! To connect through browser, navigate to "http://localhost:3000" url which should redirect to +//! "https://localhost:3443". + +use axum::{http::uri::Uri, response::Redirect, routing::get, Router}; +use axum_server::tls_rustls::RustlsConfig; +use std::net::SocketAddr; + +#[tokio::main] +async fn main() { + let http = tokio::spawn(http_server()); + let https = tokio::spawn(https_server()); + + // Ignore errors. + let _ = tokio::join!(http, https); +} + +async fn http_server() { + let app = Router::new().route("/", get(http_handler)); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + println!("http listening on {}", addr); + axum_server::bind(addr) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +async fn http_handler(uri: Uri) -> Redirect { + let uri = format!("https://127.0.0.1:3443{}", uri.path()); + + Redirect::temporary(&uri) +} + +async fn https_server() { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let config = RustlsConfig::from_pem_file( + "examples/self-signed-certs/cert.pem", + "examples/self-signed-certs/key.pem", + ) + .await + .unwrap(); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3443)); + println!("https listening on {}", addr); + axum_server::bind_rustls(addr, config) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/xks-axum/axum-server/examples/remote_address.rs b/xks-axum/axum-server/examples/remote_address.rs new file mode 100644 index 0000000..f48ea02 --- /dev/null +++ b/xks-axum/axum-server/examples/remote_address.rs @@ -0,0 +1,21 @@ +//! Run with `cargo run --example remote_address` command. +//! +//! To connect through browser, navigate to "http://localhost:3000" url. + +use axum::{extract::ConnectInfo, routing::get, Router}; +use std::net::SocketAddr; + +#[tokio::main] +async fn main() { + let app = Router::new() + .route("/", get(handler)) + .into_make_service_with_connect_info::(); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + + axum_server::bind(addr).serve(app).await.unwrap(); +} + +async fn handler(ConnectInfo(addr): ConnectInfo) -> String { + format!("your ip address is: {}", addr) +} diff --git a/xks-axum/axum-server/examples/remote_address_using_tower.rs b/xks-axum/axum-server/examples/remote_address_using_tower.rs new file mode 100644 index 0000000..00857b8 --- /dev/null +++ b/xks-axum/axum-server/examples/remote_address_using_tower.rs @@ -0,0 +1,27 @@ +//! Run with `cargo run --example remote_address_using_tower` command. +//! +//! To connect through browser, navigate to "http://localhost:3000" url. + +use hyper::{server::conn::AddrStream, Body, Request, Response}; +use std::{convert::Infallible, net::SocketAddr}; +use tower::service_fn; +use tower_http::add_extension::AddExtension; + +#[tokio::main] +async fn main() { + let service = service_fn(|mut req: Request| async move { + let addr: SocketAddr = req.extensions_mut().remove().unwrap(); + let body = Body::from(format!("IP Address: {}", addr)); + + Ok::<_, Infallible>(Response::new(body)) + }); + + axum_server::bind(SocketAddr::from(([127, 0, 0, 1], 3000))) + .serve(service_fn(|addr: &AddrStream| { + let addr = addr.remote_addr(); + + async move { Ok::<_, Infallible>(AddExtension::new(service, addr)) } + })) + .await + .unwrap(); +} diff --git a/xks-axum/axum-server/examples/rustls_reload.rs b/xks-axum/axum-server/examples/rustls_reload.rs new file mode 100644 index 0000000..08c2a85 --- /dev/null +++ b/xks-axum/axum-server/examples/rustls_reload.rs @@ -0,0 +1,52 @@ +//! Run with `cargo run --all-features --example rustls_reload` command. +//! +//! To connect through browser, navigate to "https://localhost:3000" url. +//! +//! Certificate common name will be "localhost". +//! +//! After 20 seconds, certificate common name will be "reloaded". + +use axum::{routing::get, Router}; +use axum_server::tls_rustls::RustlsConfig; +use std::{net::SocketAddr, time::Duration}; +use tokio::time::sleep; + +#[tokio::main] +async fn main() { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let config = RustlsConfig::from_pem_file( + "examples/self-signed-certs/cert.pem", + "examples/self-signed-certs/key.pem", + ) + .await + .unwrap(); + + // Spawn a task to reload tls. + tokio::spawn(reload(config.clone())); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + println!("listening on {}", addr); + axum_server::bind_rustls(addr, config) + .serve(app.into_make_service()) + .await + .unwrap(); +} + +async fn reload(config: RustlsConfig) { + // Wait for 20 seconds. + sleep(Duration::from_secs(20)).await; + + println!("reloading rustls configuration"); + + // Reload rustls configuration from new files. + config + .reload_from_pem_file( + "examples/self-signed-certs/reload/cert.pem", + "examples/self-signed-certs/reload/key.pem", + ) + .await + .unwrap(); + + println!("rustls configuration reloaded"); +} diff --git a/xks-axum/axum-server/examples/rustls_server.rs b/xks-axum/axum-server/examples/rustls_server.rs new file mode 100644 index 0000000..44ea023 --- /dev/null +++ b/xks-axum/axum-server/examples/rustls_server.rs @@ -0,0 +1,26 @@ +//! Run with `cargo run --all-features --example rustls_server` command. +//! +//! To connect through browser, navigate to "https://localhost:3000" url. + +use axum::{routing::get, Router}; +use axum_server::tls_rustls::RustlsConfig; +use std::net::SocketAddr; + +#[tokio::main] +async fn main() { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let config = RustlsConfig::from_pem_file( + "examples/self-signed-certs/cert.pem", + "examples/self-signed-certs/key.pem", + ) + .await + .unwrap(); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + println!("listening on {}", addr); + axum_server::bind_rustls(addr, config) + .serve(app.into_make_service()) + .await + .unwrap(); +} diff --git a/xks-axum/axum-server/examples/rustls_session.rs b/xks-axum/axum-server/examples/rustls_session.rs new file mode 100644 index 0000000..2697d9d --- /dev/null +++ b/xks-axum/axum-server/examples/rustls_session.rs @@ -0,0 +1,80 @@ +//! Run with `cargo run --all-features --example rustls_session` command. +//! +//! To connect through browser, navigate to "https://localhost:3000" url. + +use axum::{middleware::AddExtension, routing::get, Extension, Router}; +use axum_server::{ + accept::Accept, + tls_rustls::{RustlsAcceptor, RustlsConfig}, +}; +use futures_util::future::BoxFuture; +use std::{io, net::SocketAddr, sync::Arc}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_rustls::server::TlsStream; +use tower::Layer; + +#[tokio::main] +async fn main() { + let app = Router::new().route("/", get(handler)); + + let config = RustlsConfig::from_pem_file( + "examples/self-signed-certs/cert.pem", + "examples/self-signed-certs/key.pem", + ) + .await + .unwrap(); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + + println!("listening on {}", addr); + + let acceptor = CustomAcceptor::new(RustlsAcceptor::new(config)); + let server = axum_server::bind(addr).acceptor(acceptor); + + server.serve(app.into_make_service()).await.unwrap(); +} + +async fn handler(tls_data: Extension) -> String { + format!("{:?}", tls_data) +} + +#[derive(Debug, Clone)] +struct TlsData { + _hostname: Option>, +} + +#[derive(Debug, Clone)] +struct CustomAcceptor { + inner: RustlsAcceptor, +} + +impl CustomAcceptor { + fn new(inner: RustlsAcceptor) -> Self { + Self { inner } + } +} + +impl Accept for CustomAcceptor +where + I: AsyncRead + AsyncWrite + Unpin + Send + 'static, + S: Send + 'static, +{ + type Stream = TlsStream; + type Service = AddExtension; + type Future = BoxFuture<'static, io::Result<(Self::Stream, Self::Service)>>; + + fn accept(&self, stream: I, service: S) -> Self::Future { + let acceptor = self.inner.clone(); + + Box::pin(async move { + let (stream, service) = acceptor.accept(stream, service).await?; + let server_conn = stream.get_ref().1; + let sni_hostname = TlsData { + _hostname: server_conn.sni_hostname().map(From::from), + }; + let service = Extension(sni_hostname).layer(service); + + Ok((stream, service)) + }) + } +} diff --git a/xks-axum/axum-server/examples/self-signed-certs/cert.pem b/xks-axum/axum-server/examples/self-signed-certs/cert.pem new file mode 100644 index 0000000..8227f32 --- /dev/null +++ b/xks-axum/axum-server/examples/self-signed-certs/cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQZiKeBISKUZoglT8J8CCPpGbgTkwDQYJKoZIhvcNAQEL +BQAwWTELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X +DTIxMDgyOTEyMDE0NVoXDTIyMDgyOTEyMDE0NVowWTELMAkGA1UEBhMCVVMxEzAR +BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 +IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAoeDJnuh1lhcpKCt5VEBqO9JcSoz2wqD3SLj4i2qrEOvqb4X0ZZeN +5GQXQlOG2N6+9FOxTzaTTigTecYzI3hqKn1fiuvaS4EeTC7E1sVOj7tY0yVySjXM +pC/3t1n1s3B25m7eQ0G2JypZFCobGqY0kaRoO+mCTjI4bdCd769shIerCO4Z8FD5 +uj1+hBC7ZY/sqmRkGTLX1ZzkXzaeNeWGlkXKU8/V3qdveFQ/sGe+KoZpOPXb0yR7 +H8zf6NE2CFCNJDhytOkYLOsnvCJOvibJ3kbM2GfI9iCd0/QhQAOcrVhcOgI4aIxr +wP3zvF4PFUhFKEWHqK5IFq41xKyMYu2fw3bmKXg4zsQGcB0avBD7z+7ENEBvLkNI +7O20wKJp8u0RfjStNHWPmWLXPjkadVB5JHJjsktvgNZkbs9ugxhZWW2AzrrIuqwR +NOWnjHE7J3jvcHP6jE5O9LHpnlh6BMoKPsQuRu/bkrD34rNzwH7IX1To1CyDazMR +yhUiARYh43gg6hrrQdVjDFMHd51mgWHtOPzSLb0uzToglAa3FClGlCeaiacu4H2V +EfJrlCbVlftmIub9/EILZ6XpyYWMxt2mm4mCcMtXmBsHolP4lU3keK8AGNFOr3PC +B7NHLNp1RHgx8+Q3kzobJ1Lk+zEjraWPb5gyByUvZySbd/JTGgNCmZsCAwEAAaNT +MFEwHQYDVR0OBBYEFGsIv6GsbDS+dEWwWlA/3TG5Oi88MB8GA1UdIwQYMBaAFGsI +v6GsbDS+dEWwWlA/3TG5Oi88MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggIBAHhjzP8WtkLJVfZXXUPAAekR7kaqk2hb3hIgDABBJ7xNxcktLOH7V/ng +nhbnwSH5mCkHHXx78TOhWqokHp5wru8K3de5wvAD8uz0UwNDHK5EzqtjYLzxbxAr +ht89WoXGPEZIz6MuOxVYx/HHXdgNEXUcujzfpAfvznVxvzBVqpHNgc7qO8wJd0cG +nit1XubxKoIVTEUjDfxGa2TsmBI7CZ8MLjIyztp/b3txpVl36hPC/uFLwKC780Jc +eO9saA5ISbJh7EaISRr8MKpBpJcraL+055bMjM+kzRFA18NWuuo9Y8fXnXE8e/af +k8FvclVdH/YyezaLkjW7lXjo7QoSXHhAuSzvsGmIsh+HuH+3Fs22AN3aGdmimOmp +7JiNe42mwEpJydwgGlKOysw4ht6MA6yOcQJw73QAYYwusOmNjFZtfCUqJx/JO7mn +Sb1/PW58xYSJhDxdGhoh6Rd3xPMW1T4YwpapkAC/htciK3XkwCcG1VKSmCIErkXf +vllmdahH/QkNooNAHMZl/ipYMik8pp5eRjVjCvpQTDBOI97U0+bgXydHVowP9ExE +dGcm6pP8FU1LyBZdYTdlMRC5Z0L0ltcZn7bqKcyzZB3UcWJv7Uhn3MYbmqGsUVly +a/e3kH2t5pEWRTsrNrRD94LzEYKvcNHy6PYkrgpGjh2G2VBZgNzh +-----END CERTIFICATE----- diff --git a/xks-axum/axum-server/examples/self-signed-certs/key.pem b/xks-axum/axum-server/examples/self-signed-certs/key.pem new file mode 100644 index 0000000..c329a2d --- /dev/null +++ b/xks-axum/axum-server/examples/self-signed-certs/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +HUGWsrhnzvWoCOV+9+MdApoim8FI +-----END PRIVATE KEY----- diff --git a/xks-axum/axum-server/examples/self-signed-certs/reload/cert.pem b/xks-axum/axum-server/examples/self-signed-certs/reload/cert.pem new file mode 100644 index 0000000..545c0ae --- /dev/null +++ b/xks-axum/axum-server/examples/self-signed-certs/reload/cert.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFkTCCA3mgAwIBAgIUXN/Uw2uyZ6/Uj4LRuuK0/RdRHRswDQYJKoZIhvcNAQEL +BQAwWDELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDERMA8GA1UEAwwIcmVsb2FkZWQwHhcN +MjEwODI5MTI1NjU2WhcNMjIwODI5MTI1NjU2WjBYMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkg +THRkMREwDwYDVQQDDAhyZWxvYWRlZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAL/OR5KG8PqJgZSDza1lBVpZjW3jw1MA9eegePoK/4dYjd0Mdw+DeYOu +J/UmXoLHUDi/YWwZSmeY3YW0Wimwo1C5VqQL3GapSyFibvyTFE2fpoK0QtlgTKJ4 +G0mzdZ9NjibhvK23UOW5VbzlBujrYAaF2ynUha/cgVZ9uzvdwd6ooi+1i6XfHnkG +AQqGi6u/SIB+eHXn0w+tTYXmMp44jqIkjsK2vPNeifWj3MQxvgg7JTR/AKTmFCMm +BJIEP62BTFEnHJF+pRd2Hj0GIAiNBq1uA1F+HoUhxyX3OWHYCkRwPMnrSbPQOyxO +g4oFaUzAvMd2lHN/GjJS0kLwDy7WF/iXZuFxdEsmEmH62fE7N4P2uEnNw5OcHS82 +8Mc2EoMrV8zUBl4ZJ2eFo6w9lAx2bzMZyGXdOHsZWnJ5+1co6gfRfv51TeJGQx8f +JaHWFrn55qKBQmgQpKmCt/sG3HrqTviw1PtecsrzTliEXPoWdx6AhYaV+I4u8c8S +Q0NfdfjXx+5EMFDe5CvfWp/D5C1AQIV5E0Ao3Q+VfjoU/2tz9WcE5voHfyl3mBMI +FHvAPCZC18E+ZpiYyhRJLxP4z0MzTiuxp25lRi0Yt/5QTzEzFfH1UNQYe2xljPtf +syg5RtHoijcL+MncE1NUXz+B/qC4uJm8llPjFoL94Yg3/dwWOPtPAgMBAAGjUzBR +MB0GA1UdDgQWBBThri9Jq8CLFMEHJ+wE7WsCODVRwjAfBgNVHSMEGDAWgBThri9J +q8CLFMEHJ+wE7WsCODVRwjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA +A4ICAQCiR8yJ2YQyJfYDd9BT9eb9H8/S+Yz/9ayNS3zSJk4StQZaS1V6XjexzDBr +MRSr/hHGtO9G2qeocuJ/ArUJS5yYsf69g9AjuB+b41k0E4BVpiB/lENAhMbMbl+D ++ysRifUR2svHnZzKnL7DRrpS3vEUQhO37GXwbEi192rXAr2N6VE0LhxGyE7EwCzw +7gNkzoB3/Y4Fb+6zCYZorg3PmPZHrfu9vGFiP9nh+JVos9aq2JHZgZJ2N5Hcdh1H +Bci372+i1SHKfYutXrcSnUcPd4UgGQt6F63fOFHJEGsSVbHpJujqjpIscuPqgfn8 +DSkm9SEyVEV8MrY2vtwtVFOre4yjsaZ2fHDU7rCXOO88kIBBdvIpdIO4mBKV14ug +k9M1xzqK/KvgMUztuw/oLxOp7Vnii9sQ9bjzjbFEMiJ07V5Egr88Zh+VnN3ED1MH +Ri6Ho/CI/ttAwzZVhrKumOb6AprPVUteZFedpV80UaYmIthkeW0i9QcUOMkr4bL3 +gCghJeBSETTGEYCKOpcIFbvXwlc8d3KlL0Fa4EbQiw5vlPY28UChnxuZ3I0Vtetf +2F+3bLoVxfZD2Gc7p5bjGHgzUbGLFM4GgqQ6EbRh261Om9/bUxBao7mhKa23XWna +3Y4qISAqus6OolerflYJCCuWUF4N6e6fES5bqnZD49qAaIEg0A== +-----END CERTIFICATE----- diff --git a/xks-axum/axum-server/examples/self-signed-certs/reload/key.pem b/xks-axum/axum-server/examples/self-signed-certs/reload/key.pem new file mode 100644 index 0000000..bc7dccf --- /dev/null +++ b/xks-axum/axum-server/examples/self-signed-certs/reload/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +85DaC33fAFuevGxpS147+sAiW6doqQ== +-----END PRIVATE KEY----- diff --git a/xks-axum/axum-server/examples/shutdown.rs b/xks-axum/axum-server/examples/shutdown.rs new file mode 100644 index 0000000..d84d5a2 --- /dev/null +++ b/xks-axum/axum-server/examples/shutdown.rs @@ -0,0 +1,40 @@ +//! Run with `cargo run --example shutdown` command. +//! +//! To connect through browser, navigate to "http://localhost:3000" url. +//! +//! Server will shutdown in 20 seconds. + +use axum::{routing::get, Router}; +use axum_server::Handle; +use std::{net::SocketAddr, time::Duration}; +use tokio::time::sleep; + +#[tokio::main] +async fn main() { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let handle = Handle::new(); + + // Spawn a task to shutdown server. + tokio::spawn(shutdown(handle.clone())); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + println!("listening on {}", addr); + axum_server::bind(addr) + .handle(handle) + .serve(app.into_make_service()) + .await + .unwrap(); + + println!("server is shut down"); +} + +async fn shutdown(handle: Handle) { + // Wait 20 seconds. + sleep(Duration::from_secs(20)).await; + + println!("sending shutdown signal"); + + // Signal the server to shutdown using Handle. + handle.shutdown(); +} diff --git a/xks-axum/axum-server/src/accept.rs b/xks-axum/axum-server/src/accept.rs new file mode 100644 index 0000000..702337a --- /dev/null +++ b/xks-axum/axum-server/src/accept.rs @@ -0,0 +1,42 @@ +//! [`Accept`] trait and utilities. + +use std::{ + future::{Future, Ready}, + io, +}; + +/// An asynchronous function to modify io stream and service. +pub trait Accept { + /// IO stream produced by accept. + type Stream; + + /// Service produced by accept. + type Service; + + /// Future return value. + type Future: Future>; + + /// Process io stream and service asynchronously. + fn accept(&self, stream: I, service: S) -> Self::Future; +} + +/// A no-op acceptor. +#[derive(Clone, Copy, Debug, Default)] +pub struct DefaultAcceptor; + +impl DefaultAcceptor { + /// Create a new default acceptor. + pub fn new() -> Self { + Self::default() + } +} + +impl Accept for DefaultAcceptor { + type Stream = I; + type Service = S; + type Future = Ready>; + + fn accept(&self, stream: I, service: S) -> Self::Future { + std::future::ready(Ok((stream, service))) + } +} diff --git a/xks-axum/axum-server/src/addr_incoming_config.rs b/xks-axum/axum-server/src/addr_incoming_config.rs new file mode 100644 index 0000000..b75aed2 --- /dev/null +++ b/xks-axum/axum-server/src/addr_incoming_config.rs @@ -0,0 +1,76 @@ +use std::time::Duration; + +/// A configuration for [`AddrIncoming`](hyper::server::conn::AddrIncoming). +#[derive(Debug, Clone)] +pub struct AddrIncomingConfig { + pub(crate) tcp_sleep_on_accept_errors: bool, + pub(crate) tcp_keepalive: Option, + pub(crate) tcp_keepalive_interval: Option, + pub(crate) tcp_keepalive_retries: Option, + pub(crate) tcp_nodelay: bool, +} + +impl Default for AddrIncomingConfig { + fn default() -> Self { + Self::new() + } +} + +impl AddrIncomingConfig { + /// Creates a default [`AddrIncoming`](hyper::server::conn::AddrIncoming) config. + pub fn new() -> AddrIncomingConfig { + Self { + tcp_sleep_on_accept_errors: true, + tcp_keepalive: None, + tcp_keepalive_interval: None, + tcp_keepalive_retries: None, + tcp_nodelay: false, + } + } + + /// Builds the config, creating an owned version of it. + pub fn build(&mut self) -> Self { + self.clone() + } + + /// Set whether to sleep on accept errors, to avoid exhausting file descriptor limits. + /// + /// Default is `true`. + pub fn tcp_sleep_on_accept_errors(&mut self, val: bool) -> &mut Self { + self.tcp_sleep_on_accept_errors = val; + self + } + + /// Set how often to send TCP keepalive probes. + /// + /// By default TCP keepalive probes is disabled. + pub fn tcp_keepalive(&mut self, val: Option) -> &mut Self { + self.tcp_keepalive = val; + self + } + + /// Set the duration between two successive TCP keepalive retransmissions, + /// if acknowledgement to the previous keepalive transmission is not received. + /// + /// Default is no interval. + pub fn tcp_keepalive_interval(&mut self, val: Option) -> &mut Self { + self.tcp_keepalive_interval = val; + self + } + + /// Set the number of retransmissions to be carried out before declaring that remote end is not available. + /// + /// Default is no retry. + pub fn tcp_keepalive_retries(&mut self, val: Option) -> &mut Self { + self.tcp_keepalive_retries = val; + self + } + + /// Set the value of `TCP_NODELAY` option for accepted connections. + /// + /// Default is `false`. + pub fn tcp_nodelay(&mut self, val: bool) -> &mut Self { + self.tcp_nodelay = val; + self + } +} diff --git a/xks-axum/axum-server/src/handle.rs b/xks-axum/axum-server/src/handle.rs new file mode 100644 index 0000000..9171fa2 --- /dev/null +++ b/xks-axum/axum-server/src/handle.rs @@ -0,0 +1,127 @@ +use crate::notify_once::NotifyOnce; +use std::{ + net::SocketAddr, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, + }, + time::Duration, +}; +use tokio::{sync::Notify, time::sleep}; + +/// A handle for [`Server`](crate::server::Server). +#[derive(Clone, Debug, Default)] +pub struct Handle { + inner: Arc, +} + +#[derive(Debug, Default)] +struct HandleInner { + addr: Mutex>, + addr_notify: Notify, + conn_count: AtomicUsize, + shutdown: NotifyOnce, + graceful: NotifyOnce, + graceful_dur: Mutex>, + conn_end: NotifyOnce, +} + +impl Handle { + /// Create a new handle. + pub fn new() -> Self { + Self::default() + } + + /// Get the number of connections. + pub fn connection_count(&self) -> usize { + self.inner.conn_count.load(Ordering::SeqCst) + } + + /// Shutdown the server. + pub fn shutdown(&self) { + self.inner.shutdown.notify_waiters(); + } + + /// Gracefully shutdown the server. + pub fn graceful_shutdown(&self, duration: Option) { + *self.inner.graceful_dur.lock().unwrap() = duration; + + self.inner.graceful.notify_waiters(); + } + + /// Returns local address and port when server starts listening. + /// + /// Returns `None` if server fails to bind. + pub async fn listening(&self) -> Option { + let notified = self.inner.addr_notify.notified(); + + if let Some(addr) = *self.inner.addr.lock().unwrap() { + return Some(addr); + } + + notified.await; + + *self.inner.addr.lock().unwrap() + } + + pub(crate) fn notify_listening(&self, addr: Option) { + *self.inner.addr.lock().unwrap() = addr; + + self.inner.addr_notify.notify_waiters(); + } + + pub(crate) fn watcher(&self) -> Watcher { + Watcher::new(self.clone()) + } + + pub(crate) async fn wait_shutdown(&self) { + self.inner.shutdown.notified().await; + } + + pub(crate) async fn wait_graceful_shutdown(&self) { + self.inner.graceful.notified().await; + } + + pub(crate) async fn wait_connections_end(&self) { + if self.inner.conn_count.load(Ordering::SeqCst) == 0 { + return; + } + + let deadline = *self.inner.graceful_dur.lock().unwrap(); + + match deadline { + Some(duration) => tokio::select! { + biased; + _ = sleep(duration) => self.shutdown(), + _ = self.inner.conn_end.notified() => (), + }, + None => self.inner.conn_end.notified().await, + } + } +} + +pub(crate) struct Watcher { + handle: Handle, +} + +impl Watcher { + fn new(handle: Handle) -> Self { + handle.inner.conn_count.fetch_add(1, Ordering::SeqCst); + + Self { handle } + } + + pub(crate) async fn wait_shutdown(&self) { + self.handle.wait_shutdown().await + } +} + +impl Drop for Watcher { + fn drop(&mut self) { + let count = self.handle.inner.conn_count.fetch_sub(1, Ordering::SeqCst) - 1; + + if count == 0 && self.handle.inner.graceful.is_notified() { + self.handle.inner.conn_end.notify_waiters(); + } + } +} diff --git a/xks-axum/axum-server/src/http_config.rs b/xks-axum/axum-server/src/http_config.rs new file mode 100644 index 0000000..87ff3b2 --- /dev/null +++ b/xks-axum/axum-server/src/http_config.rs @@ -0,0 +1,215 @@ +use hyper::server::conn::Http; +use std::time::Duration; + +/// A configuration for [`Http`]. +#[derive(Debug, Clone)] +pub struct HttpConfig { + pub(crate) inner: Http, +} + +impl Default for HttpConfig { + fn default() -> Self { + Self::new() + } +} + +impl HttpConfig { + /// Creates a default [`Http`] config. + pub fn new() -> HttpConfig { + Self { inner: Http::new() } + } + + /// Builds the config, creating an owned version of it. + pub fn build(&mut self) -> Self { + self.clone() + } + + /// Sets whether HTTP1 is required. + /// + /// Default is `false`. + pub fn http1_only(&mut self, val: bool) -> &mut Self { + self.inner.http1_only(val); + self + } + + /// Set whether HTTP/1 connections should support half-closures. + /// + /// Clients can chose to shutdown their write-side while waiting + /// for the server to respond. Setting this to `true` will + /// prevent closing the connection immediately if `read` + /// detects an EOF in the middle of a request. + /// + /// Default is `false`. + pub fn http1_half_close(&mut self, val: bool) -> &mut Self { + self.inner.http1_half_close(val); + self + } + + /// Enables or disables HTTP/1 keep-alive. + /// + /// Default is true. + pub fn http1_keep_alive(&mut self, val: bool) -> &mut Self { + self.inner.http1_keep_alive(val); + self + } + + /// Set whether HTTP/1 connections will write header names as title case at + /// the socket level. + /// + /// Note that this setting does not affect HTTP/2. + /// + /// Default is false. + pub fn http1_title_case_headers(&mut self, enabled: bool) -> &mut Self { + self.inner.http1_title_case_headers(enabled); + self + } + + /// Set whether HTTP/1 connections will write header names as provided + /// at the socket level. + /// + /// Note that this setting does not affect HTTP/2. + /// + /// Default is false. + pub fn http1_preserve_header_case(&mut self, enabled: bool) -> &mut Self { + self.inner.http1_preserve_header_case(enabled); + self + } + + /// Set a timeout for reading client request headers. If a client does not + /// transmit the entire header within this time, the connection is closed. + /// + /// Default is None. + pub fn http1_header_read_timeout(&mut self, val: Duration) -> &mut Self { + self.inner.http1_header_read_timeout(val); + self + } + + /// Set whether HTTP/1 connections should try to use vectored writes, + /// or always flatten into a single buffer. + /// + /// Note that setting this to false may mean more copies of body data, + /// but may also improve performance when an IO transport doesn't + /// support vectored writes well, such as most TLS implementations. + /// + /// Setting this to true will force hyper to use queued strategy + /// which may eliminate unnecessary cloning on some TLS backends + /// + /// Default is `auto`. In this mode hyper will try to guess which + /// mode to use + pub fn http1_writev(&mut self, val: bool) -> &mut Self { + self.inner.http1_writev(val); + self + } + + /// Sets whether HTTP2 is required. + /// + /// Default is false + pub fn http2_only(&mut self, val: bool) -> &mut Self { + self.inner.http2_only(val); + self + } + + /// Sets the [`SETTINGS_INITIAL_WINDOW_SIZE`][spec] option for HTTP2 + /// stream-level flow control. + /// + /// Passing `None` will do nothing. + /// + /// If not set, hyper will use a default. + /// + /// [spec]: https://http2.github.io/http2-spec/#SETTINGS_INITIAL_WINDOW_SIZE + pub fn http2_initial_stream_window_size(&mut self, sz: impl Into>) -> &mut Self { + self.inner.http2_initial_stream_window_size(sz); + self + } + + /// Sets the max connection-level flow control for HTTP2. + /// + /// Passing `None` will do nothing. + /// + /// If not set, hyper will use a default. + pub fn http2_initial_connection_window_size( + &mut self, + sz: impl Into>, + ) -> &mut Self { + self.inner.http2_initial_connection_window_size(sz); + self + } + + /// Sets whether to use an adaptive flow control. + /// + /// Enabling this will override the limits set in + /// `http2_initial_stream_window_size` and + /// `http2_initial_connection_window_size`. + pub fn http2_adaptive_window(&mut self, enabled: bool) -> &mut Self { + self.inner.http2_adaptive_window(enabled); + self + } + + /// Sets the maximum frame size to use for HTTP2. + /// + /// Passing `None` will do nothing. + /// + /// If not set, hyper will use a default. + pub fn http2_max_frame_size(&mut self, sz: impl Into>) -> &mut Self { + self.inner.http2_max_frame_size(sz); + self + } + + /// Sets the [`SETTINGS_MAX_CONCURRENT_STREAMS`][spec] option for HTTP2 + /// connections. + /// + /// Default is no limit (`std::u32::MAX`). Passing `None` will do nothing. + /// + /// [spec]: https://http2.github.io/http2-spec/#SETTINGS_MAX_CONCURRENT_STREAMS + pub fn http2_max_concurrent_streams(&mut self, max: impl Into>) -> &mut Self { + self.inner.http2_max_concurrent_streams(max); + self + } + + /// Sets an interval for HTTP2 Ping frames should be sent to keep a + /// connection alive. + /// + /// Pass `None` to disable HTTP2 keep-alive. + /// + /// Default is currently disabled. + pub fn http2_keep_alive_interval( + &mut self, + interval: impl Into>, + ) -> &mut Self { + self.inner.http2_keep_alive_interval(interval); + self + } + + /// Sets a timeout for receiving an acknowledgement of the keep-alive ping. + /// + /// If the ping is not acknowledged within the timeout, the connection will + /// be closed. Does nothing if `http2_keep_alive_interval` is disabled. + /// + /// Default is 20 seconds. + pub fn http2_keep_alive_timeout(&mut self, timeout: Duration) -> &mut Self { + self.inner.http2_keep_alive_timeout(timeout); + self + } + + /// Set the maximum buffer size for the connection. + /// + /// Default is ~400kb. + /// + /// # Panics + /// + /// The minimum value allowed is 8192. This method panics if the passed `max` is less than the minimum. + pub fn max_buf_size(&mut self, max: usize) -> &mut Self { + self.inner.max_buf_size(max); + self + } + + /// Aggregates flushes to better support pipelined responses. + /// + /// Experimental, may have bugs. + /// + /// Default is false. + pub fn pipeline_flush(&mut self, enabled: bool) -> &mut Self { + self.inner.pipeline_flush(enabled); + self + } +} diff --git a/xks-axum/axum-server/src/lib.rs b/xks-axum/axum-server/src/lib.rs new file mode 100644 index 0000000..1c3a6ed --- /dev/null +++ b/xks-axum/axum-server/src/lib.rs @@ -0,0 +1,112 @@ +//! axum-server is a [hyper] server implementation designed to be used with [axum] framework. +//! +//! # Features +//! +//! - HTTP/1 and HTTP/2 +//! - HTTPS through [rustls]. +//! - High performance through [hyper]. +//! - Using [tower] make service API. +//! - Very good [axum] compatibility. Likely to work with future [axum] releases. +//! +//! # Guide +//! +//! axum-server can [`serve`] items that implement [`MakeService`] with some additional [trait +//! bounds](crate::service::MakeServiceRef). Make services that are [created] using [`axum`] +//! complies with those trait bounds out of the box. Therefore it is more convenient to use this +//! crate with [`axum`]. +//! +//! All examples in this crate uses [`axum`]. If you want to use this crate without [`axum`] it is +//! highly recommended to learn how [tower] works. +//! +//! [`Server::bind`] or [`bind`] function can be called to create a server that will bind to +//! provided [`SocketAddr`] when [`serve`] is called. +//! +//! A [`Handle`] can be passed to [`Server`](Server::handle) for additional utilities like shutdown +//! and graceful shutdown. +//! +//! [`bind_rustls`] can be called by providing [`RustlsConfig`] to create a HTTPS [`Server`] that +//! will bind on provided [`SocketAddr`]. [`RustlsConfig`] can be cloned, reload methods can be +//! used on clone to reload tls configuration. +//! +//! # Example +//! +//! A simple hello world application can be served like: +//! +//! ```rust,no_run +//! use axum::{routing::get, Router}; +//! use std::net::SocketAddr; +//! +//! #[tokio::main] +//! async fn main() { +//! let app = Router::new().route("/", get(|| async { "Hello, world!" })); +//! +//! let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); +//! println!("listening on {}", addr); +//! axum_server::bind(addr) +//! .serve(app.into_make_service()) +//! .await +//! .unwrap(); +//! } +//! ``` +//! +//! You can find more examples in [repository]. +//! +//! [axum]: https://crates.io/crates/axum +//! [bind]: crate::bind +//! [bind_rustls]: crate::bind_rustls +//! [created]: https://docs.rs/axum/0.3/axum/struct.Router.html#method.into_make_service +//! [hyper]: https://crates.io/crates/hyper +//! [repository]: https://github.com/programatik29/axum-server/tree/v0.3.0/examples +//! [rustls]: https://crates.io/crates/rustls +//! [tower]: https://crates.io/crates/tower +//! [`axum`]: https://docs.rs/axum/0.3 +//! [`serve`]: crate::server::Server::serve +//! [`MakeService`]: https://docs.rs/tower/0.4/tower/make/trait.MakeService.html +//! [`RustlsConfig`]: crate::tls_rustls::RustlsConfig +//! [`SocketAddr`]: std::net::SocketAddr + +#![forbid(unsafe_code)] +#![warn( + clippy::await_holding_lock, + clippy::cargo_common_metadata, + clippy::dbg_macro, + clippy::doc_markdown, + clippy::empty_enum, + clippy::enum_glob_use, + clippy::inefficient_to_string, + clippy::mem_forget, + clippy::mutex_integer, + clippy::needless_continue, + clippy::todo, + clippy::unimplemented, + clippy::wildcard_imports, + future_incompatible, + missing_docs, + missing_debug_implementations, + unreachable_pub +)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +mod addr_incoming_config; +mod handle; +mod http_config; +mod notify_once; +mod server; + +pub mod accept; +pub mod service; + +pub use self::{ + addr_incoming_config::AddrIncomingConfig, + handle::Handle, + http_config::HttpConfig, + server::{bind, from_tcp, Server}, +}; + +#[cfg(feature = "tls-rustls")] +#[cfg_attr(docsrs, doc(cfg(feature = "tls-rustls")))] +pub mod tls_rustls; + +#[doc(inline)] +#[cfg(feature = "tls-rustls")] +pub use self::tls_rustls::export::{bind_rustls, from_tcp_rustls}; diff --git a/xks-axum/axum-server/src/notify_once.rs b/xks-axum/axum-server/src/notify_once.rs new file mode 100644 index 0000000..452906e --- /dev/null +++ b/xks-axum/axum-server/src/notify_once.rs @@ -0,0 +1,28 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use tokio::sync::Notify; + +#[derive(Debug, Default)] +pub(crate) struct NotifyOnce { + notified: AtomicBool, + notify: Notify, +} + +impl NotifyOnce { + pub(crate) fn notify_waiters(&self) { + self.notified.store(true, Ordering::SeqCst); + + self.notify.notify_waiters(); + } + + pub(crate) fn is_notified(&self) -> bool { + self.notified.load(Ordering::SeqCst) + } + + pub(crate) async fn notified(&self) { + let future = self.notify.notified(); + + if !self.notified.load(Ordering::SeqCst) { + future.await; + } + } +} diff --git a/xks-axum/axum-server/src/server.rs b/xks-axum/axum-server/src/server.rs new file mode 100644 index 0000000..dc4da0a --- /dev/null +++ b/xks-axum/axum-server/src/server.rs @@ -0,0 +1,407 @@ +use crate::{ + accept::{Accept, DefaultAcceptor}, + addr_incoming_config::AddrIncomingConfig, + handle::Handle, + http_config::HttpConfig, + service::{MakeServiceRef, SendService}, +}; +use futures_util::future::poll_fn; +use http::Request; +use hyper::server::{ + accept::Accept as HyperAccept, + conn::{AddrIncoming, AddrStream}, +}; +use std::{ + io::{self, ErrorKind}, + net::SocketAddr, + pin::Pin, +}; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::TcpListener, +}; + +/// HTTP server. +#[derive(Debug)] +pub struct Server { + acceptor: A, + listener: Listener, + addr_incoming_conf: AddrIncomingConfig, + handle: Handle, + http_conf: HttpConfig, +} + +#[derive(Debug)] +enum Listener { + Bind(SocketAddr), + Std(std::net::TcpListener), +} + +/// Create a [`Server`] that will bind to provided address. +pub fn bind(addr: SocketAddr) -> Server { + Server::bind(addr) +} + +/// Create a [`Server`] from existing `std::net::TcpListener`. +pub fn from_tcp(listener: std::net::TcpListener) -> Server { + Server::from_tcp(listener) +} + +impl Server { + /// Create a server that will bind to provided address. + pub fn bind(addr: SocketAddr) -> Self { + let acceptor = DefaultAcceptor::new(); + let handle = Handle::new(); + + Self { + acceptor, + listener: Listener::Bind(addr), + addr_incoming_conf: AddrIncomingConfig::default(), + handle, + http_conf: HttpConfig::default(), + } + } + + /// Create a server from existing `std::net::TcpListener`. + pub fn from_tcp(listener: std::net::TcpListener) -> Self { + let acceptor = DefaultAcceptor::new(); + let handle = Handle::new(); + + Self { + acceptor, + listener: Listener::Std(listener), + addr_incoming_conf: AddrIncomingConfig::default(), + handle, + http_conf: HttpConfig::default(), + } + } +} + +impl Server { + /// Overwrite acceptor. + pub fn acceptor(self, acceptor: Acceptor) -> Server { + Server { + acceptor, + listener: self.listener, + addr_incoming_conf: self.addr_incoming_conf, + handle: self.handle, + http_conf: self.http_conf, + } + } + + /// Map acceptor. + pub fn map(self, acceptor: F) -> Server + where + F: FnOnce(A) -> Acceptor, + { + Server { + acceptor: acceptor(self.acceptor), + listener: self.listener, + addr_incoming_conf: self.addr_incoming_conf, + handle: self.handle, + http_conf: self.http_conf, + } + } + + /// Returns a reference to the acceptor. + pub fn get_ref(&self) -> &A { + &self.acceptor + } + + /// Returns a mutable reference to the acceptor. + pub fn get_mut(&mut self) -> &mut A { + &mut self.acceptor + } + + /// Provide a handle for additional utilities. + pub fn handle(mut self, handle: Handle) -> Self { + self.handle = handle; + self + } + + /// Overwrite http configuration. + pub fn http_config(mut self, config: HttpConfig) -> Self { + self.http_conf = config; + self + } + + /// Overwrite addr incoming configuration. + pub fn addr_incoming_config(mut self, config: AddrIncomingConfig) -> Self { + self.addr_incoming_conf = config; + self + } + + /// Serve provided [`MakeService`]. + /// + /// To create [`MakeService`] easily, `Shared` from [`tower`] can be used. + /// + /// # Errors + /// + /// An error will be returned when: + /// + /// - Binding to an address fails. + /// - `make_service` returns an error when `poll_ready` is called. This never happens on + /// [`axum`] make services. + /// + /// [`axum`]: https://docs.rs/axum/0.3 + /// [`tower`]: https://docs.rs/tower + /// [`MakeService`]: https://docs.rs/tower/0.4/tower/make/trait.MakeService.html + pub async fn serve(self, mut make_service: M) -> io::Result<()> + where + M: MakeServiceRef>, + A: Accept + Clone + Send + Sync + 'static, + A::Stream: AsyncRead + AsyncWrite + Unpin + Send, + A::Service: SendService> + Send, + A::Future: Send, + { + let acceptor = self.acceptor; + let addr_incoming_conf = self.addr_incoming_conf; + let handle = self.handle; + let http_conf = self.http_conf; + + let mut incoming = match bind_incoming(self.listener, addr_incoming_conf).await { + Ok(v) => v, + Err(e) => { + handle.notify_listening(None); + return Err(e); + } + }; + + handle.notify_listening(Some(incoming.local_addr())); + + let accept_loop_future = async { + loop { + let addr_stream = tokio::select! { + biased; + result = accept(&mut incoming) => result?, + _ = handle.wait_graceful_shutdown() => return Ok(()), + }; + + poll_fn(|cx| make_service.poll_ready(cx)) + .await + .map_err(io_other)?; + + let service = match make_service.make_service(&addr_stream).await { + Ok(service) => service, + Err(_) => continue, + }; + + let acceptor = acceptor.clone(); + let watcher = handle.watcher(); + let http_conf = http_conf.clone(); + + tokio::spawn(async move { + if let Ok((stream, send_service)) = acceptor.accept(addr_stream, service).await + { + let service = send_service.into_service(); + + let serve_future = http_conf + .inner + .serve_connection(stream, service) + .with_upgrades(); + + tokio::select! { + biased; + _ = watcher.wait_shutdown() => (), + _ = serve_future => (), + } + } + }); + } + }; + + let result = tokio::select! { + biased; + _ = handle.wait_shutdown() => return Ok(()), + result = accept_loop_future => result, + }; + + if let Err(e) = result { + return Err(e); + } + + handle.wait_connections_end().await; + + Ok(()) + } +} + +async fn bind_incoming( + listener: Listener, + addr_incoming_conf: AddrIncomingConfig, +) -> io::Result { + let listener = match listener { + Listener::Bind(addr) => TcpListener::bind(addr).await?, + Listener::Std(std_listener) => { + std_listener.set_nonblocking(true)?; + TcpListener::from_std(std_listener)? + } + }; + let mut incoming = AddrIncoming::from_listener(listener).map_err(io_other)?; + + incoming.set_sleep_on_errors(addr_incoming_conf.tcp_sleep_on_accept_errors); + incoming.set_keepalive(addr_incoming_conf.tcp_keepalive); + incoming.set_keepalive_interval(addr_incoming_conf.tcp_keepalive_interval); + incoming.set_keepalive_retries(addr_incoming_conf.tcp_keepalive_retries); + incoming.set_nodelay(addr_incoming_conf.tcp_nodelay); + + Ok(incoming) +} + +pub(crate) async fn accept(incoming: &mut AddrIncoming) -> io::Result { + let mut incoming = Pin::new(incoming); + + // Always [`Option::Some`]. + // https://docs.rs/hyper/0.14.14/src/hyper/server/tcp.rs.html#165 + poll_fn(|cx| incoming.as_mut().poll_accept(cx)) + .await + .unwrap() +} + +type BoxError = Box; + +pub(crate) fn io_other>(error: E) -> io::Error { + io::Error::new(ErrorKind::Other, error) +} + +#[cfg(test)] +mod tests { + use crate::{handle::Handle, server::Server}; + use axum::{routing::get, Router}; + use bytes::Bytes; + use http::{response, Request}; + use hyper::{ + client::conn::{handshake, SendRequest}, + Body, + }; + use std::{io, net::SocketAddr, time::Duration}; + use tokio::{net::TcpStream, task::JoinHandle, time::timeout}; + use tower::{Service, ServiceExt}; + + #[tokio::test] + async fn start_and_request() { + let (_handle, _server_task, addr) = start_server().await; + + let (mut client, _conn) = connect(addr).await; + + let (_parts, body) = send_empty_request(&mut client).await; + + assert_eq!(body.as_ref(), b"Hello, world!"); + } + + #[tokio::test] + async fn test_shutdown() { + let (handle, _server_task, addr) = start_server().await; + + let (mut client, conn) = connect(addr).await; + + handle.shutdown(); + + let response_future_result = client + .ready() + .await + .unwrap() + .call(Request::new(Body::empty())) + .await; + + assert!(response_future_result.is_err()); + + // Connection task should finish soon. + let _ = timeout(Duration::from_secs(1), conn).await.unwrap(); + } + + #[tokio::test] + async fn test_graceful_shutdown() { + let (handle, server_task, addr) = start_server().await; + + let (mut client, conn) = connect(addr).await; + + handle.graceful_shutdown(None); + + let (_parts, body) = send_empty_request(&mut client).await; + + assert_eq!(body.as_ref(), b"Hello, world!"); + + // Disconnect client. + conn.abort(); + + // Server task should finish soon. + let server_result = timeout(Duration::from_secs(1), server_task) + .await + .unwrap() + .unwrap(); + + assert!(server_result.is_ok()); + } + + #[ignore] + #[tokio::test] + async fn test_graceful_shutdown_timed() { + let (handle, server_task, addr) = start_server().await; + + let (mut client, _conn) = connect(addr).await; + + handle.graceful_shutdown(Some(Duration::from_millis(250))); + + let (_parts, body) = send_empty_request(&mut client).await; + + assert_eq!(body.as_ref(), b"Hello, world!"); + + // Don't disconnect client. + // conn.abort(); + + // Server task should finish soon. + let server_result = timeout(Duration::from_secs(1), server_task) + .await + .unwrap() + .unwrap(); + + assert!(server_result.is_ok()); + } + + async fn start_server() -> (Handle, JoinHandle>, SocketAddr) { + let handle = Handle::new(); + + let server_handle = handle.clone(); + let server_task = tokio::spawn(async move { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + + Server::bind(addr) + .handle(server_handle) + .serve(app.into_make_service()) + .await + }); + + let addr = handle.listening().await.unwrap(); + + (handle, server_task, addr) + } + + async fn connect(addr: SocketAddr) -> (SendRequest, JoinHandle<()>) { + let stream = TcpStream::connect(addr).await.unwrap(); + + let (send_request, connection) = handshake(stream).await.unwrap(); + + let task = tokio::spawn(async move { + let _ = connection.await; + }); + + (send_request, task) + } + + async fn send_empty_request(client: &mut SendRequest) -> (response::Parts, Bytes) { + let (parts, body) = client + .ready() + .await + .unwrap() + .call(Request::new(Body::empty())) + .await + .unwrap() + .into_parts(); + let body = hyper::body::to_bytes(body).await.unwrap(); + + (parts, body) + } +} diff --git a/xks-axum/axum-server/src/service.rs b/xks-axum/axum-server/src/service.rs new file mode 100644 index 0000000..e2f335f --- /dev/null +++ b/xks-axum/axum-server/src/service.rs @@ -0,0 +1,156 @@ +//! Service traits. + +use http::Response; +use http_body::Body; +use std::{ + future::Future, + task::{Context, Poll}, +}; +use tower_service::Service; + +/// Trait alias for [`Service`] with bounds required for [`serve`](crate::server::Server::serve). +/// +/// This trait is sealed and cannot be implemented for types outside this crate. +#[allow(missing_docs)] +pub trait SendService: send_service::Sealed { + type Service: Service< + Request, + Response = Response, + Error = Self::Error, + Future = Self::Future, + > + Send + + 'static; + + type Body: Body + Send + 'static; + type BodyData: Send + 'static; + type BodyError: Into>; + + type Error: Into>; + + type Future: Future, Self::Error>> + Send + 'static; + + fn into_service(self) -> Self::Service; +} + +impl send_service::Sealed for T +where + T: Service>, + T::Error: Into>, + T::Future: Send + 'static, + B: Body + Send + 'static, + B::Data: Send + 'static, + B::Error: Into>, +{ +} + +impl SendService for T +where + T: Service> + Send + 'static, + T::Error: Into>, + T::Future: Send + 'static, + B: Body + Send + 'static, + B::Data: Send + 'static, + B::Error: Into>, +{ + type Service = T; + + type Body = B; + type BodyData = B::Data; + type BodyError = B::Error; + + type Error = T::Error; + + type Future = T::Future; + + fn into_service(self) -> Self::Service { + self + } +} + +/// Modified version of [`MakeService`] that takes a `&Target` and has required trait bounds for +/// [`serve`](crate::server::Server::serve). +/// +/// This trait is sealed and cannot be implemented for types outside this crate. +/// +/// [`MakeService`]: https://docs.rs/tower/0.4/tower/make/trait.MakeService.html +#[allow(missing_docs)] +pub trait MakeServiceRef: make_service_ref::Sealed<(Target, Request)> { + type Service: Service< + Request, + Response = Response, + Error = Self::Error, + Future = Self::Future, + > + Send + + 'static; + + type Body: Body + Send + 'static; + type BodyData: Send + 'static; + type BodyError: Into>; + + type Error: Into>; + + type Future: Future, Self::Error>> + Send + 'static; + + type MakeError: Into>; + type MakeFuture: Future>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll>; + + fn make_service(&mut self, target: &Target) -> Self::MakeFuture; +} + +impl make_service_ref::Sealed<(Target, Request)> for T +where + T: for<'a> Service<&'a Target, Response = S, Error = E, Future = F>, + S: Service> + Send + 'static, + S::Error: Into>, + S::Future: Send + 'static, + B: Body + Send + 'static, + B::Data: Send + 'static, + B::Error: Into>, + E: Into>, + F: Future>, +{ +} + +impl MakeServiceRef for T +where + T: for<'a> Service<&'a Target, Response = S, Error = E, Future = F>, + S: Service> + Send + 'static, + S::Error: Into>, + S::Future: Send + 'static, + B: Body + Send + 'static, + B::Data: Send + 'static, + B::Error: Into>, + E: Into>, + F: Future>, +{ + type Service = S; + + type Body = B; + type BodyData = B::Data; + type BodyError = B::Error; + + type Error = S::Error; + + type Future = S::Future; + + type MakeError = E; + type MakeFuture = F; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.poll_ready(cx) + } + + fn make_service(&mut self, target: &Target) -> Self::MakeFuture { + self.call(target) + } +} + +mod send_service { + pub trait Sealed {} +} + +mod make_service_ref { + pub trait Sealed {} +} diff --git a/xks-axum/axum-server/src/tls_rustls/future.rs b/xks-axum/axum-server/src/tls_rustls/future.rs new file mode 100644 index 0000000..5b7594b --- /dev/null +++ b/xks-axum/axum-server/src/tls_rustls/future.rs @@ -0,0 +1,114 @@ +//! Future types. + +use crate::tls_rustls::RustlsConfig; +use pin_project_lite::pin_project; +use std::io::{Error, ErrorKind}; +use std::time::Duration; +use std::{ + fmt, + future::Future, + io, + pin::Pin, + task::{Context, Poll}, +}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::time::{timeout, Timeout}; +use tokio_rustls::{server::TlsStream, Accept, TlsAcceptor}; + +pin_project! { + /// Future type for [`RustlsAcceptor`](crate::tls_rustls::RustlsAcceptor). + pub struct RustlsAcceptorFuture { + #[pin] + inner: AcceptFuture, + config: Option, + } +} + +impl RustlsAcceptorFuture { + pub(crate) fn new(future: F, config: RustlsConfig, handshake_timeout: Duration) -> Self { + let inner = AcceptFuture::Inner { + future, + handshake_timeout, + }; + let config = Some(config); + + Self { inner, config } + } +} + +impl fmt::Debug for RustlsAcceptorFuture { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RustlsAcceptorFuture").finish() + } +} + +pin_project! { + #[project = AcceptFutureProj] + enum AcceptFuture { + Inner { + #[pin] + future: F, + handshake_timeout: Duration, + }, + Accept { + #[pin] + future: Timeout>, + service: Option, + }, + } +} + +impl Future for RustlsAcceptorFuture +where + F: Future>, + I: AsyncRead + AsyncWrite + Unpin, +{ + type Output = io::Result<(TlsStream, S)>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + + loop { + match this.inner.as_mut().project() { + AcceptFutureProj::Inner { + future, + handshake_timeout, + } => { + match future.poll(cx) { + Poll::Ready(Ok((stream, service))) => { + let server_config = this.config + .take() + .expect("config is not set. this is a bug in axum-server, please report") + .get_inner(); + + let acceptor = TlsAcceptor::from(server_config); + let future = acceptor.accept(stream); + + let service = Some(service); + let handshake_timeout = *handshake_timeout; + + this.inner.set(AcceptFuture::Accept { + future: timeout(handshake_timeout, future), + service, + }); + } + Poll::Ready(Err(e)) => return Poll::Ready(Err(e)), + Poll::Pending => return Poll::Pending, + } + } + AcceptFutureProj::Accept { future, service } => match future.poll(cx) { + Poll::Ready(Ok(Ok(stream))) => { + let service = service.take().expect("future polled after ready"); + + return Poll::Ready(Ok((stream, service))); + } + Poll::Ready(Ok(Err(e))) => return Poll::Ready(Err(e)), + Poll::Ready(Err(timeout)) => { + return Poll::Ready(Err(Error::new(ErrorKind::TimedOut, timeout))) + } + Poll::Pending => return Poll::Pending, + }, + } + } + } +} diff --git a/xks-axum/axum-server/src/tls_rustls/mod.rs b/xks-axum/axum-server/src/tls_rustls/mod.rs new file mode 100644 index 0000000..898588e --- /dev/null +++ b/xks-axum/axum-server/src/tls_rustls/mod.rs @@ -0,0 +1,578 @@ +//! Tls implementation using [`rustls`]. +//! +//! # Example +//! +//! ```rust,no_run +//! use axum::{routing::get, Router}; +//! use axum_server::tls_rustls::RustlsConfig; +//! use std::net::SocketAddr; +//! +//! #[tokio::main] +//! async fn main() { +//! let app = Router::new().route("/", get(|| async { "Hello, world!" })); +//! +//! let config = RustlsConfig::from_pem_file( +//! "examples/self-signed-certs/cert.pem", +//! "examples/self-signed-certs/key.pem", +//! ) +//! .await +//! .unwrap(); +//! +//! let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); +//! println!("listening on {}", addr); +//! axum_server::bind_rustls(addr, config) +//! .serve(app.into_make_service()) +//! .await +//! .unwrap(); +//! } +//! ``` + +use self::future::RustlsAcceptorFuture; +use crate::{ + accept::{Accept, DefaultAcceptor}, + server::{io_other, Server}, +}; +use arc_swap::ArcSwap; +use rustls::{Certificate, PrivateKey, ServerConfig}; +use std::time::Duration; +use std::{fmt, io, net::SocketAddr, path::Path, sync::Arc}; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + task::spawn_blocking, +}; +use tokio_rustls::server::TlsStream; + +pub(crate) mod export { + use super::*; + + /// Create a tls server that will bind to provided address. + #[cfg_attr(docsrs, doc(cfg(feature = "tls-rustls")))] + pub fn bind_rustls(addr: SocketAddr, config: RustlsConfig) -> Server { + super::bind_rustls(addr, config) + } + + /// Create a tls server from existing `std::net::TcpListener`. + #[cfg_attr(docsrs, doc(cfg(feature = "tls-rustls")))] + pub fn from_tcp_rustls( + listener: std::net::TcpListener, + config: RustlsConfig, + ) -> Server { + let acceptor = RustlsAcceptor::new(config); + + Server::from_tcp(listener).acceptor(acceptor) + } +} + +pub mod future; + +/// Create a tls server that will bind to provided address. +pub fn bind_rustls(addr: SocketAddr, config: RustlsConfig) -> Server { + let acceptor = RustlsAcceptor::new(config); + + Server::bind(addr).acceptor(acceptor) +} + +/// Create a tls server from existing `std::net::TcpListener`. +pub fn from_tcp_rustls( + listener: std::net::TcpListener, + config: RustlsConfig, +) -> Server { + let acceptor = RustlsAcceptor::new(config); + + Server::from_tcp(listener).acceptor(acceptor) +} + +/// Tls acceptor using rustls. +#[derive(Clone)] +pub struct RustlsAcceptor { + inner: A, + config: RustlsConfig, + handshake_timeout: Duration, +} + +impl RustlsAcceptor { + /// Create a new rustls acceptor. + pub fn new(config: RustlsConfig) -> Self { + let inner = DefaultAcceptor::new(); + + #[cfg(not(test))] + let handshake_timeout = Duration::from_secs(10); + + // Don't force tests to wait too long. + #[cfg(test)] + let handshake_timeout = Duration::from_secs(1); + + Self { + inner, + config, + handshake_timeout, + } + } + + /// Override the default TLS handshake timeout of 10 seconds, except during testing. + pub fn handshake_timeout(mut self, val: Duration) -> Self { + self.handshake_timeout = val; + self + } +} + +impl RustlsAcceptor { + /// Overwrite inner acceptor. + pub fn acceptor(self, acceptor: Acceptor) -> RustlsAcceptor { + RustlsAcceptor { + inner: acceptor, + config: self.config, + handshake_timeout: self.handshake_timeout, + } + } +} + +impl Accept for RustlsAcceptor +where + A: Accept, + A::Stream: AsyncRead + AsyncWrite + Unpin, +{ + type Stream = TlsStream; + type Service = A::Service; + type Future = RustlsAcceptorFuture; + + fn accept(&self, stream: I, service: S) -> Self::Future { + let inner_future = self.inner.accept(stream, service); + let config = self.config.clone(); + + RustlsAcceptorFuture::new(inner_future, config, self.handshake_timeout) + } +} + +impl fmt::Debug for RustlsAcceptor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RustlsAcceptor").finish() + } +} + +/// Rustls configuration. +#[derive(Clone)] +pub struct RustlsConfig { + inner: Arc>, +} + +impl RustlsConfig { + /// Create config from `Arc<`[`ServerConfig`]`>`. + pub fn from_config(config: Arc) -> Self { + let inner = Arc::new(ArcSwap::new(config)); + + Self { inner } + } + + /// Create config from DER-encoded data. + /// + /// The certificate must be DER-encoded X.509. + /// + /// The private key must be DER-encoded ASN.1 in either PKCS#8 or PKCS#1 format. + pub async fn from_der(cert: Vec>, key: Vec) -> io::Result { + let server_config = spawn_blocking(|| config_from_der(cert, key)) + .await + .unwrap()?; + let inner = Arc::new(ArcSwap::from_pointee(server_config)); + + Ok(Self { inner }) + } + + /// Create config from PEM formatted data. + /// + /// Certificate and private key must be in PEM format. + pub async fn from_pem(cert: Vec, key: Vec) -> io::Result { + let server_config = spawn_blocking(|| config_from_pem(cert, key)) + .await + .unwrap()?; + let inner = Arc::new(ArcSwap::from_pointee(server_config)); + + Ok(Self { inner }) + } + + /// Create config from PEM formatted files. + /// + /// Contents of certificate file and private key file must be in PEM format. + pub async fn from_pem_file(cert: impl AsRef, key: impl AsRef) -> io::Result { + let server_config = config_from_pem_file(cert, key).await?; + let inner = Arc::new(ArcSwap::from_pointee(server_config)); + + Ok(Self { inner }) + } + + /// Get inner `Arc<`[`ServerConfig`]`>`. + pub fn get_inner(&self) -> Arc { + self.inner.load_full() + } + + /// Reload config from `Arc<`[`ServerConfig`]`>`. + pub fn reload_from_config(&self, config: Arc) { + self.inner.store(config); + } + + /// Reload config from DER-encoded data. + /// + /// The certificate must be DER-encoded X.509. + /// + /// The private key must be DER-encoded ASN.1 in either PKCS#8 or PKCS#1 format. + pub async fn reload_from_der(&self, cert: Vec>, key: Vec) -> io::Result<()> { + let server_config = spawn_blocking(|| config_from_der(cert, key)) + .await + .unwrap()?; + let inner = Arc::new(server_config); + + self.inner.store(inner); + + Ok(()) + } + + /// Reload config from PEM formatted data. + /// + /// Certificate and private key must be in PEM format. + pub async fn reload_from_pem(&self, cert: Vec, key: Vec) -> io::Result<()> { + let server_config = spawn_blocking(|| config_from_pem(cert, key)) + .await + .unwrap()?; + let inner = Arc::new(server_config); + + self.inner.store(inner); + + Ok(()) + } + + /// Reload config from PEM formatted files. + /// + /// Contents of certificate file and private key file must be in PEM format. + pub async fn reload_from_pem_file( + &self, + cert: impl AsRef, + key: impl AsRef, + ) -> io::Result<()> { + let server_config = config_from_pem_file(cert, key).await?; + let inner = Arc::new(server_config); + + self.inner.store(inner); + + Ok(()) + } +} + +impl fmt::Debug for RustlsConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RustlsConfig").finish() + } +} + +fn config_from_der(cert: Vec>, key: Vec) -> io::Result { + let cert = cert.into_iter().map(Certificate).collect(); + let key = PrivateKey(key); + + let mut config = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert, key) + .map_err(io_other)?; + + config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + + Ok(config) +} + +fn config_from_pem(cert: Vec, key: Vec) -> io::Result { + use rustls_pemfile::Item; + + let cert = rustls_pemfile::certs(&mut cert.as_ref())?; + let key = match rustls_pemfile::read_one(&mut key.as_ref())? { + Some(Item::RSAKey(key)) | Some(Item::PKCS8Key(key)) | Some(Item::ECKey(key)) => key, + _ => return Err(io_other("private key format not supported")), + }; + + config_from_der(cert, key) +} + +async fn config_from_pem_file( + cert: impl AsRef, + key: impl AsRef, +) -> io::Result { + let cert = tokio::fs::read(cert.as_ref()).await?; + let key = tokio::fs::read(key.as_ref()).await?; + + config_from_pem(cert, key) +} + +#[cfg(test)] +mod tests { + use crate::{ + handle::Handle, + tls_rustls::{self, RustlsConfig}, + }; + use axum::{routing::get, Router}; + use bytes::Bytes; + use http::{response, Request}; + use hyper::{ + client::conn::{handshake, SendRequest}, + Body, + }; + use rustls::{ + client::{ServerCertVerified, ServerCertVerifier}, + Certificate, ClientConfig, ServerName, + }; + use std::{ + convert::TryFrom, + io, + net::SocketAddr, + sync::Arc, + time::{Duration, SystemTime}, + }; + use tokio::time::sleep; + use tokio::{net::TcpStream, task::JoinHandle, time::timeout}; + use tokio_rustls::TlsConnector; + use tower::{Service, ServiceExt}; + + #[tokio::test] + async fn start_and_request() { + let (_handle, _server_task, addr) = start_server().await; + + let (mut client, _conn) = connect(addr).await; + + let (_parts, body) = send_empty_request(&mut client).await; + + assert_eq!(body.as_ref(), b"Hello, world!"); + } + + #[ignore] + #[tokio::test] + async fn tls_timeout() { + let (handle, _server_task, addr) = start_server().await; + assert_eq!(handle.connection_count(), 0); + + // We intentionally avoid driving a TLS handshake to completion. + let _stream = TcpStream::connect(addr).await.unwrap(); + + sleep(Duration::from_millis(500)).await; + assert_eq!(handle.connection_count(), 1); + + tokio::time::sleep(Duration::from_millis(1000)).await; + // Timeout defaults to 1s during testing, and we have waited 1.5 seconds. + assert_eq!(handle.connection_count(), 0); + } + + #[tokio::test] + async fn test_reload() { + let handle = Handle::new(); + + let config = RustlsConfig::from_pem_file( + "examples/self-signed-certs/cert.pem", + "examples/self-signed-certs/key.pem", + ) + .await + .unwrap(); + + let server_handle = handle.clone(); + let rustls_config = config.clone(); + tokio::spawn(async move { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + + tls_rustls::bind_rustls(addr, rustls_config) + .handle(server_handle) + .serve(app.into_make_service()) + .await + }); + + let addr = handle.listening().await.unwrap(); + + let cert_a = get_first_cert(addr).await; + let mut cert_b = get_first_cert(addr).await; + + assert_eq!(cert_a, cert_b); + + config + .reload_from_pem_file( + "examples/self-signed-certs/reload/cert.pem", + "examples/self-signed-certs/reload/key.pem", + ) + .await + .unwrap(); + + cert_b = get_first_cert(addr).await; + + assert_ne!(cert_a, cert_b); + + config + .reload_from_pem_file( + "examples/self-signed-certs/cert.pem", + "examples/self-signed-certs/key.pem", + ) + .await + .unwrap(); + + cert_b = get_first_cert(addr).await; + + assert_eq!(cert_a, cert_b); + } + + #[tokio::test] + async fn test_shutdown() { + let (handle, _server_task, addr) = start_server().await; + + let (mut client, conn) = connect(addr).await; + + handle.shutdown(); + + let response_future_result = client + .ready() + .await + .unwrap() + .call(Request::new(Body::empty())) + .await; + + assert!(response_future_result.is_err()); + + // Connection task should finish soon. + let _ = timeout(Duration::from_secs(1), conn).await.unwrap(); + } + + #[tokio::test] + async fn test_graceful_shutdown() { + let (handle, server_task, addr) = start_server().await; + + let (mut client, conn) = connect(addr).await; + + handle.graceful_shutdown(None); + + let (_parts, body) = send_empty_request(&mut client).await; + + assert_eq!(body.as_ref(), b"Hello, world!"); + + // Disconnect client. + conn.abort(); + + // Server task should finish soon. + let server_result = timeout(Duration::from_secs(1), server_task) + .await + .unwrap() + .unwrap(); + + assert!(server_result.is_ok()); + } + + #[ignore] + #[tokio::test] + async fn test_graceful_shutdown_timed() { + let (handle, server_task, addr) = start_server().await; + + let (mut client, _conn) = connect(addr).await; + + handle.graceful_shutdown(Some(Duration::from_millis(250))); + + let (_parts, body) = send_empty_request(&mut client).await; + + assert_eq!(body.as_ref(), b"Hello, world!"); + + // Don't disconnect client. + // conn.abort(); + + // Server task should finish soon. + let server_result = timeout(Duration::from_secs(1), server_task) + .await + .unwrap() + .unwrap(); + + assert!(server_result.is_ok()); + } + + async fn start_server() -> (Handle, JoinHandle>, SocketAddr) { + let handle = Handle::new(); + + let server_handle = handle.clone(); + let server_task = tokio::spawn(async move { + let app = Router::new().route("/", get(|| async { "Hello, world!" })); + + let config = RustlsConfig::from_pem_file( + "examples/self-signed-certs/cert.pem", + "examples/self-signed-certs/key.pem", + ) + .await?; + + let addr = SocketAddr::from(([127, 0, 0, 1], 0)); + + tls_rustls::bind_rustls(addr, config) + .handle(server_handle) + .serve(app.into_make_service()) + .await + }); + + let addr = handle.listening().await.unwrap(); + + (handle, server_task, addr) + } + + async fn get_first_cert(addr: SocketAddr) -> Certificate { + let stream = TcpStream::connect(addr).await.unwrap(); + let tls_stream = tls_connector().connect(dns_name(), stream).await.unwrap(); + + let (_io, client_connection) = tls_stream.into_inner(); + + client_connection.peer_certificates().unwrap()[0].clone() + } + + async fn connect(addr: SocketAddr) -> (SendRequest, JoinHandle<()>) { + let stream = TcpStream::connect(addr).await.unwrap(); + let tls_stream = tls_connector().connect(dns_name(), stream).await.unwrap(); + + let (send_request, connection) = handshake(tls_stream).await.unwrap(); + + let task = tokio::spawn(async move { + let _ = connection.await; + }); + + (send_request, task) + } + + async fn send_empty_request(client: &mut SendRequest) -> (response::Parts, Bytes) { + let (parts, body) = client + .ready() + .await + .unwrap() + .call(Request::new(Body::empty())) + .await + .unwrap() + .into_parts(); + let body = hyper::body::to_bytes(body).await.unwrap(); + + (parts, body) + } + + fn tls_connector() -> TlsConnector { + struct NoVerify; + + impl ServerCertVerifier for NoVerify { + fn verify_server_cert( + &self, + _end_entity: &Certificate, + _intermediates: &[Certificate], + _server_name: &ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + } + + let mut client_config = ClientConfig::builder() + .with_safe_defaults() + .with_custom_certificate_verifier(Arc::new(NoVerify)) + .with_no_client_auth(); + + client_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + + TlsConnector::from(Arc::new(client_config)) + } + + fn dns_name() -> ServerName { + ServerName::try_from("localhost").unwrap() + } +} diff --git a/xks-axum/configuration/settings.toml b/xks-axum/configuration/settings.toml index 91c0815..1ba9c66 100644 --- a/xks-axum/configuration/settings.toml +++ b/xks-axum/configuration/settings.toml @@ -13,9 +13,17 @@ service = "kms-xks-proxy" # Optional configuration of ciphertext metadata in base 64 encoding # ciphertext_metadata_b64 = "djAuMC4x" -# Optional configuration of how often to send TCP keepalive probes +# Configuration of TCP keepalive probes +# https://en.wikipedia.org/wiki/Keepalive +[server.tcp_keepalive] +# (Optional) Number of seconds between two keepalive transmissions in idle condition # No configuration means TCP keepalive probes is disabled. tcp_keepalive_secs = 60 +# (Optional) Number of retransmissions to be carried out before declaring that remote end is not available +tcp_keepalive_retries = 3 +# (Optional) Number of seconds between two successive keepalive retransmissions, +# if acknowledgement to the previous keepalive transmission is not received +tcp_keepalive_interval_secs = 1 [tracing] # Used to control logging to stdout diff --git a/xks-axum/configuration/settings_cloudhsm.toml b/xks-axum/configuration/settings_cloudhsm.toml index 47149a5..c4e45bc 100644 --- a/xks-axum/configuration/settings_cloudhsm.toml +++ b/xks-axum/configuration/settings_cloudhsm.toml @@ -11,9 +11,17 @@ service = "kms-xks-proxy" # Optional configuration of ciphertext metadata in base 64 encoding # ciphertext_metadata_b64 = "djAuMC4x" -# Optional configuration of how often to send TCP keepalive probes +# Configuration of TCP keepalive probes +# https://en.wikipedia.org/wiki/Keepalive +[server.tcp_keepalive] +# (Optional) Number of seconds between two keepalive transmissions in idle condition # No configuration means TCP keepalive probes is disabled. tcp_keepalive_secs = 60 +# (Optional) Number of retransmissions to be carried out before declaring that remote end is not available +tcp_keepalive_retries = 3 +# (Optional) Number of seconds between two successive keepalive retransmissions, +# if acknowledgement to the previous keepalive transmission is not received +tcp_keepalive_interval_secs = 1 [tracing] # Used to control logging to stdout diff --git a/xks-axum/configuration/settings_docker.toml b/xks-axum/configuration/settings_docker.toml index 8986f49..b3b104f 100644 --- a/xks-axum/configuration/settings_docker.toml +++ b/xks-axum/configuration/settings_docker.toml @@ -12,9 +12,17 @@ service = "kms-xks-proxy" # Optional configuration of ciphertext metadata in base 64 encoding # ciphertext_metadata_b64 = "djAuMC4x" -# Optional configuration of how often to send TCP keepalive probes +# Configuration of TCP keepalive probes +# https://en.wikipedia.org/wiki/Keepalive +[server.tcp_keepalive] +# (Optional) Number of seconds between two keepalive transmissions in idle condition # No configuration means TCP keepalive probes is disabled. tcp_keepalive_secs = 60 +# (Optional) Number of retransmissions to be carried out before declaring that remote end is not available +tcp_keepalive_retries = 3 +# (Optional) Number of seconds between two successive keepalive retransmissions, +# if acknowledgement to the previous keepalive transmission is not received +tcp_keepalive_interval_secs = 1 [tracing] # Used to control logging to stdout diff --git a/xks-axum/configuration/settings_luna.toml b/xks-axum/configuration/settings_luna.toml index 1d99c05..6d61d89 100644 --- a/xks-axum/configuration/settings_luna.toml +++ b/xks-axum/configuration/settings_luna.toml @@ -11,9 +11,17 @@ service = "kms-xks-proxy" # Optional configuration of ciphertext metadata in base 64 encoding # ciphertext_metadata_b64 = "djAuMC4x" -# Optional configuration of how often to send TCP keepalive probes +# Configuration of TCP keepalive probes +# https://en.wikipedia.org/wiki/Keepalive +[server.tcp_keepalive] +# (Optional) Number of seconds between two keepalive transmissions in idle condition # No configuration means TCP keepalive probes is disabled. tcp_keepalive_secs = 60 +# (Optional) Number of retransmissions to be carried out before declaring that remote end is not available +tcp_keepalive_retries = 3 +# (Optional) Number of seconds between two successive keepalive retransmissions, +# if acknowledgement to the previous keepalive transmission is not received +tcp_keepalive_interval_secs = 1 [tracing] # Used to control logging to stdout diff --git a/xks-axum/configuration/settings_softhsmv2.toml b/xks-axum/configuration/settings_softhsmv2.toml index c62cae5..729004a 100644 --- a/xks-axum/configuration/settings_softhsmv2.toml +++ b/xks-axum/configuration/settings_softhsmv2.toml @@ -12,9 +12,17 @@ service = "kms-xks-proxy" # Optional configuration of ciphertext metadata in base 64 encoding # ciphertext_metadata_b64 = "djAuMC4x" -# Optional configuration of how often to send TCP keepalive probes +# Configuration of TCP keepalive probes +# https://en.wikipedia.org/wiki/Keepalive +[server.tcp_keepalive] +# (Optional) Number of seconds between two keepalive transmissions in idle condition # No configuration means TCP keepalive probes is disabled. tcp_keepalive_secs = 60 +# (Optional) Number of retransmissions to be carried out before declaring that remote end is not available +tcp_keepalive_retries = 3 +# (Optional) Number of seconds between two successive keepalive retransmissions, +# if acknowledgement to the previous keepalive transmission is not received +tcp_keepalive_interval_secs = 1 [tracing] # Used to control logging to stdout diff --git a/xks-axum/configuration/settings_softhsmv2_osx.toml b/xks-axum/configuration/settings_softhsmv2_osx.toml index 4ca3173..47adea9 100644 --- a/xks-axum/configuration/settings_softhsmv2_osx.toml +++ b/xks-axum/configuration/settings_softhsmv2_osx.toml @@ -13,9 +13,17 @@ service = "kms-xks-proxy" # Optional configuration of ciphertext metadata in base 64 encoding # ciphertext_metadata_b64 = "djAuMC4x" -# Optional configuration of how often to send TCP keepalive probes +# Configuration of TCP keepalive probes +# https://en.wikipedia.org/wiki/Keepalive +[server.tcp_keepalive] +# (Optional) Number of seconds between two keepalive transmissions in idle condition # No configuration means TCP keepalive probes is disabled. tcp_keepalive_secs = 60 +# (Optional) Number of retransmissions to be carried out before declaring that remote end is not available +tcp_keepalive_retries = 3 +# (Optional) Number of seconds between two successive keepalive retransmissions, +# if acknowledgement to the previous keepalive transmission is not received +tcp_keepalive_interval_secs = 1 [tracing] # Used to control logging to stdout diff --git a/xks-axum/src/main.rs b/xks-axum/src/main.rs index 501260c..12f12f0 100644 --- a/xks-axum/src/main.rs +++ b/xks-axum/src/main.rs @@ -133,7 +133,8 @@ async fn main() { .unwrap_or_else(|_| panic!("unable to parse server ip address {}", server_config.ip)); let socket_addr = SocketAddr::from((ip_addr, server_config.port)); tracing::info!("v{CARGO_PKG_VERSION} listening on {socket_addr}"); - tracing::info!(tcp_keepalive_secs = ?server_config.tcp_keepalive_secs, "TCP keepalive interval"); + tracing::info!(tcp_keepalive = ?server_config.tcp_keepalive, "TCP keepalive"); + let ka_config = &server_config.tcp_keepalive; if security_config.is_tls_enabled { let rustls_server_config: rustls::ServerConfig = tls::make_tls_server_config( @@ -147,7 +148,9 @@ async fn main() { axum_server::bind_rustls(socket_addr, rustls_config) .addr_incoming_config( AddrIncomingConfig::default() - .tcp_keepalive(server_config.tcp_keepalive_secs) + .tcp_keepalive(ka_config.tcp_keepalive_secs) + .tcp_keepalive_interval(ka_config.tcp_keepalive_interval_secs) + .tcp_keepalive_retries(ka_config.tcp_keepalive_retries) .build(), ) .serve(router.into_make_service()) @@ -157,7 +160,9 @@ async fn main() { axum_server::bind(socket_addr) .addr_incoming_config( AddrIncomingConfig::default() - .tcp_keepalive(server_config.tcp_keepalive_secs) + .tcp_keepalive(ka_config.tcp_keepalive_secs) + .tcp_keepalive_interval(ka_config.tcp_keepalive_interval_secs) + .tcp_keepalive_retries(ka_config.tcp_keepalive_retries) .build(), ) .serve(router.into_make_service()) diff --git a/xks-axum/src/settings.rs b/xks-axum/src/settings.rs index d31db94..c47615b 100644 --- a/xks-axum/src/settings.rs +++ b/xks-axum/src/settings.rs @@ -44,16 +44,27 @@ pub struct Settings { } #[serde_as] -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Debug)] pub struct ServerConfig { pub ip: String, pub port: u16, pub region: String, pub service: String, pub ciphertext_metadata_b64: Option, + pub tcp_keepalive: TcpKeepaliveConfig, +} + +#[serde_as] +#[derive(Deserialize, Debug, Clone)] +pub struct TcpKeepaliveConfig { // https://stackoverflow.com/questions/70184303/how-to-serialize-and-deserialize-chronoduration #[serde_as(as = "Option>")] pub tcp_keepalive_secs: Option, + + #[serde_as(as = "Option>")] + pub tcp_keepalive_interval_secs: Option, + + pub tcp_keepalive_retries: Option, } #[non_exhaustive] @@ -228,6 +239,7 @@ mod settings_test { use std::env; use std::net::IpAddr; use std::str::FromStr; + use std::time::Duration; use crate::settings::SecondaryAuth::Oso; use crate::settings::PKCS11_HSM_MODULE; @@ -241,6 +253,16 @@ mod settings_test { assert!(!server_config.service.is_empty()); let _ip: IpAddr = server_config.ip.parse().unwrap(); + + assert_eq!( + server_config.tcp_keepalive.tcp_keepalive_secs, + Some(Duration::from_secs(60)) + ); + assert_eq!( + server_config.tcp_keepalive.tcp_keepalive_interval_secs, + Some(Duration::from_secs(1)) + ); + assert_eq!(server_config.tcp_keepalive.tcp_keepalive_retries, Some(3)); } #[test]