diff --git a/Cargo.lock b/Cargo.lock index 63a2279c5d..09a71d859c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,6 +333,7 @@ dependencies = [ "sync_wrapper", "time", "tokio", + "tokio-rustls 0.26.1", "tokio-stream", "tokio-tungstenite", "tower", @@ -444,7 +445,7 @@ dependencies = [ "rustls-pemfile", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.1", "tower-service", ] @@ -1641,7 +1642,7 @@ dependencies = [ "hyper", "hyper-util", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.1", "tower-service", "tracing", "tracing-subscriber", @@ -2580,7 +2581,7 @@ dependencies = [ "rustls", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.1", "tower-service", "webpki-roots", ] @@ -3252,7 +3253,7 @@ dependencies = [ "take_mut", "thiserror 1.0.69", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.1", "tokio-util", "typed-builder", "uuid", @@ -4034,7 +4035,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.26.1", "tokio-util", "tower", "tower-service", @@ -5077,6 +5078,18 @@ dependencies = [ "whoami", ] +[[package]] +name = "tokio-rustls" +version = "0.1.0" +dependencies = [ + "axum", + "tokio", + "tokio-rustls 0.26.1", + "tower-service", + "tracing", + "tracing-subscriber", +] + [[package]] name = "tokio-rustls" version = "0.26.1" diff --git a/axum/Cargo.toml b/axum/Cargo.toml index b449815465..95b1f17f68 100644 --- a/axum/Cargo.toml +++ b/axum/Cargo.toml @@ -37,6 +37,8 @@ allowed = [ "tokio", # for the `__private` feature "reqwest", + "rustls", + "tokio-rustls", ] [features] @@ -95,44 +97,45 @@ __private = ["tokio", "http1", "dep:reqwest"] [dependencies] axum-core = { path = "../axum-core", version = "0.5.5" } + +# optional dependencies +axum-macros = { path = "../axum-macros", version = "0.5.0", optional = true } +base64 = { version = "0.22.1", optional = true } bytes = "1.7" +form_urlencoded = { version = "1.1.0", optional = true } futures-core = "0.3" +futures-sink = { version = "0.3", optional = true } futures-util = { version = "0.3", default-features = false, features = ["alloc"] } http = "1.0.0" http-body = "1.0.0" http-body-util = "0.1.0" +hyper = { version = "1.1.0", optional = true } +hyper-util = { version = "0.1.3", features = ["tokio", "server", "service"], optional = true } itoa = "1.0.5" matchit = "=0.8.4" memchr = "2.4.1" mime = "0.3.16" +multer = { version = "3.0.0", optional = true } percent-encoding = "2.1" pin-project-lite = "0.2.7" -serde_core = "1.0.221" -sync_wrapper = "1.0.0" -tower = { version = "0.5.2", default-features = false, features = ["util"] } -tower-layer = "0.3.2" -tower-service = "0.3" - -# optional dependencies -axum-macros = { path = "../axum-macros", version = "0.5.0", optional = true } -base64 = { version = "0.22.1", optional = true } -form_urlencoded = { version = "1.1.0", optional = true } -futures-sink = { version = "0.3", optional = true } -hyper = { version = "1.1.0", optional = true } -hyper-util = { version = "0.1.3", features = ["tokio", "server", "service"], optional = true } -multer = { version = "3.0.0", optional = true } reqwest = { version = "0.12", optional = true, default-features = false, features = ["json", "stream", "multipart"] } + +# doc dependencies +serde = { version = "1.0.211", optional = true } +serde_core = "1.0.221" serde_json = { version = "1.0", features = ["raw_value"], optional = true } serde_path_to_error = { version = "0.1.8", optional = true } serde_urlencoded = { version = "0.7", optional = true } sha1 = { version = "0.10", optional = true } +sync_wrapper = "1.0.0" tokio = { package = "tokio", version = "1.44", features = ["time"], optional = true } +tokio-rustls = { version = "0.26", optional = true, default-features = false } tokio-tungstenite = { version = "0.28.0", optional = true } +tower = { version = "0.5.2", default-features = false, features = ["util"] } +tower-layer = "0.3.2" +tower-service = "0.3" tracing = { version = "0.1", default-features = false, optional = true } -# doc dependencies -serde = { version = "1.0.211", optional = true } - [dependencies.tower-http] version = "0.6.0" optional = true diff --git a/axum/src/serve/listener.rs b/axum/src/serve/listener.rs index 94e58fe493..d5526b3bc6 100644 --- a/axum/src/serve/listener.rs +++ b/axum/src/serve/listener.rs @@ -271,6 +271,110 @@ fn is_connection_error(e: &io::Error) -> bool { ) } +/// A TLS Listener is a simple wrapper to allow axum accept TLS connections natively. +/// +/// # Examples +/// +/// ```rust,no_run +/// # use tokio::net::TcpListener; +/// # use tokio_rustls::rustls::pki_types::pem::PemObject; +/// # use axum::serve::TlsListener; +/// # async { +/// let cert = tokio_rustls::rustls::pki_types::CertificateDer::from_slice(&[0]); +/// let key = tokio_rustls::rustls::pki_types::PrivateKeyDer::from_pem_slice(&[0]).unwrap(); +/// let config = tokio_rustls::rustls::ServerConfig::builder() +/// .with_no_client_auth() +/// .with_single_cert(vec![cert], key).unwrap(); +/// +/// let tcp = TcpListener::bind(("0.0.0.0", 8443)).await.unwrap(); +/// let tls_listener = TlsListener::new(tcp, config); +/// let app = axum::Router::new().route("/", axum::routing::get(|| async { "Hello" })); +/// +/// let _ = axum::serve(tls_listener, app.into_make_service()); +/// # }; +/// # () +/// ```` +#[cfg(feature = "tokio-rustls")] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio-rustls")))] +#[allow(missing_debug_implementations)] +pub struct TlsListener { + _inner: TcpListener, + acceptor: tokio_rustls::TlsAcceptor, +} + +#[cfg(feature = "tokio-rustls")] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio-rustls")))] +impl TlsListener { + /// Construct a new `TlsListener` from given [`TcpListener`] with provided + /// [`ServerConfig`]. + /// + /// `TlsListener` wraps underlying `TcpListener` which enables axum server to listen HTTPS + /// connections natively with `rustls`. + /// + /// # Examples + /// + /// ```rust + /// # use tokio::net::TcpListener; + /// # use tokio_rustls::rustls::pki_types::pem::PemObject; + /// # use axum::serve::TlsListener; + /// # async { + /// let cert = tokio_rustls::rustls::pki_types::CertificateDer::from_slice(&[0]); + /// let key = tokio_rustls::rustls::pki_types::PrivateKeyDer::from_pem_slice(&[0]).unwrap(); + /// let config = tokio_rustls::rustls::ServerConfig::builder() + /// .with_no_client_auth() + /// .with_single_cert(vec![cert], key).unwrap(); + /// + /// let tcp = TcpListener::bind(("0.0.0.0", 8443)).await.unwrap(); + /// let tls_listener = TlsListener::new(tcp, config); + /// # }; + /// # () + /// ```` + /// + /// [`ServerConfig`]: https://docs.rs/rustls/0.23.31/rustls/server/struct.ServerConfig.html + /// [`TcpListener`]: https://docs.rs/tokio/latest/tokio/net/struct.TcpListener.html + pub fn new(listener: TcpListener, server_config: tokio_rustls::rustls::ServerConfig) -> Self { + Self { + _inner: listener, + acceptor: tokio_rustls::TlsAcceptor::from(Arc::new(server_config)), + } + } + + async fn accept( + &self, + ) -> io::Result<( + tokio_rustls::server::TlsStream, + std::net::SocketAddr, + )> { + let (tcp, addr) = self._inner.accept().await?; + + // with valid tcp, try to acquire tls connection + match self.acceptor.accept(tcp).await { + Ok(tls) => Ok((tls, addr)), + Err(error) => Err(error), + } + } +} + +#[cfg(feature = "tokio-rustls")] +#[cfg_attr(docsrs, doc(cfg(feature = "tokio-rustls")))] +impl Listener for TlsListener { + type Io = tokio_rustls::server::TlsStream; + type Addr = std::net::SocketAddr; + + async fn accept(&mut self) -> (Self::Io, Self::Addr) { + loop { + match Self::accept(self).await { + Ok(tup) => return tup, + Err(e) => handle_accept_error(e).await, + } + } + } + + fn local_addr(&self) -> io::Result { + self._inner.local_addr() + } +} + #[cfg(test)] mod tests { use std::sync::atomic::{AtomicUsize, Ordering}; diff --git a/axum/src/serve/mod.rs b/axum/src/serve/mod.rs index 1f50c9ec83..33cca41b16 100644 --- a/axum/src/serve/mod.rs +++ b/axum/src/serve/mod.rs @@ -23,6 +23,8 @@ use tower_service::Service; mod listener; +#[cfg(feature = "tokio-rustls")] +pub use self::listener::TlsListener; pub use self::listener::{ConnLimiter, ConnLimiterIo, Listener, ListenerExt, TapIo}; /// Serve the service with the supplied listener. diff --git a/deny.toml b/deny.toml index 6bb50f12e6..87a486f358 100644 --- a/deny.toml +++ b/deny.toml @@ -12,6 +12,7 @@ allow = [ "BSD-3-Clause", "MIT", "Unicode-3.0", + "ISC" ] [bans] diff --git a/examples/tokio-rustls/Cargo.toml b/examples/tokio-rustls/Cargo.toml new file mode 100644 index 0000000000..f07b4741c0 --- /dev/null +++ b/examples/tokio-rustls/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tokio-rustls" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +axum = { path = "../../axum", features = ["tokio-rustls"] } +tokio = { version = "1", features = ["full"] } +tokio-rustls = "0.26" +tower-service = "0.3.2" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/examples/tokio-rustls/README.md b/examples/tokio-rustls/README.md new file mode 100644 index 0000000000..d895e1f9ad --- /dev/null +++ b/examples/tokio-rustls/README.md @@ -0,0 +1,10 @@ +# tokio-rustls + +This example demonstrates axum with `tokio-rustls` natively which can be enabled with `tokio-rustls` feature flag. + +```bash +cargo run -p tokio-rustls +``` + + +Browse to `https://localhost:8433/` or try with curl `curl --insecure https://localhost:8443/`. diff --git a/examples/tokio-rustls/self_signed_certs/cert.pem b/examples/tokio-rustls/self_signed_certs/cert.pem new file mode 100644 index 0000000000..656aa88055 --- /dev/null +++ b/examples/tokio-rustls/self_signed_certs/cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDkzCCAnugAwIBAgIUXVYkRCrM/ge03DVymDtXCuybp7gwDQYJKoZIhvcNAQEL +BQAwWTELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X +DTIxMDczMTE0MjIxMloXDTIyMDczMTE0MjIxMlowWTELMAkGA1UEBhMCVVMxEzAR +BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 +IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA02V5ZjmqLB/VQwTarrz/35qsa83L+DbAoa0001+jVmmC+G9Nufi0 +daroFWj/Uicv2fZWETU8JoZKUrX4BK9og5cg5rln/CtBRWCUYIwRgY9R/CdBGPn4 +kp+XkSJaCw74ZIyLy/Zfux6h8ES1m9YRnBza+s7U+ImRBRf4MRPtXQ3/mqJxAZYq +dOnKnvssRyD2qutgVTAxwMUvJWIivRhRYDj7WOpS4CEEeQxP1iH1/T5P7FdtTGdT +bVBABCA8JhL96uFGPpOYHcM/7R5EIA3yZ5FNg931QzoDITjtXGtQ6y9/l/IYkWm6 +J67RWcN0IoTsZhz0WNU4gAeslVtJLofn8QIDAQABo1MwUTAdBgNVHQ4EFgQUzFnK +NfS4LAYuKeWwHbzooER0yZ0wHwYDVR0jBBgwFoAUzFnKNfS4LAYuKeWwHbzooER0 +yZ0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAk4O+e9jia59W +ZwetN4GU7OWcYhmOgSizRSs6u7mTfp62LDMt96WKU3THksOnZ44HnqWQxsSfdFVU +XJD12tjvVU8Z4FWzQajcHeemUYiDze8EAh6TnxnUcOrU8IcwiKGxCWRY/908jnWg ++MMscfMCMYTRdeTPqD8fGzAlUCtmyzH6KLE3s4Oo/r5+NR+Uvrwpdvb7xe0MwwO9 +Q/zR4N8ep/HwHVEObcaBofE1ssZLksX7ZgCP9wMgXRWpNAtC5EWxMbxYjBfWFH24 +fDJlBMiGJWg8HHcxK7wQhFh+fuyNzE+xEWPsI9VL1zDftd9x8/QsOagyEOnY8Vxr +AopvZ09uEQ== +-----END CERTIFICATE----- diff --git a/examples/tokio-rustls/self_signed_certs/key.pem b/examples/tokio-rustls/self_signed_certs/key.pem new file mode 100644 index 0000000000..3de14eb32f --- /dev/null +++ b/examples/tokio-rustls/self_signed_certs/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDTZXlmOaosH9VD +BNquvP/fmqxrzcv4NsChrTTTX6NWaYL4b025+LR1qugVaP9SJy/Z9lYRNTwmhkpS +tfgEr2iDlyDmuWf8K0FFYJRgjBGBj1H8J0EY+fiSn5eRIloLDvhkjIvL9l+7HqHw +RLWb1hGcHNr6ztT4iZEFF/gxE+1dDf+aonEBlip06cqe+yxHIPaq62BVMDHAxS8l +YiK9GFFgOPtY6lLgIQR5DE/WIfX9Pk/sV21MZ1NtUEAEIDwmEv3q4UY+k5gdwz/t +HkQgDfJnkU2D3fVDOgMhOO1ca1DrL3+X8hiRabonrtFZw3QihOxmHPRY1TiAB6yV +W0kuh+fxAgMBAAECggEADltu8k1qTFLhJgsXWxTFAAe+PBgfCT2WuaRM2So+qqjB +12Of0MieYPt5hbK63HaC3nfHgqWt7yPhulpXfOH45C8IcgMXl93MMg0MJr58leMI ++2ojFrIrerHSFm5R1TxwDEwrVm/mMowzDWFtQCc6zPJ8wNn5RuP48HKfTZ3/2fjw +zEjSwPO2wFMfo1EJNTjlI303lFbdFBs67NaX6puh30M7Tn+gznHKyO5a7F57wkIt +fkgnEy/sgMedQlwX7bRpUoD6f0fZzV8Qz4cHFywtYErczZJh3VGitJoO/VCIDdty +RPXOAqVDd7EpP1UUehZlKVWZ0OZMEfRgKbRCel5abQKBgQDwgwrIQ5+BiZv6a0VT +ETeXB+hRbvBinRykNo/RvLc3j1enRh9/zO/ShadZIXgOAiM1Jnr5Gp8KkNGca6K1 +myhtad7xYPODYzNXXp6T1OPgZxHZLIYzVUj6ypXeV64Te5ZiDaJ1D49czsq+PqsQ +XRcgBJSNpFtDFiXWpjXWfx8PxwKBgQDhAnLY5Sl2eeQo+ud0MvjwftB/mN2qCzJY +5AlQpRI4ThWxJgGPuHTR29zVa5iWNYuA5LWrC1y/wx+t5HKUwq+5kxvs+npYpDJD +ZX/w0Glc6s0Jc/mFySkbw9B2LePedL7lRF5OiAyC6D106Sc9V2jlL4IflmOzt4CD +ZTNbLtC6hwKBgHfIzBXxl/9sCcMuqdg1Ovp9dbcZCaATn7ApfHd5BccmHQGyav27 +k7XF2xMJGEHhzqcqAxUNrSgV+E9vTBomrHvRvrd5Ec7eGTPqbBA0d0nMC5eeFTh7 +wV0miH20LX6Gjt9G6yJiHYSbeV5G1+vOcTYBEft5X/qJjU7aePXbWh0BAoGBAJlV +5tgCCuhvFloK6fHYzqZtdT6O+PfpW20SMXrgkvMF22h2YvgDFrDwqKRUB47NfHzg +3yBpxNH1ccA5/w97QO8w3gX3h6qicpJVOAPusu6cIBACFZfjRv1hyszOZwvw+Soa +Fj5kHkqTY1YpkREPYS9V2dIW1Wjic1SXgZDw7VM/AoGAP/cZ3ZHTSCDTFlItqy5C +rIy2AiY0WJsx+K0qcvtosPOOwtnGjWHb1gdaVdfX/IRkSsX4PAOdnsyidNC5/l/m +y8oa+5WEeGFclWFhr4dnTA766o8HrM2UjIgWWYBF2VKdptGnHxFeJWFUmeQC/xeW +w37pCS7ykL+7gp7V0WShYsw= +-----END PRIVATE KEY----- diff --git a/examples/tokio-rustls/src/main.rs b/examples/tokio-rustls/src/main.rs new file mode 100644 index 0000000000..d1ccfbe487 --- /dev/null +++ b/examples/tokio-rustls/src/main.rs @@ -0,0 +1,52 @@ +use axum::{routing::get, serve::TlsListener, Router}; +use std::path::{Path, PathBuf}; +use tokio::net::TcpListener; +use tokio_rustls::{ + rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer}, + rustls::ServerConfig, +}; +use tracing::info; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +#[tokio::main] +async fn main() -> Result<(), std::io::Error> { + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| format!("{}=trace", env!("CARGO_CRATE_NAME")).into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let rustls_config = rustls_server_config( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("self_signed_certs") + .join("key.pem"), + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("self_signed_certs") + .join("cert.pem"), + ); + + let bind = "127.0.0.1:8443"; + let tcp_listener = TcpListener::bind(bind).await.unwrap(); + info!("HTTPS server listening on {bind}. To contact curl --insecure https://localhost:8443"); + let app = Router::new().route("/", get(|| async { " Hello from HTTPS" })); + + let tls_listener = TlsListener::new(tcp_listener, rustls_config); + + axum::serve(tls_listener, app.into_make_service()).await +} + +fn rustls_server_config(key: impl AsRef, cert: impl AsRef) -> ServerConfig { + let key = PrivateKeyDer::from_pem_file(key).unwrap(); + + let certs = CertificateDer::pem_file_iter(cert) + .unwrap() + .map(|cert| cert.unwrap()) + .collect(); + + ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, key) + .expect("bad certificate/key") +}