Skip to content

Commit

Permalink
Merge branch 'release/v1.0.3'
Browse files Browse the repository at this point in the history
  • Loading branch information
rousan committed May 16, 2020
2 parents 15b8c31 + c9b33c3 commit 123a39a
Show file tree
Hide file tree
Showing 18 changed files with 726 additions and 74 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,52 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

// Generate a byte stream and the boundary from somewhere e.g. server request body.
async fn get_byte_stream_from_somewhere() -> (impl Stream<Item = Result<Bytes, Infallible>>, &'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::<Bytes, Infallible>::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<dyn std::error::Error>> {
// 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/).
Expand Down
4 changes: 3 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
* [`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`.
2 changes: 1 addition & 1 deletion examples/parse_async_read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

// 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")
}
57 changes: 57 additions & 0 deletions examples/prevent_ddos_attack.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
// 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<Item = Result<Bytes, Infallible>>, &'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")
}
2 changes: 1 addition & 1 deletion examples/simple_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

// Generate a byte stream and the boundary from somewhere e.g. server request body.
async fn get_byte_stream_from_somewhere() -> (impl Stream<Item = Result<Bytes, Infallible>>, &'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())
Expand Down
10 changes: 8 additions & 2 deletions examples/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -26,10 +26,16 @@ async fn handle(req: Request<Body>) -> Result<Response<Body>, 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);
}
Expand Down
19 changes: 17 additions & 2 deletions src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@ pub(crate) struct StreamBuffer {
pub(crate) eof: bool,
pub(crate) buf: BytesMut,
pub(crate) stream: Pin<Box<dyn Stream<Item = Result<Bytes, crate::Error>> + Send>>,
pub(crate) whole_stream_size_limit: usize,
pub(crate) stream_size_counter: usize,
}

impl StreamBuffer {
pub fn new<S>(stream: S) -> Self
pub fn new<S>(stream: S, whole_stream_size_limit: usize) -> Self
where
S: Stream<Item = Result<Bytes, crate::Error>> + Send + 'static,
{
StreamBuffer {
eof: false,
buf: BytesMut::new(),
stream: Box::pin(stream),
whole_stream_size_limit,
stream_size_counter: 0,
}
}

Expand All @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
92 changes: 92 additions & 0 deletions src/constraints.rs
Original file line number Diff line number Diff line change
@@ -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::<Bytes, Infallible>::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<Vec<String>>,
}

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<N: Into<String>>(self, allowed_fields: Vec<N>) -> 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,
}
}
}
27 changes: 27 additions & 0 deletions src/content_disposition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use crate::constants;
use http::header::{self, HeaderMap};

pub(crate) struct ContentDisposition {
pub(crate) field_name: Option<String>,
pub(crate) file_name: Option<String>,
}

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 }
}
}
Loading

0 comments on commit 123a39a

Please sign in to comment.