From 1378229bb572dca94fe35a134dff78c382cb5326 Mon Sep 17 00:00:00 2001 From: Marcus Griep Date: Fri, 20 May 2022 18:18:30 -0400 Subject: [PATCH 01/19] feat: add layer that limits body size --- tower-http/Cargo.toml | 4 +- tower-http/src/builder.rs | 20 ++++ tower-http/src/lib.rs | 3 + tower-http/src/limit.rs | 246 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 tower-http/src/limit.rs diff --git a/tower-http/Cargo.toml b/tower-http/Cargo.toml index 93a126f7..3633e121 100644 --- a/tower-http/Cargo.toml +++ b/tower-http/Cargo.toml @@ -18,7 +18,7 @@ bytes = "1" futures-core = "0.3" futures-util = { version = "0.3.14", default_features = false, features = [] } http = "0.2.2" -http-body = "0.4.1" +http-body = "0.4.5" pin-project-lite = "0.2.7" tower-layer = "0.3" tower-service = "0.3" @@ -62,6 +62,7 @@ full = [ "decompression-full", "follow-redirect", "fs", + "limit", "map-request-body", "map-response-body", "metrics", @@ -82,6 +83,7 @@ catch-panic = ["tracing", "futures-util/std"] cors = [] follow-redirect = ["iri-string", "tower/util"] fs = ["tokio/fs", "tokio-util/io", "tokio/io-util", "mime_guess", "mime", "percent-encoding", "httpdate", "set-status", "futures-util/alloc"] +limit = [] map-request-body = [] map-response-body = [] metrics = ["tokio/time"] diff --git a/tower-http/src/builder.rs b/tower-http/src/builder.rs index 067d7c74..6a14feb9 100644 --- a/tower-http/src/builder.rs +++ b/tower-http/src/builder.rs @@ -343,6 +343,18 @@ pub trait ServiceBuilderExt: crate::sealed::Sealed + Sized { ) -> ServiceBuilder< Stack, L>, >; + + /// Intercept requests with over-sized payloads and convert them into + /// `413 Payload Too Large` responses. + /// + /// See [`tower_http::limit`] for more details. + /// + /// [`tower_http::limit`]: crate::limit + #[cfg(feature = "limit")] + fn length_limit( + self, + limit: usize, + ) -> ServiceBuilder, L>>; } impl crate::sealed::Sealed for ServiceBuilder {} @@ -558,4 +570,12 @@ impl ServiceBuilderExt for ServiceBuilder { > { self.layer(crate::catch_panic::CatchPanicLayer::new()) } + + #[cfg(feature = "limit")] + fn length_limit( + self, + limit: usize, + ) -> ServiceBuilder, L>> { + self.layer(crate::limit::LengthLimitedLayer::new(limit)) + } } diff --git a/tower-http/src/lib.rs b/tower-http/src/lib.rs index f0b52981..ee77f04b 100644 --- a/tower-http/src/lib.rs +++ b/tower-http/src/lib.rs @@ -285,6 +285,9 @@ pub mod trace; #[cfg(feature = "follow-redirect")] pub mod follow_redirect; +#[cfg(feature = "limit")] +pub mod limit; + #[cfg(feature = "metrics")] pub mod metrics; diff --git a/tower-http/src/limit.rs b/tower-http/src/limit.rs new file mode 100644 index 00000000..6895f456 --- /dev/null +++ b/tower-http/src/limit.rs @@ -0,0 +1,246 @@ +//! Imposes a length limit on request bodies. +//! +//! # Example +//! +//! ```rust +//! use bytes::Bytes; +//! use http::{Request, Response, StatusCode}; +//! use std::convert::Infallible; +//! use tower::{Service, ServiceExt, ServiceBuilder, service_fn}; +//! use tower_http::limit::LengthLimitedLayer; +//! use hyper::Body; +//! use http_body::Limited; +//! use tower_http::BoxError; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), BoxError> { +//! async fn handle(req: Request>) -> Result, BoxError> +//! { +//! hyper::body::to_bytes(req.into_body()).await?; +//! Ok(Response::new(Body::empty())) +//! } +//! +//! let mut svc = ServiceBuilder::new() +//! // Limit incoming requests to 4096 bytes. +//! .layer(LengthLimitedLayer::new(4096)) +//! .service_fn(handle); +//! +//! fn test_svc>>(s: &S) {} +//! test_svc(&svc); +//! +//! // Call the service. +//! let request = Request::new(Body::empty()); +//! +//! let response = svc.ready().await?.call(request).await?; +//! +//! assert_eq!(response.status(), 200); +//! +//! // Call the service with a body that is too large. +//! let request = Request::new(Body::from(Bytes::from(vec![0u8; 4097]))); +//! +//! let response = svc.ready().await?.call(request).await?; +//! +//! assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); +//! +//! # +//! # Ok(()) +//! # } +//! ``` + +use crate::BoxError; +use bytes::Bytes; +use http::{HeaderValue, Request, Response, StatusCode}; +use http_body::combinators::UnsyncBoxBody; +use http_body::{Body, Full, LengthLimitError, Limited}; +use pin_project_lite::pin_project; +use std::error::Error; +use std::future::Future; +use std::pin::Pin; +use std::{ + any, fmt, + marker::PhantomData, + task::{Context, Poll}, +}; +use tower_layer::Layer; +use tower_service::Service; + +/// Layer that applies the [`LengthLimit`] middleware that intercepts requests +/// with body lengths greater than the configured limit and converts them into +/// `413 Payload Too Large` responses. +/// +/// See the [module docs](self) for an example. +pub struct LengthLimitedLayer { + limit: usize, + _ty: PhantomData B>, +} + +impl LengthLimitedLayer { + /// Create a new `LengthLimitedLayer` with the given body length limit. + pub fn new(limit: usize) -> Self { + Self { + limit, + _ty: PhantomData, + } + } +} + +impl Clone for LengthLimitedLayer { + fn clone(&self) -> Self { + Self { + limit: self.limit, + _ty: PhantomData, + } + } +} + +impl Copy for LengthLimitedLayer {} + +impl fmt::Debug for LengthLimitedLayer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LengthLimitedLayer") + .field("body", &any::type_name::()) + .field("limit", &self.limit) + .finish() + } +} + +impl Layer for LengthLimitedLayer { + type Service = LengthLimited; + + fn layer(&self, inner: S) -> Self::Service { + LengthLimited { + inner, + limit: self.limit, + _ty: PhantomData, + } + } +} + +/// Middleware that intercepts requests with body lengths greater than the +/// configured limit and converts them into `413 Payload Too Large` responses. +/// +/// See the [module docs](self) for an example. +pub struct LengthLimited { + inner: S, + limit: usize, + _ty: PhantomData B>, +} + +impl LengthLimited { + define_inner_service_accessors!(); + + /// Create a new `LengthLimited` with the given body length limit. + pub fn new(inner: S, limit: usize) -> Self { + Self { + inner, + limit, + _ty: PhantomData, + } + } +} + +impl Clone for LengthLimited +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + limit: self.limit, + _ty: PhantomData, + } + } +} + +impl Copy for LengthLimited where S: Copy {} + +impl fmt::Debug for LengthLimited +where + S: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LengthLimited") + .field("inner", &self.inner) + .field("service", &format_args!("{}", any::type_name::())) + .finish() + } +} + +impl Service> for LengthLimited +where + S: Service>, Response = Response>, + S::Error: Into, + ResBody: Body + Send + 'static, + ResBody::Error: Into, +{ + type Response = Response>; + type Error = BoxError; + type Future = ResponseFuture; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(|e| e.into()) + } + + fn call(&mut self, req: Request) -> Self::Future { + let (parts, body) = req.into_parts(); + let body = Limited::new(body, self.limit); + let req = Request::from_parts(parts, body); + + ResponseFuture { + future: self.inner.call(req), + } + } +} + +pin_project! { + /// Response future for [`LengthLimit`]. + pub struct ResponseFuture { + #[pin] + future: F, + } +} + +impl Future for ResponseFuture +where + F: Future, E>>, + E: Into, + ResBody: Body + Send + 'static, + ResBody::Error: Into, +{ + type Output = Result>, BoxError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.project().future.poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(Ok(data)) => { + let (parts, body) = data.into_parts(); + let body = body.map_err(|err| err.into()).boxed_unsync(); + let resp = Response::from_parts(parts, body); + + Poll::Ready(Ok(resp)) + } + Poll::Ready(Err(err)) => { + let err = err.into(); + if let Some(_) = err.downcast_ref::() { + let mut res = Response::new( + Full::from("length limit exceeded") + .map_err(|err| err.into()) + .boxed_unsync(), + ); + *res.status_mut() = StatusCode::PAYLOAD_TOO_LARGE; + + #[allow(clippy::declare_interior_mutable_const)] + const TEXT_PLAIN: HeaderValue = + HeaderValue::from_static("text/plain; charset=utf-8"); + res.headers_mut() + .insert(http::header::CONTENT_TYPE, TEXT_PLAIN); + + Poll::Ready(Ok(res)) + } else { + Poll::Ready(Err(err)) + } + } + } + } +} From 73a2a598f17fe1face17756ac3bb7ae023fc47e6 Mon Sep 17 00:00:00 2001 From: Marcus Griep Date: Sat, 21 May 2022 11:39:14 -0400 Subject: [PATCH 02/19] chore: rename `LengthLimited` to `RequestBodyLimit` --- tower-http/src/builder.rs | 10 +++++----- tower-http/src/limit.rs | 34 +++++++++++++++++----------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tower-http/src/builder.rs b/tower-http/src/builder.rs index 6a14feb9..741a3328 100644 --- a/tower-http/src/builder.rs +++ b/tower-http/src/builder.rs @@ -351,10 +351,10 @@ pub trait ServiceBuilderExt: crate::sealed::Sealed + Sized { /// /// [`tower_http::limit`]: crate::limit #[cfg(feature = "limit")] - fn length_limit( + fn request_body_limit( self, limit: usize, - ) -> ServiceBuilder, L>>; + ) -> ServiceBuilder, L>>; } impl crate::sealed::Sealed for ServiceBuilder {} @@ -572,10 +572,10 @@ impl ServiceBuilderExt for ServiceBuilder { } #[cfg(feature = "limit")] - fn length_limit( + fn request_body_limit( self, limit: usize, - ) -> ServiceBuilder, L>> { - self.layer(crate::limit::LengthLimitedLayer::new(limit)) + ) -> ServiceBuilder, L>> { + self.layer(crate::limit::RequestBodyLimitLayer::new(limit)) } } diff --git a/tower-http/src/limit.rs b/tower-http/src/limit.rs index 6895f456..48b2890e 100644 --- a/tower-http/src/limit.rs +++ b/tower-http/src/limit.rs @@ -6,8 +6,8 @@ //! use bytes::Bytes; //! use http::{Request, Response, StatusCode}; //! use std::convert::Infallible; -//! use tower::{Service, ServiceExt, ServiceBuilder, service_fn}; -//! use tower_http::limit::LengthLimitedLayer; +//! use tower::{Service, ServiceExt, ServiceBuilder}; +//! use tower_http::limit::RequestBodyLimitLayer; //! use hyper::Body; //! use http_body::Limited; //! use tower_http::BoxError; @@ -22,7 +22,7 @@ //! //! let mut svc = ServiceBuilder::new() //! // Limit incoming requests to 4096 bytes. -//! .layer(LengthLimitedLayer::new(4096)) +//! .layer(RequestBodyLimitLayer::new(4096)) //! .service_fn(handle); //! //! fn test_svc>>(s: &S) {} @@ -69,13 +69,13 @@ use tower_service::Service; /// `413 Payload Too Large` responses. /// /// See the [module docs](self) for an example. -pub struct LengthLimitedLayer { +pub struct RequestBodyLimitLayer { limit: usize, _ty: PhantomData B>, } -impl LengthLimitedLayer { - /// Create a new `LengthLimitedLayer` with the given body length limit. +impl RequestBodyLimitLayer { + /// Create a new `RequestBodyLimitLayer` with the given body length limit. pub fn new(limit: usize) -> Self { Self { limit, @@ -84,7 +84,7 @@ impl LengthLimitedLayer { } } -impl Clone for LengthLimitedLayer { +impl Clone for RequestBodyLimitLayer { fn clone(&self) -> Self { Self { limit: self.limit, @@ -93,22 +93,22 @@ impl Clone for LengthLimitedLayer { } } -impl Copy for LengthLimitedLayer {} +impl Copy for RequestBodyLimitLayer {} -impl fmt::Debug for LengthLimitedLayer { +impl fmt::Debug for RequestBodyLimitLayer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("LengthLimitedLayer") + f.debug_struct("RequestBodyLimitLayer") .field("body", &any::type_name::()) .field("limit", &self.limit) .finish() } } -impl Layer for LengthLimitedLayer { - type Service = LengthLimited; +impl Layer for RequestBodyLimitLayer { + type Service = RequestBodyLimit; fn layer(&self, inner: S) -> Self::Service { - LengthLimited { + RequestBodyLimit { inner, limit: self.limit, _ty: PhantomData, @@ -120,16 +120,16 @@ impl Layer for LengthLimitedLayer { /// configured limit and converts them into `413 Payload Too Large` responses. /// /// See the [module docs](self) for an example. -pub struct LengthLimited { +pub struct RequestBodyLimit { inner: S, limit: usize, _ty: PhantomData B>, } -impl LengthLimited { +impl RequestBodyLimit { define_inner_service_accessors!(); - /// Create a new `LengthLimited` with the given body length limit. + /// Create a new `RequestBodyLimit` with the given body length limit. pub fn new(inner: S, limit: usize) -> Self { Self { inner, @@ -139,7 +139,7 @@ impl LengthLimited { } } -impl Clone for LengthLimited +impl Clone for RequestBodyLimit where S: Clone, { From ebee323941ab996092bfde413c27de7c8c37c9be Mon Sep 17 00:00:00 2001 From: Marcus Griep Date: Sat, 21 May 2022 12:01:23 -0400 Subject: [PATCH 03/19] refactor: remove request body witness from layer --- tower-http/src/builder.rs | 8 ++--- tower-http/src/limit.rs | 68 +++++++++++++-------------------------- 2 files changed, 27 insertions(+), 49 deletions(-) diff --git a/tower-http/src/builder.rs b/tower-http/src/builder.rs index 741a3328..ac93cc65 100644 --- a/tower-http/src/builder.rs +++ b/tower-http/src/builder.rs @@ -351,10 +351,10 @@ pub trait ServiceBuilderExt: crate::sealed::Sealed + Sized { /// /// [`tower_http::limit`]: crate::limit #[cfg(feature = "limit")] - fn request_body_limit( + fn request_body_limit( self, limit: usize, - ) -> ServiceBuilder, L>>; + ) -> ServiceBuilder>; } impl crate::sealed::Sealed for ServiceBuilder {} @@ -572,10 +572,10 @@ impl ServiceBuilderExt for ServiceBuilder { } #[cfg(feature = "limit")] - fn request_body_limit( + fn request_body_limit( self, limit: usize, - ) -> ServiceBuilder, L>> { + ) -> ServiceBuilder> { self.layer(crate::limit::RequestBodyLimitLayer::new(limit)) } } diff --git a/tower-http/src/limit.rs b/tower-http/src/limit.rs index 48b2890e..36823a36 100644 --- a/tower-http/src/limit.rs +++ b/tower-http/src/limit.rs @@ -14,8 +14,7 @@ //! //! # #[tokio::main] //! # async fn main() -> Result<(), BoxError> { -//! async fn handle(req: Request>) -> Result, BoxError> -//! { +//! async fn handle(req: Request>) -> Result, BoxError> { //! hyper::body::to_bytes(req.into_body()).await?; //! Ok(Response::new(Body::empty())) //! } @@ -50,15 +49,12 @@ use crate::BoxError; use bytes::Bytes; use http::{HeaderValue, Request, Response, StatusCode}; -use http_body::combinators::UnsyncBoxBody; -use http_body::{Body, Full, LengthLimitError, Limited}; +use http_body::{combinators::UnsyncBoxBody, Body, Full, LengthLimitError, Limited}; use pin_project_lite::pin_project; -use std::error::Error; -use std::future::Future; -use std::pin::Pin; use std::{ any, fmt, - marker::PhantomData, + future::Future, + pin::Pin, task::{Context, Poll}, }; use tower_layer::Layer; @@ -69,49 +65,40 @@ use tower_service::Service; /// `413 Payload Too Large` responses. /// /// See the [module docs](self) for an example. -pub struct RequestBodyLimitLayer { +pub struct RequestBodyLimitLayer { limit: usize, - _ty: PhantomData B>, } -impl RequestBodyLimitLayer { +impl RequestBodyLimitLayer { /// Create a new `RequestBodyLimitLayer` with the given body length limit. pub fn new(limit: usize) -> Self { - Self { - limit, - _ty: PhantomData, - } + Self { limit } } } -impl Clone for RequestBodyLimitLayer { +impl Clone for RequestBodyLimitLayer { fn clone(&self) -> Self { - Self { - limit: self.limit, - _ty: PhantomData, - } + Self { limit: self.limit } } } -impl Copy for RequestBodyLimitLayer {} +impl Copy for RequestBodyLimitLayer {} -impl fmt::Debug for RequestBodyLimitLayer { +impl fmt::Debug for RequestBodyLimitLayer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("RequestBodyLimitLayer") - .field("body", &any::type_name::()) .field("limit", &self.limit) .finish() } } -impl Layer for RequestBodyLimitLayer { - type Service = RequestBodyLimit; +impl Layer for RequestBodyLimitLayer { + type Service = RequestBodyLimit; fn layer(&self, inner: S) -> Self::Service { RequestBodyLimit { inner, limit: self.limit, - _ty: PhantomData, } } } @@ -120,26 +107,21 @@ impl Layer for RequestBodyLimitLayer { /// configured limit and converts them into `413 Payload Too Large` responses. /// /// See the [module docs](self) for an example. -pub struct RequestBodyLimit { +pub struct RequestBodyLimit { inner: S, limit: usize, - _ty: PhantomData B>, } -impl RequestBodyLimit { +impl RequestBodyLimit { define_inner_service_accessors!(); /// Create a new `RequestBodyLimit` with the given body length limit. pub fn new(inner: S, limit: usize) -> Self { - Self { - inner, - limit, - _ty: PhantomData, - } + Self { inner, limit } } } -impl Clone for RequestBodyLimit +impl Clone for RequestBodyLimit where S: Clone, { @@ -147,26 +129,22 @@ where Self { inner: self.inner.clone(), limit: self.limit, - _ty: PhantomData, } } } -impl Copy for LengthLimited where S: Copy {} +impl Copy for RequestBodyLimit where S: Copy {} -impl fmt::Debug for LengthLimited -where - S: fmt::Debug, -{ +impl fmt::Debug for RequestBodyLimit { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("LengthLimited") - .field("inner", &self.inner) - .field("service", &format_args!("{}", any::type_name::())) + f.debug_struct("RequestBodyLimit") + .field("service", &format_args!("{}", any::type_name::())) + .field("limit", &self.limit) .finish() } } -impl Service> for LengthLimited +impl Service> for RequestBodyLimit where S: Service>, Response = Response>, S::Error: Into, From f5cbfe4a8712910b5ae208329e1c1ca47859d1da Mon Sep 17 00:00:00 2001 From: Marcus Griep Date: Sat, 21 May 2022 12:19:55 -0400 Subject: [PATCH 04/19] refactor: remove need to box wrapped service error --- tower-http/src/limit.rs | 57 +++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/tower-http/src/limit.rs b/tower-http/src/limit.rs index 36823a36..d5388b93 100644 --- a/tower-http/src/limit.rs +++ b/tower-http/src/limit.rs @@ -46,13 +46,14 @@ //! # } //! ``` -use crate::BoxError; use bytes::Bytes; use http::{HeaderValue, Request, Response, StatusCode}; use http_body::{combinators::UnsyncBoxBody, Body, Full, LengthLimitError, Limited}; use pin_project_lite::pin_project; use std::{ - any, fmt, + any, + error::Error as StdError, + fmt, future::Future, pin::Pin, task::{Context, Poll}, @@ -147,12 +148,11 @@ impl fmt::Debug for RequestBodyLimit { impl Service> for RequestBodyLimit where S: Service>, Response = Response>, - S::Error: Into, + S::Error: StdError + 'static, ResBody: Body + Send + 'static, - ResBody::Error: Into, { - type Response = Response>; - type Error = BoxError; + type Response = Response>; + type Error = S::Error; type Future = ResponseFuture; #[inline] @@ -182,42 +182,43 @@ pin_project! { impl Future for ResponseFuture where F: Future, E>>, - E: Into, + E: StdError + 'static, ResBody: Body + Send + 'static, - ResBody::Error: Into, { - type Output = Result>, BoxError>; + type Output = Result>, E>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.project().future.poll(cx) { Poll::Pending => Poll::Pending, Poll::Ready(Ok(data)) => { let (parts, body) = data.into_parts(); - let body = body.map_err(|err| err.into()).boxed_unsync(); + let body = body.boxed_unsync(); let resp = Response::from_parts(parts, body); Poll::Ready(Ok(resp)) } Poll::Ready(Err(err)) => { - let err = err.into(); - if let Some(_) = err.downcast_ref::() { - let mut res = Response::new( - Full::from("length limit exceeded") - .map_err(|err| err.into()) - .boxed_unsync(), - ); - *res.status_mut() = StatusCode::PAYLOAD_TOO_LARGE; - - #[allow(clippy::declare_interior_mutable_const)] - const TEXT_PLAIN: HeaderValue = - HeaderValue::from_static("text/plain; charset=utf-8"); - res.headers_mut() - .insert(http::header::CONTENT_TYPE, TEXT_PLAIN); - - Poll::Ready(Ok(res)) - } else { - Poll::Ready(Err(err)) + let mut source = Some(&err as &(dyn StdError + 'static)); + while let Some(err) = source { + if let Some(_) = err.downcast_ref::() { + let mut res = Response::new( + Full::from("length limit exceeded") + .map_err(|err| match err {}) + .boxed_unsync(), + ); + *res.status_mut() = StatusCode::PAYLOAD_TOO_LARGE; + + #[allow(clippy::declare_interior_mutable_const)] + const TEXT_PLAIN: HeaderValue = + HeaderValue::from_static("text/plain; charset=utf-8"); + res.headers_mut() + .insert(http::header::CONTENT_TYPE, TEXT_PLAIN); + + return Poll::Ready(Ok(res)); + } + source = err.source(); } + Poll::Ready(Err(err)) } } } From bc723876f6bed7d31c8992cda35f69861fe012ed Mon Sep 17 00:00:00 2001 From: Marcus Griep Date: Sat, 21 May 2022 12:46:52 -0400 Subject: [PATCH 05/19] fix: impl bounds to work as a service --- tower-http/src/limit.rs | 88 +++++++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/tower-http/src/limit.rs b/tower-http/src/limit.rs index d5388b93..a32b310f 100644 --- a/tower-http/src/limit.rs +++ b/tower-http/src/limit.rs @@ -5,12 +5,10 @@ //! ```rust //! use bytes::Bytes; //! use http::{Request, Response, StatusCode}; -//! use std::convert::Infallible; -//! use tower::{Service, ServiceExt, ServiceBuilder}; +//! use tower::{Service, ServiceExt, ServiceBuilder, BoxError}; //! use tower_http::limit::RequestBodyLimitLayer; +//! use http_body::{Limited, LengthLimitError}; //! use hyper::Body; -//! use http_body::Limited; -//! use tower_http::BoxError; //! //! # #[tokio::main] //! # async fn main() -> Result<(), BoxError> { @@ -24,8 +22,74 @@ //! .layer(RequestBodyLimitLayer::new(4096)) //! .service_fn(handle); //! -//! fn test_svc>>(s: &S) {} -//! test_svc(&svc); +//! // Call the service. +//! let request = Request::new(Body::empty()); +//! +//! let response = svc.ready().await?.call(request).await?; +//! +//! assert_eq!(response.status(), 200); +//! +//! // Call the service with a body that is too large. +//! let request = Request::new(Body::from(Bytes::from(vec![0u8; 4097]))); +//! +//! let response = svc.ready().await?.call(request).await?; +//! +//! assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); +//! +//! # +//! # Ok(()) +//! # } +//! ``` +//! +//! Using a custom error type: +//! +//! ```rust +//! use bytes::Bytes; +//! use http::{Request, Response, StatusCode}; +//! use tower::{Service, ServiceExt, ServiceBuilder, BoxError}; +//! use tower_http::limit::RequestBodyLimitLayer; +//! use http_body::{Limited, LengthLimitError}; +//! use hyper::Body; +//! +//! #[derive(Debug)] +//! enum MyError { +//! MySpecificError, +//! Unknown(BoxError), +//! } +//! +//! impl std::fmt::Display for MyError { +//! // ... +//! # fn fmt(&self, _: &mut std::fmt::Formatter) -> std::fmt::Result { +//! # Ok(()) +//! # } +//! } +//! +//! impl std::error::Error for MyError { +//! fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { +//! match self { +//! Self::Unknown(err) => Some(&**err), +//! Self::MySpecificError => None, +//! } +//! } +//! } +//! +//! impl From for MyError { +//! fn from(err: BoxError) -> Self { +//! Self::Unknown(err) +//! } +//! } +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), BoxError> { +//! async fn handle(req: Request>) -> Result, MyError> { +//! hyper::body::to_bytes(req.into_body()).await?; +//! Ok(Response::new(Body::empty())) +//! } +//! +//! let mut svc = ServiceBuilder::new() +//! // Limit incoming requests to 4096 bytes. +//! .layer(RequestBodyLimitLayer::new(4096)) +//! .service_fn(handle); //! //! // Call the service. //! let request = Request::new(Body::empty()); @@ -46,6 +110,7 @@ //! # } //! ``` +use crate::BoxError; use bytes::Bytes; use http::{HeaderValue, Request, Response, StatusCode}; use http_body::{combinators::UnsyncBoxBody, Body, Full, LengthLimitError, Limited}; @@ -148,11 +213,11 @@ impl fmt::Debug for RequestBodyLimit { impl Service> for RequestBodyLimit where S: Service>, Response = Response>, - S::Error: StdError + 'static, + S::Error: Into, ResBody: Body + Send + 'static, { type Response = Response>; - type Error = S::Error; + type Error = BoxError; type Future = ResponseFuture; #[inline] @@ -182,10 +247,10 @@ pin_project! { impl Future for ResponseFuture where F: Future, E>>, - E: StdError + 'static, + E: Into, ResBody: Body + Send + 'static, { - type Output = Result>, E>; + type Output = Result>, BoxError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.project().future.poll(cx) { @@ -198,7 +263,8 @@ where Poll::Ready(Ok(resp)) } Poll::Ready(Err(err)) => { - let mut source = Some(&err as &(dyn StdError + 'static)); + let err = err.into(); + let mut source = Some(&*err as &(dyn StdError + 'static)); while let Some(err) = source { if let Some(_) = err.downcast_ref::() { let mut res = Response::new( From 0157f94c406057286135e4187a23ccd9c221e6e5 Mon Sep 17 00:00:00 2001 From: Marcus Griep Date: Sat, 21 May 2022 18:09:42 -0400 Subject: [PATCH 06/19] feat: handle `Content-Length` case, improve docs --- tower-http/src/limit.rs | 152 ++++++++++++++++++++++++++++++---------- 1 file changed, 116 insertions(+), 36 deletions(-) diff --git a/tower-http/src/limit.rs b/tower-http/src/limit.rs index a32b310f..2f2f7b9d 100644 --- a/tower-http/src/limit.rs +++ b/tower-http/src/limit.rs @@ -1,6 +1,17 @@ //! Imposes a length limit on request bodies. //! -//! # Example +//! This layer will also intercept requests with a `Content-Length` header +//! larger than the allowable limit and return an immediate error before +//! reading any of the body. +//! +//! Handling of any unread payload beyond the length limit depends on the +//! underlying server implementation. +//! +//! # Examples +//! +//! If the `Content-Length` header indicates a payload that is larger than +//! the acceptable limit, then the response will be rejected whether or not +//! the body is read. //! //! ```rust //! use bytes::Bytes; @@ -13,6 +24,49 @@ //! # #[tokio::main] //! # async fn main() -> Result<(), BoxError> { //! async fn handle(req: Request>) -> Result, BoxError> { +//! Ok(Response::new(Body::empty())) +//! } +//! +//! let mut svc = ServiceBuilder::new() +//! // Limit incoming requests to 4096 bytes. +//! .layer(RequestBodyLimitLayer::new(4096)) +//! .service_fn(handle); +//! +//! // Call the service with a header that indicates the body is too large. +//! let mut request = Request::new(Body::empty()); +//! request.headers_mut().insert( +//! http::header::CONTENT_LENGTH, +//! http::HeaderValue::from_static("5000"), +//! ); +//! +//! let response = svc.ready().await?.call(request).await?; +//! +//! assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); +//! +//! # +//! # Ok(()) +//! # } +//! ``` +//! +//! If no `Content-Length` header is present, then the body will be read +//! until the length limit has been reached. If it is reached, the body +//! will return an error. If this error is bubbled up, then this layer +//! will return an appropriate `413 Payload Too Large` response. +//! +//! Note that if the body is never read, or never attempts to consume the +//! body beyond the length limit, then no error will be generated. +//! +//! ```rust +//! # use bytes::Bytes; +//! # use http::{Request, Response, StatusCode}; +//! # use tower::{Service, ServiceExt, ServiceBuilder, BoxError}; +//! # use tower_http::limit::RequestBodyLimitLayer; +//! # use http_body::{Limited, LengthLimitError}; +//! # use hyper::Body; +//! # +//! # #[tokio::main] +//! # async fn main() -> Result<(), BoxError> { +//! async fn handle(req: Request>) -> Result, BoxError> { //! hyper::body::to_bytes(req.into_body()).await?; //! Ok(Response::new(Body::empty())) //! } @@ -41,16 +95,17 @@ //! # } //! ``` //! -//! Using a custom error type: +//! This automatic error response mechanism will also work if the error +//! returned by the body is available in the source chain. //! //! ```rust -//! use bytes::Bytes; -//! use http::{Request, Response, StatusCode}; -//! use tower::{Service, ServiceExt, ServiceBuilder, BoxError}; -//! use tower_http::limit::RequestBodyLimitLayer; -//! use http_body::{Limited, LengthLimitError}; -//! use hyper::Body; -//! +//! # use bytes::Bytes; +//! # use http::{Request, Response, StatusCode}; +//! # use tower::{Service, ServiceExt, ServiceBuilder, BoxError}; +//! # use tower_http::limit::RequestBodyLimitLayer; +//! # use http_body::{Limited, LengthLimitError}; +//! # use hyper::Body; +//! # //! #[derive(Debug)] //! enum MyError { //! MySpecificError, @@ -119,14 +174,14 @@ use std::{ any, error::Error as StdError, fmt, - future::Future, + future::{ready, Future}, pin::Pin, task::{Context, Poll}, }; use tower_layer::Layer; use tower_service::Service; -/// Layer that applies the [`LengthLimit`] middleware that intercepts requests +/// Layer that applies the [`RequestBodyLimit`] middleware that intercepts requests /// with body lengths greater than the configured limit and converts them into /// `413 Payload Too Large` responses. /// @@ -214,11 +269,12 @@ impl Service> for RequestBodyLimit where S: Service>, Response = Response>, S::Error: Into, + S::Future: 'static, ResBody: Body + Send + 'static, { type Response = Response>; type Error = BoxError; - type Future = ResponseFuture; + type Future = ResponseFuture; #[inline] fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { @@ -227,26 +283,47 @@ where fn call(&mut self, req: Request) -> Self::Future { let (parts, body) = req.into_parts(); - let body = Limited::new(body, self.limit); - let req = Request::from_parts(parts, body); + let content_length = parts + .headers + .get(http::header::CONTENT_LENGTH) + .and_then(|value| value.to_str().ok()?.parse::().ok()); - ResponseFuture { - future: self.inner.call(req), - } + let future: Pin>>> = + match content_length { + Some(len) if len > self.limit => Box::pin(ready(Ok(create_error_response()))), + _ => { + let body = Limited::new(body, self.limit); + let req = Request::from_parts(parts, body); + let fut = self.inner.call(req); + Box::pin(async move { + fut.await + .map(|res| { + let (parts, body) = res.into_parts(); + let body = body.boxed_unsync(); + Response::from_parts(parts, body) + }) + .map_err(|err| err.into()) + }) + } + }; + + ResponseFuture { future } } } pin_project! { - /// Response future for [`LengthLimit`]. - pub struct ResponseFuture { + /// Response future for [`RequestBodyLimit`]. + pub struct ResponseFuture + where + ResBody: Body + { #[pin] - future: F, + future: Pin>, E>>>>, } } -impl Future for ResponseFuture +impl Future for ResponseFuture where - F: Future, E>>, E: Into, ResBody: Body + Send + 'static, { @@ -267,20 +344,7 @@ where let mut source = Some(&*err as &(dyn StdError + 'static)); while let Some(err) = source { if let Some(_) = err.downcast_ref::() { - let mut res = Response::new( - Full::from("length limit exceeded") - .map_err(|err| match err {}) - .boxed_unsync(), - ); - *res.status_mut() = StatusCode::PAYLOAD_TOO_LARGE; - - #[allow(clippy::declare_interior_mutable_const)] - const TEXT_PLAIN: HeaderValue = - HeaderValue::from_static("text/plain; charset=utf-8"); - res.headers_mut() - .insert(http::header::CONTENT_TYPE, TEXT_PLAIN); - - return Poll::Ready(Ok(res)); + return Poll::Ready(Ok(create_error_response())); } source = err.source(); } @@ -289,3 +353,19 @@ where } } } + +fn create_error_response() -> Response> { + let mut res = Response::new( + Full::from("length limit exceeded") + .map_err(|err| match err {}) + .boxed_unsync(), + ); + *res.status_mut() = StatusCode::PAYLOAD_TOO_LARGE; + + #[allow(clippy::declare_interior_mutable_const)] + const TEXT_PLAIN: HeaderValue = HeaderValue::from_static("text/plain; charset=utf-8"); + res.headers_mut() + .insert(http::header::CONTENT_TYPE, TEXT_PLAIN); + + res +} From 17feaab9c2632c0c895feebf69c8c06dda71fcc4 Mon Sep 17 00:00:00 2001 From: Marcus Griep Date: Sat, 21 May 2022 18:36:34 -0400 Subject: [PATCH 07/19] docs: add example and recommendation for without 413 handling --- tower-http/src/limit.rs | 62 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tower-http/src/limit.rs b/tower-http/src/limit.rs index 2f2f7b9d..ee110003 100644 --- a/tower-http/src/limit.rs +++ b/tower-http/src/limit.rs @@ -164,6 +164,68 @@ //! # Ok(()) //! # } //! ``` +//! +//! If the automatic `413 Payload Too Large` response and handling +//! of `Content-Length` headers is not desired, consider directly using +//! [`MapRequestBody`] to wrap the request body with [`http_body::Limited`]. +//! +//! [`MapRequestBody`]: crate::map_request_body +//! +//! ```rust +//! # use bytes::Bytes; +//! # use http::{Request, Response, StatusCode}; +//! # use tower::{Service, ServiceExt, ServiceBuilder, BoxError}; +//! # use tower_http::limit::RequestBodyLimitLayer; +//! # use http_body::{Limited, LengthLimitError}; +//! # use hyper::Body; +//! # use std::convert::Infallible; +//! use tower_http::map_request_body::MapRequestBodyLayer; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), BoxError> { +//! async fn handle(req: Request>) -> Result, Infallible> { +//! let data = hyper::body::to_bytes(req.into_body()).await; +//! let resp = match data { +//! Ok(data) => Response::new(Body::from(data)), +//! Err(err) => { +//! if err.downcast_ref::().is_some() { +//! let body = Body::from("Whoa there! Too much data! Teapot mode!"); +//! let mut resp = Response::new(body); +//! *resp.status_mut() = StatusCode::IM_A_TEAPOT; +//! resp +//! } else { +//! let mut resp = Response::new(Body::from(err.to_string())); +//! *resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; +//! resp +//! } +//! } +//! }; +//! Ok(resp) +//! } +//! +//! let mut svc = ServiceBuilder::new() +//! // Limit incoming requests to 4096 bytes, but no automatic response. +//! .layer(MapRequestBodyLayer::new(|b| Limited::new(b, 4096))) +//! .service_fn(handle); +//! +//! // Call the service. +//! let request = Request::new(Body::empty()); +//! +//! let response = svc.ready().await?.call(request).await?; +//! +//! assert_eq!(response.status(), 200); +//! +//! // Call the service with a body that is too large. +//! let request = Request::new(Body::from(Bytes::from(vec![0u8; 4097]))); +//! +//! let response = svc.ready().await?.call(request).await?; +//! +//! assert_eq!(response.status(), StatusCode::IM_A_TEAPOT); +//! +//! # +//! # Ok(()) +//! # } +//! ``` use crate::BoxError; use bytes::Bytes; From 706029212e3579ef33a6142f888f407982013055 Mon Sep 17 00:00:00 2001 From: Marcus Griep Date: Mon, 23 May 2022 11:20:31 -0400 Subject: [PATCH 08/19] refactor: split out body and data --- tower-http/src/limit/body.rs | 188 ++++++++++++++++++ tower-http/src/limit/future.rs | 89 +++++++++ tower-http/src/limit/layer.rs | 32 +++ tower-http/src/{limit.rs => limit/mod.rs} | 225 ++-------------------- tower-http/src/limit/service.rs | 70 +++++++ 5 files changed, 397 insertions(+), 207 deletions(-) create mode 100644 tower-http/src/limit/body.rs create mode 100644 tower-http/src/limit/future.rs create mode 100644 tower-http/src/limit/layer.rs rename tower-http/src/{limit.rs => limit/mod.rs} (53%) create mode 100644 tower-http/src/limit/service.rs diff --git a/tower-http/src/limit/body.rs b/tower-http/src/limit/body.rs new file mode 100644 index 00000000..88c85d6e --- /dev/null +++ b/tower-http/src/limit/body.rs @@ -0,0 +1,188 @@ +use bytes::Buf; +use futures_core::ready; +use http::{HeaderMap, HeaderValue, Response, StatusCode}; +use http_body::{Body, SizeHint}; +use pin_project_lite::pin_project; +use std::convert::TryFrom; +use std::pin::Pin; +use std::task::{Context, Poll}; + +pin_project! { + /// Response body for [`RequestBodyLimit`]. + /// + /// [`RequestBodyLimit`]: super::RequestBodyLimit + pub struct ResponseBody + where + B: Body, + { + #[pin] + inner: ResponseBodyInner + } +} + +impl ResponseBody +where + B: Body, +{ + fn payload_too_large() -> Self { + Self { + inner: ResponseBodyInner::PayloadTooLarge { + data: Some(ResponseData::payload_too_large()), + }, + } + } + + pub(crate) fn new(body: B) -> Self { + Self { + inner: ResponseBodyInner::Body { body }, + } + } +} + +pin_project! { + #[project = BodyProj] + enum ResponseBodyInner + where + B: Body, + { + PayloadTooLarge { + data: Option>, + }, + Body { + #[pin] + body: B + } + } +} + +impl Body for ResponseBody +where + B: Body, +{ + type Data = ResponseData; + type Error = B::Error; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + match self.project().inner.project() { + BodyProj::PayloadTooLarge { data } => Poll::Ready(Ok(data.take()).transpose()), + BodyProj::Body { body } => { + let or_data = ready!(body.poll_data(cx)); + Poll::Ready(or_data.map(|r_data| r_data.map(|data| ResponseData::new(data)))) + } + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + match self.project().inner.project() { + BodyProj::PayloadTooLarge { .. } => Poll::Ready(Ok(None)), + BodyProj::Body { body } => body.poll_trailers(cx), + } + } + + fn is_end_stream(&self) -> bool { + match &self.inner { + ResponseBodyInner::PayloadTooLarge { data } => data.is_none(), + ResponseBodyInner::Body { body } => body.is_end_stream(), + } + } + + fn size_hint(&self) -> SizeHint { + match &self.inner { + ResponseBodyInner::PayloadTooLarge { data } => data + .as_ref() + .map(|data| { + // The static payload will always be small. + let rem = u64::try_from(data.remaining()).unwrap(); + SizeHint::with_exact(rem) + }) + .unwrap_or_else(|| SizeHint::with_exact(0)), + ResponseBodyInner::Body { body } => body.size_hint(), + } + } +} + +/// Response data for [`RequestBodyLimit`]. +/// +/// [`RequestBodyLimit`]: super::RequestBodyLimit +pub struct ResponseData +where + B: Body, +{ + inner: ResponseDataInner, +} + +enum ResponseDataInner +where + B: Body, +{ + PayloadTooLarge { sent: &'static [u8] }, + Data { data: B::Data }, +} + +impl ResponseData +where + B: Body, +{ + fn payload_too_large() -> Self { + Self { + inner: ResponseDataInner::PayloadTooLarge { sent: BODY }, + } + } + + fn new(data: B::Data) -> Self { + Self { + inner: ResponseDataInner::Data { data }, + } + } +} + +impl Buf for ResponseData +where + B: Body, +{ + fn remaining(&self) -> usize { + match &self.inner { + ResponseDataInner::PayloadTooLarge { sent } => sent.remaining(), + ResponseDataInner::Data { data } => data.remaining(), + } + } + + fn chunk(&self) -> &[u8] { + match &self.inner { + ResponseDataInner::PayloadTooLarge { sent } => sent.chunk(), + ResponseDataInner::Data { data } => data.chunk(), + } + } + + fn advance(&mut self, cnt: usize) { + match &mut self.inner { + ResponseDataInner::PayloadTooLarge { sent } => sent.advance(cnt), + ResponseDataInner::Data { data } => data.advance(cnt), + } + } +} + +const BODY: &[u8] = b"length limit exceeded"; + +pub(super) fn create_error_response() -> Response> +where + B: Body, +{ + let mut res = Response::new(ResponseBody::payload_too_large()); + *res.status_mut() = StatusCode::PAYLOAD_TOO_LARGE; + + #[allow(clippy::declare_interior_mutable_const)] + const TEXT_PLAIN: HeaderValue = HeaderValue::from_static("text/plain; charset=utf-8"); + const CLOSE: HeaderValue = HeaderValue::from_static("close"); + res.headers_mut() + .insert(http::header::CONTENT_TYPE, TEXT_PLAIN); + res.headers_mut().insert(http::header::CONNECTION, CLOSE); + + res +} diff --git a/tower-http/src/limit/future.rs b/tower-http/src/limit/future.rs new file mode 100644 index 00000000..9fa3067d --- /dev/null +++ b/tower-http/src/limit/future.rs @@ -0,0 +1,89 @@ +use super::body::create_error_response; +use super::ResponseBody; +use futures_core::ready; +use http::Response; +use http_body::{Body, LengthLimitError}; +use pin_project_lite::pin_project; +use std::error::Error as StdError; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +pin_project! { + /// Response future for [`RequestBodyLimit`]. + /// + /// [`RequestBodyLimit`]: super::RequestBodyLimit + pub struct ResponseFuture { + #[pin] + inner: ResponseFutureInner, + } +} + +impl ResponseFuture { + pub(crate) fn payload_too_large() -> Self { + Self { + inner: ResponseFutureInner::PayloadTooLarge, + } + } + + pub(crate) fn new(future: F) -> Self { + Self { + inner: ResponseFutureInner::Future { future }, + } + } +} + +pin_project! { + #[project = ResFutProj] + enum ResponseFutureInner { + PayloadTooLarge, + Future { + #[pin] + future: F, + } + } +} + +impl Future for ResponseFuture +where + ResBody: Body, + F: Future, E>>, + E: StdError + 'static, +{ + type Output = Result>, E>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let val = match self.project().inner.project() { + ResFutProj::PayloadTooLarge => Ok(create_error_response()), + ResFutProj::Future { future } => match ready!(future.poll(cx)) { + Ok(data) => { + let (parts, body) = data.into_parts(); + let body = ResponseBody::new(body); + let resp = Response::from_parts(parts, body); + + Ok(resp) + } + Err(err) => { + if is_length_limit_error(&err) { + Ok(create_error_response()) + } else { + Err(err) + } + } + }, + }; + + Poll::Ready(val) + } +} + +fn is_length_limit_error(err: &(dyn StdError + 'static)) -> bool { + let mut source = Some(err); + while let Some(err) = source { + if let Some(_) = err.downcast_ref::() { + return true; + } + source = err.source(); + } + false +} diff --git a/tower-http/src/limit/layer.rs b/tower-http/src/limit/layer.rs new file mode 100644 index 00000000..2dcff71a --- /dev/null +++ b/tower-http/src/limit/layer.rs @@ -0,0 +1,32 @@ +use super::RequestBodyLimit; +use tower_layer::Layer; + +/// Layer that applies the [`RequestBodyLimit`] middleware that intercepts requests +/// with body lengths greater than the configured limit and converts them into +/// `413 Payload Too Large` responses. +/// +/// See the [module docs](crate::limit) for an example. +/// +/// [`RequestBodyLimit`]: super::RequestBodyLimit +#[derive(Clone, Copy, Debug)] +pub struct RequestBodyLimitLayer { + limit: usize, +} + +impl RequestBodyLimitLayer { + /// Create a new `RequestBodyLimitLayer` with the given body length limit. + pub fn new(limit: usize) -> Self { + Self { limit } + } +} + +impl Layer for RequestBodyLimitLayer { + type Service = RequestBodyLimit; + + fn layer(&self, inner: S) -> Self::Service { + RequestBodyLimit { + inner, + limit: self.limit, + } + } +} diff --git a/tower-http/src/limit.rs b/tower-http/src/limit/mod.rs similarity index 53% rename from tower-http/src/limit.rs rename to tower-http/src/limit/mod.rs index ee110003..2ae516b4 100644 --- a/tower-http/src/limit.rs +++ b/tower-http/src/limit/mod.rs @@ -2,10 +2,8 @@ //! //! This layer will also intercept requests with a `Content-Length` header //! larger than the allowable limit and return an immediate error before -//! reading any of the body. -//! -//! Handling of any unread payload beyond the length limit depends on the -//! underlying server implementation. +//! reading any of the body. The response returned will request that the +//! connection be reset to prevent request smuggling attempts. //! //! # Examples //! @@ -169,6 +167,9 @@ //! of `Content-Length` headers is not desired, consider directly using //! [`MapRequestBody`] to wrap the request body with [`http_body::Limited`]. //! +//! Note: In order to prevent request smuggling, it is important to reset +//! the connection using a `Connection: close` header. +//! //! [`MapRequestBody`]: crate::map_request_body //! //! ```rust @@ -192,6 +193,10 @@ //! let body = Body::from("Whoa there! Too much data! Teapot mode!"); //! let mut resp = Response::new(body); //! *resp.status_mut() = StatusCode::IM_A_TEAPOT; +//! resp.headers_mut().insert( +//! http::header::CONNECTION, +//! http::HeaderValue::from_static("close"), +//! ); //! resp //! } else { //! let mut resp = Response::new(Body::from(err.to_string())); @@ -227,207 +232,13 @@ //! # } //! ``` -use crate::BoxError; -use bytes::Bytes; -use http::{HeaderValue, Request, Response, StatusCode}; -use http_body::{combinators::UnsyncBoxBody, Body, Full, LengthLimitError, Limited}; -use pin_project_lite::pin_project; -use std::{ - any, - error::Error as StdError, - fmt, - future::{ready, Future}, - pin::Pin, - task::{Context, Poll}, -}; -use tower_layer::Layer; -use tower_service::Service; - -/// Layer that applies the [`RequestBodyLimit`] middleware that intercepts requests -/// with body lengths greater than the configured limit and converts them into -/// `413 Payload Too Large` responses. -/// -/// See the [module docs](self) for an example. -pub struct RequestBodyLimitLayer { - limit: usize, -} - -impl RequestBodyLimitLayer { - /// Create a new `RequestBodyLimitLayer` with the given body length limit. - pub fn new(limit: usize) -> Self { - Self { limit } - } -} - -impl Clone for RequestBodyLimitLayer { - fn clone(&self) -> Self { - Self { limit: self.limit } - } -} - -impl Copy for RequestBodyLimitLayer {} - -impl fmt::Debug for RequestBodyLimitLayer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RequestBodyLimitLayer") - .field("limit", &self.limit) - .finish() - } -} - -impl Layer for RequestBodyLimitLayer { - type Service = RequestBodyLimit; - - fn layer(&self, inner: S) -> Self::Service { - RequestBodyLimit { - inner, - limit: self.limit, - } - } -} - -/// Middleware that intercepts requests with body lengths greater than the -/// configured limit and converts them into `413 Payload Too Large` responses. -/// -/// See the [module docs](self) for an example. -pub struct RequestBodyLimit { - inner: S, - limit: usize, -} - -impl RequestBodyLimit { - define_inner_service_accessors!(); - - /// Create a new `RequestBodyLimit` with the given body length limit. - pub fn new(inner: S, limit: usize) -> Self { - Self { inner, limit } - } -} - -impl Clone for RequestBodyLimit -where - S: Clone, -{ - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - limit: self.limit, - } - } -} - -impl Copy for RequestBodyLimit where S: Copy {} - -impl fmt::Debug for RequestBodyLimit { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RequestBodyLimit") - .field("service", &format_args!("{}", any::type_name::())) - .field("limit", &self.limit) - .finish() - } -} - -impl Service> for RequestBodyLimit -where - S: Service>, Response = Response>, - S::Error: Into, - S::Future: 'static, - ResBody: Body + Send + 'static, -{ - type Response = Response>; - type Error = BoxError; - type Future = ResponseFuture; - - #[inline] - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.inner.poll_ready(cx).map_err(|e| e.into()) - } - - fn call(&mut self, req: Request) -> Self::Future { - let (parts, body) = req.into_parts(); - let content_length = parts - .headers - .get(http::header::CONTENT_LENGTH) - .and_then(|value| value.to_str().ok()?.parse::().ok()); - - let future: Pin>>> = - match content_length { - Some(len) if len > self.limit => Box::pin(ready(Ok(create_error_response()))), - _ => { - let body = Limited::new(body, self.limit); - let req = Request::from_parts(parts, body); - let fut = self.inner.call(req); - Box::pin(async move { - fut.await - .map(|res| { - let (parts, body) = res.into_parts(); - let body = body.boxed_unsync(); - Response::from_parts(parts, body) - }) - .map_err(|err| err.into()) - }) - } - }; - - ResponseFuture { future } - } -} - -pin_project! { - /// Response future for [`RequestBodyLimit`]. - pub struct ResponseFuture - where - ResBody: Body - { - #[pin] - future: Pin>, E>>>>, - } -} - -impl Future for ResponseFuture -where - E: Into, - ResBody: Body + Send + 'static, -{ - type Output = Result>, BoxError>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - match self.project().future.poll(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Ok(data)) => { - let (parts, body) = data.into_parts(); - let body = body.boxed_unsync(); - let resp = Response::from_parts(parts, body); - - Poll::Ready(Ok(resp)) - } - Poll::Ready(Err(err)) => { - let err = err.into(); - let mut source = Some(&*err as &(dyn StdError + 'static)); - while let Some(err) = source { - if let Some(_) = err.downcast_ref::() { - return Poll::Ready(Ok(create_error_response())); - } - source = err.source(); - } - Poll::Ready(Err(err)) - } - } - } -} - -fn create_error_response() -> Response> { - let mut res = Response::new( - Full::from("length limit exceeded") - .map_err(|err| match err {}) - .boxed_unsync(), - ); - *res.status_mut() = StatusCode::PAYLOAD_TOO_LARGE; - - #[allow(clippy::declare_interior_mutable_const)] - const TEXT_PLAIN: HeaderValue = HeaderValue::from_static("text/plain; charset=utf-8"); - res.headers_mut() - .insert(http::header::CONTENT_TYPE, TEXT_PLAIN); +mod body; +mod future; +mod layer; +mod service; - res -} +pub use body::ResponseBody; +pub use body::ResponseData; +pub use future::ResponseFuture; +pub use layer::RequestBodyLimitLayer; +pub use service::RequestBodyLimit; diff --git a/tower-http/src/limit/service.rs b/tower-http/src/limit/service.rs new file mode 100644 index 00000000..aee27e21 --- /dev/null +++ b/tower-http/src/limit/service.rs @@ -0,0 +1,70 @@ +use super::{ResponseBody, ResponseFuture}; +use http::{Request, Response}; +use http_body::{Body, Limited}; +use std::error::Error as StdError; +use std::task::{Context, Poll}; +use std::{any, fmt}; +use tower_service::Service; + +/// Middleware that intercepts requests with body lengths greater than the +/// configured limit and converts them into `413 Payload Too Large` responses. +/// +/// See the [module docs](crate::limit) for an example. +#[derive(Clone, Copy)] +pub struct RequestBodyLimit { + pub(crate) inner: S, + pub(crate) limit: usize, +} + +impl RequestBodyLimit { + define_inner_service_accessors!(); + + /// Create a new `RequestBodyLimit` with the given body length limit. + pub fn new(inner: S, limit: usize) -> Self { + Self { inner, limit } + } +} + +impl fmt::Debug for RequestBodyLimit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RequestBodyLimit") + .field("service", &format_args!("{}", any::type_name::())) + .field("limit", &self.limit) + .finish() + } +} + +impl Service> for RequestBodyLimit +where + ResBody: Body, + S: Service>, Response = Response>, + S::Error: StdError + 'static, +{ + type Response = Response>; + type Error = S::Error; + type Future = ResponseFuture; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let (parts, body) = req.into_parts(); + let content_length = parts + .headers + .get(http::header::CONTENT_LENGTH) + .and_then(|value| value.to_str().ok()?.parse::().ok()); + + match content_length { + Some(len) if len > self.limit => ResponseFuture::payload_too_large(), + _ => { + let body = Limited::new(body, self.limit); + let req = Request::from_parts(parts, body); + let future = self.inner.call(req); + + ResponseFuture::new(future) + } + } + } +} From cace843c2ceb68d93739ea349c0f41e46f23ffc2 Mon Sep 17 00:00:00 2001 From: Marcus Griep Date: Mon, 23 May 2022 11:37:09 -0400 Subject: [PATCH 09/19] refactor: get rid of custom data type --- tower-http/src/limit/body.rs | 92 ++++-------------------------------- tower-http/src/limit/mod.rs | 1 - 2 files changed, 9 insertions(+), 84 deletions(-) diff --git a/tower-http/src/limit/body.rs b/tower-http/src/limit/body.rs index 88c85d6e..a7f5c38d 100644 --- a/tower-http/src/limit/body.rs +++ b/tower-http/src/limit/body.rs @@ -1,5 +1,4 @@ -use bytes::Buf; -use futures_core::ready; +use bytes::{Buf, Bytes}; use http::{HeaderMap, HeaderValue, Response, StatusCode}; use http_body::{Body, SizeHint}; use pin_project_lite::pin_project; @@ -11,23 +10,17 @@ pin_project! { /// Response body for [`RequestBodyLimit`]. /// /// [`RequestBodyLimit`]: super::RequestBodyLimit - pub struct ResponseBody - where - B: Body, - { + pub struct ResponseBody { #[pin] inner: ResponseBodyInner } } -impl ResponseBody -where - B: Body, -{ +impl ResponseBody { fn payload_too_large() -> Self { Self { inner: ResponseBodyInner::PayloadTooLarge { - data: Some(ResponseData::payload_too_large()), + data: Some(Bytes::from_static(BODY)), }, } } @@ -41,12 +34,9 @@ where pin_project! { #[project = BodyProj] - enum ResponseBodyInner - where - B: Body, - { + enum ResponseBodyInner { PayloadTooLarge { - data: Option>, + data: Option, }, Body { #[pin] @@ -57,9 +47,9 @@ pin_project! { impl Body for ResponseBody where - B: Body, + B: Body, { - type Data = ResponseData; + type Data = Bytes; type Error = B::Error; fn poll_data( @@ -68,10 +58,7 @@ where ) -> Poll>> { match self.project().inner.project() { BodyProj::PayloadTooLarge { data } => Poll::Ready(Ok(data.take()).transpose()), - BodyProj::Body { body } => { - let or_data = ready!(body.poll_data(cx)); - Poll::Ready(or_data.map(|r_data| r_data.map(|data| ResponseData::new(data)))) - } + BodyProj::Body { body } => body.poll_data(cx), } } @@ -107,67 +94,6 @@ where } } -/// Response data for [`RequestBodyLimit`]. -/// -/// [`RequestBodyLimit`]: super::RequestBodyLimit -pub struct ResponseData -where - B: Body, -{ - inner: ResponseDataInner, -} - -enum ResponseDataInner -where - B: Body, -{ - PayloadTooLarge { sent: &'static [u8] }, - Data { data: B::Data }, -} - -impl ResponseData -where - B: Body, -{ - fn payload_too_large() -> Self { - Self { - inner: ResponseDataInner::PayloadTooLarge { sent: BODY }, - } - } - - fn new(data: B::Data) -> Self { - Self { - inner: ResponseDataInner::Data { data }, - } - } -} - -impl Buf for ResponseData -where - B: Body, -{ - fn remaining(&self) -> usize { - match &self.inner { - ResponseDataInner::PayloadTooLarge { sent } => sent.remaining(), - ResponseDataInner::Data { data } => data.remaining(), - } - } - - fn chunk(&self) -> &[u8] { - match &self.inner { - ResponseDataInner::PayloadTooLarge { sent } => sent.chunk(), - ResponseDataInner::Data { data } => data.chunk(), - } - } - - fn advance(&mut self, cnt: usize) { - match &mut self.inner { - ResponseDataInner::PayloadTooLarge { sent } => sent.advance(cnt), - ResponseDataInner::Data { data } => data.advance(cnt), - } - } -} - const BODY: &[u8] = b"length limit exceeded"; pub(super) fn create_error_response() -> Response> diff --git a/tower-http/src/limit/mod.rs b/tower-http/src/limit/mod.rs index 2ae516b4..19ed17ed 100644 --- a/tower-http/src/limit/mod.rs +++ b/tower-http/src/limit/mod.rs @@ -238,7 +238,6 @@ mod layer; mod service; pub use body::ResponseBody; -pub use body::ResponseData; pub use future::ResponseFuture; pub use layer::RequestBodyLimitLayer; pub use service::RequestBodyLimit; From 79da01d8d59e4fd9c0650e85daeaa44ed83b16be Mon Sep 17 00:00:00 2001 From: Marcus Griep Date: Mon, 23 May 2022 14:01:54 -0400 Subject: [PATCH 10/19] refactor: stop interpreting service error This removes the `StdError + 'static` bound on the service error. Also sets the read limit for a given message to the lesser of `Content-Length` or the configured limit. --- tower-http/src/limit/body.rs | 18 ++--- tower-http/src/limit/future.rs | 23 +----- tower-http/src/limit/mod.rs | 136 +++++++++++--------------------- tower-http/src/limit/service.rs | 21 +++-- 4 files changed, 65 insertions(+), 133 deletions(-) diff --git a/tower-http/src/limit/body.rs b/tower-http/src/limit/body.rs index a7f5c38d..7dced085 100644 --- a/tower-http/src/limit/body.rs +++ b/tower-http/src/limit/body.rs @@ -1,4 +1,4 @@ -use bytes::{Buf, Bytes}; +use bytes::Bytes; use http::{HeaderMap, HeaderValue, Response, StatusCode}; use http_body::{Body, SizeHint}; use pin_project_lite::pin_project; @@ -81,14 +81,10 @@ where fn size_hint(&self) -> SizeHint { match &self.inner { - ResponseBodyInner::PayloadTooLarge { data } => data - .as_ref() - .map(|data| { - // The static payload will always be small. - let rem = u64::try_from(data.remaining()).unwrap(); - SizeHint::with_exact(rem) - }) - .unwrap_or_else(|| SizeHint::with_exact(0)), + ResponseBodyInner::PayloadTooLarge { data: None } => SizeHint::with_exact(0), + ResponseBodyInner::PayloadTooLarge { data: Some(_) } => { + SizeHint::with_exact(u64::try_from(BODY.len()).unwrap()) + } ResponseBodyInner::Body { body } => body.size_hint(), } } @@ -96,7 +92,7 @@ where const BODY: &[u8] = b"length limit exceeded"; -pub(super) fn create_error_response() -> Response> +pub(crate) fn create_error_response() -> Response> where B: Body, { @@ -105,10 +101,8 @@ where #[allow(clippy::declare_interior_mutable_const)] const TEXT_PLAIN: HeaderValue = HeaderValue::from_static("text/plain; charset=utf-8"); - const CLOSE: HeaderValue = HeaderValue::from_static("close"); res.headers_mut() .insert(http::header::CONTENT_TYPE, TEXT_PLAIN); - res.headers_mut().insert(http::header::CONNECTION, CLOSE); res } diff --git a/tower-http/src/limit/future.rs b/tower-http/src/limit/future.rs index 9fa3067d..82549350 100644 --- a/tower-http/src/limit/future.rs +++ b/tower-http/src/limit/future.rs @@ -2,9 +2,8 @@ use super::body::create_error_response; use super::ResponseBody; use futures_core::ready; use http::Response; -use http_body::{Body, LengthLimitError}; +use http_body::Body; use pin_project_lite::pin_project; -use std::error::Error as StdError; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; @@ -48,7 +47,6 @@ impl Future for ResponseFuture where ResBody: Body, F: Future, E>>, - E: StdError + 'static, { type Output = Result>, E>; @@ -63,27 +61,10 @@ where Ok(resp) } - Err(err) => { - if is_length_limit_error(&err) { - Ok(create_error_response()) - } else { - Err(err) - } - } + Err(err) => Err(err), }, }; Poll::Ready(val) } } - -fn is_length_limit_error(err: &(dyn StdError + 'static)) -> bool { - let mut source = Some(err); - while let Some(err) = source { - if let Some(_) = err.downcast_ref::() { - return true; - } - source = err.source(); - } - false -} diff --git a/tower-http/src/limit/mod.rs b/tower-http/src/limit/mod.rs index 19ed17ed..4a22b0fd 100644 --- a/tower-http/src/limit/mod.rs +++ b/tower-http/src/limit/mod.rs @@ -2,8 +2,14 @@ //! //! This layer will also intercept requests with a `Content-Length` header //! larger than the allowable limit and return an immediate error before -//! reading any of the body. The response returned will request that the -//! connection be reset to prevent request smuggling attempts. +//! reading any of the body. +//! +//! Note that payload length errors can be used by adversaries to attempt to +//! smuggle requests. When an incoming stream is dropped due to an over-sized +//! payload, servers should close the connection or resynchronize by +//! optimistically consuming some data in an attempt to reach the end of the +//! current HTTP frame. If the incoming stream cannot be resynchronized, +//! then the connection should be closed. //! //! # Examples //! @@ -13,15 +19,16 @@ //! //! ```rust //! use bytes::Bytes; +//! use std::convert::Infallible; //! use http::{Request, Response, StatusCode}; -//! use tower::{Service, ServiceExt, ServiceBuilder, BoxError}; -//! use tower_http::limit::RequestBodyLimitLayer; //! use http_body::{Limited, LengthLimitError}; +//! use tower::{Service, ServiceExt, ServiceBuilder}; +//! use tower_http::limit::RequestBodyLimitLayer; //! use hyper::Body; //! //! # #[tokio::main] -//! # async fn main() -> Result<(), BoxError> { -//! async fn handle(req: Request>) -> Result, BoxError> { +//! # async fn main() -> Result<(), Box> { +//! async fn handle(req: Request>) -> Result, Infallible> { //! Ok(Response::new(Body::empty())) //! } //! @@ -48,97 +55,38 @@ //! //! If no `Content-Length` header is present, then the body will be read //! until the length limit has been reached. If it is reached, the body -//! will return an error. If this error is bubbled up, then this layer -//! will return an appropriate `413 Payload Too Large` response. +//! will return an error. This error should be checked to determine if +//! it is a //! //! Note that if the body is never read, or never attempts to consume the //! body beyond the length limit, then no error will be generated. //! //! ```rust //! # use bytes::Bytes; +//! # use std::convert::Infallible; //! # use http::{Request, Response, StatusCode}; +//! # use http_body::{Limited, LengthLimitError}; //! # use tower::{Service, ServiceExt, ServiceBuilder, BoxError}; //! # use tower_http::limit::RequestBodyLimitLayer; -//! # use http_body::{Limited, LengthLimitError}; //! # use hyper::Body; //! # //! # #[tokio::main] //! # async fn main() -> Result<(), BoxError> { //! async fn handle(req: Request>) -> Result, BoxError> { -//! hyper::body::to_bytes(req.into_body()).await?; -//! Ok(Response::new(Body::empty())) -//! } -//! -//! let mut svc = ServiceBuilder::new() -//! // Limit incoming requests to 4096 bytes. -//! .layer(RequestBodyLimitLayer::new(4096)) -//! .service_fn(handle); -//! -//! // Call the service. -//! let request = Request::new(Body::empty()); -//! -//! let response = svc.ready().await?.call(request).await?; -//! -//! assert_eq!(response.status(), 200); -//! -//! // Call the service with a body that is too large. -//! let request = Request::new(Body::from(Bytes::from(vec![0u8; 4097]))); -//! -//! let response = svc.ready().await?.call(request).await?; -//! -//! assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); -//! -//! # -//! # Ok(()) -//! # } -//! ``` -//! -//! This automatic error response mechanism will also work if the error -//! returned by the body is available in the source chain. -//! -//! ```rust -//! # use bytes::Bytes; -//! # use http::{Request, Response, StatusCode}; -//! # use tower::{Service, ServiceExt, ServiceBuilder, BoxError}; -//! # use tower_http::limit::RequestBodyLimitLayer; -//! # use http_body::{Limited, LengthLimitError}; -//! # use hyper::Body; -//! # -//! #[derive(Debug)] -//! enum MyError { -//! MySpecificError, -//! Unknown(BoxError), -//! } -//! -//! impl std::fmt::Display for MyError { -//! // ... -//! # fn fmt(&self, _: &mut std::fmt::Formatter) -> std::fmt::Result { -//! # Ok(()) -//! # } -//! } -//! -//! impl std::error::Error for MyError { -//! fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { -//! match self { -//! Self::Unknown(err) => Some(&**err), -//! Self::MySpecificError => None, +//! match hyper::body::to_bytes(req.into_body()).await { +//! Ok(data) => Ok(Response::new(Body::empty())), +//! Err(err) => { +//! if let Some(_) = tower_http::limit::try_as_length_limit_error(&*err) { +//! let mut resp = Response::new(Body::empty()); +//! *resp.status_mut() = StatusCode::PAYLOAD_TOO_LARGE; +//! Ok(resp) +//! } else { +//! Err(err) +//! } //! } //! } //! } //! -//! impl From for MyError { -//! fn from(err: BoxError) -> Self { -//! Self::Unknown(err) -//! } -//! } -//! -//! # #[tokio::main] -//! # async fn main() -> Result<(), BoxError> { -//! async fn handle(req: Request>) -> Result, MyError> { -//! hyper::body::to_bytes(req.into_body()).await?; -//! Ok(Response::new(Body::empty())) -//! } -//! //! let mut svc = ServiceBuilder::new() //! // Limit incoming requests to 4096 bytes. //! .layer(RequestBodyLimitLayer::new(4096)) @@ -167,15 +115,12 @@ //! of `Content-Length` headers is not desired, consider directly using //! [`MapRequestBody`] to wrap the request body with [`http_body::Limited`]. //! -//! Note: In order to prevent request smuggling, it is important to reset -//! the connection using a `Connection: close` header. -//! //! [`MapRequestBody`]: crate::map_request_body //! //! ```rust //! # use bytes::Bytes; //! # use http::{Request, Response, StatusCode}; -//! # use tower::{Service, ServiceExt, ServiceBuilder, BoxError}; +//! # use tower::{Service, ServiceExt, ServiceBuilder}; //! # use tower_http::limit::RequestBodyLimitLayer; //! # use http_body::{Limited, LengthLimitError}; //! # use hyper::Body; @@ -183,20 +128,16 @@ //! use tower_http::map_request_body::MapRequestBodyLayer; //! //! # #[tokio::main] -//! # async fn main() -> Result<(), BoxError> { +//! # async fn main() -> Result<(), Box> { //! async fn handle(req: Request>) -> Result, Infallible> { //! let data = hyper::body::to_bytes(req.into_body()).await; //! let resp = match data { //! Ok(data) => Response::new(Body::from(data)), //! Err(err) => { -//! if err.downcast_ref::().is_some() { +//! if let Some(_) = tower_http::limit::try_as_length_limit_error(&*err) { //! let body = Body::from("Whoa there! Too much data! Teapot mode!"); //! let mut resp = Response::new(body); //! *resp.status_mut() = StatusCode::IM_A_TEAPOT; -//! resp.headers_mut().insert( -//! http::header::CONNECTION, -//! http::HeaderValue::from_static("close"), -//! ); //! resp //! } else { //! let mut resp = Response::new(Body::from(err.to_string())); @@ -241,3 +182,20 @@ pub use body::ResponseBody; pub use future::ResponseFuture; pub use layer::RequestBodyLimitLayer; pub use service::RequestBodyLimit; + +use http_body::LengthLimitError; +use std::error::Error as StdError; + +/// Identifies whether a given error is caused by a length limit error. +pub fn try_as_length_limit_error<'err>( + err: &'err (dyn StdError + 'static), +) -> Option<&'err LengthLimitError> { + let mut source = Some(err); + while let Some(err) = source { + if let Some(lle) = err.downcast_ref::() { + return Some(lle); + } + source = err.source(); + } + None +} diff --git a/tower-http/src/limit/service.rs b/tower-http/src/limit/service.rs index aee27e21..1740e5dc 100644 --- a/tower-http/src/limit/service.rs +++ b/tower-http/src/limit/service.rs @@ -1,7 +1,6 @@ use super::{ResponseBody, ResponseFuture}; use http::{Request, Response}; use http_body::{Body, Limited}; -use std::error::Error as StdError; use std::task::{Context, Poll}; use std::{any, fmt}; use tower_service::Service; @@ -38,7 +37,6 @@ impl Service> for RequestBodyLimit where ResBody: Body, S: Service>, Response = Response>, - S::Error: StdError + 'static, { type Response = Response>; type Error = S::Error; @@ -56,15 +54,16 @@ where .get(http::header::CONTENT_LENGTH) .and_then(|value| value.to_str().ok()?.parse::().ok()); - match content_length { - Some(len) if len > self.limit => ResponseFuture::payload_too_large(), - _ => { - let body = Limited::new(body, self.limit); - let req = Request::from_parts(parts, body); - let future = self.inner.call(req); + let body_limit = match content_length { + Some(len) if len > self.limit => return ResponseFuture::payload_too_large(), + Some(len) => self.limit.min(len), + None => self.limit, + }; - ResponseFuture::new(future) - } - } + let body = Limited::new(body, body_limit); + let req = Request::from_parts(parts, body); + let future = self.inner.call(req); + + ResponseFuture::new(future) } } From e17d3756fa6796ffa237ac74089ef853090f5c81 Mon Sep 17 00:00:00 2001 From: Marcus Griep Date: Mon, 23 May 2022 14:03:43 -0400 Subject: [PATCH 11/19] refactor: remove accessory function for finding nested source --- tower-http/src/limit/mod.rs | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/tower-http/src/limit/mod.rs b/tower-http/src/limit/mod.rs index 4a22b0fd..ffb249d5 100644 --- a/tower-http/src/limit/mod.rs +++ b/tower-http/src/limit/mod.rs @@ -76,7 +76,7 @@ //! match hyper::body::to_bytes(req.into_body()).await { //! Ok(data) => Ok(Response::new(Body::empty())), //! Err(err) => { -//! if let Some(_) = tower_http::limit::try_as_length_limit_error(&*err) { +//! if let Some(_) = err.downcast_ref::() { //! let mut resp = Response::new(Body::empty()); //! *resp.status_mut() = StatusCode::PAYLOAD_TOO_LARGE; //! Ok(resp) @@ -134,7 +134,7 @@ //! let resp = match data { //! Ok(data) => Response::new(Body::from(data)), //! Err(err) => { -//! if let Some(_) = tower_http::limit::try_as_length_limit_error(&*err) { +//! if let Some(_) = err.downcast_ref::() { //! let body = Body::from("Whoa there! Too much data! Teapot mode!"); //! let mut resp = Response::new(body); //! *resp.status_mut() = StatusCode::IM_A_TEAPOT; @@ -182,20 +182,3 @@ pub use body::ResponseBody; pub use future::ResponseFuture; pub use layer::RequestBodyLimitLayer; pub use service::RequestBodyLimit; - -use http_body::LengthLimitError; -use std::error::Error as StdError; - -/// Identifies whether a given error is caused by a length limit error. -pub fn try_as_length_limit_error<'err>( - err: &'err (dyn StdError + 'static), -) -> Option<&'err LengthLimitError> { - let mut source = Some(err); - while let Some(err) = source { - if let Some(lle) = err.downcast_ref::() { - return Some(lle); - } - source = err.source(); - } - None -} From 716848d5317a29e473bf9dfb4921d7239cb6d62e Mon Sep 17 00:00:00 2001 From: Marcus Griep Date: Mon, 23 May 2022 16:09:59 -0400 Subject: [PATCH 12/19] docs: improve limit module documentation --- tower-http/src/limit/mod.rs | 46 ++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/tower-http/src/limit/mod.rs b/tower-http/src/limit/mod.rs index ffb249d5..fbc40a25 100644 --- a/tower-http/src/limit/mod.rs +++ b/tower-http/src/limit/mod.rs @@ -1,21 +1,21 @@ //! Imposes a length limit on request bodies. //! //! This layer will also intercept requests with a `Content-Length` header -//! larger than the allowable limit and return an immediate error before -//! reading any of the body. -//! -//! Note that payload length errors can be used by adversaries to attempt to -//! smuggle requests. When an incoming stream is dropped due to an over-sized -//! payload, servers should close the connection or resynchronize by -//! optimistically consuming some data in an attempt to reach the end of the -//! current HTTP frame. If the incoming stream cannot be resynchronized, +//! larger than the allowable limit and return an immediate error response +//! before reading any of the body. +//! +//! Note that payload length errors can be used by adversaries in an attempt +//! to smuggle requests. When an incoming stream is dropped due to an +//! over-sized payload, servers should close the connection or resynchronize +//! by optimistically consuming some data in an attempt to reach the end of +//! the current HTTP frame. If the incoming stream cannot be resynchronized, //! then the connection should be closed. //! //! # Examples //! -//! If the `Content-Length` header indicates a payload that is larger than -//! the acceptable limit, then the response will be rejected whether or not -//! the body is read. +//! If a `Content-Length` header is present and indicates a payload that is +//! larger than the acceptable limit, then the underlying service will not +//! be called and a `413 Payload Too Large` response will be generated. //! //! ```rust //! use bytes::Bytes; @@ -29,7 +29,7 @@ //! # #[tokio::main] //! # async fn main() -> Result<(), Box> { //! async fn handle(req: Request>) -> Result, Infallible> { -//! Ok(Response::new(Body::empty())) +//! panic!("This will not be hit") //! } //! //! let mut svc = ServiceBuilder::new() @@ -53,13 +53,16 @@ //! # } //! ``` //! -//! If no `Content-Length` header is present, then the body will be read -//! until the length limit has been reached. If it is reached, the body -//! will return an error. This error should be checked to determine if -//! it is a +//! If a `Content-Length` header is not present, then the body will be read +//! until the configured limit has been reached. If the payload is larger than +//! the limit, the [`http_body::Limited`] body will return an error. This +//! error can be inspected to determine if is a [`http_body::LengthLimitError`] +//! and return an appropriate response in such case. //! -//! Note that if the body is never read, or never attempts to consume the -//! body beyond the length limit, then no error will be generated. +//! Note that no error will be generated if the body is never read. Similarly, +//! if the body _would be_ to large, but is never consumed beyond the length +//! limit, then no error is generated, and handling of the remaining incoming +//! data stream is left to the server implementation as described above. //! //! ```rust //! # use bytes::Bytes; @@ -111,9 +114,10 @@ //! # } //! ``` //! -//! If the automatic `413 Payload Too Large` response and handling -//! of `Content-Length` headers is not desired, consider directly using -//! [`MapRequestBody`] to wrap the request body with [`http_body::Limited`]. +//! If enforcement of body size limits is desired without preemptively +//! handling requests with a `Content-Length` header indicating an over-sized +//! request, consider using [`MapRequestBody`] to wrap the request body with +//! [`http_body::Limited`]. //! //! [`MapRequestBody`]: crate::map_request_body //! From 4af63890efcaa931a8bd045c134963c1823607ca Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Mon, 6 Jun 2022 14:11:43 +0200 Subject: [PATCH 13/19] misc docs changes --- tower-http/src/limit/mod.rs | 96 +++++++++---------------------------- 1 file changed, 23 insertions(+), 73 deletions(-) diff --git a/tower-http/src/limit/mod.rs b/tower-http/src/limit/mod.rs index fbc40a25..c9ccec1b 100644 --- a/tower-http/src/limit/mod.rs +++ b/tower-http/src/limit/mod.rs @@ -1,4 +1,4 @@ -//! Imposes a length limit on request bodies. +//! Middleware for limiting request bodies. //! //! This layer will also intercept requests with a `Content-Length` header //! larger than the allowable limit and return an immediate error response @@ -13,6 +13,8 @@ //! //! # Examples //! +//! ## Limiting based on `Content-Length` +//! //! If a `Content-Length` header is present and indicates a payload that is //! larger than the acceptable limit, then the underlying service will not //! be called and a `413 Payload Too Large` response will be generated. @@ -20,7 +22,7 @@ //! ```rust //! use bytes::Bytes; //! use std::convert::Infallible; -//! use http::{Request, Response, StatusCode}; +//! use http::{Request, Response, StatusCode, HeaderValue, header::CONTENT_LENGTH}; //! use http_body::{Limited, LengthLimitError}; //! use tower::{Service, ServiceExt, ServiceBuilder}; //! use tower_http::limit::RequestBodyLimitLayer; @@ -38,25 +40,25 @@ //! .service_fn(handle); //! //! // Call the service with a header that indicates the body is too large. -//! let mut request = Request::new(Body::empty()); -//! request.headers_mut().insert( -//! http::header::CONTENT_LENGTH, -//! http::HeaderValue::from_static("5000"), -//! ); +//! let mut request = Request::builder() +//! .header(CONTENT_LENGTH, HeaderValue::from_static("5000")) +//! .body(Body::empty()) +//! .unwrap(); //! //! let response = svc.ready().await?.call(request).await?; //! //! assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); -//! //! # //! # Ok(()) //! # } //! ``` //! +//! ## Limiting without known `Content-Length` +//! //! If a `Content-Length` header is not present, then the body will be read //! until the configured limit has been reached. If the payload is larger than //! the limit, the [`http_body::Limited`] body will return an error. This -//! error can be inspected to determine if is a [`http_body::LengthLimitError`] +//! error can be inspected to determine if it is a [`http_body::LengthLimitError`] //! and return an appropriate response in such case. //! //! Note that no error will be generated if the body is never read. Similarly, @@ -76,18 +78,20 @@ //! # #[tokio::main] //! # async fn main() -> Result<(), BoxError> { //! async fn handle(req: Request>) -> Result, BoxError> { -//! match hyper::body::to_bytes(req.into_body()).await { -//! Ok(data) => Ok(Response::new(Body::empty())), +//! let data = match hyper::body::to_bytes(req.into_body()).await { +//! Ok(data) => data, //! Err(err) => { //! if let Some(_) = err.downcast_ref::() { //! let mut resp = Response::new(Body::empty()); //! *resp.status_mut() = StatusCode::PAYLOAD_TOO_LARGE; -//! Ok(resp) +//! return Ok(resp); //! } else { -//! Err(err) +//! return Err(err); //! } //! } -//! } +//! }; +//! +//! Ok(Response::new(Body::empty())) //! } //! //! let mut svc = ServiceBuilder::new() @@ -100,7 +104,7 @@ //! //! let response = svc.ready().await?.call(request).await?; //! -//! assert_eq!(response.status(), 200); +//! assert_eq!(response.status(), StatusCode::OK); //! //! // Call the service with a body that is too large. //! let request = Request::new(Body::from(Bytes::from(vec![0u8; 4097]))); @@ -108,74 +112,20 @@ //! let response = svc.ready().await?.call(request).await?; //! //! assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); -//! //! # //! # Ok(()) //! # } //! ``` //! +//! ## Limiting without `Content-Length` +//! //! If enforcement of body size limits is desired without preemptively //! handling requests with a `Content-Length` header indicating an over-sized //! request, consider using [`MapRequestBody`] to wrap the request body with -//! [`http_body::Limited`]. +//! [`http_body::Limited`] and checking for [`http_body::LengthLimitError`] +//! like in the previous example. //! //! [`MapRequestBody`]: crate::map_request_body -//! -//! ```rust -//! # use bytes::Bytes; -//! # use http::{Request, Response, StatusCode}; -//! # use tower::{Service, ServiceExt, ServiceBuilder}; -//! # use tower_http::limit::RequestBodyLimitLayer; -//! # use http_body::{Limited, LengthLimitError}; -//! # use hyper::Body; -//! # use std::convert::Infallible; -//! use tower_http::map_request_body::MapRequestBodyLayer; -//! -//! # #[tokio::main] -//! # async fn main() -> Result<(), Box> { -//! async fn handle(req: Request>) -> Result, Infallible> { -//! let data = hyper::body::to_bytes(req.into_body()).await; -//! let resp = match data { -//! Ok(data) => Response::new(Body::from(data)), -//! Err(err) => { -//! if let Some(_) = err.downcast_ref::() { -//! let body = Body::from("Whoa there! Too much data! Teapot mode!"); -//! let mut resp = Response::new(body); -//! *resp.status_mut() = StatusCode::IM_A_TEAPOT; -//! resp -//! } else { -//! let mut resp = Response::new(Body::from(err.to_string())); -//! *resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; -//! resp -//! } -//! } -//! }; -//! Ok(resp) -//! } -//! -//! let mut svc = ServiceBuilder::new() -//! // Limit incoming requests to 4096 bytes, but no automatic response. -//! .layer(MapRequestBodyLayer::new(|b| Limited::new(b, 4096))) -//! .service_fn(handle); -//! -//! // Call the service. -//! let request = Request::new(Body::empty()); -//! -//! let response = svc.ready().await?.call(request).await?; -//! -//! assert_eq!(response.status(), 200); -//! -//! // Call the service with a body that is too large. -//! let request = Request::new(Body::from(Bytes::from(vec![0u8; 4097]))); -//! -//! let response = svc.ready().await?.call(request).await?; -//! -//! assert_eq!(response.status(), StatusCode::IM_A_TEAPOT); -//! -//! # -//! # Ok(()) -//! # } -//! ``` mod body; mod future; From fa3ea143a987606930e80c08505ada5b78efa010 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Mon, 6 Jun 2022 14:12:21 +0200 Subject: [PATCH 14/19] Derive `Debug` --- tower-http/src/limit/service.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tower-http/src/limit/service.rs b/tower-http/src/limit/service.rs index 1740e5dc..8aad6315 100644 --- a/tower-http/src/limit/service.rs +++ b/tower-http/src/limit/service.rs @@ -2,14 +2,13 @@ use super::{ResponseBody, ResponseFuture}; use http::{Request, Response}; use http_body::{Body, Limited}; use std::task::{Context, Poll}; -use std::{any, fmt}; use tower_service::Service; /// Middleware that intercepts requests with body lengths greater than the /// configured limit and converts them into `413 Payload Too Large` responses. /// /// See the [module docs](crate::limit) for an example. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub struct RequestBodyLimit { pub(crate) inner: S, pub(crate) limit: usize, @@ -24,15 +23,6 @@ impl RequestBodyLimit { } } -impl fmt::Debug for RequestBodyLimit { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RequestBodyLimit") - .field("service", &format_args!("{}", any::type_name::())) - .field("limit", &self.limit) - .finish() - } -} - impl Service> for RequestBodyLimit where ResBody: Body, From f1a090a1d965dd6c56ab02e30603c03cf2ae2bc8 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Mon, 6 Jun 2022 14:12:28 +0200 Subject: [PATCH 15/19] Order functions like we normally do --- tower-http/src/limit/service.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tower-http/src/limit/service.rs b/tower-http/src/limit/service.rs index 8aad6315..92c6a9b9 100644 --- a/tower-http/src/limit/service.rs +++ b/tower-http/src/limit/service.rs @@ -15,12 +15,12 @@ pub struct RequestBodyLimit { } impl RequestBodyLimit { - define_inner_service_accessors!(); - /// Create a new `RequestBodyLimit` with the given body length limit. pub fn new(inner: S, limit: usize) -> Self { Self { inner, limit } } + + define_inner_service_accessors!(); } impl Service> for RequestBodyLimit From fb3b95a55282aa3837d4615b1fce9b4e90e76dc1 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Mon, 6 Jun 2022 14:14:01 +0200 Subject: [PATCH 16/19] Add `RequestBodyLimit::layer` --- tower-http/src/limit/service.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tower-http/src/limit/service.rs b/tower-http/src/limit/service.rs index 92c6a9b9..6ff373f2 100644 --- a/tower-http/src/limit/service.rs +++ b/tower-http/src/limit/service.rs @@ -1,4 +1,4 @@ -use super::{ResponseBody, ResponseFuture}; +use super::{RequestBodyLimitLayer, ResponseBody, ResponseFuture}; use http::{Request, Response}; use http_body::{Body, Limited}; use std::task::{Context, Poll}; @@ -21,6 +21,13 @@ impl RequestBodyLimit { } define_inner_service_accessors!(); + + /// Returns a new [`Layer`] that wraps services with a `RequestBodyLimit` middleware. + /// + /// [`Layer`]: tower_layer::Layer + pub fn layer(limit: usize) -> RequestBodyLimitLayer { + RequestBodyLimitLayer::new(limit) + } } impl Service> for RequestBodyLimit From 1dc4f178a618dcfb4fe530c5785c6f1e259f2f7d Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Mon, 6 Jun 2022 14:25:03 +0200 Subject: [PATCH 17/19] fix nit picks --- tower-http/src/limit/body.rs | 21 ++++++++++----------- tower-http/src/limit/future.rs | 17 ++++------------- tower-http/src/limit/service.rs | 11 ++++------- 3 files changed, 18 insertions(+), 31 deletions(-) diff --git a/tower-http/src/limit/body.rs b/tower-http/src/limit/body.rs index 7dced085..4e746a5d 100644 --- a/tower-http/src/limit/body.rs +++ b/tower-http/src/limit/body.rs @@ -1,8 +1,7 @@ use bytes::Bytes; use http::{HeaderMap, HeaderValue, Response, StatusCode}; -use http_body::{Body, SizeHint}; +use http_body::{Body, Full, SizeHint}; use pin_project_lite::pin_project; -use std::convert::TryFrom; use std::pin::Pin; use std::task::{Context, Poll}; @@ -20,7 +19,7 @@ impl ResponseBody { fn payload_too_large() -> Self { Self { inner: ResponseBodyInner::PayloadTooLarge { - data: Some(Bytes::from_static(BODY)), + body: Full::from(BODY), }, } } @@ -36,7 +35,8 @@ pin_project! { #[project = BodyProj] enum ResponseBodyInner { PayloadTooLarge { - data: Option, + #[pin] + body: Full, }, Body { #[pin] @@ -57,7 +57,7 @@ where cx: &mut Context<'_>, ) -> Poll>> { match self.project().inner.project() { - BodyProj::PayloadTooLarge { data } => Poll::Ready(Ok(data.take()).transpose()), + BodyProj::PayloadTooLarge { body } => body.poll_data(cx).map_err(|err| match err {}), BodyProj::Body { body } => body.poll_data(cx), } } @@ -67,24 +67,23 @@ where cx: &mut Context<'_>, ) -> Poll, Self::Error>> { match self.project().inner.project() { - BodyProj::PayloadTooLarge { .. } => Poll::Ready(Ok(None)), + BodyProj::PayloadTooLarge { body } => { + body.poll_trailers(cx).map_err(|err| match err {}) + } BodyProj::Body { body } => body.poll_trailers(cx), } } fn is_end_stream(&self) -> bool { match &self.inner { - ResponseBodyInner::PayloadTooLarge { data } => data.is_none(), + ResponseBodyInner::PayloadTooLarge { body } => body.is_end_stream(), ResponseBodyInner::Body { body } => body.is_end_stream(), } } fn size_hint(&self) -> SizeHint { match &self.inner { - ResponseBodyInner::PayloadTooLarge { data: None } => SizeHint::with_exact(0), - ResponseBodyInner::PayloadTooLarge { data: Some(_) } => { - SizeHint::with_exact(u64::try_from(BODY.len()).unwrap()) - } + ResponseBodyInner::PayloadTooLarge { body } => body.size_hint(), ResponseBodyInner::Body { body } => body.size_hint(), } } diff --git a/tower-http/src/limit/future.rs b/tower-http/src/limit/future.rs index 82549350..4a9140eb 100644 --- a/tower-http/src/limit/future.rs +++ b/tower-http/src/limit/future.rs @@ -51,20 +51,11 @@ where type Output = Result>, E>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let val = match self.project().inner.project() { - ResFutProj::PayloadTooLarge => Ok(create_error_response()), - ResFutProj::Future { future } => match ready!(future.poll(cx)) { - Ok(data) => { - let (parts, body) = data.into_parts(); - let body = ResponseBody::new(body); - let resp = Response::from_parts(parts, body); - - Ok(resp) - } - Err(err) => Err(err), - }, + let res = match self.project().inner.project() { + ResFutProj::PayloadTooLarge => create_error_response(), + ResFutProj::Future { future } => ready!(future.poll(cx))?.map(ResponseBody::new), }; - Poll::Ready(val) + Poll::Ready(Ok(res)) } } diff --git a/tower-http/src/limit/service.rs b/tower-http/src/limit/service.rs index 6ff373f2..66ae41fe 100644 --- a/tower-http/src/limit/service.rs +++ b/tower-http/src/limit/service.rs @@ -45,9 +45,8 @@ where } fn call(&mut self, req: Request) -> Self::Future { - let (parts, body) = req.into_parts(); - let content_length = parts - .headers + let content_length = req + .headers() .get(http::header::CONTENT_LENGTH) .and_then(|value| value.to_str().ok()?.parse::().ok()); @@ -57,10 +56,8 @@ where None => self.limit, }; - let body = Limited::new(body, body_limit); - let req = Request::from_parts(parts, body); - let future = self.inner.call(req); + let req = req.map(|body| Limited::new(body, body_limit)); - ResponseFuture::new(future) + ResponseFuture::new(self.inner.call(req)) } } From 14a9476af4fb0ce4db1586e35f726aa7facc037e Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Mon, 6 Jun 2022 14:27:58 +0200 Subject: [PATCH 18/19] changelog --- tower-http/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tower-http/CHANGELOG.md b/tower-http/CHANGELOG.md index 898969c8..684b147e 100644 --- a/tower-http/CHANGELOG.md +++ b/tower-http/CHANGELOG.md @@ -10,8 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Added - Add `Timeout` middleware ([#270]) +- Add `RequestBodyLimit` middleware ([#271]) [#270]: https://github.com/tower-rs/tower-http/pull/270 +[#271]: https://github.com/tower-rs/tower-http/pull/271 ## Changed From d057c7256c39d8a56e763d84e5743a0fc075b30d Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Mon, 6 Jun 2022 17:50:56 +0200 Subject: [PATCH 19/19] add note about hyper --- tower-http/src/limit/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tower-http/src/limit/mod.rs b/tower-http/src/limit/mod.rs index c9ccec1b..a71ddbe7 100644 --- a/tower-http/src/limit/mod.rs +++ b/tower-http/src/limit/mod.rs @@ -9,7 +9,8 @@ //! over-sized payload, servers should close the connection or resynchronize //! by optimistically consuming some data in an attempt to reach the end of //! the current HTTP frame. If the incoming stream cannot be resynchronized, -//! then the connection should be closed. +//! then the connection should be closed. If you're using [hyper] this is +//! automatically handled for you. //! //! # Examples //! @@ -126,6 +127,7 @@ //! like in the previous example. //! //! [`MapRequestBody`]: crate::map_request_body +//! [hyper]: https://crates.io/crates/hyper mod body; mod future;