From ed2233e5b9e66c540145adc959b2525e4b03b96d Mon Sep 17 00:00:00 2001 From: Rousan Ali Date: Sat, 16 May 2020 22:19:29 +0530 Subject: [PATCH 1/4] Implement constraints and size limit --- README.md | 41 ++++++- examples/README.md | 4 +- examples/parse_async_read.rs | 2 +- examples/prevent_ddos_attack.rs | 57 +++++++++ examples/simple_example.rs | 2 +- examples/test.rs | 10 +- src/buffer.rs | 19 ++- src/constants.rs | 3 + src/constraints.rs | 92 +++++++++++++++ src/content_disposition.rs | 27 +++++ src/field.rs | 95 ++++++++------- src/helpers.rs | 9 +- src/lib.rs | 51 +++++++- src/multipart.rs | 126 ++++++++++++++++++-- src/size_limit.rs | 57 +++++++++ src/state.rs | 3 + tests/integration.rs | 200 +++++++++++++++++++++++++++++++- 17 files changed, 725 insertions(+), 73 deletions(-) create mode 100644 examples/prevent_ddos_attack.rs create mode 100644 src/constraints.rs create mode 100644 src/content_disposition.rs create mode 100644 src/size_limit.rs diff --git a/README.md b/README.md index b55063c..14c6041 100644 --- a/README.md +++ b/README.md @@ -60,13 +60,52 @@ async fn main() -> Result<(), Box> { // Generate a byte stream and the boundary from somewhere e.g. server request body. async fn get_byte_stream_from_somewhere() -> (impl Stream>, &'static str) { - let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; let stream = once(async move { Result::::Ok(Bytes::from(data)) }); (stream, "X-BOUNDARY") } ``` +## Prevent DDoS Attack + +This crate also provides some APIs to prevent potential `DDoS attack` with fine grained control. It's recommended to add some constraints +on field (specially text field) size to avoid potential `DDoS attack` from attackers running the server out of memory. + +An example: + +```rust +use multer::{Multipart, Constraints, SizeLimit}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create some constraints to be applied to the fields to prevent DDoS attack. + let constraints = Constraints::new() + // We only accept `my_text_field` and `my_file_field` fields, + // For any unknown field, we will throw an error. + .allowed_fields(vec!["my_text_field", "my_file_field"]) + .size_limit( + SizeLimit::new() + // Set 15mb as size limit for the whole stream body. + .whole_stream(15 * 1024 * 1024) + // Set 10mb as size limit for all fields. + .per_field(10 * 1024 * 1024) + // Set 30kb as size limit for our text field only. + .for_field("my_text_field", 30 * 1024), + ); + + // Create a `Multipart` instance from a stream and the constraints. + let mut multipart = Multipart::new_with_constraints(some_stream, "X-BOUNDARY", constraints); + + while let Some(field) = multipart.next_field().await.unwrap() { + let content = field.text().await.unwrap(); + assert_eq!(content, "abcd"); + } + + Ok(()) +} +``` + ## Usage with [hyper.rs](https://hyper.rs/) server An [example](https://github.com/rousan/multer-rs/blob/master/examples/hyper_server_example.rs) showing usage with [hyper.rs](https://hyper.rs/). diff --git a/examples/README.md b/examples/README.md index 08aeee3..806742d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,4 +16,6 @@ Run an example: * [`routerify_example`](routerify_example.rs) - Shows how to use this crate with [hyper](https://hyper.rs/) router implementation [Routerify](https://github.com/routerify/routerify). -* [`parse_async_read`](parse_async_read.rs) - Shows how to parse `multipart/form-data` from an [`AsyncRead`](https://docs.rs/tokio/0.2.20/tokio/io/trait.AsyncRead.html). \ No newline at end of file +* [`parse_async_read`](parse_async_read.rs) - Shows how to parse `multipart/form-data` from an [`AsyncRead`](https://docs.rs/tokio/0.2.20/tokio/io/trait.AsyncRead.html). + +* [`prevent_ddos_attack`](prevent_ddos_attack.rs) - Shows how to apply some rules to prevent potential DDoS attack while handling `multipart/form-data`. \ No newline at end of file diff --git a/examples/parse_async_read.rs b/examples/parse_async_read.rs index 111859a..8d42d72 100644 --- a/examples/parse_async_read.rs +++ b/examples/parse_async_read.rs @@ -34,7 +34,7 @@ async fn main() -> Result<(), Box> { // Generate an `AsyncRead` and the boundary from somewhere e.g. server request body. async fn get_async_reader_from_somewhere() -> (impl AsyncRead, &'static str) { - let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"File Field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; (data.as_bytes(), "X-BOUNDARY") } diff --git a/examples/prevent_ddos_attack.rs b/examples/prevent_ddos_attack.rs new file mode 100644 index 0000000..680a441 --- /dev/null +++ b/examples/prevent_ddos_attack.rs @@ -0,0 +1,57 @@ +use bytes::Bytes; +use futures::stream::Stream; +// Import multer types. +use multer::{Constraints, Multipart, SizeLimit}; +use std::convert::Infallible; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Generate a byte stream and the boundary from somewhere e.g. server request body. + let (stream, boundary) = get_byte_stream_from_somewhere().await; + + // Create some constraints to be applied to the fields to prevent DDoS attack. + let constraints = Constraints::new() + // We only accept `my_text_field` and `my_file_field` fields, + // For any unknown field, we will throw an error. + .allowed_fields(vec!["my_text_field", "my_file_field"]) + .size_limit( + SizeLimit::new() + // Set 15mb as size limit for the whole stream body. + .whole_stream(15 * 1024 * 1024) + // Set 10mb as size limit for all fields. + .per_field(10 * 1024 * 1024) + // Set 30kb as size limit for our text field only. + .for_field("my_text_field", 30 * 1024), + ); + + // Create a `Multipart` instance from that byte stream and the constraints. + let mut multipart = Multipart::new_with_constraints(stream, boundary, constraints); + + // Iterate over the fields, use `next_field()` to get the next field. + while let Some(field) = multipart.next_field().await? { + // Get field name. + let name = field.name(); + // Get the field's filename if provided in "Content-Disposition" header. + let file_name = field.file_name(); + + println!("Name: {:?}, File Name: {:?}", name, file_name); + + // Read field content as text. + let content = field.text().await?; + println!("Content: {:?}", content); + } + + Ok(()) +} + +// Generate a byte stream and the boundary from somewhere e.g. server request body. +async fn get_byte_stream_from_somewhere() -> (impl Stream>, &'static str) { + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; + let stream = futures::stream::iter( + data.chars() + .map(|ch| ch.to_string()) + .map(|part| Ok(Bytes::copy_from_slice(part.as_bytes()))), + ); + + (stream, "X-BOUNDARY") +} diff --git a/examples/simple_example.rs b/examples/simple_example.rs index a1b1965..63a5fd3 100644 --- a/examples/simple_example.rs +++ b/examples/simple_example.rs @@ -31,7 +31,7 @@ async fn main() -> Result<(), Box> { // Generate a byte stream and the boundary from somewhere e.g. server request body. async fn get_byte_stream_from_somewhere() -> (impl Stream>, &'static str) { - let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"File Field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = futures::stream::iter( data.chars() .map(|ch| ch.to_string()) diff --git a/examples/test.rs b/examples/test.rs index 8b3586e..75de05d 100644 --- a/examples/test.rs +++ b/examples/test.rs @@ -3,7 +3,7 @@ use futures::stream::{Stream, StreamExt}; use futures::TryStreamExt; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server}; -use multer::{Error, Field, Multipart}; +use multer::{Constraints, Error, Field, Multipart, SizeLimit}; use multer::{ErrorExt, ResultExt}; use std::{convert::Infallible, net::SocketAddr}; use tokio::fs::{File, OpenOptions}; @@ -26,10 +26,16 @@ async fn handle(req: Request) -> Result, Infallible> { // // let mut multipart = Multipart::with_reader(reader, "X-INSOMNIA-BOUNDARY"); - let mut multipart = Multipart::new(stream, "X-INSOMNIA-BOUNDARY"); + let multipart_constraints = Constraints::new() + .allowed_fields(vec!["a"]) + .size_limit(SizeLimit::new().per_field(30).for_field("a", 10)); + + let mut multipart = Multipart::new_with_constraints(stream, "X-INSOMNIA-BOUNDARY", multipart_constraints); while let Some(field) = multipart.next_field().await.unwrap() { println!("{:?}", field.name()); + let text = field.text().await.unwrap(); + println!("{}", text); // let text = field.text().await.unwrap(); // println!("{}", text); } diff --git a/src/buffer.rs b/src/buffer.rs index 3c07e0c..bafbae6 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -8,10 +8,12 @@ pub(crate) struct StreamBuffer { pub(crate) eof: bool, pub(crate) buf: BytesMut, pub(crate) stream: Pin> + Send>>, + pub(crate) whole_stream_size_limit: usize, + pub(crate) stream_size_counter: usize, } impl StreamBuffer { - pub fn new(stream: S) -> Self + pub fn new(stream: S, whole_stream_size_limit: usize) -> Self where S: Stream> + Send + 'static, { @@ -19,6 +21,8 @@ impl StreamBuffer { eof: false, buf: BytesMut::new(), stream: Box::pin(stream), + whole_stream_size_limit, + stream_size_counter: 0, } } @@ -29,7 +33,18 @@ impl StreamBuffer { loop { match self.stream.as_mut().poll_next(cx) { - Poll::Ready(Some(Ok(data))) => self.buf.extend_from_slice(&data), + Poll::Ready(Some(Ok(data))) => { + self.stream_size_counter += data.len(); + + if self.stream_size_counter > self.whole_stream_size_limit { + return Err(crate::Error::new(format!( + "Stream size exceeded the maximum limit: {} bytes", + self.whole_stream_size_limit + ))); + } + + self.buf.extend_from_slice(&data) + } Poll::Ready(Some(Err(err))) => return Err(err), Poll::Ready(None) => { self.eof = true; diff --git a/src/constants.rs b/src/constants.rs index 0084ed1..bc0923f 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,6 +1,9 @@ use lazy_static::lazy_static; use regex::Regex; +pub(crate) const DEFAULT_WHOLE_STREAM_SIZE_LIMIT: usize = usize::MAX; +pub(crate) const DEFAULT_PER_FIELD_SIZE_LIMIT: usize = usize::MAX; + pub(crate) const MAX_HEADERS: usize = 32; pub(crate) const BOUNDARY_EXT: &'static str = "--"; pub(crate) const CR: &'static str = "\r"; diff --git a/src/constraints.rs b/src/constraints.rs new file mode 100644 index 0000000..c6ef6c8 --- /dev/null +++ b/src/constraints.rs @@ -0,0 +1,92 @@ +use crate::size_limit::SizeLimit; + +/// Represents some rules to be applied on the stream and field's content size to prevent `DDoS attack`. +/// +/// It's recommended to add some rules on field (specially text field) size to avoid potential `DDoS attack` from attackers running the server out of memory. +/// This type provides some API to apply constraints on very granular level to make `multipart/form-data` safe. +/// By default, it does not apply any constraint. +/// +/// # Examples +/// +/// ``` +/// use multer::{Multipart, Constraints, SizeLimit}; +/// # use bytes::Bytes; +/// # use std::convert::Infallible; +/// # use futures::stream::once; +/// +/// # async fn run() { +/// # let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; +/// # let some_stream = once(async move { Result::::Ok(Bytes::from(data)) }); +/// // Create some constraints to be applied to the fields to prevent DDoS attack. +/// let constraints = Constraints::new() +/// // We only accept `my_text_field` and `my_file_field` fields, +/// // For any unknown field, we will throw an error. +/// .allowed_fields(vec!["my_text_field", "my_file_field"]) +/// .size_limit( +/// SizeLimit::new() +/// // Set 15mb as size limit for the whole stream body. +/// .whole_stream(15 * 1024 * 1024) +/// // Set 10mb as size limit for all fields. +/// .per_field(10 * 1024 * 1024) +/// // Set 30kb as size limit for our text field only. +/// .for_field("my_text_field", 30 * 1024), +/// ); +/// +/// // Create a `Multipart` instance from a stream and the constraints. +/// let mut multipart = Multipart::new_with_constraints(some_stream, "X-BOUNDARY", constraints); +/// +/// while let Some(field) = multipart.next_field().await.unwrap() { +/// let content = field.text().await.unwrap(); +/// assert_eq!(content, "abcd"); +/// } +/// # } +/// # tokio::runtime::Runtime::new().unwrap().block_on(run()); +/// ``` +pub struct Constraints { + pub(crate) size_limit: SizeLimit, + pub(crate) allowed_fields: Option>, +} + +impl Constraints { + /// Creates a set of rules with default behaviour. + pub fn new() -> Constraints { + Constraints::default() + } + + /// Applies rules on field's content length. + pub fn size_limit(self, size_limit: SizeLimit) -> Constraints { + Constraints { + size_limit, + allowed_fields: self.allowed_fields, + } + } + + /// Specify which fields should be allowed, for any unknown field, the [`next_field`](./struct.Multipart.html#method.next_field) will throw an error. + pub fn allowed_fields>(self, allowed_fields: Vec) -> Constraints { + let allowed_fields = allowed_fields.into_iter().map(|item| item.into()).collect(); + + Constraints { + size_limit: self.size_limit, + allowed_fields: Some(allowed_fields), + } + } + + pub(crate) fn is_it_allowed(&self, field: Option<&str>) -> bool { + if let Some(ref allowed_fields) = self.allowed_fields { + field + .map(|field| allowed_fields.iter().any(|item| item == field)) + .unwrap_or(false) + } else { + true + } + } +} + +impl Default for Constraints { + fn default() -> Self { + Constraints { + size_limit: SizeLimit::default(), + allowed_fields: None, + } + } +} diff --git a/src/content_disposition.rs b/src/content_disposition.rs new file mode 100644 index 0000000..66f0493 --- /dev/null +++ b/src/content_disposition.rs @@ -0,0 +1,27 @@ +use crate::constants; +use http::header::{self, HeaderMap}; + +pub(crate) struct ContentDisposition { + pub(crate) field_name: Option, + pub(crate) file_name: Option, +} + +impl ContentDisposition { + pub fn parse(headers: &HeaderMap) -> ContentDisposition { + let content_disposition = headers + .get(header::CONTENT_DISPOSITION) + .and_then(|val| val.to_str().ok()); + + let field_name = content_disposition + .and_then(|val| constants::CONTENT_DISPOSITION_FIELD_NAME_RE.captures(val)) + .and_then(|cap| cap.get(1)) + .map(|m| m.as_str().to_owned()); + + let file_name = content_disposition + .and_then(|val| constants::CONTENT_DISPOSITION_FILE_NAME_RE.captures(val)) + .and_then(|cap| cap.get(1)) + .map(|m| m.as_str().to_owned()); + + ContentDisposition { field_name, file_name } + } +} diff --git a/src/field.rs b/src/field.rs index bdd5671..50c464f 100644 --- a/src/field.rs +++ b/src/field.rs @@ -1,11 +1,13 @@ +use crate::content_disposition::ContentDisposition; +use crate::helpers; use crate::state::{MultipartState, StreamingStage}; +use crate::ErrorExt; #[cfg(feature = "json")] use crate::ResultExt; -use crate::{constants, ErrorExt}; use bytes::{Bytes, BytesMut}; use encoding_rs::{Encoding, UTF_8}; use futures::stream::{Stream, TryStreamExt}; -use http::header::{self, HeaderMap}; +use http::header::HeaderMap; #[cfg(feature = "json")] use serde::de::DeserializeOwned; #[cfg(feature = "json")] @@ -29,7 +31,7 @@ use std::task::{Context, Poll}; /// use futures::stream::once; /// /// # async fn run() { -/// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; +/// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// @@ -59,63 +61,48 @@ pub struct Field { } struct FieldMeta { - name: Option, - file_name: Option, + content_disposition: ContentDisposition, content_type: Option, idx: usize, } impl Field { - pub(crate) fn new(state: Arc>, headers: HeaderMap, idx: usize) -> Self { - let (name, file_name) = Self::parse_content_disposition(&headers); - let content_type = Self::parse_content_type(&headers); + pub(crate) fn new( + state: Arc>, + headers: HeaderMap, + idx: usize, + content_disposition: ContentDisposition, + ) -> Self { + let content_type = helpers::parse_content_type(&headers); Field { state, headers, done: false, meta: FieldMeta { - name, - file_name, + content_disposition, content_type, idx, }, } } - fn parse_content_disposition(headers: &HeaderMap) -> (Option, Option) { - let content_disposition = headers - .get(header::CONTENT_DISPOSITION) - .and_then(|val| val.to_str().ok()); - - let name = content_disposition - .and_then(|val| constants::CONTENT_DISPOSITION_FIELD_NAME_RE.captures(val)) - .and_then(|cap| cap.get(1)) - .map(|m| m.as_str().to_owned()); - - let file_name = content_disposition - .and_then(|val| constants::CONTENT_DISPOSITION_FILE_NAME_RE.captures(val)) - .and_then(|cap| cap.get(1)) - .map(|m| m.as_str().to_owned()); - - (name, file_name) - } - - fn parse_content_type(headers: &HeaderMap) -> Option { - headers - .get(header::CONTENT_TYPE) - .and_then(|val| val.to_str().ok()) - .and_then(|val| val.parse::().ok()) - } - /// The field name found in the [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header. pub fn name(&self) -> Option<&str> { - self.meta.name.as_ref().map(|name| name.as_str()) + self.meta + .content_disposition + .field_name + .as_ref() + .map(|name| name.as_str()) } /// The file name found in the [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header. pub fn file_name(&self) -> Option<&str> { - self.meta.file_name.as_ref().map(|file_name| file_name.as_str()) + self.meta + .content_disposition + .file_name + .as_ref() + .map(|file_name| file_name.as_str()) } /// Get the content type of the field. @@ -139,7 +126,7 @@ impl Field { /// use futures::stream::once; /// /// # async fn run() { - /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; + /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// @@ -174,7 +161,7 @@ impl Field { /// use futures::stream::once; /// /// # async fn run() { - /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; + /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// @@ -212,7 +199,7 @@ impl Field { /// } /// /// # async fn run() { - /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\n{ \"name\": \"Alice\" }\r\n--X-BOUNDARY--\r\n"; + /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\n{ \"name\": \"Alice\" }\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// @@ -251,7 +238,7 @@ impl Field { /// use futures::stream::once; /// /// # async fn run() { - /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; + /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// @@ -281,7 +268,7 @@ impl Field { /// use futures::stream::once; /// /// # async fn run() { - /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; + /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// @@ -322,7 +309,7 @@ impl Field { /// use futures::stream::once; /// /// # async fn run() { - /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; + /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// @@ -364,14 +351,26 @@ impl Stream for Field { } match stream_buffer.read_field_data(state.boundary.as_str()) { - Ok(Some((true, bytes))) => { - drop(mutex_guard); + Ok(Some((done, bytes))) => { + state.curr_field_size_counter += bytes.len(); - self.done = true; + if state.curr_field_size_counter > state.curr_field_size_limit { + return Poll::Ready(Some(Err(crate::Error::new(format!( + "Incoming Field size exceeded the maximum limit: {} bytes, field name: {}", + state.curr_field_size_limit, + state.curr_field_name.as_deref().unwrap_or("") + ))))); + } + + drop(mutex_guard); - Poll::Ready(Some(Ok(bytes))) + if done { + self.done = true; + Poll::Ready(Some(Ok(bytes))) + } else { + Poll::Ready(Some(Ok(bytes))) + } } - Ok(Some((false, bytes))) => Poll::Ready(Some(Ok(bytes))), Ok(None) => Poll::Pending, Err(err) => Poll::Ready(Some(Err(err))), } diff --git a/src/helpers.rs b/src/helpers.rs index 16dee39..85e11c8 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,5 +1,5 @@ use crate::error::ResultExt; -use http::header::{HeaderMap, HeaderName, HeaderValue}; +use http::header::{self, HeaderMap, HeaderName, HeaderValue}; use httparse::Header; use std::convert::TryFrom; @@ -18,3 +18,10 @@ pub(crate) fn convert_raw_headers_to_header_map(raw_headers: &[Header]) -> crate Ok(headers) } + +pub(crate) fn parse_content_type(headers: &HeaderMap) -> Option { + headers + .get(header::CONTENT_TYPE) + .and_then(|val| val.to_str().ok()) + .and_then(|val| val.parse::().ok()) +} diff --git a/src/lib.rs b/src/lib.rs index e26bc65..6333a55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,31 +42,80 @@ //! //! // Generate a byte stream and the boundary from somewhere e.g. server request body. //! async fn get_byte_stream_from_somewhere() -> (impl Stream>, &'static str) { -//! let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; +//! let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; //! let stream = once(async move { Result::::Ok(Bytes::from(data)) }); //! //! (stream, "X-BOUNDARY") //! } //! ``` //! +//! ## Prevent DDoS Attack +//! +//! This crate also provides some APIs to prevent potential `DDoS attack` with fine grained control. It's recommended to add some constraints +//! on field (specially text field) size to avoid potential `DDoS attack` from attackers running the server out of memory. +//! +//! An example: +//! +//! ``` +//! use multer::{Multipart, Constraints, SizeLimit}; +//! # use bytes::Bytes; +//! # use std::convert::Infallible; +//! # use futures::stream::once; +//! +//! # async fn run() { +//! # let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; +//! # let some_stream = once(async move { Result::::Ok(Bytes::from(data)) }); +//! // Create some constraints to be applied to the fields to prevent DDoS attack. +//! let constraints = Constraints::new() +//! // We only accept `my_text_field` and `my_file_field` fields, +//! // For any unknown field, we will throw an error. +//! .allowed_fields(vec!["my_text_field", "my_file_field"]) +//! .size_limit( +//! SizeLimit::new() +//! // Set 15mb as size limit for the whole stream body. +//! .whole_stream(15 * 1024 * 1024) +//! // Set 10mb as size limit for all fields. +//! .per_field(10 * 1024 * 1024) +//! // Set 30kb as size limit for our text field only. +//! .for_field("my_text_field", 30 * 1024), +//! ); +//! +//! // Create a `Multipart` instance from a stream and the constraints. +//! let mut multipart = Multipart::new_with_constraints(some_stream, "X-BOUNDARY", constraints); +//! +//! while let Some(field) = multipart.next_field().await.unwrap() { +//! let content = field.text().await.unwrap(); +//! assert_eq!(content, "abcd"); +//! } +//! # } +//! # to +//! ``` +//! +//! Please refer [`Constraints`](./struct.Constraints.html) for more info. +//! //! ## Usage with [hyper.rs](https://hyper.rs/) server //! //! An [example](https://github.com/rousan/multer-rs/blob/master/examples/hyper_server_example.rs) showing usage with [hyper.rs](https://hyper.rs/). //! //! For more examples, please visit [examples](https://github.com/rousan/multer-rs/tree/master/examples). +pub use constraints::Constraints; pub use error::Error; #[doc(hidden)] pub use error::{ErrorExt, ResultExt}; pub use field::Field; pub use multipart::Multipart; +pub use size_limit::SizeLimit; mod buffer; mod constants; +mod constraints; +mod content_disposition; mod error; mod field; mod helpers; mod multipart; +mod size_limit; mod state; /// A Result type often returned from methods that can have `multer` errors. diff --git a/src/multipart.rs b/src/multipart.rs index 6742918..9645d05 100644 --- a/src/multipart.rs +++ b/src/multipart.rs @@ -1,5 +1,7 @@ use crate::buffer::StreamBuffer; use crate::constants; +use crate::constraints::Constraints; +use crate::content_disposition::ContentDisposition; use crate::helpers; use crate::state::{MultipartState, StreamingStage}; use crate::{ErrorExt, Field}; @@ -34,7 +36,7 @@ use tokio_util::codec::{BytesCodec, FramedRead}; /// use futures::stream::once; /// /// # async fn run() { -/// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; +/// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let stream = once(async move { Result::::Ok(Bytes::from(data)) }); /// let mut multipart = Multipart::new(stream, "X-BOUNDARY"); /// @@ -46,11 +48,44 @@ use tokio_util::codec::{BytesCodec, FramedRead}; /// ``` pub struct Multipart { state: Arc>, + constraints: Constraints, } impl Multipart { /// Construct a new `Multipart` instance with the given [`Bytes`](https://docs.rs/bytes/0.5.4/bytes/struct.Bytes.html) stream and the boundary. pub fn new(stream: S, boundary: B) -> Multipart + where + S: Stream> + Send + 'static, + O: Into + 'static, + E: Into> + 'static, + B: Into, + { + let constraints = Constraints::default(); + + let stream = stream + .map_ok(|b| b.into()) + .map_err(|err| crate::Error::new(err.into().to_string())); + + let state = MultipartState { + buffer: StreamBuffer::new(stream, constraints.size_limit.whole_stream), + boundary: boundary.into(), + stage: StreamingStage::ReadingBoundary, + is_prev_field_consumed: true, + next_field_waker: None, + next_field_idx: 0, + curr_field_name: None, + curr_field_size_limit: constraints.size_limit.per_field, + curr_field_size_counter: 0, + }; + + Multipart { + state: Arc::new(Mutex::new(state)), + constraints, + } + } + + /// Construct a new `Multipart` instance with the given [`Bytes`](https://docs.rs/bytes/0.5.4/bytes/struct.Bytes.html) stream and the boundary. + pub fn new_with_constraints(stream: S, boundary: B, constraints: Constraints) -> Multipart where S: Stream> + Send + 'static, O: Into + 'static, @@ -62,16 +97,20 @@ impl Multipart { .map_err(|err| crate::Error::new(err.into().to_string())); let state = MultipartState { - buffer: StreamBuffer::new(stream), + buffer: StreamBuffer::new(stream, constraints.size_limit.whole_stream), boundary: boundary.into(), stage: StreamingStage::ReadingBoundary, is_prev_field_consumed: true, next_field_waker: None, next_field_idx: 0, + curr_field_name: None, + curr_field_size_limit: constraints.size_limit.per_field, + curr_field_size_counter: 0, }; Multipart { state: Arc::new(Mutex::new(state)), + constraints, } } @@ -90,7 +129,7 @@ impl Multipart { /// use futures::stream::once; /// /// # async fn run() { - /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; + /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let reader = data.as_bytes(); /// let mut multipart = Multipart::with_reader(reader, "X-BOUNDARY"); /// @@ -112,6 +151,43 @@ impl Multipart { Multipart::new(stream, boundary) } + /// Construct a new `Multipart` instance with the given [`AsyncRead`](https://docs.rs/tokio/0.2.20/tokio/io/trait.AsyncRead.html) reader and the boundary. + /// + /// # Optional + /// + /// This requires the optional `reader` feature to be enabled. + /// + /// # Examples + /// + /// ``` + /// use multer::Multipart; + /// use bytes::Bytes; + /// use std::convert::Infallible; + /// use futures::stream::once; + /// + /// # async fn run() { + /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; + /// let reader = data.as_bytes(); + /// let mut multipart = Multipart::with_reader(reader, "X-BOUNDARY"); + /// + /// while let Some(mut field) = multipart.next_field().await.unwrap() { + /// while let Some(chunk) = field.chunk().await.unwrap() { + /// println!("Chunk: {:?}", chunk); + /// } + /// } + /// # } + /// # tokio::runtime::Runtime::new().unwrap().block_on(run()); + /// ``` + #[cfg(feature = "reader")] + pub fn with_reader_with_constraints(reader: R, boundary: B, constraints: Constraints) -> Multipart + where + R: AsyncRead + Send + 'static, + B: Into, + { + let stream = FramedRead::new(reader, BytesCodec::new()); + Multipart::new_with_constraints(stream, boundary, constraints) + } + /// Yields the next [`Field`](./struct.Field.html) if available. /// /// For more info, go to [`Field`](./struct.Field.html#warning-about-leaks). @@ -132,7 +208,7 @@ impl Multipart { /// use futures::stream::once; /// /// # async fn run() { - /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; + /// let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"; /// let reader = data.as_bytes(); /// let mut multipart = Multipart::with_reader(reader, "X-BOUNDARY"); /// @@ -179,11 +255,22 @@ impl Stream for Multipart { if state.stage == StreamingStage::CleaningPrevFieldData { match stream_buffer.read_field_data(state.boundary.as_str()) { - Ok(Some((true, _))) => { - state.stage = StreamingStage::ReadingBoundary; - } - Ok(Some((false, _))) => { - return Poll::Pending; + Ok(Some((done, bytes))) => { + state.curr_field_size_counter += bytes.len(); + + if state.curr_field_size_counter > state.curr_field_size_limit { + return Poll::Ready(Some(Err(crate::Error::new(format!( + "Incoming Field size exceeded the maximum limit: {} bytes, field name: {}", + state.curr_field_size_limit, + state.curr_field_name.as_deref().unwrap_or("") + ))))); + } + + if done { + state.stage = StreamingStage::ReadingBoundary; + } else { + return Poll::Pending; + } } Ok(None) => { return Poll::Pending; @@ -268,9 +355,28 @@ impl Stream for Multipart { let field_idx = state.next_field_idx; state.next_field_idx += 1; + let content_disposition = ContentDisposition::parse(&headers); + let field_size_limit = self + .constraints + .size_limit + .extract_size_limit_for(content_disposition.field_name.as_deref()); + + state.curr_field_name = content_disposition.field_name.clone(); + state.curr_field_size_limit = field_size_limit; + state.curr_field_size_counter = 0; + drop(mutex_guard); - let next_field = Field::new(Arc::clone(&self.state), headers, field_idx); + let next_field = Field::new(Arc::clone(&self.state), headers, field_idx, content_disposition); + let field_name = next_field.name().map(|name| name.to_owned()); + + if !self.constraints.is_it_allowed(field_name.as_deref()) { + return Poll::Ready(Some(Err(crate::Error::new(format!( + "Unknown field detected: {}", + field_name.as_deref().unwrap_or("") + ))))); + } + return Poll::Ready(Some(Ok(next_field))); } diff --git a/src/size_limit.rs b/src/size_limit.rs new file mode 100644 index 0000000..e43a711 --- /dev/null +++ b/src/size_limit.rs @@ -0,0 +1,57 @@ +use crate::constants; +use std::collections::HashMap; + +/// Represents size limit of the stream to prevent DDoS attack. +/// +/// Please refer [`Constraints`](./struct.Constraints.html) for more info. +pub struct SizeLimit { + pub(crate) whole_stream: usize, + pub(crate) per_field: usize, + pub(crate) field_map: HashMap, +} + +impl SizeLimit { + /// Creates a default size limit which is [`usize::MAX`](https://doc.rust-lang.org/stable/std/primitive.usize.html#associatedconstant.MAX) for the whole stream + /// and for each field. + pub fn new() -> SizeLimit { + SizeLimit::default() + } + + /// Sets size limit for the whole stream. + pub fn whole_stream(mut self, limit: usize) -> SizeLimit { + self.whole_stream = limit; + self + } + + /// Sets size limit for each field. + pub fn per_field(mut self, limit: usize) -> SizeLimit { + self.per_field = limit; + self + } + + /// Sets size limit for a specific field, it overrides the `per_field` value for this field. + /// + /// It is useful when you want to set a size limit on a textual field which will be stored in memory + /// to avoid potential `DDoS attack` from attackers running the server out of memory. + pub fn for_field>(mut self, field_name: N, limit: usize) -> SizeLimit { + self.field_map.insert(field_name.into(), limit); + self + } + + pub(crate) fn extract_size_limit_for(&self, field: Option<&str>) -> usize { + field + .and_then(|field| self.field_map.get(&field.to_owned())) + .copied() + .unwrap_or(self.per_field) + } +} + +impl Default for SizeLimit { + fn default() -> Self { + SizeLimit { + whole_stream: constants::DEFAULT_WHOLE_STREAM_SIZE_LIMIT, + per_field: constants::DEFAULT_PER_FIELD_SIZE_LIMIT, + field_map: HashMap::default(), + } + } +} diff --git a/src/state.rs b/src/state.rs index 6afcf55..5737c08 100644 --- a/src/state.rs +++ b/src/state.rs @@ -8,6 +8,9 @@ pub(crate) struct MultipartState { pub(crate) is_prev_field_consumed: bool, pub(crate) next_field_waker: Option, pub(crate) next_field_idx: usize, + pub(crate) curr_field_name: Option, + pub(crate) curr_field_size_limit: usize, + pub(crate) curr_field_size_counter: usize, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/tests/integration.rs b/tests/integration.rs index 0b1327b..9c9eb4c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,10 +1,10 @@ use bytes::Bytes; use futures::stream; -use multer::Multipart; +use multer::{Constraints, Multipart, SizeLimit}; #[tokio::test] async fn test_multipart_basic() { - let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"File Field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = stream::iter( data.chars() .map(|ch| ch.to_string()) @@ -15,14 +15,14 @@ async fn test_multipart_basic() { while let Some((idx, field)) = m.next_field_with_idx().await.unwrap() { if idx == 0 { - assert_eq!(field.name(), Some("My Field")); + assert_eq!(field.name(), Some("my_text_field")); assert_eq!(field.file_name(), None); assert_eq!(field.content_type(), None); assert_eq!(field.index(), 0); assert_eq!(field.text().await, Ok("abcd".to_owned())); } else if idx == 1 { - assert_eq!(field.name(), Some("File Field")); + assert_eq!(field.name(), Some("my_file_field")); assert_eq!(field.file_name(), Some("a-text-file.txt")); assert_eq!(field.content_type(), Some(&mime::TEXT_PLAIN)); assert_eq!(field.index(), 1); @@ -49,7 +49,7 @@ async fn test_multipart_empty() { #[tokio::test] async fn test_multipart_clean_field() { - let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"My Field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"File Field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; let stream = stream::iter( data.chars() .map(|ch| ch.to_string()) @@ -62,3 +62,193 @@ async fn test_multipart_clean_field() { assert!(m.next_field().await.unwrap().is_some()); assert!(m.next_field().await.unwrap().is_none()); } + +#[tokio::test] +async fn test_multipart_constraint_allowed_fields_normal() { + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; + let stream = stream::iter( + data.chars() + .map(|ch| ch.to_string()) + .map(|part| multer::Result::Ok(Bytes::copy_from_slice(part.as_bytes()))), + ); + + let constraints = Constraints::new().allowed_fields(vec!["my_text_field", "my_file_field"]); + let mut m = Multipart::new_with_constraints(stream, "X-BOUNDARY", constraints); + + assert_eq!( + m.next_field().await.unwrap().unwrap().text().await.unwrap(), + "abcd".to_owned() + ); + assert_eq!( + m.next_field().await.unwrap().unwrap().text().await.unwrap(), + "Hello world\nHello\r\nWorld\rAgain".to_owned() + ); +} + +#[tokio::test] +#[should_panic] +async fn test_multipart_constraint_allowed_fields_unknown_field() { + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; + let stream = stream::iter( + data.chars() + .map(|ch| ch.to_string()) + .map(|part| multer::Result::Ok(Bytes::copy_from_slice(part.as_bytes()))), + ); + + let constraints = Constraints::new().allowed_fields(vec!["my_text_field"]); + let mut m = Multipart::new_with_constraints(stream, "X-BOUNDARY", constraints); + + assert!(m.next_field().await.unwrap().is_some()); + assert!(m.next_field().await.unwrap().is_some()); + assert!(m.next_field().await.unwrap().is_none()); +} + +#[tokio::test] +async fn test_multipart_constraint_size_limit_whole_stream() { + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; + let stream = stream::iter( + data.chars() + .map(|ch| ch.to_string()) + .map(|part| multer::Result::Ok(Bytes::copy_from_slice(part.as_bytes()))), + ); + + let constraints = Constraints::new() + .allowed_fields(vec!["my_text_field", "my_file_field"]) + .size_limit(SizeLimit::new().whole_stream(240)); + + let mut m = Multipart::new_with_constraints(stream, "X-BOUNDARY", constraints); + + assert_eq!( + m.next_field().await.unwrap().unwrap().text().await.unwrap(), + "abcd".to_owned() + ); + assert_eq!( + m.next_field().await.unwrap().unwrap().text().await.unwrap(), + "Hello world\nHello\r\nWorld\rAgain".to_owned() + ); +} + +#[tokio::test] +#[should_panic] +async fn test_multipart_constraint_size_limit_whole_stream_size_exceeded() { + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; + let stream = stream::iter( + data.chars() + .map(|ch| ch.to_string()) + .map(|part| multer::Result::Ok(Bytes::copy_from_slice(part.as_bytes()))), + ); + + let constraints = Constraints::new() + .allowed_fields(vec!["my_text_field", "my_file_field"]) + .size_limit(SizeLimit::new().whole_stream(100)); + + let mut m = Multipart::new_with_constraints(stream, "X-BOUNDARY", constraints); + + assert!(m.next_field().await.unwrap().is_some()); + assert!(m.next_field().await.unwrap().is_some()); + assert!(m.next_field().await.unwrap().is_none()); +} + +#[tokio::test] +async fn test_multipart_constraint_size_limit_per_field() { + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; + let stream = stream::iter( + data.chars() + .map(|ch| ch.to_string()) + .map(|part| multer::Result::Ok(Bytes::copy_from_slice(part.as_bytes()))), + ); + + let constraints = Constraints::new() + .allowed_fields(vec!["my_text_field", "my_file_field"]) + .size_limit(SizeLimit::new().whole_stream(240).per_field(100)); + + let mut m = Multipart::new_with_constraints(stream, "X-BOUNDARY", constraints); + + assert_eq!( + m.next_field().await.unwrap().unwrap().text().await.unwrap(), + "abcd".to_owned() + ); + assert_eq!( + m.next_field().await.unwrap().unwrap().text().await.unwrap(), + "Hello world\nHello\r\nWorld\rAgain".to_owned() + ); +} + +#[tokio::test] +#[should_panic] +async fn test_multipart_constraint_size_limit_per_field_size_exceeded() { + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; + let stream = stream::iter( + data.chars() + .map(|ch| ch.to_string()) + .map(|part| multer::Result::Ok(Bytes::copy_from_slice(part.as_bytes()))), + ); + + let constraints = Constraints::new() + .allowed_fields(vec!["my_text_field", "my_file_field"]) + .size_limit(SizeLimit::new().whole_stream(240).per_field(10)); + + let mut m = Multipart::new_with_constraints(stream, "X-BOUNDARY", constraints); + + assert!(m.next_field().await.unwrap().is_some()); + assert!(m.next_field().await.unwrap().is_some()); + assert!(m.next_field().await.unwrap().is_none()); +} + +#[tokio::test] +async fn test_multipart_constraint_size_limit_for_field() { + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; + let stream = stream::iter( + data.chars() + .map(|ch| ch.to_string()) + .map(|part| multer::Result::Ok(Bytes::copy_from_slice(part.as_bytes()))), + ); + + let constraints = Constraints::new() + .allowed_fields(vec!["my_text_field", "my_file_field"]) + .size_limit( + SizeLimit::new() + .whole_stream(240) + .per_field(100) + .for_field("my_text_field", 4) + .for_field("my_file_field", 30), + ); + + let mut m = Multipart::new_with_constraints(stream, "X-BOUNDARY", constraints); + + assert_eq!( + m.next_field().await.unwrap().unwrap().text().await.unwrap(), + "abcd".to_owned() + ); + assert_eq!( + m.next_field().await.unwrap().unwrap().text().await.unwrap(), + "Hello world\nHello\r\nWorld\rAgain".to_owned() + ); +} + +#[tokio::test] +#[should_panic] +async fn test_multipart_constraint_size_limit_for_field_size_exceeded() { + let data = "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_file_field\"; filename=\"a-text-file.txt\"\r\nContent-Type: text/plain\r\n\r\nHello world\nHello\r\nWorld\rAgain\r\n--X-BOUNDARY--\r\n"; + let stream = stream::iter( + data.chars() + .map(|ch| ch.to_string()) + .map(|part| multer::Result::Ok(Bytes::copy_from_slice(part.as_bytes()))), + ); + + let constraints = Constraints::new() + .allowed_fields(vec!["my_text_field", "my_file_field"]) + .size_limit( + SizeLimit::new() + .whole_stream(240) + .per_field(100) + .for_field("my_text_field", 4) + .for_field("my_file_field", 10), + ); + + let mut m = Multipart::new_with_constraints(stream, "X-BOUNDARY", constraints); + + assert!(m.next_field().await.unwrap().is_some()); + assert!(m.next_field().await.unwrap().is_some()); + assert!(m.next_field().await.unwrap().is_none()); +} From 99b722ac3cd1c092206ec2d7b106e79d6c83f20a Mon Sep 17 00:00:00 2001 From: Rousan Ali Date: Sat, 16 May 2020 22:26:45 +0530 Subject: [PATCH 2/4] Update test cases --- tests/integration.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration.rs b/tests/integration.rs index 9c9eb4c..415e32f 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -114,7 +114,7 @@ async fn test_multipart_constraint_size_limit_whole_stream() { let constraints = Constraints::new() .allowed_fields(vec!["my_text_field", "my_file_field"]) - .size_limit(SizeLimit::new().whole_stream(240)); + .size_limit(SizeLimit::new().whole_stream(248)); let mut m = Multipart::new_with_constraints(stream, "X-BOUNDARY", constraints); @@ -160,7 +160,7 @@ async fn test_multipart_constraint_size_limit_per_field() { let constraints = Constraints::new() .allowed_fields(vec!["my_text_field", "my_file_field"]) - .size_limit(SizeLimit::new().whole_stream(240).per_field(100)); + .size_limit(SizeLimit::new().whole_stream(248).per_field(100)); let mut m = Multipart::new_with_constraints(stream, "X-BOUNDARY", constraints); @@ -186,7 +186,7 @@ async fn test_multipart_constraint_size_limit_per_field_size_exceeded() { let constraints = Constraints::new() .allowed_fields(vec!["my_text_field", "my_file_field"]) - .size_limit(SizeLimit::new().whole_stream(240).per_field(10)); + .size_limit(SizeLimit::new().whole_stream(248).per_field(10)); let mut m = Multipart::new_with_constraints(stream, "X-BOUNDARY", constraints); @@ -208,7 +208,7 @@ async fn test_multipart_constraint_size_limit_for_field() { .allowed_fields(vec!["my_text_field", "my_file_field"]) .size_limit( SizeLimit::new() - .whole_stream(240) + .whole_stream(248) .per_field(100) .for_field("my_text_field", 4) .for_field("my_file_field", 30), @@ -240,7 +240,7 @@ async fn test_multipart_constraint_size_limit_for_field_size_exceeded() { .allowed_fields(vec!["my_text_field", "my_file_field"]) .size_limit( SizeLimit::new() - .whole_stream(240) + .whole_stream(248) .per_field(100) .for_field("my_text_field", 4) .for_field("my_file_field", 10), From f2416f213f06986cf7df74651215264866288f89 Mon Sep 17 00:00:00 2001 From: Rousan Ali Date: Sat, 16 May 2020 22:29:28 +0530 Subject: [PATCH 3/4] Update test cases --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 6333a55..ca35642 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,7 +88,7 @@ //! assert_eq!(content, "abcd"); //! } //! # } -//! # to +//! # tokio::runtime::Runtime::new().unwrap().block_on(run()); //! ``` //! //! Please refer [`Constraints`](./struct.Constraints.html) for more info. From c9b33c3d0c7a2c8ba98411fcbd59c5d702cc30fb Mon Sep 17 00:00:00 2001 From: Rousan Ali Date: Sat, 16 May 2020 22:30:45 +0530 Subject: [PATCH 4/4] Bump version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 03ce6c5..0d2cd9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multer" -version = "1.0.2" +version = "1.0.3" description = "An async parser for `multipart/form-data` content-type in Rust." homepage = "https://github.com/rousan/multer-rs" repository = "https://github.com/rousan/multer-rs"