diff --git a/axum-extra/CHANGELOG.md b/axum-extra/CHANGELOG.md index f6cddd36..5e721961 100644 --- a/axum-extra/CHANGELOG.md +++ b/axum-extra/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog], and this project adheres to [Semantic Versioning]. +# Unreleased + +- **breaking:** Remove the deprecated `Host`, `Scheme` and `OptionalPath` + extractors ([#3599]) + +[#3599]: https://github.com/tokio-rs/axum/pull/3599 + # 0.12.5 - **fixed:** `JsonLines` now correctly respects the default body limit ([#3591]) diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index 1e0d31b1..bf565145 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -73,10 +73,8 @@ json-lines = [ ] middleware = ["dep:axum"] multipart = ["dep:multer", "dep:fastrand"] -optional-path = ["dep:axum", "dep:serde_core"] protobuf = ["dep:prost"] routing = ["axum/original-uri", "dep:rustversion"] -scheme = [] query = [ "dep:form_urlencoded", "dep:serde_core", diff --git a/axum-extra/src/extract/host.rs b/axum-extra/src/extract/host.rs deleted file mode 100644 index 104d4bb2..00000000 --- a/axum-extra/src/extract/host.rs +++ /dev/null @@ -1,249 +0,0 @@ -#![allow(deprecated)] - -use super::rejection::{FailedToResolveHost, HostRejection}; -use axum_core::{ - extract::{FromRequestParts, OptionalFromRequestParts}, - RequestPartsExt, -}; -use http::{ - header::{HeaderMap, FORWARDED}, - request::Parts, - uri::Authority, -}; -use std::convert::Infallible; - -const X_FORWARDED_HOST_HEADER_KEY: &str = "X-Forwarded-Host"; - -/// Extractor that resolves the host of the request. -/// -/// Host is resolved through the following, in order: -/// - `Forwarded` header -/// - `X-Forwarded-Host` header -/// - `Host` header -/// - Authority of the request URI -/// -/// See for the definition of -/// host. -/// -/// Note that user agents can set `X-Forwarded-Host` and `Host` headers to arbitrary values so make -/// sure to validate them to avoid security issues. -#[deprecated = "will be removed in the next version; see https://github.com/tokio-rs/axum/issues/3442"] -#[derive(Debug, Clone)] -pub struct Host(pub String); - -impl FromRequestParts for Host -where - S: Send + Sync, -{ - type Rejection = HostRejection; - - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - parts - .extract::>() - .await - .ok() - .flatten() - .ok_or(HostRejection::FailedToResolveHost(FailedToResolveHost)) - } -} - -impl OptionalFromRequestParts for Host -where - S: Send + Sync, -{ - type Rejection = Infallible; - - async fn from_request_parts( - parts: &mut Parts, - _state: &S, - ) -> Result, Self::Rejection> { - if let Some(host) = parse_forwarded(&parts.headers) { - return Ok(Some(Self(host.to_owned()))); - } - - if let Some(host) = parts - .headers - .get(X_FORWARDED_HOST_HEADER_KEY) - .and_then(|host| host.to_str().ok()) - { - return Ok(Some(Self(host.to_owned()))); - } - - if let Some(host) = parts - .headers - .get(http::header::HOST) - .and_then(|host| host.to_str().ok()) - { - return Ok(Some(Self(host.to_owned()))); - } - - if let Some(authority) = parts.uri.authority() { - return Ok(Some(Self(parse_authority(authority).to_owned()))); - } - - Ok(None) - } -} - -#[allow(warnings)] -fn parse_forwarded(headers: &HeaderMap) -> Option<&str> { - // if there are multiple `Forwarded` `HeaderMap::get` will return the first one - let forwarded_values = headers.get(FORWARDED)?.to_str().ok()?; - - // get the first set of values - let first_value = forwarded_values.split(',').nth(0)?; - - // find the value of the `host` field - first_value.split(';').find_map(|pair| { - let (key, value) = pair.split_once('=')?; - key.trim() - .eq_ignore_ascii_case("host") - .then(|| value.trim().trim_matches('"')) - }) -} - -fn parse_authority(auth: &Authority) -> &str { - auth.as_str() - .rsplit('@') - .next() - .expect("split always has at least 1 item") -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_helpers::TestClient; - use axum::{routing::get, Router}; - use http::{header::HeaderName, Request}; - - fn test_client() -> TestClient { - async fn host_as_body(Host(host): Host) -> String { - host - } - - TestClient::new(Router::new().route("/", get(host_as_body))) - } - - #[crate::test] - async fn host_header() { - let original_host = "some-domain:123"; - let host = test_client() - .get("/") - .header(http::header::HOST, original_host) - .await - .text() - .await; - assert_eq!(host, original_host); - } - - #[crate::test] - async fn x_forwarded_host_header() { - let original_host = "some-domain:456"; - let host = test_client() - .get("/") - .header(X_FORWARDED_HOST_HEADER_KEY, original_host) - .await - .text() - .await; - assert_eq!(host, original_host); - } - - #[crate::test] - async fn x_forwarded_host_precedence_over_host_header() { - let x_forwarded_host_header = "some-domain:456"; - let host_header = "some-domain:123"; - let host = test_client() - .get("/") - .header(X_FORWARDED_HOST_HEADER_KEY, x_forwarded_host_header) - .header(http::header::HOST, host_header) - .await - .text() - .await; - assert_eq!(host, x_forwarded_host_header); - } - - #[crate::test] - async fn uri_host() { - let client = test_client(); - let port = client.server_port(); - let host = client.get("/").await.text().await; - assert_eq!(host, format!("127.0.0.1:{port}")); - } - - #[crate::test] - async fn ip4_uri_host() { - let mut parts = Request::new(()).into_parts().0; - parts.uri = "https://127.0.0.1:1234/image.jpg".parse().unwrap(); - let host = parts.extract::().await.unwrap(); - assert_eq!(host.0, "127.0.0.1:1234"); - } - - #[crate::test] - async fn ip6_uri_host() { - let mut parts = Request::new(()).into_parts().0; - parts.uri = "http://cool:user@[::1]:456/file.txt".parse().unwrap(); - let host = parts.extract::().await.unwrap(); - assert_eq!(host.0, "[::1]:456"); - } - - #[crate::test] - async fn missing_host() { - let mut parts = Request::new(()).into_parts().0; - let host = parts.extract::().await.unwrap_err(); - assert!(matches!(host, HostRejection::FailedToResolveHost(_))); - } - - #[crate::test] - async fn optional_extractor() { - let mut parts = Request::new(()).into_parts().0; - parts.uri = "https://127.0.0.1:1234/image.jpg".parse().unwrap(); - let host = parts.extract::>().await.unwrap(); - assert!(host.is_some()); - } - - #[crate::test] - async fn optional_extractor_none() { - let mut parts = Request::new(()).into_parts().0; - let host = parts.extract::>().await.unwrap(); - assert!(host.is_none()); - } - - #[test] - fn forwarded_parsing() { - // the basic case - let headers = header_map(&[(FORWARDED, "host=192.0.2.60;proto=http;by=203.0.113.43")]); - let value = parse_forwarded(&headers).unwrap(); - assert_eq!(value, "192.0.2.60"); - - // is case insensitive - let headers = header_map(&[(FORWARDED, "host=192.0.2.60;proto=http;by=203.0.113.43")]); - let value = parse_forwarded(&headers).unwrap(); - assert_eq!(value, "192.0.2.60"); - - // ipv6 - let headers = header_map(&[(FORWARDED, "host=\"[2001:db8:cafe::17]:4711\"")]); - let value = parse_forwarded(&headers).unwrap(); - assert_eq!(value, "[2001:db8:cafe::17]:4711"); - - // multiple values in one header - let headers = header_map(&[(FORWARDED, "host=192.0.2.60, host=127.0.0.1")]); - let value = parse_forwarded(&headers).unwrap(); - assert_eq!(value, "192.0.2.60"); - - // multiple header values - let headers = header_map(&[ - (FORWARDED, "host=192.0.2.60"), - (FORWARDED, "host=127.0.0.1"), - ]); - let value = parse_forwarded(&headers).unwrap(); - assert_eq!(value, "192.0.2.60"); - } - - fn header_map(values: &[(HeaderName, &str)]) -> HeaderMap { - let mut headers = HeaderMap::new(); - for (key, value) in values { - headers.append(key, value.parse().unwrap()); - } - headers - } -} diff --git a/axum-extra/src/extract/mod.rs b/axum-extra/src/extract/mod.rs index 6ec4ca36..9ee90e46 100644 --- a/axum-extra/src/extract/mod.rs +++ b/axum-extra/src/extract/mod.rs @@ -1,11 +1,7 @@ //! Additional extractors. -mod host; pub mod rejection; -#[cfg(feature = "optional-path")] -mod optional_path; - #[cfg(feature = "cached")] mod cached; @@ -27,16 +23,6 @@ mod query; #[cfg(feature = "multipart")] pub mod multipart; -#[cfg(feature = "scheme")] -mod scheme; - -#[allow(deprecated)] -#[cfg(feature = "optional-path")] -pub use self::optional_path::OptionalPath; - -#[allow(deprecated)] -pub use self::host::Host; - #[cfg(feature = "cached")] pub use self::cached::Cached; @@ -65,11 +51,6 @@ pub use self::query::{OptionalQueryRejection, Query, QueryRejection}; #[cfg(feature = "multipart")] pub use self::multipart::Multipart; -#[allow(deprecated)] -#[cfg(feature = "scheme")] -#[doc(no_inline)] -pub use self::scheme::{Scheme, SchemeMissing}; - #[cfg(feature = "json-deserializer")] pub use self::json_deserializer::{ JsonDataError, JsonDeserializer, JsonDeserializerRejection, JsonSyntaxError, diff --git a/axum-extra/src/extract/optional_path.rs b/axum-extra/src/extract/optional_path.rs deleted file mode 100644 index 2b4cd6d5..00000000 --- a/axum-extra/src/extract/optional_path.rs +++ /dev/null @@ -1,98 +0,0 @@ -use axum::{ - extract::{rejection::PathRejection, FromRequestParts, Path}, - RequestPartsExt, -}; -use serde_core::de::DeserializeOwned; - -/// Extractor that extracts path arguments the same way as [`Path`], except if there aren't any. -/// -/// This extractor can be used in place of `Path` when you have two routes that you want to handle -/// in mostly the same way, where one has a path parameter and the other one doesn't. -/// -/// # Example -/// -/// ``` -/// use std::num::NonZeroU32; -/// use axum::{ -/// response::IntoResponse, -/// routing::get, -/// Router, -/// }; -/// use axum_extra::extract::OptionalPath; -/// -/// async fn render_blog(OptionalPath(page): OptionalPath) -> impl IntoResponse { -/// // Convert to u32, default to page 1 if not specified -/// let page = page.map_or(1, |param| param.get()); -/// // ... -/// } -/// -/// let app = Router::new() -/// .route("/blog", get(render_blog)) -/// .route("/blog/{page}", get(render_blog)); -/// # let app: Router = app; -/// ``` -#[deprecated = "Use Option> instead"] -#[derive(Debug)] -pub struct OptionalPath(pub Option); - -#[allow(deprecated)] -impl FromRequestParts for OptionalPath -where - T: DeserializeOwned + Send + 'static, - S: Send + Sync, -{ - type Rejection = PathRejection; - - async fn from_request_parts( - parts: &mut http::request::Parts, - _: &S, - ) -> Result { - parts - .extract::>>() - .await - .map(|opt| Self(opt.map(|Path(x)| x))) - } -} - -#[cfg(test)] -#[allow(deprecated)] -mod tests { - use std::num::NonZeroU32; - - use axum::{routing::get, Router}; - - use super::OptionalPath; - use crate::test_helpers::TestClient; - - #[crate::test] - async fn supports_128_bit_numbers() { - async fn handle(OptionalPath(param): OptionalPath) -> String { - let num = param.map_or(0, |p| p.get()); - format!("Success: {num}") - } - - let app = Router::new() - .route("/", get(handle)) - .route("/{num}", get(handle)); - - let client = TestClient::new(app); - - let res = client.get("/").await; - assert_eq!(res.text().await, "Success: 0"); - - let res = client.get("/1").await; - assert_eq!(res.text().await, "Success: 1"); - - let res = client.get("/0").await; - assert_eq!( - res.text().await, - "Invalid URL: invalid value: integer `0`, expected a nonzero u32" - ); - - let res = client.get("/NaN").await; - assert_eq!( - res.text().await, - "Invalid URL: Cannot parse `NaN` to a `u32`" - ); - } -} diff --git a/axum-extra/src/extract/scheme.rs b/axum-extra/src/extract/scheme.rs deleted file mode 100644 index 3fd230bc..00000000 --- a/axum-extra/src/extract/scheme.rs +++ /dev/null @@ -1,148 +0,0 @@ -//! Extractor that parses the scheme of a request. -//! See [`Scheme`] for more details. -#![allow(deprecated)] - -use axum_core::{__define_rejection as define_rejection, extract::FromRequestParts}; -use http::{ - header::{HeaderMap, FORWARDED}, - request::Parts, -}; -const X_FORWARDED_PROTO_HEADER_KEY: &str = "X-Forwarded-Proto"; - -/// Extractor that resolves the scheme / protocol of a request. -/// -/// The scheme is resolved through the following, in order: -/// - `Forwarded` header -/// - `X-Forwarded-Proto` header -/// - Request URI (If the request is an HTTP/2 request! e.g. use `--http2(-prior-knowledge)` with cURL) -/// -/// Note that user agents can set the `X-Forwarded-Proto` header to arbitrary values so make -/// sure to validate them to avoid security issues. -#[deprecated = "will be removed in the next version; see https://github.com/tokio-rs/axum/issues/3442"] -#[derive(Debug, Clone)] -pub struct Scheme(pub String); - -define_rejection! { - #[status = BAD_REQUEST] - #[body = "No scheme found in request"] - /// Rejection type used if the [`Scheme`] extractor is unable to - /// resolve a scheme. - pub struct SchemeMissing; -} - -impl FromRequestParts for Scheme -where - S: Send + Sync, -{ - type Rejection = SchemeMissing; - - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - // Within Forwarded header - if let Some(scheme) = parse_forwarded(&parts.headers) { - return Ok(Self(scheme.to_owned())); - } - - // X-Forwarded-Proto - if let Some(scheme) = parts - .headers - .get(X_FORWARDED_PROTO_HEADER_KEY) - .and_then(|scheme| scheme.to_str().ok()) - { - return Ok(Self(scheme.to_owned())); - } - - // From parts of an HTTP/2 request - if let Some(scheme) = parts.uri.scheme_str() { - return Ok(Self(scheme.to_owned())); - } - - Err(SchemeMissing) - } -} - -fn parse_forwarded(headers: &HeaderMap) -> Option<&str> { - // if there are multiple `Forwarded` `HeaderMap::get` will return the first one - let forwarded_values = headers.get(FORWARDED)?.to_str().ok()?; - - // get the first set of values - let first_value = forwarded_values.split(',').next()?; - - // find the value of the `proto` field - first_value.split(';').find_map(|pair| { - let (key, value) = pair.split_once('=')?; - key.trim() - .eq_ignore_ascii_case("proto") - .then(|| value.trim().trim_matches('"')) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_helpers::TestClient; - use axum::{routing::get, Router}; - use http::header::HeaderName; - - fn test_client() -> TestClient { - async fn scheme_as_body(Scheme(scheme): Scheme) -> String { - scheme - } - - TestClient::new(Router::new().route("/", get(scheme_as_body))) - } - - #[crate::test] - async fn forwarded_scheme_parsing() { - // the basic case - let headers = header_map(&[(FORWARDED, "host=192.0.2.60;proto=http;by=203.0.113.43")]); - let value = parse_forwarded(&headers).unwrap(); - assert_eq!(value, "http"); - - // is case insensitive - let headers = header_map(&[(FORWARDED, "host=192.0.2.60;PROTO=https;by=203.0.113.43")]); - let value = parse_forwarded(&headers).unwrap(); - assert_eq!(value, "https"); - - // multiple values in one header - let headers = header_map(&[(FORWARDED, "proto=ftp, proto=https")]); - let value = parse_forwarded(&headers).unwrap(); - assert_eq!(value, "ftp"); - - // multiple header values - let headers = header_map(&[(FORWARDED, "proto=ftp"), (FORWARDED, "proto=https")]); - let value = parse_forwarded(&headers).unwrap(); - assert_eq!(value, "ftp"); - } - - #[crate::test] - async fn x_forwarded_scheme_header() { - let original_scheme = "https"; - let scheme = test_client() - .get("/") - .header(X_FORWARDED_PROTO_HEADER_KEY, original_scheme) - .await - .text() - .await; - assert_eq!(scheme, original_scheme); - } - - #[crate::test] - async fn precedence_forwarded_over_x_forwarded() { - let scheme = test_client() - .get("/") - .header(X_FORWARDED_PROTO_HEADER_KEY, "https") - .header(FORWARDED, "proto=ftp") - .await - .text() - .await; - assert_eq!(scheme, "ftp"); - } - - fn header_map(values: &[(HeaderName, &str)]) -> HeaderMap { - let mut headers = HeaderMap::new(); - for (key, value) in values { - headers.append(key, value.parse().unwrap()); - } - headers - } -} diff --git a/axum-extra/src/lib.rs b/axum-extra/src/lib.rs index 14db98de..11676a65 100644 --- a/axum-extra/src/lib.rs +++ b/axum-extra/src/lib.rs @@ -24,7 +24,6 @@ //! `json-lines` | Enables the [`JsonLines`](crate::extract::JsonLines) extractor and response | //! `middleware` | Enables the [middleware] utilities | //! `multipart` | Enables the [`Multipart`](crate::extract::Multipart) extractor | -//! `optional-path` | Enables the [`OptionalPath`](crate::extract::OptionalPath) extractor | //! `protobuf` | Enables the [`Protobuf`](crate::protobuf::Protobuf) extractor and response | //! `query` (deprecated) | Enables the [`Query`](crate::extract::Query) extractor | //! `routing` | Enables the [routing] utilities | @@ -88,8 +87,5 @@ pub mod __private { pub const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/').add(b'%'); } -#[cfg(test)] -use axum_macros::__private_axum_test as test; - #[cfg(test)] pub(crate) use axum::test_helpers;