From a1de224d5030cc04f7867dba607e92d571ea6d7a Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Fri, 26 Jan 2024 11:19:25 +0100 Subject: [PATCH 01/31] Initial commit --- Cargo.toml | 2 +- stackable-webhook/Cargo.toml | 13 +++++++++ stackable-webhook/src/conversion.rs | 42 ++++++++++++++++++++++++++++ stackable-webhook/src/lib.rs | 43 +++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 stackable-webhook/Cargo.toml create mode 100644 stackable-webhook/src/conversion.rs create mode 100644 stackable-webhook/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index db3161da1..903142512 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,4 +67,4 @@ rstest = "0.18.1" tempfile = "3.7.1" [workspace] -members = ["stackable-operator-derive"] +members = ["stackable-operator-derive", "stackable-webhook"] diff --git a/stackable-webhook/Cargo.toml b/stackable-webhook/Cargo.toml new file mode 100644 index 000000000..2cbdd04fe --- /dev/null +++ b/stackable-webhook/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "stackable-webhook" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true + +[dependencies] +axum = "0.7.4" +kube = { version = "0.87.1", default-features = false } +serde_json = "1.0.104" +tokio = "1.29.1" diff --git a/stackable-webhook/src/conversion.rs b/stackable-webhook/src/conversion.rs new file mode 100644 index 000000000..3f0ab8421 --- /dev/null +++ b/stackable-webhook/src/conversion.rs @@ -0,0 +1,42 @@ +use std::{net::SocketAddr, ops::Deref}; + +use axum::{ + routing::{post, MethodRouter}, + Json, +}; +use kube::core::conversion::{ConversionRequest, ConversionResponse}; + +use crate::{Handlers, WebhookServer}; + +pub struct ConversionWebhookServer(WebhookServer<ConversionHandlers>); + +impl Deref for ConversionWebhookServer { + type Target = WebhookServer<ConversionHandlers>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ConversionWebhookServer { + pub async fn new(socket_addr: SocketAddr) -> Self { + Self(WebhookServer::new(socket_addr, ConversionHandlers).await) + } +} + +pub struct ConversionHandlers; + +impl Handlers for ConversionHandlers { + fn endpoints<T>(&self) -> Vec<(&str, MethodRouter<T>)> + where + T: Clone + Sync + Send + 'static, + { + vec![("/convert", post(convert_handler))] + } +} + +async fn convert_handler( + Json(_conversion_request): Json<ConversionRequest>, +) -> Json<ConversionResponse> { + todo!() +} diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs new file mode 100644 index 000000000..164d81bbc --- /dev/null +++ b/stackable-webhook/src/lib.rs @@ -0,0 +1,43 @@ +use std::net::SocketAddr; + +use axum::{routing::MethodRouter, Router}; +use tokio::net::TcpListener; + +pub mod conversion; + +pub trait Handlers { + fn endpoints<T>(&self) -> Vec<(&str, MethodRouter<T>)> + where + T: Clone + Sync + Send + 'static; +} + +pub struct WebhookServer<T> +where + T: Handlers, +{ + socket_addr: SocketAddr, + handlers: T, +} + +impl<T> WebhookServer<T> +where + T: Handlers, +{ + pub async fn new(socket_addr: SocketAddr, handlers: T) -> Self { + Self { + socket_addr, + handlers, + } + } + + pub async fn run(&self) { + let mut router = Router::new(); + + for (path, method_router) in self.handlers.endpoints() { + router = router.route(path, method_router) + } + + let listener = TcpListener::bind(self.socket_addr).await.unwrap(); + axum::serve(listener, router).await.unwrap() + } +} From 13612c16938a0738ee576022814af60953a58359 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Mon, 5 Feb 2024 13:16:59 +0100 Subject: [PATCH 02/31] Start redirector impl --- stackable-webhook/Cargo.toml | 3 ++ stackable-webhook/src/lib.rs | 12 +---- stackable-webhook/src/redirect.rs | 90 +++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 11 deletions(-) create mode 100644 stackable-webhook/src/redirect.rs diff --git a/stackable-webhook/Cargo.toml b/stackable-webhook/Cargo.toml index 2cbdd04fe..d4e0e3847 100644 --- a/stackable-webhook/Cargo.toml +++ b/stackable-webhook/Cargo.toml @@ -10,4 +10,7 @@ repository.workspace = true axum = "0.7.4" kube = { version = "0.87.1", default-features = false } serde_json = "1.0.104" +snafu = "0.8.0" tokio = "1.29.1" +tower = "0.4.13" +tracing = "0.1.40" diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index 164d81bbc..59d9d101d 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -4,17 +4,7 @@ use axum::{routing::MethodRouter, Router}; use tokio::net::TcpListener; pub mod conversion; - -pub trait Handlers { - fn endpoints<T>(&self) -> Vec<(&str, MethodRouter<T>)> - where - T: Clone + Sync + Send + 'static; -} - -pub struct WebhookServer<T> -where - T: Handlers, -{ +pub mod redirect; socket_addr: SocketAddr, handlers: T, } diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs new file mode 100644 index 000000000..01ea3e4c9 --- /dev/null +++ b/stackable-webhook/src/redirect.rs @@ -0,0 +1,90 @@ +use std::net::{IpAddr, SocketAddr}; + +use axum::{ + extract::Host, + handler::HandlerWithoutStateExt, + http::{ + uri::{InvalidUri, InvalidUriParts, Scheme}, + StatusCode, Uri, + }, + response::Redirect, +}; +use snafu::{ResultExt, Snafu}; +use tokio::net::TcpListener; +use tracing::warn; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to parse HTTPS host as authority"))] + ParseAuthority { source: InvalidUri }, + + #[snafu(display("failed to convert URI parts into URI"))] + ConvertPartsToUri { source: InvalidUriParts }, +} + +/// A redirector which redirects HTTP connections at "/" to HTTPS automatically. +/// +/// Internally it uses a simple handler function which is registered as a +/// singular [`Service`][tower::MakeService] at the root "/" path. If the +/// conversion from HTTP to HTTPS fails, the [`Redirector`] returns a HTTP +/// status code 400 (Bad Request). Additionally, a warning trace is emitted. +pub struct Redirector { + ip_addr: IpAddr, + https_port: u16, + http_port: u16, +} + +impl Redirector { + pub fn new(ip_addr: IpAddr, http_port: u16, https_port: u16) -> Self { + Self { + https_port, + http_port, + ip_addr, + } + } + + pub async fn run(self) { + // The redirector only binds to the HTTP port. The actual HTTPS + // application runs in a separate task and is completely independent + // of this redirector. + let socket_addr = SocketAddr::new(self.ip_addr, self.http_port); + let listener = TcpListener::bind(socket_addr).await.unwrap(); + + // This converts the HTTP request URI into HTTPS. If this fails, the + // redirector emits a warning trace and returns HTTP status code 400 + // (Bad Request). + let redirect = move |Host(host): Host, uri: Uri| async move { + match http_to_https(host, uri, self.http_port, self.https_port) { + Ok(uri) => Ok(Redirect::permanent(&uri.to_string())), + Err(err) => { + warn!(%err, "failed to convert HTTP URI to HTTPS"); + Err(StatusCode::BAD_REQUEST) + } + } + }; + + // This registers the handler function as the only handler at the root + // path "/". See https://docs.rs/axum/latest/axum/fn.serve.html#examples + axum::serve(listener, redirect.into_make_service()) + .await + .unwrap(); + } +} + +fn http_to_https(host: String, uri: Uri, http_port: u16, https_port: u16) -> Result<Uri, Error> { + let mut parts = uri.into_parts(); + + parts.scheme = Some(Scheme::HTTPS); + + if parts.path_and_query.is_none() { + // NOTE (@Techassi): This should never fail and is this save to unwrap. + // If this will change into a user-controlled value, then this isn't + // save to unwrap anymore and will require explicit error handling. + parts.path_and_query = Some("/".parse().unwrap()); + } + + let https_host = host.replace(&http_port.to_string(), &https_port.to_string()); + parts.authority = Some(https_host.parse().context(ParseAuthoritySnafu)?); + + Ok(Uri::from_parts(parts).context(ConvertPartsToUriSnafu)?) +} From 66a54ae2ad18f6bd0b5b8a1a47849d963964d417 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Mon, 5 Feb 2024 15:28:41 +0100 Subject: [PATCH 03/31] Improve redirector impl --- stackable-webhook/src/constants.rs | 4 ++ stackable-webhook/src/lib.rs | 108 ++++++++++++++++++++++++----- stackable-webhook/src/redirect.rs | 2 +- 3 files changed, 95 insertions(+), 19 deletions(-) create mode 100644 stackable-webhook/src/constants.rs diff --git a/stackable-webhook/src/constants.rs b/stackable-webhook/src/constants.rs new file mode 100644 index 000000000..b1c53a526 --- /dev/null +++ b/stackable-webhook/src/constants.rs @@ -0,0 +1,4 @@ +pub const DEFAULT_HTTPS_PORT: u16 = 443; +pub const DEFAULT_HTTP_PORT: u16 = 80; + +pub const DEFAULT_IP_ADDRESS: [u8; 4] = [127, 0, 0, 1]; diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index 59d9d101d..d9fc64187 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -1,33 +1,105 @@ -use std::net::SocketAddr; +use std::{net::SocketAddr, sync::Arc}; -use axum::{routing::MethodRouter, Router}; +use axum::Router; use tokio::net::TcpListener; +use tokio_rustls::rustls::ServerConfig; +use tracing::warn; +use crate::{ + constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS}, + redirect::Redirector, +}; + +pub mod constants; pub mod conversion; pub mod redirect; - socket_addr: SocketAddr, - handlers: T, + +/// A ready-to-use webhook server. +pub struct WebhookServer { + options: Options, + router: Router, } -impl<T> WebhookServer<T> -where - T: Handlers, -{ - pub async fn new(socket_addr: SocketAddr, handlers: T) -> Self { - Self { - socket_addr, - handlers, - } +impl WebhookServer { + /// Creates a new ready-to-use webhook server. + /// + /// The server listens on `socket_addr` which is provided via the [`Options`] + /// and handles routing based on the provided Axum `router`. Most of the time + /// it is sufficient to use [`Options::default()`]. See the documentation + /// for [`Options`] for more details on the default values. + pub async fn new(router: Router, options: Options) -> Self { + Self { options, router } } - pub async fn run(&self) { - let mut router = Router::new(); + /// Runs the webhook server by creating a TCP listener and binding it to + /// the specified socket address. + pub async fn run(self) { + // Only run the auto redirector when enabled + match self.options.redirect { + RedirectOption::Enabled(http_port) => { + let redirector = Redirector::new( + self.options.socket_addr.ip(), + self.options.socket_addr.port(), + http_port, + ); - for (path, method_router) in self.handlers.endpoints() { - router = router.route(path, method_router) + tokio::spawn(redirector.run()); + } + RedirectOption::Disabled => { + warn!("webhook runs without automatic HTTP to HTTPS redirect which is not recommended"); + } } - let listener = TcpListener::bind(self.socket_addr).await.unwrap(); + let mut router = Router::new(); + router = router.merge(self.router); + + let listener = TcpListener::bind(self.options.socket_addr).await.unwrap(); axum::serve(listener, router).await.unwrap() } } + +pub struct TlsServer { + config: Arc<ServerConfig>, +} + +impl TlsServer { + // pub fn new() -> Self { + // let config = ServerConfig::builder() + // .with_no_client_auth() + // .with_cert_resolver(cert_resolver); + // let config = Arc::new(config); + + // Self { config } + // } +} + +/// Specifies available webhook server options. +/// +/// The [`Default`] implemention for this struct contains the following +/// values: +/// +/// - Redirect from HTTP to HTTPS is enabled, HTTP listens on port 8080 +/// - The socket binds to 127.0.0.1 on port 8443 (HTTPS) +pub struct Options { + /// Enables or disables the automatic HTTP to HTTPS redirect. If enabled, + /// it is required to specify the HTTP port. + pub redirect: RedirectOption, + + /// The default HTTPS socket address the [`TcpListener`] binds to. The same + /// IP adress is used for the auto HTTP to HTTPS redirect handler. + pub socket_addr: SocketAddr, +} + +impl Default for Options { + fn default() -> Self { + Self { + socket_addr: SocketAddr::from((DEFAULT_IP_ADDRESS, DEFAULT_HTTPS_PORT)), + redirect: RedirectOption::Enabled(DEFAULT_HTTP_PORT), + } + } +} + +pub enum RedirectOption { + Enabled(u16), + Disabled, +} diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs index 01ea3e4c9..c3b1a8607 100644 --- a/stackable-webhook/src/redirect.rs +++ b/stackable-webhook/src/redirect.rs @@ -35,7 +35,7 @@ pub struct Redirector { } impl Redirector { - pub fn new(ip_addr: IpAddr, http_port: u16, https_port: u16) -> Self { + pub fn new(ip_addr: IpAddr, https_port: u16, http_port: u16) -> Self { Self { https_port, http_port, From 5dc336e4e200875b36797c4e018f0edb61f8a67d Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 6 Feb 2024 09:35:57 +0100 Subject: [PATCH 04/31] Add doc comments, add traces --- stackable-webhook/src/lib.rs | 25 ++++++++++++++++++++++++- stackable-webhook/src/redirect.rs | 18 +++++++++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index d9fc64187..28e13b197 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -1,9 +1,17 @@ +//! Utility types and functions to easily create ready-to-use webhook servers +//! which can handle different tasks, for example CRD conversions. All webhook +//! servers use HTTPS per default and provide options to enable HTTP to HTTPS +//! redirection as well. +//! +//! The crate is also fully compatible with [`tracing`], and emits multiple +//! levels of tracing data. + use std::{net::SocketAddr, sync::Arc}; use axum::Router; use tokio::net::TcpListener; use tokio_rustls::rustls::ServerConfig; -use tracing::warn; +use tracing::{debug, warn}; use crate::{ constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS}, @@ -27,16 +35,31 @@ impl WebhookServer { /// and handles routing based on the provided Axum `router`. Most of the time /// it is sufficient to use [`Options::default()`]. See the documentation /// for [`Options`] for more details on the default values. + /// + /// ### Example + /// + /// ``` + /// use stackable_webhook::{WebhookServer, Options}; + /// use axum::Router; + /// + /// let router = Router::new(); + /// let server = WebhookServer::new(router, Options::default()); + /// ``` pub async fn new(router: Router, options: Options) -> Self { + debug!("create new webhook server"); Self { options, router } } /// Runs the webhook server by creating a TCP listener and binding it to /// the specified socket address. pub async fn run(self) { + debug!("run webhook server"); + // Only run the auto redirector when enabled match self.options.redirect { RedirectOption::Enabled(http_port) => { + debug!("run webhook server with automatic HTTP to HTTPS redirect enabled"); + let redirector = Redirector::new( self.options.socket_addr.ip(), self.options.socket_addr.port(), diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs index c3b1a8607..1774bc2b0 100644 --- a/stackable-webhook/src/redirect.rs +++ b/stackable-webhook/src/redirect.rs @@ -11,7 +11,7 @@ use axum::{ }; use snafu::{ResultExt, Snafu}; use tokio::net::TcpListener; -use tracing::warn; +use tracing::{debug, info, instrument, warn}; #[derive(Debug, Snafu)] pub enum Error { @@ -28,6 +28,7 @@ pub enum Error { /// singular [`Service`][tower::MakeService] at the root "/" path. If the /// conversion from HTTP to HTTPS fails, the [`Redirector`] returns a HTTP /// status code 400 (Bad Request). Additionally, a warning trace is emitted. +#[derive(Debug)] pub struct Redirector { ip_addr: IpAddr, https_port: u16, @@ -35,7 +36,10 @@ pub struct Redirector { } impl Redirector { + #[instrument] pub fn new(ip_addr: IpAddr, https_port: u16, http_port: u16) -> Self { + debug!("create new HTTP to HTTPS redirector"); + Self { https_port, http_port, @@ -43,7 +47,10 @@ impl Redirector { } } + #[instrument] pub async fn run(self) { + debug!("run redirector"); + // The redirector only binds to the HTTP port. The actual HTTPS // application runs in a separate task and is completely independent // of this redirector. @@ -54,8 +61,13 @@ impl Redirector { // redirector emits a warning trace and returns HTTP status code 400 // (Bad Request). let redirect = move |Host(host): Host, uri: Uri| async move { - match http_to_https(host, uri, self.http_port, self.https_port) { - Ok(uri) => Ok(Redirect::permanent(&uri.to_string())), + // NOTE (@Techassi): Is it worth to clone here just to be able to + // print it in the trace? + match http_to_https(host, uri.clone(), self.http_port, self.https_port) { + Ok(redirect_uri) => { + info!("redirecting from {} to {}", uri, redirect_uri); + Ok(Redirect::permanent(&redirect_uri.to_string())) + } Err(err) => { warn!(%err, "failed to convert HTTP URI to HTTPS"); Err(StatusCode::BAD_REQUEST) From 74b874ad669b5870f08fec3344f975a7ed77701b Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 6 Feb 2024 11:14:09 +0100 Subject: [PATCH 05/31] Add options builder, move into own module --- stackable-webhook/src/lib.rs | 67 ++++++++------------ stackable-webhook/src/options.rs | 104 +++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 40 deletions(-) create mode 100644 stackable-webhook/src/options.rs diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index 28e13b197..df8bdac05 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -6,21 +6,20 @@ //! The crate is also fully compatible with [`tracing`], and emits multiple //! levels of tracing data. -use std::{net::SocketAddr, sync::Arc}; +use std::sync::Arc; use axum::Router; use tokio::net::TcpListener; use tokio_rustls::rustls::ServerConfig; use tracing::{debug, warn}; -use crate::{ - constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS}, - redirect::Redirector, -}; - pub mod constants; pub mod conversion; -pub mod redirect; +mod options; +mod redirect; + +pub use options::*; +pub use redirect::*; /// A ready-to-use webhook server. pub struct WebhookServer { @@ -36,7 +35,10 @@ impl WebhookServer { /// it is sufficient to use [`Options::default()`]. See the documentation /// for [`Options`] for more details on the default values. /// - /// ### Example + /// To start the server, use the [`WebhookServer::run()`] function. This will + /// run the server using the Tokio runtime until it is terminated. + /// + /// ### Basic Example /// /// ``` /// use stackable_webhook::{WebhookServer, Options}; @@ -45,7 +47,22 @@ impl WebhookServer { /// let router = Router::new(); /// let server = WebhookServer::new(router, Options::default()); /// ``` - pub async fn new(router: Router, options: Options) -> Self { + /// + /// ### Example with Custom Options + /// + /// ``` + /// use stackable_webhook::{WebhookServer, Options}; + /// use axum::Router; + /// + /// let options = Options::builder() + /// .disable_redirect() + /// .socket_addr(([127, 0, 0, 1], 8080)) + /// .build(); + /// + /// let router = Router::new(); + /// let server = WebhookServer::new(router, options); + /// ``` + pub fn new(router: Router, options: Options) -> Self { debug!("create new webhook server"); Self { options, router } } @@ -73,6 +90,7 @@ impl WebhookServer { } } + // Create the root router and merge the provided router into it. let mut router = Router::new(); router = router.merge(self.router); @@ -95,34 +113,3 @@ impl TlsServer { // Self { config } // } } - -/// Specifies available webhook server options. -/// -/// The [`Default`] implemention for this struct contains the following -/// values: -/// -/// - Redirect from HTTP to HTTPS is enabled, HTTP listens on port 8080 -/// - The socket binds to 127.0.0.1 on port 8443 (HTTPS) -pub struct Options { - /// Enables or disables the automatic HTTP to HTTPS redirect. If enabled, - /// it is required to specify the HTTP port. - pub redirect: RedirectOption, - - /// The default HTTPS socket address the [`TcpListener`] binds to. The same - /// IP adress is used for the auto HTTP to HTTPS redirect handler. - pub socket_addr: SocketAddr, -} - -impl Default for Options { - fn default() -> Self { - Self { - socket_addr: SocketAddr::from((DEFAULT_IP_ADDRESS, DEFAULT_HTTPS_PORT)), - redirect: RedirectOption::Enabled(DEFAULT_HTTP_PORT), - } - } -} - -pub enum RedirectOption { - Enabled(u16), - Disabled, -} diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs new file mode 100644 index 000000000..1a665d748 --- /dev/null +++ b/stackable-webhook/src/options.rs @@ -0,0 +1,104 @@ +use std::net::SocketAddr; + +use crate::constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS}; + +/// Specifies available webhook server options. +/// +/// The [`Default`] implemention for this struct contains the following +/// values: +/// +/// - Redirect from HTTP to HTTPS is enabled, HTTP listens on port 8080 +/// - The socket binds to 127.0.0.1 on port 8443 (HTTPS) +pub struct Options { + /// Enables or disables the automatic HTTP to HTTPS redirect. If enabled, + /// it is required to specify the HTTP port. + pub redirect: RedirectOption, + + /// The default HTTPS socket address the [`TcpListener`] binds to. The same + /// IP adress is used for the auto HTTP to HTTPS redirect handler. + pub socket_addr: SocketAddr, + + /// Either auto-generate or use an injected TLS certificate. + pub tls: TlsOption, +} + +impl Default for Options { + fn default() -> Self { + Self::builder().build() + } +} + +impl Options { + pub fn builder() -> OptionsBuilder { + OptionsBuilder::default() + } +} + +#[derive(Debug, Default)] +pub struct OptionsBuilder { + redirect: Option<RedirectOption>, + socket_addr: Option<SocketAddr>, + tls: Option<TlsOption>, +} + +impl OptionsBuilder { + pub fn redirect(mut self, redirect: RedirectOption) -> Self { + self.redirect = Some(redirect); + self + } + + pub fn disable_redirect(self) -> Self { + self.redirect(RedirectOption::Disabled) + } + + pub fn enable_redirect(self, http_port: u16) -> Self { + self.redirect(RedirectOption::Enabled(http_port)) + } + + pub fn socket_addr<T>(mut self, socket_addr: T) -> Self + where + T: Into<SocketAddr>, + { + self.socket_addr = Some(socket_addr.into()); + self + } + + pub fn tls(mut self, tls: TlsOption) -> Self { + self.tls = Some(tls); + self + } + + pub fn build(self) -> Options { + Options { + redirect: self.redirect.unwrap_or_default(), + socket_addr: self + .socket_addr + .unwrap_or(SocketAddr::from((DEFAULT_IP_ADDRESS, DEFAULT_HTTPS_PORT))), + tls: self.tls.unwrap_or_default(), + } + } +} + +#[derive(Debug)] +pub enum RedirectOption { + Enabled(u16), + Disabled, +} + +impl Default for RedirectOption { + fn default() -> Self { + Self::Enabled(DEFAULT_HTTP_PORT) + } +} + +#[derive(Debug)] +pub enum TlsOption { + AutoGenerate, + Inject, +} + +impl Default for TlsOption { + fn default() -> Self { + Self::AutoGenerate + } +} From bda6e597a7189890241e69d25734e9b9a3d36a10 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 6 Feb 2024 12:17:11 +0100 Subject: [PATCH 06/31] Add initial TLS acceptor --- stackable-webhook/Cargo.toml | 11 ++++ stackable-webhook/src/lib.rs | 116 ++++++++++++++++++++++++++++++++--- 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/stackable-webhook/Cargo.toml b/stackable-webhook/Cargo.toml index d4e0e3847..807719341 100644 --- a/stackable-webhook/Cargo.toml +++ b/stackable-webhook/Cargo.toml @@ -9,8 +9,19 @@ repository.workspace = true [dependencies] axum = "0.7.4" kube = { version = "0.87.1", default-features = false } +tokio-rustls = "0.24.1" serde_json = "1.0.104" snafu = "0.8.0" tokio = "1.29.1" +tokio-test = "0.4.3" tower = "0.4.13" tracing = "0.1.40" +rustls-pemfile = "1.0.4" +futures-util = "0.3.30" +hyper-util = "0.1.3" +hyper = { version = "1.0.0", features = ["full"] } + +[dev-dependencies] +k8s-openapi = { version = "0.20.0", default-features = false, features = [ + "v1_28", +] } diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index df8bdac05..6a451c2a4 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -6,12 +6,20 @@ //! The crate is also fully compatible with [`tracing`], and emits multiple //! levels of tracing data. -use std::sync::Arc; +use std::{fs::File, io::BufReader, sync::Arc}; -use axum::Router; +use axum::{extract::Request, Router}; +use futures_util::pin_mut; +use hyper::body::Incoming; +use hyper_util::rt::{TokioExecutor, TokioIo}; +use rustls_pemfile::{certs, pkcs8_private_keys}; use tokio::net::TcpListener; -use tokio_rustls::rustls::ServerConfig; -use tracing::{debug, warn}; +use tokio_rustls::{ + rustls::{Certificate, PrivateKey, ServerConfig}, + TlsAcceptor, +}; +use tower::Service; +use tracing::{debug, error, warn}; pub mod constants; pub mod conversion; @@ -90,12 +98,81 @@ impl WebhookServer { } } + let mut cert_file = + &mut BufReader::new(File::open("/tmp/webhook-certs/serverCert.pem").unwrap()); + let mut key_file = + &mut BufReader::new(File::open("/tmp/webhook-certs/serverKey.pem").unwrap()); + + let key = PrivateKey(pkcs8_private_keys(&mut key_file).unwrap().remove(0)); + let certs = certs(&mut cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); + + let mut config = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(certs, key) + .expect("bad certificate/key"); + + config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + + let config = Arc::new(config); + let tls_acceptor = TlsAcceptor::from(config); + // Create the root router and merge the provided router into it. - let mut router = Router::new(); - router = router.merge(self.router); + let mut app = Router::new(); + app = app.merge(self.router); + + let tcp_listener = TcpListener::bind(self.options.socket_addr).await.unwrap(); + println!("Binded"); + + pin_mut!(tcp_listener); + loop { + let tower_service = app.clone(); + let tls_acceptor = tls_acceptor.clone(); + + // Wait for new tcp connection + let (cnx, addr) = tcp_listener.accept().await.unwrap(); + println!("TCP Accepted"); + + tokio::spawn(async move { + // Wait for tls handshake to happen + let Ok(stream) = tls_acceptor.accept(cnx).await else { + error!("error during tls handshake connection from {}", addr); + return; + }; + + println!("TLS Accepted"); + + // Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio. + // `TokioIo` converts between them. + let stream = TokioIo::new(stream); - let listener = TcpListener::bind(self.options.socket_addr).await.unwrap(); - axum::serve(listener, router).await.unwrap() + // Hyper also has its own `Service` trait and doesn't use tower. We can use + // `hyper::service::service_fn` to create a hyper `Service` that calls our app through + // `tower::Service::call`. + let hyper_service = + hyper::service::service_fn(move |request: Request<Incoming>| { + // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas + // tower's `Service` requires `&mut self`. + // + // We don't need to call `poll_ready` since `Router` is always ready. + tower_service.clone().call(request) + }); + + let ret = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) + .serve_connection_with_upgrades(stream, hyper_service) + .await; + + if let Err(err) = ret { + warn!("error serving connection from {}: {}", addr, err); + } + }); + } + + // axum::serve(listener, router).await.unwrap() } } @@ -113,3 +190,26 @@ impl TlsServer { // Self { config } // } } + +#[cfg(test)] +mod test { + use super::*; + use axum::{routing::post, Router}; + + #[tokio::test] + async fn test() { + let router = Router::new().route("/", post(handler)); + let options = Options::builder() + .disable_redirect() + .socket_addr(([127, 0, 0, 1], 8080)) + .build(); + + let server = WebhookServer::new(router, options); + server.run().await + } + + async fn handler() -> &'static str { + println!("Test"); + "Ok" + } +} From f1af892bef01b116f78365bcf27b7d60bfa255e2 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 6 Feb 2024 12:44:25 +0100 Subject: [PATCH 07/31] Remove unused module --- stackable-webhook/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index 6a451c2a4..b3616c495 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -22,7 +22,6 @@ use tower::Service; use tracing::{debug, error, warn}; pub mod constants; -pub mod conversion; mod options; mod redirect; From 67194d12a2c020f847560de7067f16dfd0e3ed06 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 6 Feb 2024 13:14:03 +0100 Subject: [PATCH 08/31] Change hard-coded cert paths --- stackable-webhook/src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index b3616c495..c1d06a4b3 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -97,10 +97,12 @@ impl WebhookServer { } } - let mut cert_file = - &mut BufReader::new(File::open("/tmp/webhook-certs/serverCert.pem").unwrap()); - let mut key_file = - &mut BufReader::new(File::open("/tmp/webhook-certs/serverKey.pem").unwrap()); + let mut cert_file = &mut BufReader::new( + File::open("/apiserver.local.config/certificates/apiserver.crt").unwrap(), + ); + let mut key_file = &mut BufReader::new( + File::open("/apiserver.local.config/certificates/apiserver.key").unwrap(), + ); let key = PrivateKey(pkcs8_private_keys(&mut key_file).unwrap().remove(0)); let certs = certs(&mut cert_file) From 2c3739f3bbd8f1aec2f6e52d337ada3d78eea18d Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 6 Feb 2024 15:35:04 +0100 Subject: [PATCH 09/31] Start TLS server cleanup --- stackable-webhook/src/conversion.rs | 42 ------- stackable-webhook/src/lib.rs | 122 +++----------------- stackable-webhook/src/servers/conversion.rs | 42 +++++++ stackable-webhook/src/servers/mod.rs | 0 stackable-webhook/src/tls.rs | 105 +++++++++++++++++ 5 files changed, 164 insertions(+), 147 deletions(-) delete mode 100644 stackable-webhook/src/conversion.rs create mode 100644 stackable-webhook/src/servers/conversion.rs create mode 100644 stackable-webhook/src/servers/mod.rs create mode 100644 stackable-webhook/src/tls.rs diff --git a/stackable-webhook/src/conversion.rs b/stackable-webhook/src/conversion.rs deleted file mode 100644 index 3f0ab8421..000000000 --- a/stackable-webhook/src/conversion.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::{net::SocketAddr, ops::Deref}; - -use axum::{ - routing::{post, MethodRouter}, - Json, -}; -use kube::core::conversion::{ConversionRequest, ConversionResponse}; - -use crate::{Handlers, WebhookServer}; - -pub struct ConversionWebhookServer(WebhookServer<ConversionHandlers>); - -impl Deref for ConversionWebhookServer { - type Target = WebhookServer<ConversionHandlers>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl ConversionWebhookServer { - pub async fn new(socket_addr: SocketAddr) -> Self { - Self(WebhookServer::new(socket_addr, ConversionHandlers).await) - } -} - -pub struct ConversionHandlers; - -impl Handlers for ConversionHandlers { - fn endpoints<T>(&self) -> Vec<(&str, MethodRouter<T>)> - where - T: Clone + Sync + Send + 'static, - { - vec![("/convert", post(convert_handler))] - } -} - -async fn convert_handler( - Json(_conversion_request): Json<ConversionRequest>, -) -> Json<ConversionResponse> { - todo!() -} diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index c1d06a4b3..d344f7b56 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -5,28 +5,19 @@ //! //! The crate is also fully compatible with [`tracing`], and emits multiple //! levels of tracing data. - -use std::{fs::File, io::BufReader, sync::Arc}; - -use axum::{extract::Request, Router}; -use futures_util::pin_mut; -use hyper::body::Incoming; -use hyper_util::rt::{TokioExecutor, TokioIo}; -use rustls_pemfile::{certs, pkcs8_private_keys}; -use tokio::net::TcpListener; -use tokio_rustls::{ - rustls::{Certificate, PrivateKey, ServerConfig}, - TlsAcceptor, -}; -use tower::Service; -use tracing::{debug, error, warn}; +use axum::Router; +use tracing::{debug, warn}; pub mod constants; +pub mod servers; + mod options; mod redirect; +mod tls; pub use options::*; pub use redirect::*; +pub use tls::*; /// A ready-to-use webhook server. pub struct WebhookServer { @@ -97,101 +88,22 @@ impl WebhookServer { } } - let mut cert_file = &mut BufReader::new( - File::open("/apiserver.local.config/certificates/apiserver.crt").unwrap(), - ); - let mut key_file = &mut BufReader::new( - File::open("/apiserver.local.config/certificates/apiserver.key").unwrap(), - ); - - let key = PrivateKey(pkcs8_private_keys(&mut key_file).unwrap().remove(0)); - let certs = certs(&mut cert_file) - .unwrap() - .into_iter() - .map(Certificate) - .collect(); - - let mut config = ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth() - .with_single_cert(certs, key) - .expect("bad certificate/key"); - - config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; - - let config = Arc::new(config); - let tls_acceptor = TlsAcceptor::from(config); - // Create the root router and merge the provided router into it. - let mut app = Router::new(); - app = app.merge(self.router); - - let tcp_listener = TcpListener::bind(self.options.socket_addr).await.unwrap(); - println!("Binded"); - - pin_mut!(tcp_listener); - loop { - let tower_service = app.clone(); - let tls_acceptor = tls_acceptor.clone(); - - // Wait for new tcp connection - let (cnx, addr) = tcp_listener.accept().await.unwrap(); - println!("TCP Accepted"); - - tokio::spawn(async move { - // Wait for tls handshake to happen - let Ok(stream) = tls_acceptor.accept(cnx).await else { - error!("error during tls handshake connection from {}", addr); - return; - }; - - println!("TLS Accepted"); - - // Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio. - // `TokioIo` converts between them. - let stream = TokioIo::new(stream); - - // Hyper also has its own `Service` trait and doesn't use tower. We can use - // `hyper::service::service_fn` to create a hyper `Service` that calls our app through - // `tower::Service::call`. - let hyper_service = - hyper::service::service_fn(move |request: Request<Incoming>| { - // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas - // tower's `Service` requires `&mut self`. - // - // We don't need to call `poll_ready` since `Router` is always ready. - tower_service.clone().call(request) - }); - - let ret = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) - .serve_connection_with_upgrades(stream, hyper_service) - .await; - - if let Err(err) = ret { - warn!("error serving connection from {}: {}", addr, err); - } - }); - } + let mut router = Router::new(); + router = router.merge(self.router); + + // Create server for TLS termination + let tls_server = TlsServer::new( + self.options.socket_addr, + router, + "/apiserver.local.config/certificates/apiserver.crt", + "/apiserver.local.config/certificates/apiserver.key", + ); - // axum::serve(listener, router).await.unwrap() + tls_server.run().await; } } -pub struct TlsServer { - config: Arc<ServerConfig>, -} - -impl TlsServer { - // pub fn new() -> Self { - // let config = ServerConfig::builder() - // .with_no_client_auth() - // .with_cert_resolver(cert_resolver); - // let config = Arc::new(config); - - // Self { config } - // } -} - #[cfg(test)] mod test { use super::*; diff --git a/stackable-webhook/src/servers/conversion.rs b/stackable-webhook/src/servers/conversion.rs new file mode 100644 index 000000000..dd9ca4169 --- /dev/null +++ b/stackable-webhook/src/servers/conversion.rs @@ -0,0 +1,42 @@ +// use std::{net::SocketAddr, ops::Deref}; + +// use axum::{ +// routing::{post, MethodRouter}, +// Json, +// }; +// use kube::core::conversion::{ConversionRequest, ConversionResponse}; + +// use crate::{Handlers, WebhookServer}; + +// pub struct ConversionWebhookServer(WebhookServer<ConversionHandlers>); + +// impl Deref for ConversionWebhookServer { +// type Target = WebhookServer<ConversionHandlers>; + +// fn deref(&self) -> &Self::Target { +// &self.0 +// } +// } + +// impl ConversionWebhookServer { +// pub async fn new(socket_addr: SocketAddr) -> Self { +// Self(WebhookServer::new(socket_addr, ConversionHandlers).await) +// } +// } + +// pub struct ConversionHandlers; + +// impl Handlers for ConversionHandlers { +// fn endpoints<T>(&self) -> Vec<(&str, MethodRouter<T>)> +// where +// T: Clone + Sync + Send + 'static, +// { +// vec![("/convert", post(convert_handler))] +// } +// } + +// async fn convert_handler( +// Json(_conversion_request): Json<ConversionRequest>, +// ) -> Json<ConversionResponse> { +// todo!() +// } diff --git a/stackable-webhook/src/servers/mod.rs b/stackable-webhook/src/servers/mod.rs new file mode 100644 index 000000000..e69de29bb diff --git a/stackable-webhook/src/tls.rs b/stackable-webhook/src/tls.rs new file mode 100644 index 000000000..a6d0bd9d3 --- /dev/null +++ b/stackable-webhook/src/tls.rs @@ -0,0 +1,105 @@ +use std::{fs::File, io::BufReader, net::SocketAddr, path::Path, sync::Arc}; + +use axum::{extract::Request, Router}; +use futures_util::pin_mut; +use hyper::body::Incoming; +use hyper_util::rt::{TokioExecutor, TokioIo}; +use rustls_pemfile::{certs, pkcs8_private_keys}; +use tokio::net::TcpListener; +use tokio_rustls::{ + rustls::{Certificate, PrivateKey, ServerConfig}, + TlsAcceptor, +}; +use tower::Service; +use tracing::{error, warn}; + +pub struct TlsServer { + config: Arc<ServerConfig>, + socket_addr: SocketAddr, + router: Router, +} + +impl TlsServer { + pub fn new( + socket_addr: SocketAddr, + router: Router, + cert_file: impl AsRef<Path>, + key_file: impl AsRef<Path>, + ) -> Self { + // TODO (@Techassi): Abstract away the cert chain loading + // TODO (@Techassi): Remove unwraps + let mut cert_file = &mut BufReader::new(File::open(cert_file).unwrap()); + let mut key_file = &mut BufReader::new(File::open(key_file).unwrap()); + + // TODO (@Techassi): Remove unwrap + let key = PrivateKey(pkcs8_private_keys(&mut key_file).unwrap().remove(0)); + let certs = certs(&mut cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); + + // TODO (@Techassi): Use the latest version of rustls related crates + // TODO (@Techassi): Remove expect + let mut config = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(certs, key) + .expect("bad certificate/key"); + + config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + let config = Arc::new(config); + + Self { + socket_addr, + config, + router, + } + } + + pub async fn run(self) { + // TODO (@Techassi): Remove unwrap + let tls_acceptor = TlsAcceptor::from(self.config); + let tcp_listener = TcpListener::bind(self.socket_addr).await.unwrap(); + + pin_mut!(tcp_listener); + loop { + let tls_acceptor = tls_acceptor.clone(); + let router = self.router.clone(); + + // Wait for new tcp connection + let (tcp_stream, remote_addr) = tcp_listener.accept().await.unwrap(); + + tokio::spawn(async move { + // Wait for tls handshake to happen + let Ok(tls_stream) = tls_acceptor.accept(tcp_stream).await else { + error!("error during tls handshake connection from {}", remote_addr); + return; + }; + + // Hyper has its own `AsyncRead` and `AsyncWrite` traits and doesn't use tokio. + // `TokioIo` converts between them. + let tls_stream = TokioIo::new(tls_stream); + + // Hyper also has its own `Service` trait and doesn't use tower. We can use + // `hyper::service::service_fn` to create a hyper `Service` that calls our app through + // `tower::Service::call`. + let hyper_service = + hyper::service::service_fn(move |request: Request<Incoming>| { + // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas + // tower's `Service` requires `&mut self`. + // + // We don't need to call `poll_ready` since `Router` is always ready. + router.clone().call(request) + }); + + if let Err(err) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) + .serve_connection_with_upgrades(tls_stream, hyper_service) + .await + { + warn!(%err, "failed to serve connection from {}", remote_addr); + } + }); + } + } +} From d00d38c1b8291a558bc146faf287c10a1f9cd6d4 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 6 Feb 2024 16:35:20 +0100 Subject: [PATCH 10/31] Move code, abstract away certificate chain --- stackable-webhook/src/options.rs | 12 ++- stackable-webhook/src/redirect.rs | 10 ++- stackable-webhook/src/tls/certs.rs | 76 +++++++++++++++++++ stackable-webhook/src/tls/mod.rs | 7 ++ .../src/{tls.rs => tls/server.rs} | 49 +++++------- 5 files changed, 118 insertions(+), 36 deletions(-) create mode 100644 stackable-webhook/src/tls/certs.rs create mode 100644 stackable-webhook/src/tls/mod.rs rename stackable-webhook/src/{tls.rs => tls/server.rs} (66%) diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs index 1a665d748..dcb5f4f97 100644 --- a/stackable-webhook/src/options.rs +++ b/stackable-webhook/src/options.rs @@ -1,4 +1,4 @@ -use std::net::SocketAddr; +use std::{net::SocketAddr, path::PathBuf}; use crate::constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS}; @@ -14,8 +14,9 @@ pub struct Options { /// it is required to specify the HTTP port. pub redirect: RedirectOption, - /// The default HTTPS socket address the [`TcpListener`] binds to. The same - /// IP adress is used for the auto HTTP to HTTPS redirect handler. + /// The default HTTPS socket address the [`TcpListener`][tokio::net::TcpListener] + /// binds to. The same IP adress is used for the auto HTTP to HTTPS redirect + /// handler. pub socket_addr: SocketAddr, /// Either auto-generate or use an injected TLS certificate. @@ -94,7 +95,10 @@ impl Default for RedirectOption { #[derive(Debug)] pub enum TlsOption { AutoGenerate, - Inject, + Mount { + cert_path: PathBuf, + key_path: PathBuf, + }, } impl Default for TlsOption { diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs index 1774bc2b0..0823fd4f5 100644 --- a/stackable-webhook/src/redirect.rs +++ b/stackable-webhook/src/redirect.rs @@ -22,12 +22,14 @@ pub enum Error { ConvertPartsToUri { source: InvalidUriParts }, } -/// A redirector which redirects HTTP connections at "/" to HTTPS automatically. +/// A redirector which redirects all incoming HTTP connections to HTTPS +/// automatically. /// /// Internally it uses a simple handler function which is registered as a -/// singular [`Service`][tower::MakeService] at the root "/" path. If the -/// conversion from HTTP to HTTPS fails, the [`Redirector`] returns a HTTP -/// status code 400 (Bad Request). Additionally, a warning trace is emitted. +/// singular [`Service`][tower::MakeService] at the root "/" path. The request +/// paths are preserved. If the conversion from HTTP to HTTPS fails, the +/// [`Redirector`] returns a HTTP status code 400 (Bad Request). Additionally, +/// a warning trace is emitted. #[derive(Debug)] pub struct Redirector { ip_addr: IpAddr, diff --git a/stackable-webhook/src/tls/certs.rs b/stackable-webhook/src/tls/certs.rs new file mode 100644 index 000000000..b4b147b67 --- /dev/null +++ b/stackable-webhook/src/tls/certs.rs @@ -0,0 +1,76 @@ +use std::{fs::File, io::BufReader, path::Path}; + +use rustls_pemfile::{certs, pkcs8_private_keys}; +use snafu::{ResultExt, Snafu}; +use tokio_rustls::rustls::{Certificate, PrivateKey}; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to read certificate file"))] + ReadCertFile { source: std::io::Error }, + + #[snafu(display("failed to read buffered certificate file"))] + ReadBufferedCertFile { source: std::io::Error }, + + #[snafu(display("failed to read private key file"))] + ReadKeyFile { source: std::io::Error }, + + #[snafu(display("failed to read buffered private key file"))] + ReadBufferedKeyFile { source: std::io::Error }, +} + +pub struct CertificateChain { + chain: Vec<Certificate>, + private_key: PrivateKey, +} + +impl<C, P> TryFrom<(&mut C, &mut P)> for CertificateChain +where + C: std::io::BufRead, + P: std::io::BufRead, +{ + type Error = Error; + + fn try_from(readers: (&mut C, &mut P)) -> Result<Self, Self::Error> { + let chain = certs(readers.0) + .context(ReadBufferedCertFileSnafu)? + .into_iter() + .map(Certificate) + .collect(); + + let private_key = pkcs8_private_keys(readers.1) + .context(ReadBufferedKeyFileSnafu)? + .remove(0); + let private_key = PrivateKey(private_key); + + Ok(Self { chain, private_key }) + } +} + +impl CertificateChain { + pub fn from_files<C, P>(certificate_path: C, private_key_path: P) -> Result<Self, Error> + where + C: AsRef<Path>, + P: AsRef<Path>, + { + let cert_file = File::open(certificate_path).context(ReadCertFileSnafu)?; + let cert_reader = &mut BufReader::new(cert_file); + + let key_file = File::open(private_key_path).context(ReadKeyFileSnafu)?; + let key_reader = &mut BufReader::new(key_file); + + Self::try_from((cert_reader, key_reader)) + } + + pub fn chain(&self) -> &[Certificate] { + &self.chain + } + + pub fn private_key(&self) -> &PrivateKey { + &self.private_key + } + + pub fn into_parts(self) -> (Vec<Certificate>, PrivateKey) { + (self.chain, self.private_key) + } +} diff --git a/stackable-webhook/src/tls/mod.rs b/stackable-webhook/src/tls/mod.rs new file mode 100644 index 000000000..0d35fe57a --- /dev/null +++ b/stackable-webhook/src/tls/mod.rs @@ -0,0 +1,7 @@ +//! This module contains structs and functions to easily create a TLS termination +//! server, which can be used in combination with an Axum [`Router`]. +mod certs; +mod server; + +pub use certs::*; +pub use server::*; diff --git a/stackable-webhook/src/tls.rs b/stackable-webhook/src/tls/server.rs similarity index 66% rename from stackable-webhook/src/tls.rs rename to stackable-webhook/src/tls/server.rs index a6d0bd9d3..6549f18f7 100644 --- a/stackable-webhook/src/tls.rs +++ b/stackable-webhook/src/tls/server.rs @@ -1,18 +1,20 @@ -use std::{fs::File, io::BufReader, net::SocketAddr, path::Path, sync::Arc}; +//! This module contains structs and functions to easily create a TLS termination +//! server, which can be used in combination with an Axum [`Router`]. +use std::{net::SocketAddr, path::Path, sync::Arc}; use axum::{extract::Request, Router}; use futures_util::pin_mut; -use hyper::body::Incoming; +use hyper::{body::Incoming, service::service_fn}; use hyper_util::rt::{TokioExecutor, TokioIo}; -use rustls_pemfile::{certs, pkcs8_private_keys}; use tokio::net::TcpListener; -use tokio_rustls::{ - rustls::{Certificate, PrivateKey, ServerConfig}, - TlsAcceptor, -}; +use tokio_rustls::{rustls::ServerConfig, TlsAcceptor}; use tower::Service; use tracing::{error, warn}; +use crate::CertificateChain; + +/// A server which terminates TLS connections and allows clients to commnunicate +/// via HTTPS with the underlying HTTP router. pub struct TlsServer { config: Arc<ServerConfig>, socket_addr: SocketAddr, @@ -23,28 +25,20 @@ impl TlsServer { pub fn new( socket_addr: SocketAddr, router: Router, - cert_file: impl AsRef<Path>, - key_file: impl AsRef<Path>, + cert_path: impl AsRef<Path>, + key_path: impl AsRef<Path>, ) -> Self { - // TODO (@Techassi): Abstract away the cert chain loading - // TODO (@Techassi): Remove unwraps - let mut cert_file = &mut BufReader::new(File::open(cert_file).unwrap()); - let mut key_file = &mut BufReader::new(File::open(key_file).unwrap()); - // TODO (@Techassi): Remove unwrap - let key = PrivateKey(pkcs8_private_keys(&mut key_file).unwrap().remove(0)); - let certs = certs(&mut cert_file) + let (chain, private_key) = CertificateChain::from_files(cert_path, key_path) .unwrap() - .into_iter() - .map(Certificate) - .collect(); + .into_parts(); // TODO (@Techassi): Use the latest version of rustls related crates // TODO (@Techassi): Remove expect let mut config = ServerConfig::builder() .with_safe_defaults() .with_no_client_auth() - .with_single_cert(certs, key) + .with_single_cert(chain, private_key) .expect("bad certificate/key"); config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; @@ -84,14 +78,13 @@ impl TlsServer { // Hyper also has its own `Service` trait and doesn't use tower. We can use // `hyper::service::service_fn` to create a hyper `Service` that calls our app through // `tower::Service::call`. - let hyper_service = - hyper::service::service_fn(move |request: Request<Incoming>| { - // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas - // tower's `Service` requires `&mut self`. - // - // We don't need to call `poll_ready` since `Router` is always ready. - router.clone().call(request) - }); + let hyper_service = service_fn(move |request: Request<Incoming>| { + // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas + // tower's `Service` requires `&mut self`. + // + // We don't need to call `poll_ready` since `Router` is always ready. + router.clone().call(request) + }); if let Err(err) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) .serve_connection_with_upgrades(tls_stream, hyper_service) From 96037fcad22426b3831841f9729a71d4ba9730f9 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 6 Feb 2024 17:08:28 +0100 Subject: [PATCH 11/31] Change private key load function --- stackable-webhook/src/tls/certs.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/stackable-webhook/src/tls/certs.rs b/stackable-webhook/src/tls/certs.rs index b4b147b67..1dc819adc 100644 --- a/stackable-webhook/src/tls/certs.rs +++ b/stackable-webhook/src/tls/certs.rs @@ -1,6 +1,6 @@ use std::{fs::File, io::BufReader, path::Path}; -use rustls_pemfile::{certs, pkcs8_private_keys}; +use rustls_pemfile::{certs, ec_private_keys}; use snafu::{ResultExt, Snafu}; use tokio_rustls::rustls::{Certificate, PrivateKey}; @@ -38,7 +38,8 @@ where .map(Certificate) .collect(); - let private_key = pkcs8_private_keys(readers.1) + // TODO (@Techassi): Make this function configurable + let private_key = ec_private_keys(readers.1) .context(ReadBufferedKeyFileSnafu)? .remove(0); let private_key = PrivateKey(private_key); @@ -74,3 +75,21 @@ impl CertificateChain { (self.chain, self.private_key) } } + +#[cfg(test)] +mod test { + + use rustls_pemfile::ec_private_keys; + use tokio_rustls::rustls::PrivateKey; + + #[test] + fn test() { + let t = "-----BEGIN EC PRIVATE KEY-----\n +MHcCAQEEIFMX2VakgYH6/5+aj7vinwmwVlBvTjCkw8/HjE3YE3xeoAoGCCqGSM49\n +AwEHoUQDQgAE6lU4Z0tU8A+0jlwCFB1Efaq6nV+gbIDv1poXLf0d+wkMkiopOWlE\n +QVYafabw9A/ziUVWTCovvuI7RWzA4l4Pqg==\n +-----END EC PRIVATE KEY-----"; + let key = ec_private_keys(&mut t.as_bytes()).unwrap().remove(0); + let key = PrivateKey(key); + } +} From c62d65c9d615cb596f7fa907e033f9efcbf75743 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 6 Feb 2024 17:17:58 +0100 Subject: [PATCH 12/31] Remove private key test --- stackable-webhook/src/tls/certs.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/stackable-webhook/src/tls/certs.rs b/stackable-webhook/src/tls/certs.rs index 1dc819adc..21c1255e6 100644 --- a/stackable-webhook/src/tls/certs.rs +++ b/stackable-webhook/src/tls/certs.rs @@ -75,21 +75,3 @@ impl CertificateChain { (self.chain, self.private_key) } } - -#[cfg(test)] -mod test { - - use rustls_pemfile::ec_private_keys; - use tokio_rustls::rustls::PrivateKey; - - #[test] - fn test() { - let t = "-----BEGIN EC PRIVATE KEY-----\n -MHcCAQEEIFMX2VakgYH6/5+aj7vinwmwVlBvTjCkw8/HjE3YE3xeoAoGCCqGSM49\n -AwEHoUQDQgAE6lU4Z0tU8A+0jlwCFB1Efaq6nV+gbIDv1poXLf0d+wkMkiopOWlE\n -QVYafabw9A/ziUVWTCovvuI7RWzA4l4Pqg==\n ------END EC PRIVATE KEY-----"; - let key = ec_private_keys(&mut t.as_bytes()).unwrap().remove(0); - let key = PrivateKey(key); - } -} From 7bc7329e2bcb1193c6b7304b3aaeaaf5ec13b44c Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Thu, 8 Feb 2024 15:10:59 +0100 Subject: [PATCH 13/31] Add many doc comments, start bubbling up errors --- stackable-webhook/src/constants.rs | 9 +- stackable-webhook/src/lib.rs | 49 ++++++----- stackable-webhook/src/options.rs | 123 +++++++++++++++++++++++----- stackable-webhook/src/tls/certs.rs | 9 +- stackable-webhook/src/tls/server.rs | 93 ++++++++++++++------- 5 files changed, 208 insertions(+), 75 deletions(-) diff --git a/stackable-webhook/src/constants.rs b/stackable-webhook/src/constants.rs index b1c53a526..31fca207f 100644 --- a/stackable-webhook/src/constants.rs +++ b/stackable-webhook/src/constants.rs @@ -1,4 +1,7 @@ -pub const DEFAULT_HTTPS_PORT: u16 = 443; -pub const DEFAULT_HTTP_PORT: u16 = 80; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -pub const DEFAULT_IP_ADDRESS: [u8; 4] = [127, 0, 0, 1]; +pub const DEFAULT_HTTPS_PORT: u16 = 8443; +pub const DEFAULT_HTTP_PORT: u16 = 8080; + +pub const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); +pub const DEFAULT_SOCKET_ADDR: SocketAddr = SocketAddr::new(DEFAULT_IP_ADDRESS, DEFAULT_HTTPS_PORT); diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index d344f7b56..61dc7c597 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -6,20 +6,32 @@ //! The crate is also fully compatible with [`tracing`], and emits multiple //! levels of tracing data. use axum::Router; +use snafu::Snafu; use tracing::{debug, warn}; +use crate::{ + options::{Options, RedirectOption}, + redirect::Redirector, + tls::TlsServer, +}; + pub mod constants; +pub mod options; +pub mod redirect; pub mod servers; +pub mod tls; -mod options; -mod redirect; -mod tls; +pub type Result<T, E = Error> = std::result::Result<T, E>; -pub use options::*; -pub use redirect::*; -pub use tls::*; +#[derive(Debug, Snafu)] +pub enum Error {} /// A ready-to-use webhook server. +/// +/// This server abstracts away lower-level details like TLS termination +/// and other various configurations, validations or middlewares. The routes +/// and their handlers are completely customizable by bringing your own +/// Axum [`Router`]. pub struct WebhookServer { options: Options, router: Router, @@ -93,13 +105,9 @@ impl WebhookServer { router = router.merge(self.router); // Create server for TLS termination - let tls_server = TlsServer::new( - self.options.socket_addr, - router, - "/apiserver.local.config/certificates/apiserver.crt", - "/apiserver.local.config/certificates/apiserver.key", - ); - + // TODO (@Techassi): Remove unwrap + let tls_server = + TlsServer::new(self.options.socket_addr, router, self.options.tls).unwrap(); tls_server.run().await; } } @@ -107,22 +115,19 @@ impl WebhookServer { #[cfg(test)] mod test { use super::*; - use axum::{routing::post, Router}; + use axum::{routing::get, Router}; #[tokio::test] async fn test() { - let router = Router::new().route("/", post(handler)); + let router = Router::new().route("/", get(|| async { "Ok" })); let options = Options::builder() - .disable_redirect() - .socket_addr(([127, 0, 0, 1], 8080)) + .tls_mount( + "/tmp/webhook-certs/serverCert.pem", + "/tmp/webhook-certs/serverKey.pem", + ) .build(); let server = WebhookServer::new(router, options); server.run().await } - - async fn handler() -> &'static str { - println!("Test"); - "Ok" - } } diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs index dcb5f4f97..8b81b330c 100644 --- a/stackable-webhook/src/options.rs +++ b/stackable-webhook/src/options.rs @@ -1,6 +1,9 @@ -use std::{net::SocketAddr, path::PathBuf}; +use std::{ + net::{IpAddr, SocketAddr}, + path::PathBuf, +}; -use crate::constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS}; +use crate::constants::{DEFAULT_HTTP_PORT, DEFAULT_SOCKET_ADDR}; /// Specifies available webhook server options. /// @@ -9,9 +12,58 @@ use crate::constants::{DEFAULT_HTTPS_PORT, DEFAULT_HTTP_PORT, DEFAULT_IP_ADDRESS /// /// - Redirect from HTTP to HTTPS is enabled, HTTP listens on port 8080 /// - The socket binds to 127.0.0.1 on port 8443 (HTTPS) +/// - The TLS cert used gets auto-generated +/// +/// ### Example with Custom HTTPS IP Address and Port +/// +/// ``` +/// use stackable_webhook::Options; +/// +/// // Set IP address and port at the same time +/// let options = Options::builder() +/// .socket_addr([0, 0, 0, 0], 12345) +/// .build(); +/// +/// // Set IP address only +/// let options = Options::builder() +/// .socket_ip([0, 0, 0, 0]) +/// .build(); +/// +/// // Set port only +/// let options = Options::builder() +/// .socket_port(12345) +/// .build(); +/// ``` +/// +/// ### Example with Custom Redirects +/// +/// ``` +/// use stackable_webhook::Options; +/// +/// // Use a custom HTTP port +/// let options = Options::builder() +/// .enable_redirect(12345) +/// .build(); +/// +/// // Disable auto-redirect +/// let options = Options::builder() +/// .disable_redirect() +/// .build(); +/// ``` +/// +/// ### Example with Mounted TLS Certificate +/// +/// ``` +/// use stackable_webhook::Options; +/// +/// let options = Options::builder() +/// .tls_mount("path/to/pem/cert", "path/to/pem/key") +/// .build(); +/// ``` pub struct Options { /// Enables or disables the automatic HTTP to HTTPS redirect. If enabled, - /// it is required to specify the HTTP port. + /// it is required to specify the HTTP port. If disabled, the webhook + /// server **only** listens on HTTPS. pub redirect: RedirectOption, /// The default HTTPS socket address the [`TcpListener`][tokio::net::TcpListener] @@ -30,11 +82,19 @@ impl Default for Options { } impl Options { + /// Returns the default [`OptionsBuilder`] which allows to selectively + /// customize the options. See the documention for [`Options`] for more + /// information on available functions. pub fn builder() -> OptionsBuilder { OptionsBuilder::default() } } +/// The [`OptionsBuilder`] which allows to selectively customize the webhook +/// server [`Options`]. +/// +/// Usually, this struct is not constructed manually, but instead by calling +/// [`Options::builder()`] or [`OptionsBuilder::default()`]. #[derive(Debug, Default)] pub struct OptionsBuilder { redirect: Option<RedirectOption>, @@ -43,38 +103,63 @@ pub struct OptionsBuilder { } impl OptionsBuilder { - pub fn redirect(mut self, redirect: RedirectOption) -> Self { - self.redirect = Some(redirect); + /// Disables HTPP to HTTPS auto-redirect entirely. The webhook server + /// will only listen on HTTPS. + pub fn disable_redirect(mut self) -> Self { + self.redirect = Some(RedirectOption::Disabled); + self + } + + /// Enables HTTP to HTTPS auto-redirect on `http_port`. The webhook + /// server will listen on both HTTP and HTTPS. + pub fn enable_redirect(mut self, http_port: u16) -> Self { + self.redirect = Some(RedirectOption::Enabled(http_port)); self } - pub fn disable_redirect(self) -> Self { - self.redirect(RedirectOption::Disabled) + /// Sets the socket address the webhook server uses to bind for HTTPS. + pub fn socket_addr(mut self, socket_ip: impl Into<IpAddr>, socket_port: u16) -> Self { + self.socket_addr = Some(SocketAddr::new(socket_ip.into(), socket_port)); + self } - pub fn enable_redirect(self, http_port: u16) -> Self { - self.redirect(RedirectOption::Enabled(http_port)) + /// Sets the IP address of the socket address the webhook server uses to + /// bind for HTTPS. + pub fn socket_ip(mut self, socket_ip: impl Into<IpAddr>) -> Self { + let addr = self.socket_addr.get_or_insert(DEFAULT_SOCKET_ADDR); + addr.set_ip(socket_ip.into()); + self + } + + /// Sets the port of the socket address the webhook server uses to bind + /// for HTTPS. + pub fn socket_port(mut self, socket_port: u16) -> Self { + let addr = self.socket_addr.get_or_insert(DEFAULT_SOCKET_ADDR); + addr.set_port(socket_port); + self } - pub fn socket_addr<T>(mut self, socket_addr: T) -> Self - where - T: Into<SocketAddr>, - { - self.socket_addr = Some(socket_addr.into()); + pub fn tls_autogenerate(mut self) -> Self { + self.tls = Some(TlsOption::AutoGenerate); self } - pub fn tls(mut self, tls: TlsOption) -> Self { - self.tls = Some(tls); + pub fn tls_mount( + mut self, + cert_path: impl Into<PathBuf>, + key_path: impl Into<PathBuf>, + ) -> Self { + self.tls = Some(TlsOption::Mount { + cert_path: cert_path.into(), + key_path: key_path.into(), + }); self } pub fn build(self) -> Options { Options { redirect: self.redirect.unwrap_or_default(), - socket_addr: self - .socket_addr - .unwrap_or(SocketAddr::from((DEFAULT_IP_ADDRESS, DEFAULT_HTTPS_PORT))), + socket_addr: self.socket_addr.unwrap_or(DEFAULT_SOCKET_ADDR), tls: self.tls.unwrap_or_default(), } } diff --git a/stackable-webhook/src/tls/certs.rs b/stackable-webhook/src/tls/certs.rs index 21c1255e6..adc082869 100644 --- a/stackable-webhook/src/tls/certs.rs +++ b/stackable-webhook/src/tls/certs.rs @@ -5,7 +5,7 @@ use snafu::{ResultExt, Snafu}; use tokio_rustls::rustls::{Certificate, PrivateKey}; #[derive(Debug, Snafu)] -pub enum Error { +pub enum CertifacteError { #[snafu(display("failed to read certificate file"))] ReadCertFile { source: std::io::Error }, @@ -29,7 +29,7 @@ where C: std::io::BufRead, P: std::io::BufRead, { - type Error = Error; + type Error = CertifacteError; fn try_from(readers: (&mut C, &mut P)) -> Result<Self, Self::Error> { let chain = certs(readers.0) @@ -49,7 +49,10 @@ where } impl CertificateChain { - pub fn from_files<C, P>(certificate_path: C, private_key_path: P) -> Result<Self, Error> + pub fn from_files<C, P>( + certificate_path: C, + private_key_path: P, + ) -> Result<Self, CertifacteError> where C: AsRef<Path>, P: AsRef<Path>, diff --git a/stackable-webhook/src/tls/server.rs b/stackable-webhook/src/tls/server.rs index 6549f18f7..af5b30d7c 100644 --- a/stackable-webhook/src/tls/server.rs +++ b/stackable-webhook/src/tls/server.rs @@ -1,17 +1,29 @@ //! This module contains structs and functions to easily create a TLS termination //! server, which can be used in combination with an Axum [`Router`]. -use std::{net::SocketAddr, path::Path, sync::Arc}; +use std::{net::SocketAddr, sync::Arc}; use axum::{extract::Request, Router}; use futures_util::pin_mut; use hyper::{body::Incoming, service::service_fn}; use hyper_util::rt::{TokioExecutor, TokioIo}; +use snafu::Snafu; use tokio::net::TcpListener; use tokio_rustls::{rustls::ServerConfig, TlsAcceptor}; use tower::Service; use tracing::{error, warn}; -use crate::CertificateChain; +use crate::{ + options::TlsOption, + tls::{CertifacteError, CertificateChain}, +}; + +pub type Result<T, E = Error> = std::result::Result<T, E>; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to create TLS certificate chain"))] + TlsCertificateChain { source: CertifacteError }, +} /// A server which terminates TLS connections and allows clients to commnunicate /// via HTTPS with the underlying HTTP router. @@ -22,35 +34,50 @@ pub struct TlsServer { } impl TlsServer { - pub fn new( - socket_addr: SocketAddr, - router: Router, - cert_path: impl AsRef<Path>, - key_path: impl AsRef<Path>, - ) -> Self { - // TODO (@Techassi): Remove unwrap - let (chain, private_key) = CertificateChain::from_files(cert_path, key_path) - .unwrap() - .into_parts(); - - // TODO (@Techassi): Use the latest version of rustls related crates - // TODO (@Techassi): Remove expect - let mut config = ServerConfig::builder() - .with_safe_defaults() - .with_no_client_auth() - .with_single_cert(chain, private_key) - .expect("bad certificate/key"); - - config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; - let config = Arc::new(config); - - Self { + pub fn new(socket_addr: SocketAddr, router: Router, tls: TlsOption) -> Result<Self> { + let config = match tls { + TlsOption::AutoGenerate => { + // let mut config = ServerConfig::builder() + // .with_safe_defaults() + // .with_no_client_auth() + // .with_cert_resolver(cert_resolver); + // config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + todo!() + } + TlsOption::Mount { + cert_path, + key_path, + } => { + // TODO (@Techassi): Remove unwrap + let (chain, private_key) = CertificateChain::from_files(cert_path, key_path) + .unwrap() + .into_parts(); + + // TODO (@Techassi): Use the latest version of rustls related crates + // TODO (@Techassi): Remove expect + let mut config = ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(chain, private_key) + .expect("bad certificate/key"); + + config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + config + } + } + .wrap(Arc::new); // Silly little thing, drop it again I guess + + Ok(Self { socket_addr, config, router, - } + }) } + /// Runs the TLS server by listening for incoming TCP connections on the + /// bound socket address. It only accepts TLS connections. Internally each + /// TLS stream get handled by a Hyper service, which in turn is an Axum + /// router. pub async fn run(self) { // TODO (@Techassi): Remove unwrap let tls_acceptor = TlsAcceptor::from(self.config); @@ -78,7 +105,7 @@ impl TlsServer { // Hyper also has its own `Service` trait and doesn't use tower. We can use // `hyper::service::service_fn` to create a hyper `Service` that calls our app through // `tower::Service::call`. - let hyper_service = service_fn(move |request: Request<Incoming>| { + let service = service_fn(move |request: Request<Incoming>| { // We have to clone `tower_service` because hyper's `Service` uses `&self` whereas // tower's `Service` requires `&mut self`. // @@ -87,7 +114,7 @@ impl TlsServer { }); if let Err(err) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) - .serve_connection_with_upgrades(tls_stream, hyper_service) + .serve_connection_with_upgrades(tls_stream, service) .await { warn!(%err, "failed to serve connection from {}", remote_addr); @@ -96,3 +123,13 @@ impl TlsServer { } } } + +trait Wrap<S, T>: Sized { + fn wrap(self, f: impl Fn(S) -> T) -> T; +} + +impl<S, T> Wrap<S, T> for S { + fn wrap(self, f: impl Fn(S) -> T) -> T { + f(self) + } +} From 05c2d65ceafe97abe119d4237ee574b4b9eb866c Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Fri, 9 Feb 2024 16:16:08 +0100 Subject: [PATCH 14/31] Add doc comments, continue error handling --- stackable-webhook/src/constants.rs | 2 + stackable-webhook/src/lib.rs | 68 +++++++++++++++++++++++------ stackable-webhook/src/options.rs | 10 +++++ stackable-webhook/src/redirect.rs | 1 + stackable-webhook/src/tls/mod.rs | 4 +- stackable-webhook/src/tls/server.rs | 59 +++++++++++++++---------- 6 files changed, 105 insertions(+), 39 deletions(-) diff --git a/stackable-webhook/src/constants.rs b/stackable-webhook/src/constants.rs index 31fca207f..d124133f8 100644 --- a/stackable-webhook/src/constants.rs +++ b/stackable-webhook/src/constants.rs @@ -1,3 +1,5 @@ +//! Contains various constant definitions, mostly for default ports and IP +//! addresses. use std::net::{IpAddr, Ipv4Addr, SocketAddr}; pub const DEFAULT_HTTPS_PORT: u16 = 8443; diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index 61dc7c597..a5732c140 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -1,13 +1,32 @@ //! Utility types and functions to easily create ready-to-use webhook servers //! which can handle different tasks, for example CRD conversions. All webhook -//! servers use HTTPS per default and provide options to enable HTTP to HTTPS -//! redirection as well. +//! servers use HTTPS by default and provides options to enable HTTP to HTTPS +//! redirection as well. This library is fully compatible with the [`tracing`] +//! crate and emits multiple levels of tracing data. //! -//! The crate is also fully compatible with [`tracing`], and emits multiple -//! levels of tracing data. +//! Most users will only use the top-level exported generic [`WebhookServer`] +//! which enables complete control over the [Router] which handles registering +//! routes and their handler functions. +//! +//! ``` +//! use stackable_webhook::{WebhookServer, Options}; +//! use axum::Router; +//! +//! let router = Router::new(); +//! let server = WebhookServer::new(router, Options::default()); +//! ``` +//! +//! For some application complete end-to-end [`WebhookServer`] implementations +//! exist. One such implementation is the [`ConversionWebhookServer`][1]. The +//! only required parameters are a conversion handler function and [`Options`]. +//! +//! [1]: crate::servers::ConversionWebhookServer +//! +//! This library additionally also exposes lower-level structs and functions to +//! enable complete controll over these details if needed. use axum::Router; -use snafu::Snafu; -use tracing::{debug, warn}; +use snafu::{ResultExt, Snafu}; +use tracing::{debug, info, instrument, warn}; use crate::{ options::{Options, RedirectOption}, @@ -23,8 +42,25 @@ pub mod tls; pub type Result<T, E = Error> = std::result::Result<T, E>; +/// A generic webhook handler receiving a request and sending back a response. +/// +/// This trait is usually not implemented by external callers and this library +/// provides various ready-to-use implementations for it. One such an +/// implementation is part of the [`ConversionWebhookServer`][1]. +/// +/// [1]: crate::servers::ConversionWebhookServer +pub trait WebhookHandler<Req, Res> { + fn call(self, req: Res) -> Res; +} + #[derive(Debug, Snafu)] -pub enum Error {} +pub enum Error { + #[snafu(display("failed to create TLS server"))] + CreateTlsServer { source: tls::Error }, + + #[snafu(display("failed to run TLS server"))] + RunTlsServer { source: tls::Error }, +} /// A ready-to-use webhook server. /// @@ -72,6 +108,7 @@ impl WebhookServer { /// let router = Router::new(); /// let server = WebhookServer::new(router, options); /// ``` + #[instrument(name = "create_webhook_server", skip(router))] pub fn new(router: Router, options: Options) -> Self { debug!("create new webhook server"); Self { options, router } @@ -79,7 +116,8 @@ impl WebhookServer { /// Runs the webhook server by creating a TCP listener and binding it to /// the specified socket address. - pub async fn run(self) { + #[instrument(name = "run_webhook_server", skip(self), fields(self.options))] + pub async fn run(self) -> Result<()> { debug!("run webhook server"); // Only run the auto redirector when enabled @@ -93,6 +131,7 @@ impl WebhookServer { http_port, ); + info!(http_port, "spawning redirector in separate task"); tokio::spawn(redirector.run()); } RedirectOption::Disabled => { @@ -101,14 +140,17 @@ impl WebhookServer { } // Create the root router and merge the provided router into it. + debug!("create core couter and merge provided router"); let mut router = Router::new(); router = router.merge(self.router); // Create server for TLS termination - // TODO (@Techassi): Remove unwrap - let tls_server = - TlsServer::new(self.options.socket_addr, router, self.options.tls).unwrap(); - tls_server.run().await; + debug!("create TLS server"); + let tls_server = TlsServer::new(self.options.socket_addr, router, self.options.tls) + .context(CreateTlsServerSnafu)?; + + info!("running TLS server"); + tls_server.run().await.context(RunTlsServerSnafu) } } @@ -128,6 +170,6 @@ mod test { .build(); let server = WebhookServer::new(router, options); - server.run().await + server.run().await.unwrap() } } diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs index 8b81b330c..0687b05f3 100644 --- a/stackable-webhook/src/options.rs +++ b/stackable-webhook/src/options.rs @@ -1,3 +1,4 @@ +//! Contains available options to configure the [WebhookServer][crate::WebhookServer]. use std::{ net::{IpAddr, SocketAddr}, path::PathBuf, @@ -60,6 +61,7 @@ use crate::constants::{DEFAULT_HTTP_PORT, DEFAULT_SOCKET_ADDR}; /// .tls_mount("path/to/pem/cert", "path/to/pem/key") /// .build(); /// ``` +#[derive(Debug)] pub struct Options { /// Enables or disables the automatic HTTP to HTTPS redirect. If enabled, /// it is required to specify the HTTP port. If disabled, the webhook @@ -139,11 +141,17 @@ impl OptionsBuilder { self } + /// Enables TLS certificate auto-generation instead of using a mounted + /// one. If instead a mounted TLS certificate is needed, use the + /// [`OptionsBuilder::tls_mount()`] function. pub fn tls_autogenerate(mut self) -> Self { self.tls = Some(TlsOption::AutoGenerate); self } + /// Uses a mounted TLS certificate instead of auto-generating one. If + /// instead a auto-generated TLS certificate is needed, us ethe + /// [`OptionsBuilder::tls_autogenerate()`] function. pub fn tls_mount( mut self, cert_path: impl Into<PathBuf>, @@ -156,6 +164,8 @@ impl OptionsBuilder { self } + /// Builds the final [`Options`] by using default values for any not + /// explicitly set option. pub fn build(self) -> Options { Options { redirect: self.redirect.unwrap_or_default(), diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs index 0823fd4f5..fb8b0b992 100644 --- a/stackable-webhook/src/redirect.rs +++ b/stackable-webhook/src/redirect.rs @@ -1,3 +1,4 @@ +//! Contains structs and functions to enable auto HTTP to HTTPS redirection. use std::net::{IpAddr, SocketAddr}; use axum::{ diff --git a/stackable-webhook/src/tls/mod.rs b/stackable-webhook/src/tls/mod.rs index 0d35fe57a..c180c65ec 100644 --- a/stackable-webhook/src/tls/mod.rs +++ b/stackable-webhook/src/tls/mod.rs @@ -1,5 +1,5 @@ -//! This module contains structs and functions to easily create a TLS termination -//! server, which can be used in combination with an Axum [`Router`]. +//! Contains structs and functions to easily create a TLS termination server, +//! which can be used in combination with an Axum [`Router`][axum::Router]. mod certs; mod server; diff --git a/stackable-webhook/src/tls/server.rs b/stackable-webhook/src/tls/server.rs index af5b30d7c..a9638a4db 100644 --- a/stackable-webhook/src/tls/server.rs +++ b/stackable-webhook/src/tls/server.rs @@ -6,11 +6,11 @@ use axum::{extract::Request, Router}; use futures_util::pin_mut; use hyper::{body::Incoming, service::service_fn}; use hyper_util::rt::{TokioExecutor, TokioIo}; -use snafu::Snafu; +use snafu::{ResultExt, Snafu}; use tokio::net::TcpListener; use tokio_rustls::{rustls::ServerConfig, TlsAcceptor}; use tower::Service; -use tracing::{error, warn}; +use tracing::{error, instrument, warn}; use crate::{ options::TlsOption, @@ -23,6 +23,17 @@ pub type Result<T, E = Error> = std::result::Result<T, E>; pub enum Error { #[snafu(display("failed to create TLS certificate chain"))] TlsCertificateChain { source: CertifacteError }, + + #[snafu(display("failed to construct TLS server config, bad certificate/key"))] + InvalidTlsPrivateKey { source: tokio_rustls::rustls::Error }, + + #[snafu(display( + "failed to create TCP listener by binding to socket address {socket_addr:?}" + ))] + BindTcpListener { + source: std::io::Error, + socket_addr: SocketAddr, + }, } /// A server which terminates TLS connections and allows clients to commnunicate @@ -34,6 +45,7 @@ pub struct TlsServer { } impl TlsServer { + #[instrument(name = "create_tls_server", skip(router))] pub fn new(socket_addr: SocketAddr, router: Router, tls: TlsOption) -> Result<Self> { let config = match tls { TlsOption::AutoGenerate => { @@ -48,24 +60,22 @@ impl TlsServer { cert_path, key_path, } => { - // TODO (@Techassi): Remove unwrap let (chain, private_key) = CertificateChain::from_files(cert_path, key_path) - .unwrap() + .context(TlsCertificateChainSnafu)? .into_parts(); // TODO (@Techassi): Use the latest version of rustls related crates - // TODO (@Techassi): Remove expect let mut config = ServerConfig::builder() .with_safe_defaults() .with_no_client_auth() .with_single_cert(chain, private_key) - .expect("bad certificate/key"); + .context(InvalidTlsPrivateKeySnafu)?; config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; config } - } - .wrap(Arc::new); // Silly little thing, drop it again I guess + }; + let config = Arc::new(config); Ok(Self { socket_addr, @@ -78,10 +88,15 @@ impl TlsServer { /// bound socket address. It only accepts TLS connections. Internally each /// TLS stream get handled by a Hyper service, which in turn is an Axum /// router. - pub async fn run(self) { - // TODO (@Techassi): Remove unwrap + #[instrument(name = "run_tls_server", skip(self), fields(self.socket_addr, self.config))] + pub async fn run(self) -> Result<()> { let tls_acceptor = TlsAcceptor::from(self.config); - let tcp_listener = TcpListener::bind(self.socket_addr).await.unwrap(); + let tcp_listener = + TcpListener::bind(self.socket_addr) + .await + .context(BindTcpListenerSnafu { + socket_addr: self.socket_addr, + })?; pin_mut!(tcp_listener); loop { @@ -89,12 +104,18 @@ impl TlsServer { let router = self.router.clone(); // Wait for new tcp connection - let (tcp_stream, remote_addr) = tcp_listener.accept().await.unwrap(); + let (tcp_stream, remote_addr) = match tcp_listener.accept().await { + Ok((stream, addr)) => (stream, addr), + Err(err) => { + warn!(%err, "failed to accept incoming TCP connection"); + continue; + } + }; tokio::spawn(async move { // Wait for tls handshake to happen let Ok(tls_stream) = tls_acceptor.accept(tcp_stream).await else { - error!("error during tls handshake connection from {}", remote_addr); + error!(%remote_addr, "error during tls handshake connection"); return; }; @@ -117,19 +138,9 @@ impl TlsServer { .serve_connection_with_upgrades(tls_stream, service) .await { - warn!(%err, "failed to serve connection from {}", remote_addr); + warn!(%err, %remote_addr, "failed to serve connection"); } }); } } } - -trait Wrap<S, T>: Sized { - fn wrap(self, f: impl Fn(S) -> T) -> T; -} - -impl<S, T> Wrap<S, T> for S { - fn wrap(self, f: impl Fn(S) -> T) -> T { - f(self) - } -} From 00f5a8b9da546a47a630406642e6953eeda43b02 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Fri, 9 Feb 2024 16:16:31 +0100 Subject: [PATCH 15/31] Add ready-to-use conversion webhook server --- stackable-webhook/src/servers/conversion.rs | 97 ++++++++++++--------- stackable-webhook/src/servers/mod.rs | 5 ++ 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/stackable-webhook/src/servers/conversion.rs b/stackable-webhook/src/servers/conversion.rs index dd9ca4169..3366cca1c 100644 --- a/stackable-webhook/src/servers/conversion.rs +++ b/stackable-webhook/src/servers/conversion.rs @@ -1,42 +1,55 @@ -// use std::{net::SocketAddr, ops::Deref}; - -// use axum::{ -// routing::{post, MethodRouter}, -// Json, -// }; -// use kube::core::conversion::{ConversionRequest, ConversionResponse}; - -// use crate::{Handlers, WebhookServer}; - -// pub struct ConversionWebhookServer(WebhookServer<ConversionHandlers>); - -// impl Deref for ConversionWebhookServer { -// type Target = WebhookServer<ConversionHandlers>; - -// fn deref(&self) -> &Self::Target { -// &self.0 -// } -// } - -// impl ConversionWebhookServer { -// pub async fn new(socket_addr: SocketAddr) -> Self { -// Self(WebhookServer::new(socket_addr, ConversionHandlers).await) -// } -// } - -// pub struct ConversionHandlers; - -// impl Handlers for ConversionHandlers { -// fn endpoints<T>(&self) -> Vec<(&str, MethodRouter<T>)> -// where -// T: Clone + Sync + Send + 'static, -// { -// vec![("/convert", post(convert_handler))] -// } -// } - -// async fn convert_handler( -// Json(_conversion_request): Json<ConversionRequest>, -// ) -> Json<ConversionResponse> { -// todo!() -// } +use axum::{routing::post, Json, Router}; +use kube::core::conversion::ConversionReview; + +use crate::{options::Options, WebhookHandler, WebhookServer}; + +impl<F> WebhookHandler<ConversionReview, ConversionReview> for F +where + F: FnOnce(ConversionReview) -> ConversionReview, +{ + fn call(self, req: ConversionReview) -> ConversionReview { + self(req) + } +} + +pub struct ConversionWebhookServer { + options: Options, + router: Router, +} + +impl ConversionWebhookServer { + pub fn new<T>(handler: T, options: Options) -> Self + where + T: WebhookHandler<ConversionReview, ConversionReview> + Clone + Send + Sync + 'static, + { + let handler_fn = |Json(review): Json<ConversionReview>| async { + let review = handler.call(review); + Json(review) + }; + + let router = Router::new().route("/convert", post(handler_fn)); + Self { router, options } + } + + pub async fn run(self) -> Result<(), crate::Error> { + let server = WebhookServer::new(self.router, self.options); + server.run().await + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Options; + + fn handler(req: ConversionReview) -> ConversionReview { + // In here we can do the CRD conversion + req + } + + #[tokio::test] + async fn test() { + let server = ConversionWebhookServer::new(handler, Options::default()); + server.run().await.unwrap(); + } +} diff --git a/stackable-webhook/src/servers/mod.rs b/stackable-webhook/src/servers/mod.rs index e69de29bb..b242df779 100644 --- a/stackable-webhook/src/servers/mod.rs +++ b/stackable-webhook/src/servers/mod.rs @@ -0,0 +1,5 @@ +//! Contains high-level ready-to-use webhook server implementations for specific +//! purposes. +mod conversion; + +pub use conversion::*; From 69714e84e11ef720a13dee15bfae63c44b3331d5 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Mon, 12 Feb 2024 11:48:44 +0100 Subject: [PATCH 16/31] Add support for state in ready-to-use conversion webhook server --- stackable-webhook/src/lib.rs | 4 ++ stackable-webhook/src/servers/conversion.rs | 80 ++++++++++++++++++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index a5732c140..1830498c6 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -53,6 +53,10 @@ pub trait WebhookHandler<Req, Res> { fn call(self, req: Res) -> Res; } +pub trait StatefulWebhookHandler<Req, Res, S> { + fn call(self, req: Res, state: S) -> Res; +} + #[derive(Debug, Snafu)] pub enum Error { #[snafu(display("failed to create TLS server"))] diff --git a/stackable-webhook/src/servers/conversion.rs b/stackable-webhook/src/servers/conversion.rs index 3366cca1c..68dd85ef0 100644 --- a/stackable-webhook/src/servers/conversion.rs +++ b/stackable-webhook/src/servers/conversion.rs @@ -1,7 +1,7 @@ -use axum::{routing::post, Json, Router}; +use axum::{extract::State, routing::post, Json, Router}; use kube::core::conversion::ConversionReview; -use crate::{options::Options, WebhookHandler, WebhookServer}; +use crate::{options::Options, StatefulWebhookHandler, WebhookHandler, WebhookServer}; impl<F> WebhookHandler<ConversionReview, ConversionReview> for F where @@ -12,12 +12,27 @@ where } } +impl<F, S> StatefulWebhookHandler<ConversionReview, ConversionReview, S> for F +where + F: FnOnce(ConversionReview, S) -> ConversionReview, +{ + fn call(self, req: ConversionReview, state: S) -> ConversionReview { + self(req, state) + } +} + pub struct ConversionWebhookServer { options: Options, router: Router, } impl ConversionWebhookServer { + /// Creates a new conversion webhook server **without** state which expects + /// POST requests being made to the `/convert` endpoint. + /// + /// Each request is handled by the provided `handler` function. Any function + /// with the signature `(ConversionReview) -> ConversionReview` can be + /// provided. pub fn new<T>(handler: T, options: Options) -> Self where T: WebhookHandler<ConversionReview, ConversionReview> + Clone + Send + Sync + 'static, @@ -28,6 +43,42 @@ impl ConversionWebhookServer { }; let router = Router::new().route("/convert", post(handler_fn)); + + Self { router, options } + } + + /// Creates a new conversion webhook server **with** state which expects + /// POST requests being made to the `/convert` endpoint. + /// + /// Each request is handled by the provided `handler` function. Any function + /// with the signature `(ConversionReview, S) -> ConversionReview` can be + /// provided. + /// + /// It is recommended to wrap the state in an [`Arc`][std::sync::Arc] if it + /// needs to be mutable. + /// + /// ### See + /// + /// - <https://docs.rs/axum/latest/axum/index.html#sharing-state-with-handlers> + pub fn new_with_state<T, S>(handler: T, state: S, options: Options) -> Self + where + T: StatefulWebhookHandler<ConversionReview, ConversionReview, S> + + Clone + + Send + + Sync + + 'static, + S: Clone + Send + Sync + 'static, + { + // See https://github.com/async-graphql/async-graphql/discussions/1150 + let handler_fn = |State(state): State<S>, Json(review): Json<ConversionReview>| async { + let review = handler.call(review, state); + Json(review) + }; + + let router = Router::new() + .route("/convert", post(handler_fn)) + .with_state(state); + Self { router, options } } @@ -39,17 +90,40 @@ impl ConversionWebhookServer { #[cfg(test)] mod test { + use std::sync::Arc; + use super::*; use crate::Options; + #[derive(Debug, Clone)] + struct State { + inner: usize, + } + fn handler(req: ConversionReview) -> ConversionReview { // In here we can do the CRD conversion req } + fn handler_with_state(req: ConversionReview, state: Arc<State>) -> ConversionReview { + println!("{}", state.inner); + req + } + #[tokio::test] - async fn test() { + async fn without_state() { let server = ConversionWebhookServer::new(handler, Options::default()); server.run().await.unwrap(); } + + #[tokio::test] + async fn with_state() { + let shared_state = Arc::new(State { inner: 0 }); + let server = ConversionWebhookServer::new_with_state( + handler_with_state, + shared_state, + Options::default(), + ); + server.run().await.unwrap(); + } } From d4db28d019b95404331d8816f79ede8d8518c2f3 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Mon, 12 Feb 2024 13:17:48 +0100 Subject: [PATCH 17/31] Adjust doc comments --- stackable-webhook/src/lib.rs | 33 +++++---- stackable-webhook/src/servers/conversion.rs | 82 ++++++++++++++++++--- 2 files changed, 91 insertions(+), 24 deletions(-) diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index 1830498c6..a09a02566 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -16,23 +16,17 @@ //! let server = WebhookServer::new(router, Options::default()); //! ``` //! -//! For some application complete end-to-end [`WebhookServer`] implementations -//! exist. One such implementation is the [`ConversionWebhookServer`][1]. The +//! For some usages, complete end-to-end [`WebhookServer`] implementations +//! exist. One such implementation is the [`ConversionWebhookServer`]. The //! only required parameters are a conversion handler function and [`Options`]. //! -//! [1]: crate::servers::ConversionWebhookServer -//! //! This library additionally also exposes lower-level structs and functions to //! enable complete controll over these details if needed. use axum::Router; use snafu::{ResultExt, Snafu}; use tracing::{debug, info, instrument, warn}; -use crate::{ - options::{Options, RedirectOption}, - redirect::Redirector, - tls::TlsServer, -}; +use crate::{options::RedirectOption, redirect::Redirector, tls::TlsServer}; pub mod constants; pub mod options; @@ -40,21 +34,29 @@ pub mod redirect; pub mod servers; pub mod tls; +// Selected re-exports +pub use crate::{options::Options, servers::ConversionWebhookServer}; + +/// A result type alias with the library-level [`Error`] type as teh default +/// error type. pub type Result<T, E = Error> = std::result::Result<T, E>; /// A generic webhook handler receiving a request and sending back a response. /// /// This trait is usually not implemented by external callers and this library /// provides various ready-to-use implementations for it. One such an -/// implementation is part of the [`ConversionWebhookServer`][1]. -/// -/// [1]: crate::servers::ConversionWebhookServer pub trait WebhookHandler<Req, Res> { - fn call(self, req: Res) -> Res; + fn call(self, req: Req) -> Res; } +/// A generic webhook handler receiving a request and state and sending back +/// a response. +/// +/// This trait is usually not implemented by external callers and this library +/// provides various ready-to-use implementations for it. One such an +/// implementation is part of the [`ConversionWebhookServer`]. pub trait StatefulWebhookHandler<Req, Res, S> { - fn call(self, req: Res, state: S) -> Res; + fn call(self, req: Req, state: S) -> Res; } #[derive(Debug, Snafu)] @@ -72,6 +74,9 @@ pub enum Error { /// and other various configurations, validations or middlewares. The routes /// and their handlers are completely customizable by bringing your own /// Axum [`Router`]. +/// +/// For complete complete end-to-end implementations, see +/// [`ConversionWebhookServer`]. pub struct WebhookServer { options: Options, router: Router, diff --git a/stackable-webhook/src/servers/conversion.rs b/stackable-webhook/src/servers/conversion.rs index 68dd85ef0..efa2f1f64 100644 --- a/stackable-webhook/src/servers/conversion.rs +++ b/stackable-webhook/src/servers/conversion.rs @@ -1,5 +1,8 @@ +use std::fmt::Debug; + use axum::{extract::State, routing::post, Json, Router}; use kube::core::conversion::ConversionReview; +use tracing::{debug, instrument}; use crate::{options::Options, StatefulWebhookHandler, WebhookHandler, WebhookServer}; @@ -21,6 +24,10 @@ where } } +/// A ready-to-use CRD conversion webhook server. +/// +/// See [`ConversionWebhookServer::new()`] and [`ConversionWebhookServer::new_with_state()`] +/// for usage examples. pub struct ConversionWebhookServer { options: Options, router: Router, @@ -33,17 +40,35 @@ impl ConversionWebhookServer { /// Each request is handled by the provided `handler` function. Any function /// with the signature `(ConversionReview) -> ConversionReview` can be /// provided. - pub fn new<T>(handler: T, options: Options) -> Self + /// + /// # Example + /// + /// ``` + /// use stackable_webhook::{servers::ConversionWebhookServer, Options}; + /// use kube::core::conversion::ConversionReview; + /// + /// // Construct the conversion webhook server + /// let server = ConversionWebhookServer::new(handler, Options::default()); + /// + /// // Define the handler function + /// fn handler(req: ConversionReview) -> ConversionReview { + /// // In here we can do the CRD conversion + /// req + /// } + /// ``` + #[instrument(name = "create_conversion_webhhok_server", skip(handler))] + pub fn new<H>(handler: H, options: Options) -> Self where - T: WebhookHandler<ConversionReview, ConversionReview> + Clone + Send + Sync + 'static, + H: WebhookHandler<ConversionReview, ConversionReview> + Clone + Send + Sync + 'static, { + debug!("create new conversion webhook server"); + let handler_fn = |Json(review): Json<ConversionReview>| async { let review = handler.call(review); Json(review) }; let router = Router::new().route("/convert", post(handler_fn)); - Self { router, options } } @@ -55,26 +80,58 @@ impl ConversionWebhookServer { /// provided. /// /// It is recommended to wrap the state in an [`Arc`][std::sync::Arc] if it - /// needs to be mutable. + /// needs to be mutable, see + /// <https://docs.rs/axum/latest/axum/index.html#sharing-state-with-handlers>. + /// + /// # Example + /// + /// ``` + /// use std::sync::Arc; /// - /// ### See + /// use stackable_webhook::{servers::ConversionWebhookServer, Options}; + /// use kube::core::conversion::ConversionReview; /// - /// - <https://docs.rs/axum/latest/axum/index.html#sharing-state-with-handlers> - pub fn new_with_state<T, S>(handler: T, state: S, options: Options) -> Self + /// #[derive(Debug, Clone)] + /// struct State {} + /// + /// let shared_state = Arc::new(State {}); + /// let server = ConversionWebhookServer::new_with_state( + /// handler, + /// shared_state, + /// Options::default(), + /// ); + /// + /// // Define the handler function + /// fn handler(req: ConversionReview, state: Arc<State>) -> ConversionReview { + /// // In here we can do the CRD conversion + /// req + /// } + /// ``` + #[instrument(name = "create_conversion_webhook_server_with_state", skip(handler))] + pub fn new_with_state<H, S>(handler: H, state: S, options: Options) -> Self where - T: StatefulWebhookHandler<ConversionReview, ConversionReview, S> + H: StatefulWebhookHandler<ConversionReview, ConversionReview, S> + Clone + Send + Sync + 'static, - S: Clone + Send + Sync + 'static, + S: Clone + Debug + Send + Sync + 'static, { - // See https://github.com/async-graphql/async-graphql/discussions/1150 + debug!("create new conversion webhook server with state"); + + // NOTE (@Techassi): Initially, after adding the state extractor, the + // compiler kept throwing a trait error at me stating that the closure + // below doesn't implement the Handler trait from Axum. This had nothing + // to do with the state itself, but rather the order of extractors. All + // body consuming extractors, like the JSON extractor need to come last + // in the handler. + // https://docs.rs/axum/latest/axum/extract/index.html#the-order-of-extractors let handler_fn = |State(state): State<S>, Json(review): Json<ConversionReview>| async { let review = handler.call(review, state); Json(review) }; + debug!("create router"); let router = Router::new() .route("/convert", post(handler_fn)) .with_state(state); @@ -82,7 +139,12 @@ impl ConversionWebhookServer { Self { router, options } } + /// Starts the conversion webhook server by starting the underlying + /// [`WebhookServer`]. + #[instrument(name = "run_conversion_webhook_server", skip(self), fields(self.options))] pub async fn run(self) -> Result<(), crate::Error> { + debug!("run conversion webhook server"); + let server = WebhookServer::new(self.router, self.options); server.run().await } From 94d120b43a655abe7cd2b3edaf8f2534871a1b66 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Mon, 12 Feb 2024 15:49:04 +0100 Subject: [PATCH 18/31] Add doc comments --- stackable-webhook/src/constants.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/stackable-webhook/src/constants.rs b/stackable-webhook/src/constants.rs index d124133f8..86ec23a6b 100644 --- a/stackable-webhook/src/constants.rs +++ b/stackable-webhook/src/constants.rs @@ -2,8 +2,14 @@ //! addresses. use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +/// The default HTTPS port `8443` pub const DEFAULT_HTTPS_PORT: u16 = 8443; + +/// The default HTTP port `8080`. pub const DEFAULT_HTTP_PORT: u16 = 8080; +/// The default IP address `127.0.0.1` the webhook server binds to. pub const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + +/// The default socket address `127.0.0.1:8443` the webhook server vinds to. pub const DEFAULT_SOCKET_ADDR: SocketAddr = SocketAddr::new(DEFAULT_IP_ADDRESS, DEFAULT_HTTPS_PORT); From f91ead60fc43827ccb9faa228f9190c8d9afd822 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Mon, 12 Feb 2024 15:50:18 +0100 Subject: [PATCH 19/31] Make TLS private key loading configurable --- stackable-webhook/src/lib.rs | 3 ++ stackable-webhook/src/options.rs | 14 ++++-- stackable-webhook/src/tls/certs.rs | 71 +++++++++++++++-------------- stackable-webhook/src/tls/server.rs | 10 ++-- 4 files changed, 57 insertions(+), 41 deletions(-) diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index a09a02566..9aac51df1 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -165,6 +165,8 @@ impl WebhookServer { #[cfg(test)] mod test { + use crate::tls::PrivateKeyEncoding; + use super::*; use axum::{routing::get, Router}; @@ -175,6 +177,7 @@ mod test { .tls_mount( "/tmp/webhook-certs/serverCert.pem", "/tmp/webhook-certs/serverKey.pem", + PrivateKeyEncoding::Ec, ) .build(); diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs index 0687b05f3..07568a426 100644 --- a/stackable-webhook/src/options.rs +++ b/stackable-webhook/src/options.rs @@ -4,7 +4,10 @@ use std::{ path::PathBuf, }; -use crate::constants::{DEFAULT_HTTP_PORT, DEFAULT_SOCKET_ADDR}; +use crate::{ + constants::{DEFAULT_HTTP_PORT, DEFAULT_SOCKET_ADDR}, + tls::PrivateKeyEncoding, +}; /// Specifies available webhook server options. /// @@ -155,11 +158,13 @@ impl OptionsBuilder { pub fn tls_mount( mut self, cert_path: impl Into<PathBuf>, - key_path: impl Into<PathBuf>, + pk_path: impl Into<PathBuf>, + pk_encoding: PrivateKeyEncoding, ) -> Self { self.tls = Some(TlsOption::Mount { cert_path: cert_path.into(), - key_path: key_path.into(), + pk_path: pk_path.into(), + pk_encoding, }); self } @@ -191,8 +196,9 @@ impl Default for RedirectOption { pub enum TlsOption { AutoGenerate, Mount { + pk_encoding: PrivateKeyEncoding, cert_path: PathBuf, - key_path: PathBuf, + pk_path: PathBuf, }, } diff --git a/stackable-webhook/src/tls/certs.rs b/stackable-webhook/src/tls/certs.rs index adc082869..88d7b3cd0 100644 --- a/stackable-webhook/src/tls/certs.rs +++ b/stackable-webhook/src/tls/certs.rs @@ -1,6 +1,8 @@ +// TODO (@Techassi): Move this into a separate crate which handles TLS cert +// generation and reading. use std::{fs::File, io::BufReader, path::Path}; -use rustls_pemfile::{certs, ec_private_keys}; +use rustls_pemfile::{certs, ec_private_keys, pkcs8_private_keys, rsa_private_keys}; use snafu::{ResultExt, Snafu}; use tokio_rustls::rustls::{Certificate, PrivateKey}; @@ -24,47 +26,43 @@ pub struct CertificateChain { private_key: PrivateKey, } -impl<C, P> TryFrom<(&mut C, &mut P)> for CertificateChain -where - C: std::io::BufRead, - P: std::io::BufRead, -{ - type Error = CertifacteError; +impl CertificateChain { + pub fn from_files( + cert_path: impl AsRef<Path>, + pk_path: impl AsRef<Path>, + pk_encoding: PrivateKeyEncoding, + ) -> Result<Self, CertifacteError> { + let cert_file = File::open(cert_path).context(ReadCertFileSnafu)?; + let cert_reader = &mut BufReader::new(cert_file); + + let key_file = File::open(pk_path).context(ReadKeyFileSnafu)?; + let key_reader = &mut BufReader::new(key_file); - fn try_from(readers: (&mut C, &mut P)) -> Result<Self, Self::Error> { - let chain = certs(readers.0) + Self::from_buffer(cert_reader, key_reader, pk_encoding) + } + + fn from_buffer( + cert_reader: &mut dyn std::io::BufRead, + pk_reader: &mut dyn std::io::BufRead, + pk_encoding: PrivateKeyEncoding, + ) -> Result<Self, CertifacteError> { + let chain = certs(cert_reader) .context(ReadBufferedCertFileSnafu)? .into_iter() .map(Certificate) .collect(); - // TODO (@Techassi): Make this function configurable - let private_key = ec_private_keys(readers.1) - .context(ReadBufferedKeyFileSnafu)? - .remove(0); - let private_key = PrivateKey(private_key); + let pk_bytes = match pk_encoding { + PrivateKeyEncoding::Pkcs8 => pkcs8_private_keys(pk_reader), + PrivateKeyEncoding::Rsa => rsa_private_keys(pk_reader), + PrivateKeyEncoding::Ec => ec_private_keys(pk_reader), + } + .context(ReadBufferedKeyFileSnafu)? + .remove(0); + let private_key = PrivateKey(pk_bytes); Ok(Self { chain, private_key }) } -} - -impl CertificateChain { - pub fn from_files<C, P>( - certificate_path: C, - private_key_path: P, - ) -> Result<Self, CertifacteError> - where - C: AsRef<Path>, - P: AsRef<Path>, - { - let cert_file = File::open(certificate_path).context(ReadCertFileSnafu)?; - let cert_reader = &mut BufReader::new(cert_file); - - let key_file = File::open(private_key_path).context(ReadKeyFileSnafu)?; - let key_reader = &mut BufReader::new(key_file); - - Self::try_from((cert_reader, key_reader)) - } pub fn chain(&self) -> &[Certificate] { &self.chain @@ -78,3 +76,10 @@ impl CertificateChain { (self.chain, self.private_key) } } + +#[derive(Debug)] +pub enum PrivateKeyEncoding { + Pkcs8, + Rsa, + Ec, +} diff --git a/stackable-webhook/src/tls/server.rs b/stackable-webhook/src/tls/server.rs index a9638a4db..63915aca0 100644 --- a/stackable-webhook/src/tls/server.rs +++ b/stackable-webhook/src/tls/server.rs @@ -58,11 +58,13 @@ impl TlsServer { } TlsOption::Mount { cert_path, - key_path, + pk_path, + pk_encoding, } => { - let (chain, private_key) = CertificateChain::from_files(cert_path, key_path) - .context(TlsCertificateChainSnafu)? - .into_parts(); + let (chain, private_key) = + CertificateChain::from_files(cert_path, pk_path, pk_encoding) + .context(TlsCertificateChainSnafu)? + .into_parts(); // TODO (@Techassi): Use the latest version of rustls related crates let mut config = ServerConfig::builder() From faae8b2842f4c701e7580bdb0e003b53f67a6519 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 13 Feb 2024 10:45:33 +0100 Subject: [PATCH 20/31] Remove tests These tests required a lot of manual setup, which is not desirable. In the future, we need to enable automatic testing of these servers. --- stackable-webhook/src/lib.rs | 23 ------------ stackable-webhook/src/servers/conversion.rs | 40 --------------------- 2 files changed, 63 deletions(-) diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index 9aac51df1..396b53eaf 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -162,26 +162,3 @@ impl WebhookServer { tls_server.run().await.context(RunTlsServerSnafu) } } - -#[cfg(test)] -mod test { - use crate::tls::PrivateKeyEncoding; - - use super::*; - use axum::{routing::get, Router}; - - #[tokio::test] - async fn test() { - let router = Router::new().route("/", get(|| async { "Ok" })); - let options = Options::builder() - .tls_mount( - "/tmp/webhook-certs/serverCert.pem", - "/tmp/webhook-certs/serverKey.pem", - PrivateKeyEncoding::Ec, - ) - .build(); - - let server = WebhookServer::new(router, options); - server.run().await.unwrap() - } -} diff --git a/stackable-webhook/src/servers/conversion.rs b/stackable-webhook/src/servers/conversion.rs index efa2f1f64..b1a5690f9 100644 --- a/stackable-webhook/src/servers/conversion.rs +++ b/stackable-webhook/src/servers/conversion.rs @@ -149,43 +149,3 @@ impl ConversionWebhookServer { server.run().await } } - -#[cfg(test)] -mod test { - use std::sync::Arc; - - use super::*; - use crate::Options; - - #[derive(Debug, Clone)] - struct State { - inner: usize, - } - - fn handler(req: ConversionReview) -> ConversionReview { - // In here we can do the CRD conversion - req - } - - fn handler_with_state(req: ConversionReview, state: Arc<State>) -> ConversionReview { - println!("{}", state.inner); - req - } - - #[tokio::test] - async fn without_state() { - let server = ConversionWebhookServer::new(handler, Options::default()); - server.run().await.unwrap(); - } - - #[tokio::test] - async fn with_state() { - let shared_state = Arc::new(State { inner: 0 }); - let server = ConversionWebhookServer::new_with_state( - handler_with_state, - shared_state, - Options::default(), - ); - server.run().await.unwrap(); - } -} From 11420a3360271950bc533eba86b200599e622f3e Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 13 Feb 2024 10:47:02 +0100 Subject: [PATCH 21/31] Update rustls-related crates These new versions introduced quite a few breaking changes which required a slight reworkf of the TLS cert handling code. --- Cargo.toml | 8 ++-- stackable-webhook/Cargo.toml | 8 ++-- stackable-webhook/src/options.rs | 2 +- stackable-webhook/src/tls/certs.rs | 70 +++++++++++++++++------------ stackable-webhook/src/tls/mod.rs | 3 +- stackable-webhook/src/tls/server.rs | 3 +- 6 files changed, 53 insertions(+), 41 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c491e6a32..a376a682f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,12 +27,12 @@ dockerfile-parser = "0.8.0" either = "1.9.0" futures = "0.3.28" json-patch = "1.0.0" -k8s-openapi = { version = "0.20.0", default-features = false, features = [ +k8s-openapi = { version = "0.21.0", default-features = false, features = [ "schemars", "v1_28", ] } # We use rustls instead of openssl for easier portablitly, e.g. so that we can build stackablectl without the need to vendor (build from source) openssl -kube = { version = "0.87.1", default-features = false, features = [ +kube = { version = "0.88.1", default-features = false, features = [ "client", "jsonpatch", "runtime", @@ -51,9 +51,9 @@ semver = "1.0" serde = { version = "1.0.184", features = ["derive"] } serde_json = "1.0.104" serde_yaml = "0.9.25" -snafu = "0.7.5" +snafu = "0.8.0" stackable-operator-derive = { path = "stackable-operator-derive" } -strum = { version = "0.25.0", features = ["derive"] } +strum = { version = "0.26.1", features = ["derive"] } thiserror = "1.0.44" time = { version = "0.3.29", optional = true } tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } diff --git a/stackable-webhook/Cargo.toml b/stackable-webhook/Cargo.toml index 807719341..6a1eea251 100644 --- a/stackable-webhook/Cargo.toml +++ b/stackable-webhook/Cargo.toml @@ -8,20 +8,20 @@ repository.workspace = true [dependencies] axum = "0.7.4" -kube = { version = "0.87.1", default-features = false } -tokio-rustls = "0.24.1" +kube = { version = "0.88.1", default-features = false } +tokio-rustls = "0.25.0" serde_json = "1.0.104" snafu = "0.8.0" tokio = "1.29.1" tokio-test = "0.4.3" tower = "0.4.13" tracing = "0.1.40" -rustls-pemfile = "1.0.4" +rustls-pemfile = "2.0.0" futures-util = "0.3.30" hyper-util = "0.1.3" hyper = { version = "1.0.0", features = ["full"] } [dev-dependencies] -k8s-openapi = { version = "0.20.0", default-features = false, features = [ +k8s-openapi = { version = "0.21.0", default-features = false, features = [ "v1_28", ] } diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs index 07568a426..3773b78f8 100644 --- a/stackable-webhook/src/options.rs +++ b/stackable-webhook/src/options.rs @@ -6,7 +6,7 @@ use std::{ use crate::{ constants::{DEFAULT_HTTP_PORT, DEFAULT_SOCKET_ADDR}, - tls::PrivateKeyEncoding, + tls::certs::PrivateKeyEncoding, }; /// Specifies available webhook server options. diff --git a/stackable-webhook/src/tls/certs.rs b/stackable-webhook/src/tls/certs.rs index 88d7b3cd0..5f0a5138e 100644 --- a/stackable-webhook/src/tls/certs.rs +++ b/stackable-webhook/src/tls/certs.rs @@ -4,7 +4,9 @@ use std::{fs::File, io::BufReader, path::Path}; use rustls_pemfile::{certs, ec_private_keys, pkcs8_private_keys, rsa_private_keys}; use snafu::{ResultExt, Snafu}; -use tokio_rustls::rustls::{Certificate, PrivateKey}; +use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer}; + +pub type Result<T, E = CertifacteError> = std::result::Result<T, E>; #[derive(Debug, Snafu)] pub enum CertifacteError { @@ -22,8 +24,8 @@ pub enum CertifacteError { } pub struct CertificateChain { - chain: Vec<Certificate>, - private_key: PrivateKey, + chain: Vec<CertificateDer<'static>>, + private_key: PrivateKeyDer<'static>, } impl CertificateChain { @@ -31,48 +33,60 @@ impl CertificateChain { cert_path: impl AsRef<Path>, pk_path: impl AsRef<Path>, pk_encoding: PrivateKeyEncoding, - ) -> Result<Self, CertifacteError> { + ) -> Result<Self> { let cert_file = File::open(cert_path).context(ReadCertFileSnafu)?; - let cert_reader = &mut BufReader::new(cert_file); + let mut cert_reader = BufReader::new(cert_file); let key_file = File::open(pk_path).context(ReadKeyFileSnafu)?; - let key_reader = &mut BufReader::new(key_file); + let mut pk_reader = BufReader::new(key_file); - Self::from_buffer(cert_reader, key_reader, pk_encoding) - } + let chain = certs(&mut cert_reader) + .collect::<Result<Vec<_>, _>>() + .context(ReadBufferedCertFileSnafu)?; - fn from_buffer( - cert_reader: &mut dyn std::io::BufRead, - pk_reader: &mut dyn std::io::BufRead, - pk_encoding: PrivateKeyEncoding, - ) -> Result<Self, CertifacteError> { - let chain = certs(cert_reader) - .context(ReadBufferedCertFileSnafu)? - .into_iter() - .map(Certificate) - .collect(); - - let pk_bytes = match pk_encoding { - PrivateKeyEncoding::Pkcs8 => pkcs8_private_keys(pk_reader), - PrivateKeyEncoding::Rsa => rsa_private_keys(pk_reader), - PrivateKeyEncoding::Ec => ec_private_keys(pk_reader), + let private_key = match pk_encoding { + PrivateKeyEncoding::Pkcs8 => Self::pkcs8_to_pk_der(&mut pk_reader)?, + PrivateKeyEncoding::Rsa => Self::rsa_to_pk_der(&mut pk_reader)?, + PrivateKeyEncoding::Ec => Self::ec_to_pk_der(&mut pk_reader)?, } - .context(ReadBufferedKeyFileSnafu)? .remove(0); - let private_key = PrivateKey(pk_bytes); Ok(Self { chain, private_key }) } - pub fn chain(&self) -> &[Certificate] { + fn pkcs8_to_pk_der<'a>(pk_reader: &mut dyn std::io::BufRead) -> Result<Vec<PrivateKeyDer<'a>>> { + let ders = pkcs8_private_keys(pk_reader) + .collect::<Result<Vec<_>, _>>() + .context(ReadBufferedKeyFileSnafu)?; + + Ok(ders.into_iter().map(PrivateKeyDer::from).collect()) + } + + fn rsa_to_pk_der<'a>(pk_reader: &mut dyn std::io::BufRead) -> Result<Vec<PrivateKeyDer<'a>>> { + let ders = rsa_private_keys(pk_reader) + .collect::<Result<Vec<_>, _>>() + .context(ReadBufferedKeyFileSnafu)?; + + Ok(ders.into_iter().map(PrivateKeyDer::from).collect()) + } + + fn ec_to_pk_der<'a>(pk_reader: &mut dyn std::io::BufRead) -> Result<Vec<PrivateKeyDer<'a>>> { + let ders = ec_private_keys(pk_reader) + .collect::<Result<Vec<_>, _>>() + .context(ReadBufferedKeyFileSnafu)?; + + Ok(ders.into_iter().map(PrivateKeyDer::from).collect()) + } + + pub fn chain(&self) -> &[CertificateDer] { &self.chain } - pub fn private_key(&self) -> &PrivateKey { + pub fn private_key(&self) -> &PrivateKeyDer { &self.private_key } - pub fn into_parts(self) -> (Vec<Certificate>, PrivateKey) { + pub fn into_parts(self) -> (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>) { (self.chain, self.private_key) } } diff --git a/stackable-webhook/src/tls/mod.rs b/stackable-webhook/src/tls/mod.rs index c180c65ec..7c7918041 100644 --- a/stackable-webhook/src/tls/mod.rs +++ b/stackable-webhook/src/tls/mod.rs @@ -1,7 +1,6 @@ //! Contains structs and functions to easily create a TLS termination server, //! which can be used in combination with an Axum [`Router`][axum::Router]. -mod certs; +pub mod certs; mod server; -pub use certs::*; pub use server::*; diff --git a/stackable-webhook/src/tls/server.rs b/stackable-webhook/src/tls/server.rs index 63915aca0..c06efacf3 100644 --- a/stackable-webhook/src/tls/server.rs +++ b/stackable-webhook/src/tls/server.rs @@ -14,7 +14,7 @@ use tracing::{error, instrument, warn}; use crate::{ options::TlsOption, - tls::{CertifacteError, CertificateChain}, + tls::certs::{CertifacteError, CertificateChain}, }; pub type Result<T, E = Error> = std::result::Result<T, E>; @@ -68,7 +68,6 @@ impl TlsServer { // TODO (@Techassi): Use the latest version of rustls related crates let mut config = ServerConfig::builder() - .with_safe_defaults() .with_no_client_auth() .with_single_cert(chain, private_key) .context(InvalidTlsPrivateKeySnafu)?; From 795ad648c0ffa1a58ba54b23d494e2577cf91ffa Mon Sep 17 00:00:00 2001 From: Techassi <git@techassi.dev> Date: Tue, 13 Feb 2024 15:41:41 +0100 Subject: [PATCH 22/31] Apply suggestions Co-authored-by: Nick <NickLarsenNZ@users.noreply.github.com> --- stackable-webhook/src/lib.rs | 2 +- stackable-webhook/src/options.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index 396b53eaf..93c87444f 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -75,7 +75,7 @@ pub enum Error { /// and their handlers are completely customizable by bringing your own /// Axum [`Router`]. /// -/// For complete complete end-to-end implementations, see +/// For complete end-to-end implementations, see /// [`ConversionWebhookServer`]. pub struct WebhookServer { options: Options, diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs index 3773b78f8..5f6b3be85 100644 --- a/stackable-webhook/src/options.rs +++ b/stackable-webhook/src/options.rs @@ -157,9 +157,9 @@ impl OptionsBuilder { /// [`OptionsBuilder::tls_autogenerate()`] function. pub fn tls_mount( mut self, - cert_path: impl Into<PathBuf>, - pk_path: impl Into<PathBuf>, - pk_encoding: PrivateKeyEncoding, + public_key_path: impl Into<PathBuf>, + private_key_path: impl Into<PathBuf>, + private_key_encoding: PrivateKeyEncoding, ) -> Self { self.tls = Some(TlsOption::Mount { cert_path: cert_path.into(), From 2e5341011406264b082c240213c2f49a9224bcea Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 13 Feb 2024 15:49:35 +0100 Subject: [PATCH 23/31] Adjust names according to code style guide --- stackable-webhook/src/options.rs | 12 ++++++------ stackable-webhook/src/tls/server.rs | 18 ++++++++++-------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs index 5f6b3be85..68af8a67a 100644 --- a/stackable-webhook/src/options.rs +++ b/stackable-webhook/src/options.rs @@ -162,9 +162,9 @@ impl OptionsBuilder { private_key_encoding: PrivateKeyEncoding, ) -> Self { self.tls = Some(TlsOption::Mount { - cert_path: cert_path.into(), - pk_path: pk_path.into(), - pk_encoding, + public_key_path: public_key_path.into(), + private_key_path: private_key_path.into(), + private_key_encoding, }); self } @@ -196,9 +196,9 @@ impl Default for RedirectOption { pub enum TlsOption { AutoGenerate, Mount { - pk_encoding: PrivateKeyEncoding, - cert_path: PathBuf, - pk_path: PathBuf, + private_key_encoding: PrivateKeyEncoding, + public_key_path: PathBuf, + private_key_path: PathBuf, }, } diff --git a/stackable-webhook/src/tls/server.rs b/stackable-webhook/src/tls/server.rs index c06efacf3..16de311fb 100644 --- a/stackable-webhook/src/tls/server.rs +++ b/stackable-webhook/src/tls/server.rs @@ -57,16 +57,18 @@ impl TlsServer { todo!() } TlsOption::Mount { - cert_path, - pk_path, - pk_encoding, + public_key_path, + private_key_path, + private_key_encoding, } => { - let (chain, private_key) = - CertificateChain::from_files(cert_path, pk_path, pk_encoding) - .context(TlsCertificateChainSnafu)? - .into_parts(); + let (chain, private_key) = CertificateChain::from_files( + public_key_path, + private_key_path, + private_key_encoding, + ) + .context(TlsCertificateChainSnafu)? + .into_parts(); - // TODO (@Techassi): Use the latest version of rustls related crates let mut config = ServerConfig::builder() .with_no_client_auth() .with_single_cert(chain, private_key) From d0ea673f87dfd903f7f79f78d7f83f6b823bf842 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 13 Feb 2024 15:52:42 +0100 Subject: [PATCH 24/31] Make handler traits only accessible from this crate --- stackable-webhook/src/lib.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index 93c87444f..d3e661010 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -43,19 +43,20 @@ pub type Result<T, E = Error> = std::result::Result<T, E>; /// A generic webhook handler receiving a request and sending back a response. /// -/// This trait is usually not implemented by external callers and this library -/// provides various ready-to-use implementations for it. One such an -pub trait WebhookHandler<Req, Res> { +/// This trait is not intended to be implemented by external crates and this +/// library provides various ready-to-use implementations for it. One such an +/// /// implementation is part of the [`ConversionWebhookServer`]. +pub(crate) trait WebhookHandler<Req, Res> { fn call(self, req: Req) -> Res; } /// A generic webhook handler receiving a request and state and sending back /// a response. /// -/// This trait is usually not implemented by external callers and this library -/// provides various ready-to-use implementations for it. One such an +/// This trait is not intended to be implemented by external crates and this +/// library provides various ready-to-use implementations for it. One such an /// implementation is part of the [`ConversionWebhookServer`]. -pub trait StatefulWebhookHandler<Req, Res, S> { +pub(crate) trait StatefulWebhookHandler<Req, Res, S> { fn call(self, req: Req, state: S) -> Res; } From fa30e3389e79ffbd537581235de0c964627814ee Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 13 Feb 2024 15:58:00 +0100 Subject: [PATCH 25/31] Change option builder function names --- stackable-webhook/src/options.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs index 68af8a67a..84aab0aeb 100644 --- a/stackable-webhook/src/options.rs +++ b/stackable-webhook/src/options.rs @@ -123,24 +123,24 @@ impl OptionsBuilder { } /// Sets the socket address the webhook server uses to bind for HTTPS. - pub fn socket_addr(mut self, socket_ip: impl Into<IpAddr>, socket_port: u16) -> Self { - self.socket_addr = Some(SocketAddr::new(socket_ip.into(), socket_port)); + pub fn bind_address(mut self, bind_ip: impl Into<IpAddr>, bind_port: u16) -> Self { + self.socket_addr = Some(SocketAddr::new(bind_ip.into(), bind_port)); self } /// Sets the IP address of the socket address the webhook server uses to /// bind for HTTPS. - pub fn socket_ip(mut self, socket_ip: impl Into<IpAddr>) -> Self { + pub fn bind_ip(mut self, bind_ip: impl Into<IpAddr>) -> Self { let addr = self.socket_addr.get_or_insert(DEFAULT_SOCKET_ADDR); - addr.set_ip(socket_ip.into()); + addr.set_ip(bind_ip.into()); self } /// Sets the port of the socket address the webhook server uses to bind /// for HTTPS. - pub fn socket_port(mut self, socket_port: u16) -> Self { + pub fn bind_port(mut self, bind_port: u16) -> Self { let addr = self.socket_addr.get_or_insert(DEFAULT_SOCKET_ADDR); - addr.set_port(socket_port); + addr.set_port(bind_port); self } From f7dc0b1efa890c932a6e1f10b4869edd31db661d Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Tue, 13 Feb 2024 16:10:07 +0100 Subject: [PATCH 26/31] Change info! and warn! to debug! --- stackable-webhook/src/lib.rs | 6 +++--- stackable-webhook/src/redirect.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index d3e661010..26face2f9 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -24,7 +24,7 @@ //! enable complete controll over these details if needed. use axum::Router; use snafu::{ResultExt, Snafu}; -use tracing::{debug, info, instrument, warn}; +use tracing::{debug, instrument}; use crate::{options::RedirectOption, redirect::Redirector, tls::TlsServer}; @@ -141,11 +141,11 @@ impl WebhookServer { http_port, ); - info!(http_port, "spawning redirector in separate task"); + debug!(http_port, "spawning redirector in separate task"); tokio::spawn(redirector.run()); } RedirectOption::Disabled => { - warn!("webhook runs without automatic HTTP to HTTPS redirect which is not recommended"); + debug!("webhook runs without automatic HTTP to HTTPS redirect which is not recommended"); } } diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs index fb8b0b992..e9ca12d2f 100644 --- a/stackable-webhook/src/redirect.rs +++ b/stackable-webhook/src/redirect.rs @@ -68,7 +68,7 @@ impl Redirector { // print it in the trace? match http_to_https(host, uri.clone(), self.http_port, self.https_port) { Ok(redirect_uri) => { - info!("redirecting from {} to {}", uri, redirect_uri); + debug!("redirecting from {} to {}", uri, redirect_uri); Ok(Redirect::permanent(&redirect_uri.to_string())) } Err(err) => { From e1f538b71cae0c1d17c72bd903da2d97f280ef51 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Wed, 14 Feb 2024 12:59:25 +0100 Subject: [PATCH 27/31] Add basic high-level request tracing --- stackable-webhook/Cargo.toml | 1 + stackable-webhook/src/lib.rs | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/stackable-webhook/Cargo.toml b/stackable-webhook/Cargo.toml index 6a1eea251..f46c43370 100644 --- a/stackable-webhook/Cargo.toml +++ b/stackable-webhook/Cargo.toml @@ -15,6 +15,7 @@ snafu = "0.8.0" tokio = "1.29.1" tokio-test = "0.4.3" tower = "0.4.13" +tower-http = { version = "0.5.1", features = ["trace"] } tracing = "0.1.40" rustls-pemfile = "2.0.0" futures-util = "0.3.30" diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index 26face2f9..e5a983b28 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -22,9 +22,11 @@ //! //! This library additionally also exposes lower-level structs and functions to //! enable complete controll over these details if needed. -use axum::Router; +use axum::{body::Body, http::Request, Router}; use snafu::{ResultExt, Snafu}; -use tracing::{debug, instrument}; +use tower::ServiceBuilder; +use tower_http::trace::TraceLayer; +use tracing::{debug, debug_span, instrument}; use crate::{options::RedirectOption, redirect::Redirector, tls::TlsServer}; @@ -149,9 +151,21 @@ impl WebhookServer { } } + // TODO (@Techassi): Switch out for Otel compatible tracing + // https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk + + // Create a high-level tracing layer + debug!("create tracing service (layer)"); + let layer = TraceLayer::new_for_http() + .make_span_with(|_: &Request<Body>| debug_span!("webhook_request")) + .on_body_chunk(()) + .on_eos(()); + + let service = ServiceBuilder::new().layer(layer); + // Create the root router and merge the provided router into it. debug!("create core couter and merge provided router"); - let mut router = Router::new(); + let mut router = Router::new().layer(service); router = router.merge(self.router); // Create server for TLS termination @@ -159,7 +173,7 @@ impl WebhookServer { let tls_server = TlsServer::new(self.options.socket_addr, router, self.options.tls) .context(CreateTlsServerSnafu)?; - info!("running TLS server"); + debug!("running TLS server"); tls_server.run().await.context(RunTlsServerSnafu) } } From b193cacb7377bbd74b7e6695577e0c7a16aa40e7 Mon Sep 17 00:00:00 2001 From: Techassi <git@techassi.dev> Date: Wed, 14 Feb 2024 13:02:38 +0100 Subject: [PATCH 28/31] Update stackable-webhook/src/lib.rs Co-authored-by: Nick <NickLarsenNZ@users.noreply.github.com> --- stackable-webhook/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index e5a983b28..14881a090 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -47,7 +47,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>; /// /// This trait is not intended to be implemented by external crates and this /// library provides various ready-to-use implementations for it. One such an -/// /// implementation is part of the [`ConversionWebhookServer`]. +/// implementation is part of the [`ConversionWebhookServer`]. pub(crate) trait WebhookHandler<Req, Res> { fn call(self, req: Req) -> Res; } From dbc9dd3f2a2dc372b2c0d2b96047ff1f35370b42 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Wed, 14 Feb 2024 13:08:24 +0100 Subject: [PATCH 29/31] Remove redirector --- stackable-webhook/src/constants.rs | 3 - stackable-webhook/src/lib.rs | 51 +++++++------- stackable-webhook/src/options.rs | 58 +--------------- stackable-webhook/src/redirect.rs | 105 ----------------------------- 4 files changed, 28 insertions(+), 189 deletions(-) delete mode 100644 stackable-webhook/src/redirect.rs diff --git a/stackable-webhook/src/constants.rs b/stackable-webhook/src/constants.rs index 86ec23a6b..65f7c1ebb 100644 --- a/stackable-webhook/src/constants.rs +++ b/stackable-webhook/src/constants.rs @@ -5,9 +5,6 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; /// The default HTTPS port `8443` pub const DEFAULT_HTTPS_PORT: u16 = 8443; -/// The default HTTP port `8080`. -pub const DEFAULT_HTTP_PORT: u16 = 8080; - /// The default IP address `127.0.0.1` the webhook server binds to. pub const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index 14881a090..85587989a 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -1,8 +1,7 @@ //! Utility types and functions to easily create ready-to-use webhook servers //! which can handle different tasks, for example CRD conversions. All webhook -//! servers use HTTPS by default and provides options to enable HTTP to HTTPS -//! redirection as well. This library is fully compatible with the [`tracing`] -//! crate and emits multiple levels of tracing data. +//! servers use HTTPS by defaultThis library is fully compatible with the +//! [`tracing`] crate and emits debug level tracing data. //! //! Most users will only use the top-level exported generic [`WebhookServer`] //! which enables complete control over the [Router] which handles registering @@ -28,11 +27,10 @@ use tower::ServiceBuilder; use tower_http::trace::TraceLayer; use tracing::{debug, debug_span, instrument}; -use crate::{options::RedirectOption, redirect::Redirector, tls::TlsServer}; +use crate::tls::TlsServer; pub mod constants; pub mod options; -pub mod redirect; pub mod servers; pub mod tls; @@ -113,7 +111,6 @@ impl WebhookServer { /// use axum::Router; /// /// let options = Options::builder() - /// .disable_redirect() /// .socket_addr(([127, 0, 0, 1], 8080)) /// .build(); /// @@ -132,25 +129,6 @@ impl WebhookServer { pub async fn run(self) -> Result<()> { debug!("run webhook server"); - // Only run the auto redirector when enabled - match self.options.redirect { - RedirectOption::Enabled(http_port) => { - debug!("run webhook server with automatic HTTP to HTTPS redirect enabled"); - - let redirector = Redirector::new( - self.options.socket_addr.ip(), - self.options.socket_addr.port(), - http_port, - ); - - debug!(http_port, "spawning redirector in separate task"); - tokio::spawn(redirector.run()); - } - RedirectOption::Disabled => { - debug!("webhook runs without automatic HTTP to HTTPS redirect which is not recommended"); - } - } - // TODO (@Techassi): Switch out for Otel compatible tracing // https://github.com/davidB/tracing-opentelemetry-instrumentation-sdk @@ -177,3 +155,26 @@ impl WebhookServer { tls_server.run().await.context(RunTlsServerSnafu) } } + +#[cfg(test)] +mod test { + use crate::tls::certs::PrivateKeyEncoding; + + use super::*; + use axum::{routing::get, Router}; + + #[tokio::test] + async fn test() { + let router = Router::new().route("/", get(|| async { "Ok" })); + let options = Options::builder() + .tls_mount( + "/tmp/webhook-certs/serverCert.pem", + "/tmp/webhook-certs/serverKey.pem", + PrivateKeyEncoding::Pkcs8, + ) + .build(); + + let server = WebhookServer::new(router, options); + server.run().await.unwrap() + } +} diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs index 84aab0aeb..93862d603 100644 --- a/stackable-webhook/src/options.rs +++ b/stackable-webhook/src/options.rs @@ -4,17 +4,13 @@ use std::{ path::PathBuf, }; -use crate::{ - constants::{DEFAULT_HTTP_PORT, DEFAULT_SOCKET_ADDR}, - tls::certs::PrivateKeyEncoding, -}; +use crate::{constants::DEFAULT_SOCKET_ADDR, tls::certs::PrivateKeyEncoding}; /// Specifies available webhook server options. /// /// The [`Default`] implemention for this struct contains the following /// values: /// -/// - Redirect from HTTP to HTTPS is enabled, HTTP listens on port 8080 /// - The socket binds to 127.0.0.1 on port 8443 (HTTPS) /// - The TLS cert used gets auto-generated /// @@ -39,22 +35,6 @@ use crate::{ /// .build(); /// ``` /// -/// ### Example with Custom Redirects -/// -/// ``` -/// use stackable_webhook::Options; -/// -/// // Use a custom HTTP port -/// let options = Options::builder() -/// .enable_redirect(12345) -/// .build(); -/// -/// // Disable auto-redirect -/// let options = Options::builder() -/// .disable_redirect() -/// .build(); -/// ``` -/// /// ### Example with Mounted TLS Certificate /// /// ``` @@ -66,14 +46,8 @@ use crate::{ /// ``` #[derive(Debug)] pub struct Options { - /// Enables or disables the automatic HTTP to HTTPS redirect. If enabled, - /// it is required to specify the HTTP port. If disabled, the webhook - /// server **only** listens on HTTPS. - pub redirect: RedirectOption, - /// The default HTTPS socket address the [`TcpListener`][tokio::net::TcpListener] - /// binds to. The same IP adress is used for the auto HTTP to HTTPS redirect - /// handler. + /// binds to. pub socket_addr: SocketAddr, /// Either auto-generate or use an injected TLS certificate. @@ -102,26 +76,11 @@ impl Options { /// [`Options::builder()`] or [`OptionsBuilder::default()`]. #[derive(Debug, Default)] pub struct OptionsBuilder { - redirect: Option<RedirectOption>, socket_addr: Option<SocketAddr>, tls: Option<TlsOption>, } impl OptionsBuilder { - /// Disables HTPP to HTTPS auto-redirect entirely. The webhook server - /// will only listen on HTTPS. - pub fn disable_redirect(mut self) -> Self { - self.redirect = Some(RedirectOption::Disabled); - self - } - - /// Enables HTTP to HTTPS auto-redirect on `http_port`. The webhook - /// server will listen on both HTTP and HTTPS. - pub fn enable_redirect(mut self, http_port: u16) -> Self { - self.redirect = Some(RedirectOption::Enabled(http_port)); - self - } - /// Sets the socket address the webhook server uses to bind for HTTPS. pub fn bind_address(mut self, bind_ip: impl Into<IpAddr>, bind_port: u16) -> Self { self.socket_addr = Some(SocketAddr::new(bind_ip.into(), bind_port)); @@ -173,25 +132,12 @@ impl OptionsBuilder { /// explicitly set option. pub fn build(self) -> Options { Options { - redirect: self.redirect.unwrap_or_default(), socket_addr: self.socket_addr.unwrap_or(DEFAULT_SOCKET_ADDR), tls: self.tls.unwrap_or_default(), } } } -#[derive(Debug)] -pub enum RedirectOption { - Enabled(u16), - Disabled, -} - -impl Default for RedirectOption { - fn default() -> Self { - Self::Enabled(DEFAULT_HTTP_PORT) - } -} - #[derive(Debug)] pub enum TlsOption { AutoGenerate, diff --git a/stackable-webhook/src/redirect.rs b/stackable-webhook/src/redirect.rs deleted file mode 100644 index e9ca12d2f..000000000 --- a/stackable-webhook/src/redirect.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Contains structs and functions to enable auto HTTP to HTTPS redirection. -use std::net::{IpAddr, SocketAddr}; - -use axum::{ - extract::Host, - handler::HandlerWithoutStateExt, - http::{ - uri::{InvalidUri, InvalidUriParts, Scheme}, - StatusCode, Uri, - }, - response::Redirect, -}; -use snafu::{ResultExt, Snafu}; -use tokio::net::TcpListener; -use tracing::{debug, info, instrument, warn}; - -#[derive(Debug, Snafu)] -pub enum Error { - #[snafu(display("failed to parse HTTPS host as authority"))] - ParseAuthority { source: InvalidUri }, - - #[snafu(display("failed to convert URI parts into URI"))] - ConvertPartsToUri { source: InvalidUriParts }, -} - -/// A redirector which redirects all incoming HTTP connections to HTTPS -/// automatically. -/// -/// Internally it uses a simple handler function which is registered as a -/// singular [`Service`][tower::MakeService] at the root "/" path. The request -/// paths are preserved. If the conversion from HTTP to HTTPS fails, the -/// [`Redirector`] returns a HTTP status code 400 (Bad Request). Additionally, -/// a warning trace is emitted. -#[derive(Debug)] -pub struct Redirector { - ip_addr: IpAddr, - https_port: u16, - http_port: u16, -} - -impl Redirector { - #[instrument] - pub fn new(ip_addr: IpAddr, https_port: u16, http_port: u16) -> Self { - debug!("create new HTTP to HTTPS redirector"); - - Self { - https_port, - http_port, - ip_addr, - } - } - - #[instrument] - pub async fn run(self) { - debug!("run redirector"); - - // The redirector only binds to the HTTP port. The actual HTTPS - // application runs in a separate task and is completely independent - // of this redirector. - let socket_addr = SocketAddr::new(self.ip_addr, self.http_port); - let listener = TcpListener::bind(socket_addr).await.unwrap(); - - // This converts the HTTP request URI into HTTPS. If this fails, the - // redirector emits a warning trace and returns HTTP status code 400 - // (Bad Request). - let redirect = move |Host(host): Host, uri: Uri| async move { - // NOTE (@Techassi): Is it worth to clone here just to be able to - // print it in the trace? - match http_to_https(host, uri.clone(), self.http_port, self.https_port) { - Ok(redirect_uri) => { - debug!("redirecting from {} to {}", uri, redirect_uri); - Ok(Redirect::permanent(&redirect_uri.to_string())) - } - Err(err) => { - warn!(%err, "failed to convert HTTP URI to HTTPS"); - Err(StatusCode::BAD_REQUEST) - } - } - }; - - // This registers the handler function as the only handler at the root - // path "/". See https://docs.rs/axum/latest/axum/fn.serve.html#examples - axum::serve(listener, redirect.into_make_service()) - .await - .unwrap(); - } -} - -fn http_to_https(host: String, uri: Uri, http_port: u16, https_port: u16) -> Result<Uri, Error> { - let mut parts = uri.into_parts(); - - parts.scheme = Some(Scheme::HTTPS); - - if parts.path_and_query.is_none() { - // NOTE (@Techassi): This should never fail and is this save to unwrap. - // If this will change into a user-controlled value, then this isn't - // save to unwrap anymore and will require explicit error handling. - parts.path_and_query = Some("/".parse().unwrap()); - } - - let https_host = host.replace(&http_port.to_string(), &https_port.to_string()); - parts.authority = Some(https_host.parse().context(ParseAuthoritySnafu)?); - - Ok(Uri::from_parts(parts).context(ConvertPartsToUriSnafu)?) -} From 2ea155295bbd53d1579fd125f0c079e5bf2645f6 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Wed, 14 Feb 2024 13:13:03 +0100 Subject: [PATCH 30/31] Fix doc tests --- stackable-webhook/src/lib.rs | 2 +- stackable-webhook/src/options.rs | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index 85587989a..10e65d594 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -111,7 +111,7 @@ impl WebhookServer { /// use axum::Router; /// /// let options = Options::builder() - /// .socket_addr(([127, 0, 0, 1], 8080)) + /// .bind_address([127, 0, 0, 1], 8080) /// .build(); /// /// let router = Router::new(); diff --git a/stackable-webhook/src/options.rs b/stackable-webhook/src/options.rs index 93862d603..49349da17 100644 --- a/stackable-webhook/src/options.rs +++ b/stackable-webhook/src/options.rs @@ -21,27 +21,31 @@ use crate::{constants::DEFAULT_SOCKET_ADDR, tls::certs::PrivateKeyEncoding}; /// /// // Set IP address and port at the same time /// let options = Options::builder() -/// .socket_addr([0, 0, 0, 0], 12345) +/// .bind_address([0, 0, 0, 0], 12345) /// .build(); /// /// // Set IP address only /// let options = Options::builder() -/// .socket_ip([0, 0, 0, 0]) +/// .bind_ip([0, 0, 0, 0]) /// .build(); /// /// // Set port only /// let options = Options::builder() -/// .socket_port(12345) +/// .bind_port(12345) /// .build(); /// ``` /// /// ### Example with Mounted TLS Certificate /// /// ``` -/// use stackable_webhook::Options; +/// use stackable_webhook::{Options, tls::certs::PrivateKeyEncoding}; /// /// let options = Options::builder() -/// .tls_mount("path/to/pem/cert", "path/to/pem/key") +/// .tls_mount( +/// "path/to/pem/cert", +/// "path/to/pem/key", +/// PrivateKeyEncoding::Pkcs8, +/// ) /// .build(); /// ``` #[derive(Debug)] From e9ffe0e40b49ffde97ffd60883ee741e294a28b8 Mon Sep 17 00:00:00 2001 From: Techassi <sascha.lautenschlaeger@stackable.tech> Date: Wed, 14 Feb 2024 13:15:09 +0100 Subject: [PATCH 31/31] Fix typo --- stackable-webhook/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackable-webhook/src/lib.rs b/stackable-webhook/src/lib.rs index 10e65d594..0518adaaf 100644 --- a/stackable-webhook/src/lib.rs +++ b/stackable-webhook/src/lib.rs @@ -142,7 +142,7 @@ impl WebhookServer { let service = ServiceBuilder::new().layer(layer); // Create the root router and merge the provided router into it. - debug!("create core couter and merge provided router"); + debug!("create core router and merge provided router"); let mut router = Router::new().layer(service); router = router.merge(self.router);