Repository: rousan/multer-rs
Branch: master
Commit: 09cc7300c9bb
Files: 35
Total size: 98.0 KB
Directory structure:
gitextract_jlvbhjbj/
├── .github/
│ └── workflows/
│ └── test.yml
├── .gitignore
├── Cargo.toml
├── LICENSE
├── README.md
├── build.rs
├── examples/
│ ├── README.md
│ ├── hyper_server_example.rs
│ ├── parse_async_read.rs
│ ├── prevent_dos_attack.rs
│ └── simple_example.rs
├── fuzz/
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── README.md
│ ├── corpus/
│ │ └── fuzz_multipart_bytes/
│ │ ├── multi.seed
│ │ ├── multi2.seed
│ │ ├── simple.seed
│ │ ├── simple2.seed
│ │ ├── simple3.seed
│ │ └── single.seed
│ └── fuzz_targets/
│ └── fuzz_multipart_bytes.rs
├── releez.yml
├── rustfmt.toml
├── src/
│ ├── buffer.rs
│ ├── constants.rs
│ ├── constraints.rs
│ ├── content_disposition.rs
│ ├── error.rs
│ ├── field.rs
│ ├── helpers.rs
│ ├── lib.rs
│ ├── multipart.rs
│ └── size_limit.rs
├── tests/
│ └── integration.rs
└── tusk.yml
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/test.yml
================================================
name: CI
on:
pull_request:
push:
branches:
- master
- develop
env:
RUSTFLAGS: -Dwarnings
jobs:
build_and_test:
name: Build and test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
rust: [stable, nightly]
steps:
- uses: actions/checkout@master
- name: Install ${{ matrix.rust }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- name: check
run: cargo check --all --bins --examples --all-features
- name: tests
run: cargo test --all --all-features
check_fmt_and_docs:
name: Checking fmt, clippy, and docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: clippy
run: cargo clippy --tests --examples --bins -- -D warnings
- name: fmt
run: cargo fmt --all -- --check
- name: Docs
run: cargo doc --no-deps
================================================
FILE: .gitignore
================================================
# Created by https://www.gitignore.io/api/rust,macos
# Edit at https://www.gitignore.io/?templates=rust,macos
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Rust ###
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
### Others ###
.halt.releez.yml
/.idea
/multer-rs.iml
# End of https://www.gitignore.io/api/rust,macos
================================================
FILE: Cargo.toml
================================================
[package]
name = "multer"
version = "3.1.0"
description = "An async parser for `multipart/form-data` content-type in Rust."
homepage = "https://github.com/rwf2/multer"
repository = "https://github.com/rwf2/multer"
keywords = ["multipart", "multipart-formdata", "multipart-uploads", "async", "formdata"]
categories = ["asynchronous", "web-programming"]
authors = ["Rousan Ali <hello@rousan.io>"]
readme = "README.md"
license = "MIT"
edition = "2018"
[package.metadata.docs.rs]
all-features = true
[package.metadata.playground]
features = ["all"]
[features]
default = []
all = ["json"]
json = ["serde", "serde_json"]
tokio-io = ["tokio", "tokio-util"]
log = ["dep:log"]
[dependencies]
bytes = "1.0"
futures-util = { version = "0.3", default-features = false }
memchr = "2.4"
http = "1.0"
httparse = "1.3"
mime = "0.3.10"
encoding_rs = "0.8.20"
spin = { version = "0.9", default-features = false, features = ["spin_mutex"] }
log = { version = "0.4.15", optional = true }
serde = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
tokio = { version = "1.0", features = [], optional = true }
tokio-util = { version = "0.7", features = ["io"], optional = true }
[dev-dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
hyper = { version = "1.0", features = ["server", "http1"] }
http-body-util = "0.1"
hyper-util = { version = "0.1.1", features = ["full"] }
[build-dependencies]
version_check = "0.9"
[[example]]
name = "parse_async_read"
path = "examples/parse_async_read.rs"
required-features = ["tokio-io"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Rousan Ali
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
[](https://github.com/rousan/multer-rs/actions)
[](https://crates.io/crates/multer)
[](https://docs.rs/multer)
[](./LICENSE)
# multer-rs
An async parser for `multipart/form-data` content-type in Rust.
It accepts a [`Stream`](https://docs.rs/futures/0.3/futures/stream/trait.Stream.html) of [`Bytes`](https://docs.rs/bytes/1/bytes/struct.Bytes.html) as
a source, so that It can be plugged into any async Rust environment e.g. any async server.
[Docs](https://docs.rs/multer)
## Install
Add this to your `Cargo.toml`:
```toml
[dependencies]
multer = "2.0"
```
# Basic Example
```rust
use bytes::Bytes;
use futures::stream::Stream;
// Import multer types.
use multer::Multipart;
use std::convert::Infallible;
use futures::stream::once;
#[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 a `Multipart` instance from that byte stream and the boundary.
let mut multipart = Multipart::new(stream, boundary);
// Iterate over the fields, use `next_field()` to get the next field.
while let Some(mut 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);
// Process the field data chunks e.g. store them in a file.
while let Some(chunk) = field.chunk().await? {
// Do something with field chunk.
println!("Chunk: {:?}", chunk);
}
}
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\n";
let stream = once(async move { Result::<Bytes, Infallible>::Ok(Bytes::from(data)) });
(stream, "X-BOUNDARY")
}
```
## Prevent Denial of Service (DoS) Attacks
This crate also provides some APIs to prevent potential DoS attacks with fine grained control. It's recommended to add some constraints
on field (specially text field) size to prevent DoS attacks exhausting the server's 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 DoS 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::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/).
For more examples, please visit [examples](https://github.com/rousan/multer-rs/tree/master/examples).
## Contributing
Your PRs and suggestions are always welcome.
================================================
FILE: build.rs
================================================
fn main() {
if let Some(true) = version_check::is_feature_flaggable() {
println!("cargo:rustc-cfg=nightly");
}
}
================================================
FILE: examples/README.md
================================================
# Examples of using multer-rs
These examples show of how to do common tasks using `multer-rs`.
Please visit: [Docs](https://docs.rs/multer) for the documentation.
Run an example:
```sh
cargo run --example example_name
```
* [`simple_example`](simple_example.rs) - A basic example using `multer`.
* [`hyper_server_example`](hyper_server_example.rs) - Shows how to use this crate with Rust HTTP server [hyper](https://hyper.rs/).
* [`parse_async_read`](parse_async_read.rs) - Shows how to parse `multipart/form-data` from an [`AsyncRead`](https://docs.rs/tokio/1/tokio/io/trait.AsyncRead.html).
* [`prevent_dos_attack`](prevent_dos_attack.rs) - Shows how to apply some rules to prevent potential DoS attacks while handling `multipart/form-data`.
================================================
FILE: examples/hyper_server_example.rs
================================================
use std::{convert::Infallible, net::SocketAddr};
use bytes::Bytes;
use futures_util::StreamExt;
use http_body_util::{BodyStream, Full};
use hyper::{body::Incoming, header::CONTENT_TYPE, Request, Response, StatusCode};
// Import the multer types.
use multer::Multipart;
// A handler for incoming requests.
async fn handle(req: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
// Extract the `multipart/form-data` boundary from the headers.
let boundary = req
.headers()
.get(CONTENT_TYPE)
.and_then(|ct| ct.to_str().ok())
.and_then(|ct| multer::parse_boundary(ct).ok());
// Send `BAD_REQUEST` status if the content-type is not multipart/form-data.
if boundary.is_none() {
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Full::from("BAD REQUEST"))
.unwrap());
}
// Process the multipart e.g. you can store them in files.
if let Err(err) = process_multipart(req.into_body(), boundary.unwrap()).await {
return Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::from(format!("INTERNAL SERVER ERROR: {}", err)))
.unwrap());
}
Ok(Response::new(Full::from("Success")))
}
// Process the request body as multipart/form-data.
async fn process_multipart(body: Incoming, boundary: String) -> multer::Result<()> {
// Convert the body into a stream of data frames.
let body_stream = BodyStream::new(body)
.filter_map(|result| async move { result.map(|frame| frame.into_data().ok()).transpose() });
// Create a Multipart instance from the request body.
let mut multipart = Multipart::new(body_stream, boundary);
// Iterate over the fields, `next_field` method will return the next field if
// available.
while let Some(mut field) = multipart.next_field().await? {
// Get the field name.
let name = field.name();
// Get the field's filename if provided in "Content-Disposition" header.
let file_name = field.file_name();
// Get the "Content-Type" header as `mime::Mime` type.
let content_type = field.content_type();
println!(
"Name: {:?}, FileName: {:?}, Content-Type: {:?}",
name, file_name, content_type
);
// Process the field data chunks e.g. store them in a file.
let mut field_bytes_len = 0;
while let Some(field_chunk) = field.chunk().await? {
// Do something with field chunk.
field_bytes_len += field_chunk.len();
}
println!("Field Bytes Length: {:?}", field_bytes_len);
}
Ok(())
}
#[tokio::main]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
println!("Server running at: {}", addr);
let service = hyper::service::service_fn(handle);
loop {
let (socket, _remote_addr) = listener.accept().await.unwrap();
let socket = hyper_util::rt::TokioIo::new(socket);
tokio::spawn(async move {
if let Err(e) = hyper::server::conn::http1::Builder::new()
.serve_connection(socket, service)
.await
{
eprintln!("server error: {}", e);
}
});
}
}
================================================
FILE: examples/parse_async_read.rs
================================================
use multer::Multipart;
use tokio::io::AsyncRead;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Generate an `AsyncRead` and the boundary from somewhere e.g. server request
// body.
let (reader, boundary) = get_async_reader_from_somewhere().await;
// Create a `Multipart` instance from that async reader and the boundary.
let mut multipart = Multipart::with_reader(reader, boundary);
// Iterate over the fields, use `next_field()` to get the next field.
while let Some(mut 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);
// Process the field data chunks e.g. store them in a file.
let mut field_bytes_len = 0;
while let Some(field_chunk) = field.chunk().await? {
// Do something with field chunk.
field_bytes_len += field_chunk.len();
}
println!("Field Bytes Length: {:?}", field_bytes_len);
}
Ok(())
}
// 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_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")
}
================================================
FILE: examples/prevent_dos_attack.rs
================================================
use std::convert::Infallible;
use bytes::Bytes;
use futures_util::stream::Stream;
// Import multer types.
use multer::{Constraints, Multipart, SizeLimit};
#[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 DoS attacks.
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::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_util::stream::iter(
data.chars()
.map(|ch| ch.to_string())
.map(|part| Ok(Bytes::copy_from_slice(part.as_bytes()))),
);
(stream, "X-BOUNDARY")
}
================================================
FILE: examples/simple_example.rs
================================================
use std::convert::Infallible;
use bytes::Bytes;
use futures_util::stream::Stream;
// Import multer types.
use multer::Multipart;
#[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 a `Multipart` instance from that byte stream and the boundary.
let mut multipart = Multipart::new(stream, boundary);
// 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_util::stream::iter(
data.chars()
.map(|ch| ch.to_string())
.map(|part| Ok(Bytes::copy_from_slice(part.as_bytes()))),
);
(stream, "X-BOUNDARY")
}
================================================
FILE: fuzz/.gitignore
================================================
target
corpus/*/*
artifacts
!*.seed
coverage
================================================
FILE: fuzz/Cargo.toml
================================================
[package]
name = "multer-fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
edition = "2018"
[profile.release]
debug = true
debug-assertions = true
overflow-checks = true
lto = true
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.3"
futures-util = { version = "0.3", default-features = false }
tokio = { version = "1", features = ["rt", "time"] }
[dependencies.multer]
path = ".."
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[[bin]]
name = "fuzz_multipart_bytes"
path = "fuzz_targets/fuzz_multipart_bytes.rs"
test = false
doc = false
================================================
FILE: fuzz/README.md
================================================
# Fuzzing
Install `cargo-fuzz`:
```sh
cargo install -f cargo-fuzz
```
Run any available target where `$target` is the name of the target and `$n` is
the number of CPUs to use for fuzzing:
```sh
cargo fuzz list # get list of targets
cargo fuzz run $target -j $n
```
================================================
FILE: fuzz/corpus/fuzz_multipart_bytes/multi.seed
================================================
--X-BOUNDARY
Content-Disposition: form-data; name="sometext"
some text that you wrote in your html form ...
--X-BOUNDARY
Content-Disposition: form-data; name="name_of_post_request" filename="filename.xyz"
content of filename.xyz that you upload in your form with input[type=file]
--X-BOUNDARY
Content-Disposition: form-data; name="image" filename="picture_of_sunset.jpg"
content of picture_of_sunset.jpg ...
--X-BOUNDARY--
================================================
FILE: fuzz/corpus/fuzz_multipart_bytes/multi2.seed
================================================
--X-BOUNDARY
Content-Disposition: form-data; name="text1"
text default
--X-BOUNDARY
Content-Disposition: form-data; name="text2"
aωb
--X-BOUNDARY
Content-Disposition: form-data; name="file1"; filename="a.txt"
Content-Type: text/plain
Content of a.txt.
--X-BOUNDARY
Content-Disposition: form-data; name="file2"; filename="a.html"
Content-Type: text/html
<!DOCTYPE html><title>Content of a.html.</title>
--X-BOUNDARY
Content-Disposition: form-data; name="file3"; filename="binary"
Content-Type: application/octet-stream
aωb
--X-BOUNDARY--
================================================
FILE: fuzz/corpus/fuzz_multipart_bytes/simple.seed
================================================
--X-BOUNDARY
Content-Disposition: form-data; name=my_text_field
--X-BOUNDARY--
================================================
FILE: fuzz/corpus/fuzz_multipart_bytes/simple2.seed
================================================
--X-BOUNDARY
Content-Disposition: form-data; name="field1"
value1
--X-BOUNDARY
Content-Disposition: form-data; name="field2"; filename="example.txt"
value2
--X-BOUNDARY--
================================================
FILE: fuzz/corpus/fuzz_multipart_bytes/simple3.seed
================================================
--X-BOUNDARY
Content-Disposition: form-data; name="text"
Content-Type: text/plain
Book
--X-BOUNDARY
Content-Disposition: form-data; name="file1"; filename="a.json"
Content-Type: application/json
{
"title": "Java 8 in Action",
"author": "Mario Fusco",
"year": 2014
}
--X-BOUNDARY
Content-Disposition: form-data; name="file2"; filename="a.html"
Content-Type: text/html
<title> Available for download! </title>
--X-BOUNDARY--
================================================
FILE: fuzz/corpus/fuzz_multipart_bytes/single.seed
================================================
--X-BOUNDARY
Content-Disposition: form-data; name=\"my_text_field\"
abcd
--X-BOUNDARY--
================================================
FILE: fuzz/fuzz_targets/fuzz_multipart_bytes.rs
================================================
#![no_main]
use std::convert::Infallible;
use std::time::Duration;
use multer::Multipart;
use multer::bytes::Bytes;
use futures_util::stream::once;
use libfuzzer_sys::fuzz_target;
use tokio::{runtime, time::timeout};
const FIELD_TIMEOUT: Duration = Duration::from_millis(10);
fuzz_target!(|data: &[u8]| {
let data = data.to_vec();
let stream = once(async move { Result::<Bytes, Infallible>::Ok(Bytes::from(data)) });
let mut multipart = Multipart::new(stream, "X-BOUNDARY");
let rt = runtime::Builder::new_current_thread()
.enable_time()
.build()
.expect("runtime");
rt.block_on(async {
let mut breaks = 0;
while breaks < 3 {
let field = timeout(FIELD_TIMEOUT, multipart.next_field()).await;
match field {
Err(_) => panic!("timed out waiting for field"),
Ok(Err(_)) | Ok(Ok(None)) => breaks += 1,
Ok(Ok(Some(_))) => continue,
}
}
})
});
================================================
FILE: releez.yml
================================================
version: 1.0.0
checklist:
- name: Checkout master and sync with remote
type: auto
run:
- git checkout master
- git pull
- name: Check syntax
type: auto
run:
- cargo check --release --features="all"
- name: Run tests
type: auto
run:
- cargo test --release --features="all"
- name: Make sure code is formatted
type: auto
run:
- cargo fmt
- name: Bump version
type: manual
instructions:
- Please update version with ${VERSION} in Cargo.toml file.
- Please update version with ${VERSION} in README.md file if needed.
- name: Commit changes
type: auto
run:
- git add --all && git commit -m "Bump version"
- name: Create a release tag
type: auto
run:
- git tag "v${VERSION}" -a
- name: Push branches and tags to Github
type: auto
run:
- git push origin master
- git push --tags
- name: Edit tag on Github
type: manual
instructions:
- Tag is pushed to Github(https://github.com/rousan/multer-rs/releases). Edit it there and make it a release.
- name: Publish to crates.io
type: auto
confirm: Are you sure to publish it to crates.io?
run:
- cargo publish
================================================
FILE: rustfmt.toml
================================================
max_width = 120
tab_spaces = 4
wrap_comments = true
condense_wildcard_suffixes = true
format_code_in_doc_comments = true
newline_style = "Unix"
normalize_comments = true
reorder_impl_items = true
group_imports = "StdExternalCrate"
use_field_init_shorthand = true
================================================
FILE: src/buffer.rs
================================================
use std::fmt;
use std::pin::Pin;
use std::task::{Context, Poll};
use bytes::{Buf, Bytes, BytesMut};
use futures_util::stream::Stream;
use crate::constants;
pub(crate) struct StreamBuffer<'r> {
pub(crate) eof: bool,
pub(crate) buf: BytesMut,
pub(crate) stream: Pin<Box<dyn Stream<Item = Result<Bytes, crate::Error>> + Send + 'r>>,
pub(crate) whole_stream_size_limit: u64,
pub(crate) stream_size_counter: u64,
}
impl<'r> StreamBuffer<'r> {
pub fn new<S>(stream: S, whole_stream_size_limit: u64) -> Self
where
S: Stream<Item = Result<Bytes, crate::Error>> + Send + 'r,
{
StreamBuffer {
eof: false,
buf: BytesMut::new(),
stream: Box::pin(stream),
whole_stream_size_limit,
stream_size_counter: 0,
}
}
pub fn poll_stream(&mut self, cx: &mut Context<'_>) -> Result<(), crate::Error> {
if self.eof {
return Ok(());
}
loop {
match self.stream.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(data))) => {
self.stream_size_counter += data.len() as u64;
if self.stream_size_counter > self.whole_stream_size_limit {
return Err(crate::Error::StreamSizeExceeded {
limit: 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;
return Ok(());
}
Poll::Pending => return Ok(()),
}
}
}
pub fn read_exact(&mut self, size: usize) -> Option<Bytes> {
if size <= self.buf.len() {
Some(self.buf.split_to(size).freeze())
} else {
None
}
}
pub fn peek_exact(&mut self, size: usize) -> Option<&[u8]> {
self.buf.get(..size)
}
pub fn read_until(&mut self, pattern: &[u8]) -> Option<Bytes> {
memchr::memmem::find(&self.buf, pattern).map(|idx| self.buf.split_to(idx + pattern.len()).freeze())
}
pub fn read_to(&mut self, pattern: &[u8]) -> Option<Bytes> {
memchr::memmem::find(&self.buf, pattern).map(|idx| self.buf.split_to(idx).freeze())
}
pub fn advance_past_transport_padding(&mut self) -> bool {
match self.buf.iter().position(|b| *b != b' ' && *b != b'\t') {
Some(pos) => {
self.buf.advance(pos);
true
}
None => {
self.buf.clear();
false
}
}
}
pub fn read_field_data(
&mut self,
boundary: &str,
field_name: Option<&str>,
) -> crate::Result<Option<(bool, Bytes)>> {
trace!("finding next field: {:?}", field_name);
if self.buf.is_empty() && self.eof {
trace!("empty buffer && EOF");
return Err(crate::Error::IncompleteFieldData {
field_name: field_name.map(|s| s.to_owned()),
});
} else if self.buf.is_empty() {
return Ok(None);
}
let boundary_deriv = format!("{}{}{}", constants::CRLF, constants::BOUNDARY_EXT, boundary);
let b_len = boundary_deriv.len();
match memchr::memmem::find(&self.buf, boundary_deriv.as_bytes()) {
Some(idx) => {
trace!("new field found at {}", idx);
let bytes = self.buf.split_to(idx).freeze();
// discard \r\n.
self.buf.advance(constants::CRLF.len());
Ok(Some((true, bytes)))
}
None if self.eof => {
trace!("no new field found: EOF. terminating");
Err(crate::Error::IncompleteFieldData {
field_name: field_name.map(|s| s.to_owned()),
})
}
None => {
let buf_len = self.buf.len();
let rem_boundary_part_max_len = b_len - 1;
let rem_boundary_part_idx = if buf_len >= rem_boundary_part_max_len {
buf_len - rem_boundary_part_max_len
} else {
0
};
trace!("no new field found, not EOF, checking close");
let bytes = &self.buf[rem_boundary_part_idx..];
match memchr::memmem::rfind(bytes, constants::CR.as_bytes()) {
Some(rel_idx) => {
let idx = rel_idx + rem_boundary_part_idx;
match memchr::memmem::find(boundary_deriv.as_bytes(), &self.buf[idx..]) {
Some(_) => {
let bytes = self.buf.split_to(idx).freeze();
match bytes.is_empty() {
true => Ok(None),
false => Ok(Some((false, bytes))),
}
}
None => Ok(Some((false, self.read_full_buf()))),
}
}
None => Ok(Some((false, self.read_full_buf()))),
}
}
}
}
pub fn read_full_buf(&mut self) -> Bytes {
self.buf.split_to(self.buf.len()).freeze()
}
}
impl fmt::Debug for StreamBuffer<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StreamBuffer").finish()
}
}
================================================
FILE: src/constants.rs
================================================
use std::borrow::Cow;
pub(crate) const DEFAULT_WHOLE_STREAM_SIZE_LIMIT: u64 = std::u64::MAX;
pub(crate) const DEFAULT_PER_FIELD_SIZE_LIMIT: u64 = std::u64::MAX;
pub(crate) const MAX_HEADERS: usize = 32;
pub(crate) const BOUNDARY_EXT: &str = "--";
pub(crate) const CR: &str = "\r";
#[allow(dead_code)]
pub(crate) const LF: &str = "\n";
pub(crate) const CRLF: &str = "\r\n";
pub(crate) const CRLF_CRLF: &str = "\r\n\r\n";
#[derive(PartialEq)]
pub(crate) enum ContentDispositionAttr {
Name,
FileName,
}
fn trim_ascii_ws_start(bytes: &[u8]) -> &[u8] {
bytes
.iter()
.position(|b| !b.is_ascii_whitespace())
.map_or_else(|| &bytes[bytes.len()..], |i| &bytes[i..])
}
fn trim_ascii_ws_then(bytes: &[u8], char: u8) -> Option<&[u8]> {
match trim_ascii_ws_start(bytes) {
[first, rest @ ..] if *first == char => Some(rest),
_ => None,
}
}
impl ContentDispositionAttr {
/// Extract ContentDisposition Attribute from header.
///
/// Some older clients may not quote the name or filename, so we allow them,
/// but require them to be percent encoded. Only allocates if percent
/// decoding, and there are characters that need to be decoded.
pub fn extract_from<'h>(&self, mut header: &'h [u8]) -> Option<Cow<'h, str>> {
// TODO: The prefix should be matched case-insensitively.
let prefix = match self {
ContentDispositionAttr::Name => &b"name"[..],
ContentDispositionAttr::FileName => &b"filename"[..],
};
while let Some(i) = memchr::memmem::find(header, prefix) {
// Check if we found a superstring of `prefix`; continue if so.
let suffix = &header[(i + prefix.len())..];
if i > 0 && !(header[i - 1].is_ascii_whitespace() || header[i - 1] == b';') {
header = suffix;
continue;
}
// Now find and trim the `=`. Handle quoted strings first.
let rest = trim_ascii_ws_then(suffix, b'=')?;
let (bytes, is_escaped) = if let Some(rest) = trim_ascii_ws_then(rest, b'"') {
let (mut k, mut escaped) = (memchr::memchr(b'"', rest)?, false);
while k > 0 && rest[k - 1] == b'\\' {
escaped = true;
k = k + 1 + memchr::memchr(b'"', &rest[(k + 1)..])?;
}
(&rest[..k], escaped)
} else {
let rest = trim_ascii_ws_start(rest);
let j = memchr::memchr2(b';', b' ', rest).unwrap_or(rest.len());
(&rest[..j], false)
};
return match std::str::from_utf8(bytes).ok()? {
name if is_escaped => Some(name.replace(r#"\""#, "\"").into()),
name => Some(name.into()),
};
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_content_disposition_name_only() {
let val = br#"form-data; name="my_field""#;
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(name.unwrap(), "my_field");
assert!(filename.is_none());
let val = br#"form-data; name=my_field "#;
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(name.unwrap(), "my_field");
assert!(filename.is_none());
let val = br#"form-data; name = my_field "#;
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(name.unwrap(), "my_field");
assert!(filename.is_none());
let val = br#"form-data; name = "#;
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(name.unwrap(), "");
assert!(filename.is_none());
}
#[test]
fn test_content_disposition_extraction() {
let val = br#"form-data; name="my_field"; filename="file abc.txt""#;
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(name.unwrap(), "my_field");
assert_eq!(filename.unwrap(), "file abc.txt");
let val = "form-data; name=\"你好\"; filename=\"file abc.txt\"".as_bytes();
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(name.unwrap(), "你好");
assert_eq!(filename.unwrap(), "file abc.txt");
let val = "form-data; name=\"কখগ\"; filename=\"你好.txt\"".as_bytes();
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(name.unwrap(), "কখগ");
assert_eq!(filename.unwrap(), "你好.txt");
}
#[test]
fn test_content_disposition_file_name_only() {
// These are technically malformed, as RFC 7578 says the `name`
// parameter _must_ be included. But okay.
let val = br#"form-data; filename="file-name.txt""#;
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(filename.unwrap(), "file-name.txt");
assert!(name.is_none());
let val = "form-data; filename=\"কখগ-你好.txt\"".as_bytes();
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(filename.unwrap(), "কখগ-你好.txt");
assert!(name.is_none());
}
#[test]
fn test_content_distribution_misordered_fields() {
let val = br#"form-data; filename=file-name.txt; name=file"#;
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(filename.unwrap(), "file-name.txt");
assert_eq!(name.unwrap(), "file");
let val = br#"form-data; filename="file-name.txt"; name="file""#;
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(filename.unwrap(), "file-name.txt");
assert_eq!(name.unwrap(), "file");
let val = "form-data; filename=\"你好.txt\"; name=\"কখগ\"".as_bytes();
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(name.unwrap(), "কখগ");
assert_eq!(filename.unwrap(), "你好.txt");
}
#[test]
fn test_content_disposition_name_unquoted() {
let val = br#"form-data; name=my_field"#;
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(name.unwrap(), "my_field");
assert!(filename.is_none());
let val = br#"form-data; name=my_field; filename=file-name.txt"#;
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(name.unwrap(), "my_field");
assert_eq!(filename.unwrap(), "file-name.txt");
}
#[test]
fn test_content_disposition_name_quoted() {
let val = br#"form-data; name="my;f;ield""#;
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(name.unwrap(), "my;f;ield");
assert!(filename.is_none());
let val = br#"form-data; name=my_field; filename = "file;name.txt""#;
let name = ContentDispositionAttr::Name.extract_from(val);
assert_eq!(name.unwrap(), "my_field");
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(filename.unwrap(), "file;name.txt");
let val = br#"form-data; name=; filename=filename.txt"#;
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(name.unwrap(), "");
assert_eq!(filename.unwrap(), "filename.txt");
let val = br#"form-data; name=";"; filename=";""#;
let name = ContentDispositionAttr::Name.extract_from(val);
let filename = ContentDispositionAttr::FileName.extract_from(val);
assert_eq!(name.unwrap(), ";");
assert_eq!(filename.unwrap(), ";");
}
#[test]
fn test_content_disposition_name_escaped_quote() {
let val = br#"form-data; name="my\"field\"name""#;
let name = ContentDispositionAttr::Name.extract_from(val);
assert_eq!(name.unwrap(), r#"my"field"name"#);
let val = br#"form-data; name="myfield\"name""#;
let name = ContentDispositionAttr::Name.extract_from(val);
assert_eq!(name.unwrap(), r#"myfield"name"#);
}
}
================================================
FILE: src/constraints.rs
================================================
use crate::size_limit::SizeLimit;
/// Represents some rules to be applied on the stream and field's content size
/// to prevent DoS attacks.
///
/// It's recommended to add some rules on field (specially text field) size to
/// avoid potential DoS attacks 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_util::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 DoS 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::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());
/// ```
#[derive(Debug, Default)]
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`](crate::Multipart::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
}
}
}
================================================
FILE: src/content_disposition.rs
================================================
use http::header::{self, HeaderMap};
use crate::constants::ContentDispositionAttr;
#[derive(Debug)]
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).map(|val| val.as_bytes());
let field_name = content_disposition
.and_then(|val| ContentDispositionAttr::Name.extract_from(val))
.map(|attr| attr.into_owned());
let file_name = content_disposition
.and_then(|val| ContentDispositionAttr::FileName.extract_from(val))
.map(|attr| attr.into_owned());
ContentDisposition { field_name, file_name }
}
}
================================================
FILE: src/error.rs
================================================
use std::fmt::{self, Debug, Display, Formatter};
type BoxError = Box<dyn std::error::Error + Send + Sync>;
/// A set of errors that can occur during parsing multipart stream and in other
/// operations.
#[non_exhaustive]
pub enum Error {
/// An unknown field is detected when multipart
/// [`constraints`](crate::Constraints::allowed_fields) are added.
UnknownField { field_name: Option<String> },
/// The field data is found incomplete.
IncompleteFieldData { field_name: Option<String> },
/// Couldn't read the field headers completely.
IncompleteHeaders,
/// Failed to read headers.
ReadHeaderFailed(httparse::Error),
/// Failed to decode the field's raw header name to
/// [`HeaderName`](http::header::HeaderName) type.
DecodeHeaderName { name: String, cause: BoxError },
/// Failed to decode the field's raw header value to
/// [`HeaderValue`](http::header::HeaderValue) type.
DecodeHeaderValue { value: Vec<u8>, cause: BoxError },
/// Multipart stream is incomplete.
IncompleteStream,
/// The incoming field size exceeded the maximum limit.
FieldSizeExceeded { limit: u64, field_name: Option<String> },
/// The incoming stream size exceeded the maximum limit.
StreamSizeExceeded { limit: u64 },
/// Stream read failed.
StreamReadFailed(BoxError),
/// Failed to lock the multipart shared state for any changes.
LockFailure,
/// The `Content-Type` header is not `multipart/form-data`.
NoMultipart,
/// Failed to convert the `Content-Type` to [`mime::Mime`] type.
DecodeContentType(mime::FromStrError),
/// No boundary found in `Content-Type` header.
NoBoundary,
/// Failed to decode the field data as `JSON` in
/// [`field.json()`](crate::Field::json) method.
#[cfg(feature = "json")]
#[cfg_attr(nightly, doc(cfg(feature = "json")))]
DecodeJson(serde_json::Error),
}
impl Debug for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Error::UnknownField { field_name } => {
let name = field_name.as_deref().unwrap_or("<unknown>");
write!(f, "unknown field received: {:?}", name)
}
Error::IncompleteFieldData { field_name } => {
let name = field_name.as_deref().unwrap_or("<unknown>");
write!(f, "field {:?} received with incomplete data", name)
}
Error::DecodeHeaderName { name, .. } => {
write!(f, "failed to decode field's raw header name: {:?}", name)
}
Error::DecodeHeaderValue { .. } => {
write!(f, "failed to decode field's raw header value")
}
Error::FieldSizeExceeded { limit, field_name } => {
let name = field_name.as_deref().unwrap_or("<unknown>");
write!(f, "field {:?} exceeded the size limit: {} bytes", name, limit)
}
Error::StreamSizeExceeded { limit } => {
write!(f, "stream size exceeded limit: {} bytes", limit)
}
Error::ReadHeaderFailed(_) => write!(f, "failed to read headers"),
Error::StreamReadFailed(_) => write!(f, "failed to read stream"),
Error::DecodeContentType(_) => write!(f, "failed to decode Content-Type"),
Error::IncompleteHeaders => write!(f, "failed to read field complete headers"),
Error::IncompleteStream => write!(f, "incomplete multipart stream"),
Error::LockFailure => write!(f, "failed to lock multipart state"),
Error::NoMultipart => write!(f, "Content-Type is not multipart/form-data"),
Error::NoBoundary => write!(f, "multipart boundary not found in Content-Type"),
#[cfg(feature = "json")]
Error::DecodeJson(_) => write!(f, "failed to decode field data as JSON"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::ReadHeaderFailed(e) => Some(e),
Error::DecodeHeaderName { cause, .. } => Some(cause.as_ref()),
Error::DecodeHeaderValue { cause, .. } => Some(cause.as_ref()),
Error::StreamReadFailed(e) => Some(e.as_ref()),
Error::DecodeContentType(e) => Some(e),
#[cfg(feature = "json")]
Error::DecodeJson(e) => Some(e),
Error::UnknownField { .. }
| Error::IncompleteFieldData { .. }
| Error::IncompleteHeaders
| Error::IncompleteStream
| Error::FieldSizeExceeded { .. }
| Error::StreamSizeExceeded { .. }
| Error::LockFailure
| Error::NoMultipart
| Error::NoBoundary => None,
}
}
}
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
self.to_string().eq(&other.to_string())
}
}
impl Eq for Error {}
================================================
FILE: src/field.rs
================================================
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use bytes::{Bytes, BytesMut};
use encoding_rs::{Encoding, UTF_8};
use futures_util::stream::{Stream, TryStreamExt};
use http::header::HeaderMap;
#[cfg(feature = "json")]
use serde::de::DeserializeOwned;
use spin::mutex::spin::SpinMutex as Mutex;
use crate::content_disposition::ContentDisposition;
use crate::multipart::{MultipartState, StreamingStage};
use crate::{helpers, Error};
/// A single field in a multipart stream.
///
/// Its content can be accessed via the [`Stream`] API or the methods defined in
/// this type.
///
/// # Lifetime
///
/// The lifetime of the stream `'r` corresponds to the lifetime of the
/// underlying `Stream`. If the underlying stream holds no references directly
/// or transitively, then the lifetime can be `'static`.
///
/// # Examples
///
/// ```
/// use std::convert::Infallible;
///
/// use bytes::Bytes;
/// use futures_util::stream::once;
/// use multer::Multipart;
///
/// # 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 stream = once(async move { Result::<Bytes, Infallible>::Ok(Bytes::from(data)) });
/// let mut multipart = Multipart::new(stream, "X-BOUNDARY");
///
/// 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());
/// ```
///
/// [`Multipart`]: crate::Multipart
#[derive(Debug)]
pub struct Field<'r> {
state: Arc<Mutex<MultipartState<'r>>>,
done: bool,
headers: HeaderMap,
content_disposition: ContentDisposition,
content_type: Option<mime::Mime>,
idx: usize,
}
impl<'r> Field<'r> {
pub(crate) fn new(
state: Arc<Mutex<MultipartState<'r>>>,
headers: HeaderMap,
idx: usize,
content_disposition: ContentDisposition,
) -> Self {
let content_type = helpers::parse_content_type(&headers);
Field {
state,
headers,
content_disposition,
content_type,
idx,
done: false,
}
}
/// 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.content_disposition.field_name.as_deref()
}
/// 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.content_disposition.file_name.as_deref()
}
/// Get the content type of the field.
pub fn content_type(&self) -> Option<&mime::Mime> {
self.content_type.as_ref()
}
/// Get a map of headers as [`HeaderMap`].
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
/// Get the full data of the field as [`Bytes`].
///
/// # Examples
///
/// ```
/// use std::convert::Infallible;
///
/// use bytes::Bytes;
/// use futures_util::stream::once;
/// use multer::Multipart;
///
/// # 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 stream = once(async move { Result::<Bytes, Infallible>::Ok(Bytes::from(data)) });
/// let mut multipart = Multipart::new(stream, "X-BOUNDARY");
///
/// while let Some(field) = multipart.next_field().await.unwrap() {
/// let bytes = field.bytes().await.unwrap();
/// assert_eq!(bytes.len(), 4);
/// }
/// # }
/// # tokio::runtime::Runtime::new().unwrap().block_on(run());
/// ```
pub async fn bytes(self) -> crate::Result<Bytes> {
let mut buf = BytesMut::new();
let mut this = self;
while let Some(bytes) = this.chunk().await? {
buf.extend_from_slice(&bytes);
}
Ok(buf.freeze())
}
/// Stream a chunk of the field data.
///
/// When the field data has been exhausted, this will return [`None`].
///
/// # Examples
///
/// ```
/// use std::convert::Infallible;
///
/// use bytes::Bytes;
/// use futures_util::stream::once;
/// use multer::Multipart;
///
/// # 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 stream = once(async move { Result::<Bytes, Infallible>::Ok(Bytes::from(data)) });
/// let mut multipart = Multipart::new(stream, "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());
/// ```
pub async fn chunk(&mut self) -> crate::Result<Option<Bytes>> {
self.try_next().await
}
/// Try to deserialize the field data as JSON.
///
/// # Optional
///
/// This requires the optional `json` feature to be enabled.
///
/// # Examples
///
/// ```
/// use multer::Multipart;
/// use bytes::Bytes;
/// use std::convert::Infallible;
/// use futures_util::stream::once;
/// use serde::Deserialize;
///
/// // This `derive` requires the `serde` dependency.
/// #[derive(Deserialize)]
/// struct User {
/// name: String
/// }
///
/// # async fn run() {
/// 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::<Bytes, Infallible>::Ok(Bytes::from(data)) });
/// let mut multipart = Multipart::new(stream, "X-BOUNDARY");
///
/// while let Some(field) = multipart.next_field().await.unwrap() {
/// let user = field.json::<User>().await.unwrap();
/// println!("User Name: {}", user.name);
/// }
/// # }
/// # tokio::runtime::Runtime::new().unwrap().block_on(run());
/// ```
///
/// # Errors
///
/// This method fails if the field data is not in JSON format
/// or it cannot be properly deserialized to target type `T`. For more
/// details please see [`serde_json::from_slice`].
#[cfg(feature = "json")]
#[cfg_attr(nightly, doc(cfg(feature = "json")))]
pub async fn json<T: DeserializeOwned>(self) -> crate::Result<T> {
serde_json::from_slice(&self.bytes().await?).map_err(Error::DecodeJson)
}
/// Get the full field data as text.
///
/// This method decodes the field data with `BOM sniffing` and with
/// malformed sequences replaced with the `REPLACEMENT CHARACTER`.
/// Encoding is determined from the `charset` parameter of `Content-Type`
/// header, and defaults to `utf-8` if not presented.
///
/// # Examples
///
/// ```
/// use std::convert::Infallible;
///
/// use bytes::Bytes;
/// use futures_util::stream::once;
/// use multer::Multipart;
///
/// # 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 stream = once(async move { Result::<Bytes, Infallible>::Ok(Bytes::from(data)) });
/// let mut multipart = Multipart::new(stream, "X-BOUNDARY");
///
/// 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 async fn text(self) -> crate::Result<String> {
self.text_with_charset("utf-8").await
}
/// Get the full field data as text given a specific encoding.
///
/// This method decodes the field data with `BOM sniffing` and with
/// malformed sequences replaced with the `REPLACEMENT CHARACTER`.
/// You can provide a default encoding for decoding the raw message, while
/// the `charset` parameter of `Content-Type` header is still prioritized.
/// For more information about the possible encoding name, please go to
/// [encoding_rs] docs.
///
/// # Examples
///
/// ```
/// use std::convert::Infallible;
///
/// use bytes::Bytes;
/// use futures_util::stream::once;
/// use multer::Multipart;
///
/// # 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 stream = once(async move { Result::<Bytes, Infallible>::Ok(Bytes::from(data)) });
/// let mut multipart = Multipart::new(stream, "X-BOUNDARY");
///
/// while let Some(field) = multipart.next_field().await.unwrap() {
/// let content = field.text_with_charset("utf-8").await.unwrap();
/// assert_eq!(content, "abcd");
/// }
/// # }
/// # tokio::runtime::Runtime::new().unwrap().block_on(run());
/// ```
pub async fn text_with_charset(self, default_encoding: &str) -> crate::Result<String> {
let encoding_name = self
.content_type()
.and_then(|mime| mime.get_param(mime::CHARSET))
.map(|charset| charset.as_str())
.unwrap_or(default_encoding);
let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8);
let bytes = self.bytes().await?;
Ok(encoding.decode(&bytes).0.into_owned())
}
/// Get the index of this field in order they appeared in the stream.
///
/// # Examples
///
/// ```
/// use std::convert::Infallible;
///
/// use bytes::Bytes;
/// use futures_util::stream::once;
/// use multer::Multipart;
///
/// # 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 stream = once(async move { Result::<Bytes, Infallible>::Ok(Bytes::from(data)) });
/// let mut multipart = Multipart::new(stream, "X-BOUNDARY");
///
/// while let Some(field) = multipart.next_field().await.unwrap() {
/// let idx = field.index();
/// println!("Field index: {}", idx);
/// }
/// # }
/// # tokio::runtime::Runtime::new().unwrap().block_on(run());
/// ```
pub fn index(&self) -> usize {
self.idx
}
}
impl Stream for Field<'_> {
type Item = Result<Bytes, Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.done {
return Poll::Ready(None);
}
debug_assert!(self.state.try_lock().is_some(), "expected exlusive lock");
let state = self.state.clone();
let mut lock = match state.try_lock() {
Some(lock) => lock,
None => return Poll::Ready(Some(Err(Error::LockFailure))),
};
let state = &mut *lock;
if let Err(err) = state.buffer.poll_stream(cx) {
return Poll::Ready(Some(Err(err)));
}
match state
.buffer
.read_field_data(&state.boundary, state.curr_field_name.as_deref())
{
Ok(Some((done, bytes))) => {
state.curr_field_size_counter += bytes.len() as u64;
if state.curr_field_size_counter > state.curr_field_size_limit {
return Poll::Ready(Some(Err(Error::FieldSizeExceeded {
limit: state.curr_field_size_limit,
field_name: state.curr_field_name.clone(),
})));
}
if done {
state.stage = StreamingStage::ReadingBoundary;
self.done = true;
}
Poll::Ready(Some(Ok(bytes)))
}
Ok(None) => Poll::Pending,
Err(err) => Poll::Ready(Some(Err(err))),
}
}
}
================================================
FILE: src/helpers.rs
================================================
use std::convert::TryFrom;
use http::header::{self, HeaderMap, HeaderName, HeaderValue};
use httparse::Header;
pub(crate) fn convert_raw_headers_to_header_map(raw_headers: &[Header<'_>]) -> crate::Result<HeaderMap> {
let mut headers = HeaderMap::with_capacity(raw_headers.len());
for raw_header in raw_headers {
let name = HeaderName::try_from(raw_header.name).map_err(|err| crate::Error::DecodeHeaderName {
name: raw_header.name.to_owned(),
cause: err.into(),
})?;
let value = HeaderValue::try_from(raw_header.value).map_err(|err| crate::Error::DecodeHeaderValue {
value: raw_header.value.to_owned(),
cause: err.into(),
})?;
headers.insert(name, value);
}
Ok(headers)
}
pub(crate) fn parse_content_type(headers: &HeaderMap) -> Option<mime::Mime> {
headers
.get(header::CONTENT_TYPE)
.and_then(|val| val.to_str().ok())
.and_then(|val| val.parse::<mime::Mime>().ok())
}
================================================
FILE: src/lib.rs
================================================
//! An async parser for `multipart/form-data` content-type in Rust.
//!
//! It accepts a [`Stream`](futures_util::stream::Stream) of
//! [`Bytes`](bytes::Bytes), or with the `tokio-io` feature enabled, an
//! `AsyncRead` reader as a source, so that it can be plugged into any async
//! Rust environment e.g. any async server.
//!
//! To enable trace logging via the `log` crate, enable the `log` feature.
//!
//! # Examples
//!
//! ```no_run
//! use std::convert::Infallible;
//!
//! use bytes::Bytes;
//! // Import multer types.
//! use futures_util::stream::once;
//! use futures_util::stream::Stream;
//! use multer::Multipart;
//!
//! #[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 a `Multipart` instance from that byte stream and the boundary.
//! let mut multipart = Multipart::new(stream, boundary);
//!
//! // Iterate over the fields, use `next_field()` to get the next field.
//! while let Some(mut 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);
//!
//! // Process the field data chunks e.g. store them in a file.
//! while let Some(chunk) = field.chunk().await? {
//! // Do something with field chunk.
//! println!("Chunk: {:?}", chunk);
//! }
//! }
//!
//! 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\n";
//!
//! let stream = once(async move { Result::<Bytes, Infallible>::Ok(Bytes::from(data)) });
//! (stream, "X-BOUNDARY")
//! }
//! ```
//!
//! ## Prevent Denial of Service (DoS) Attack
//!
//! This crate also provides some APIs to prevent potential DoS attacks with
//! fine grained control. It's recommended to add some constraints
//! on field (specially text field) size to avoid potential DoS attacks from
//! attackers running the server out of memory.
//!
//! An example:
//!
//! ```
//! use multer::{Constraints, Multipart, SizeLimit};
//! # use bytes::Bytes;
//! # use std::convert::Infallible;
//! # use futures_util::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 DoS 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::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());
//! ```
//!
//! Please refer [`Constraints`] 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).
#![forbid(unsafe_code)]
#![warn(
missing_debug_implementations,
rust_2018_idioms,
trivial_casts,
unused_qualifications
)]
#![cfg_attr(nightly, feature(doc_cfg))]
#![doc(test(attr(deny(rust_2018_idioms, warnings))))]
#![doc(test(attr(allow(unused_extern_crates, unused_variables))))]
pub use bytes;
pub use constraints::Constraints;
pub use error::Error;
pub use field::Field;
pub use multipart::Multipart;
pub use size_limit::SizeLimit;
#[cfg(feature = "log")]
macro_rules! trace {
($($t:tt)*) => (::log::trace!($($t)*););
}
#[cfg(not(feature = "log"))]
macro_rules! trace {
($($t:tt)*) => {};
}
mod buffer;
mod constants;
mod constraints;
mod content_disposition;
mod error;
mod field;
mod helpers;
mod multipart;
mod size_limit;
/// A Result type often returned from methods that can have `multer` errors.
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Parses the `Content-Type` header to extract the boundary value.
///
/// # Examples
///
/// ```
/// # fn run(){
/// let content_type = "multipart/form-data; boundary=ABCDEFG";
///
/// assert_eq!(
/// multer::parse_boundary(content_type),
/// Ok("ABCDEFG".to_owned())
/// );
/// # }
/// # run();
/// ```
pub fn parse_boundary<T: AsRef<str>>(content_type: T) -> Result<String> {
let m = content_type
.as_ref()
.parse::<mime::Mime>()
.map_err(Error::DecodeContentType)?;
if !(m.type_() == mime::MULTIPART && m.subtype() == mime::FORM_DATA) {
return Err(Error::NoMultipart);
}
m.get_param(mime::BOUNDARY)
.map(|name| name.as_str().to_owned())
.ok_or(Error::NoBoundary)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_boundary() {
let content_type = "multipart/form-data; boundary=ABCDEFG";
assert_eq!(parse_boundary(content_type), Ok("ABCDEFG".to_owned()));
let content_type = "multipart/form-data; boundary=------ABCDEFG";
assert_eq!(parse_boundary(content_type), Ok("------ABCDEFG".to_owned()));
let content_type = "boundary=------ABCDEFG";
assert!(parse_boundary(content_type).is_err());
let content_type = "text/plain";
assert!(parse_boundary(content_type).is_err());
let content_type = "text/plain; boundary=------ABCDEFG";
assert!(parse_boundary(content_type).is_err());
}
}
================================================
FILE: src/multipart.rs
================================================
use std::sync::Arc;
use std::task::{Context, Poll};
use bytes::Bytes;
use futures_util::future;
use futures_util::stream::{Stream, TryStreamExt};
use spin::mutex::spin::SpinMutex as Mutex;
#[cfg(feature = "tokio-io")]
use {tokio::io::AsyncRead, tokio_util::io::ReaderStream};
use crate::buffer::StreamBuffer;
use crate::constraints::Constraints;
use crate::content_disposition::ContentDisposition;
use crate::error::Error;
use crate::field::Field;
use crate::{constants, helpers, Result};
/// Represents the implementation of `multipart/form-data` formatted data.
///
/// This will parse the source stream into [`Field`] instances via
/// [`next_field()`](Self::next_field).
///
/// # Field Exclusivity
///
/// A `Field` represents a raw, self-decoding stream into multipart data. As
/// such, only _one_ `Field` from a given `Multipart` instance may be live at
/// once. That is, a `Field` emitted by `next_field()` must be dropped before
/// calling `next_field()` again. Failure to do so will result in an error.
///
/// ```rust
/// use std::convert::Infallible;
///
/// use bytes::Bytes;
/// use futures_util::stream::once;
/// use multer::Multipart;
///
/// # 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 stream = once(async move { Result::<Bytes, Infallible>::Ok(Bytes::from(data)) });
/// let mut multipart = Multipart::new(stream, "X-BOUNDARY");
///
/// let field1 = multipart.next_field().await;
/// let field2 = multipart.next_field().await;
///
/// assert!(field2.is_err());
/// # }
/// # tokio::runtime::Runtime::new().unwrap().block_on(run());
/// ```
///
/// # Examples
///
/// ```
/// use std::convert::Infallible;
///
/// use bytes::Bytes;
/// use futures_util::stream::once;
/// use multer::Multipart;
///
/// # 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 stream = once(async move { Result::<Bytes, Infallible>::Ok(Bytes::from(data)) });
/// let mut multipart = Multipart::new(stream, "X-BOUNDARY");
///
/// while let Some(field) = multipart.next_field().await.unwrap() {
/// println!("Field: {:?}", field.text().await)
/// }
/// # }
/// # tokio::runtime::Runtime::new().unwrap().block_on(run());
/// ```
#[derive(Debug)]
pub struct Multipart<'r> {
state: Arc<Mutex<MultipartState<'r>>>,
}
#[derive(Debug)]
pub(crate) struct MultipartState<'r> {
pub(crate) buffer: StreamBuffer<'r>,
pub(crate) boundary: String,
pub(crate) stage: StreamingStage,
pub(crate) next_field_idx: usize,
pub(crate) curr_field_name: Option<String>,
pub(crate) curr_field_size_limit: u64,
pub(crate) curr_field_size_counter: u64,
pub(crate) constraints: Constraints,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StreamingStage {
FindingFirstBoundary,
ReadingBoundary,
DeterminingBoundaryType,
ReadingTransportPadding,
ReadingFieldHeaders,
ReadingFieldData,
Eof,
}
impl<'r> Multipart<'r> {
/// Construct a new `Multipart` instance with the given [`Bytes`] stream and
/// the boundary.
pub fn new<S, O, E, B>(stream: S, boundary: B) -> Self
where
S: Stream<Item = Result<O, E>> + Send + 'r,
O: Into<Bytes> + 'static,
E: Into<Box<dyn std::error::Error + Send + Sync>> + 'r,
B: Into<String>,
{
Multipart::with_constraints(stream, boundary, Constraints::default())
}
/// Construct a new `Multipart` instance with the given [`Bytes`] stream and
/// the boundary.
pub fn with_constraints<S, O, E, B>(stream: S, boundary: B, constraints: Constraints) -> Self
where
S: Stream<Item = Result<O, E>> + Send + 'r,
O: Into<Bytes> + 'static,
E: Into<Box<dyn std::error::Error + Send + Sync>> + 'r,
B: Into<String>,
{
let stream = stream
.map_ok(|b| b.into())
.map_err(|err| Error::StreamReadFailed(err.into()));
Multipart {
state: Arc::new(Mutex::new(MultipartState {
buffer: StreamBuffer::new(stream, constraints.size_limit.whole_stream),
boundary: boundary.into(),
stage: StreamingStage::FindingFirstBoundary,
next_field_idx: 0,
curr_field_name: None,
curr_field_size_limit: constraints.size_limit.per_field,
curr_field_size_counter: 0,
constraints,
})),
}
}
/// Construct a new `Multipart` instance with the given [`AsyncRead`] reader
/// and the boundary.
///
/// # Optional
///
/// This requires the optional `tokio-io` feature to be enabled.
///
/// # Examples
///
/// ```
/// use multer::Multipart;
///
/// # 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 = "tokio-io")]
#[cfg_attr(nightly, doc(cfg(feature = "tokio-io")))]
pub fn with_reader<R, B>(reader: R, boundary: B) -> Self
where
R: AsyncRead + Unpin + Send + 'r,
B: Into<String>,
{
let stream = ReaderStream::new(reader);
Multipart::new(stream, boundary)
}
/// Construct a new `Multipart` instance with the given [`AsyncRead`] reader
/// and the boundary.
///
/// # Optional
///
/// This requires the optional `tokio-io` feature to be enabled.
///
/// # Examples
///
/// ```
/// use multer::Multipart;
///
/// # 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 = "tokio-io")]
#[cfg_attr(nightly, doc(cfg(feature = "tokio-io")))]
pub fn with_reader_with_constraints<R, B>(reader: R, boundary: B, constraints: Constraints) -> Self
where
R: AsyncRead + Unpin + Send + 'r,
B: Into<String>,
{
let stream = ReaderStream::new(reader);
Multipart::with_constraints(stream, boundary, constraints)
}
/// Yields the next [`Field`] if available.
///
/// Any previous `Field` returned by this method must be dropped before
/// calling this method or [`Multipart::next_field_with_idx()`] again. See
/// [field-exclusivity](#field-exclusivity) for details.
pub async fn next_field(&mut self) -> Result<Option<Field<'r>>> {
future::poll_fn(|cx| self.poll_next_field(cx)).await
}
/// Yields the next [`Field`] if available.
///
/// Any previous `Field` returned by this method must be dropped before
/// calling this method or [`Multipart::next_field_with_idx()`] again. See
/// [field-exclusivity](#field-exclusivity) for details.
///
/// This method is available since version 2.1.0.
pub fn poll_next_field(&mut self, cx: &mut Context<'_>) -> Poll<Result<Option<Field<'r>>>> {
// This is consistent as we have an `&mut` and `Field` is not `Clone`.
// Here, we are guaranteeing that the returned `Field` will be the
// _only_ field with access to the multipart parsing state. This ensure
// that lock failure can never occur. This is effectively a dynamic
// version of passing an `&mut` of `self` to the `Field`.
if Arc::strong_count(&self.state) != 1 {
return Poll::Ready(Err(Error::LockFailure));
}
debug_assert_eq!(Arc::strong_count(&self.state), 1);
debug_assert!(self.state.try_lock().is_some(), "expected exlusive lock");
let mut lock = match self.state.try_lock() {
Some(lock) => lock,
None => return Poll::Ready(Err(Error::LockFailure)),
};
let state = &mut *lock;
if state.stage == StreamingStage::Eof {
return Poll::Ready(Ok(None));
}
state.buffer.poll_stream(cx)?;
if state.stage == StreamingStage::FindingFirstBoundary {
let boundary = &state.boundary;
let boundary_deriv = format!("{}{}", constants::BOUNDARY_EXT, boundary);
match state.buffer.read_to(boundary_deriv.as_bytes()) {
Some(_) => state.stage = StreamingStage::ReadingBoundary,
None => {
state.buffer.poll_stream(cx)?;
if state.buffer.eof {
return Poll::Ready(Err(Error::IncompleteStream));
}
}
}
}
// The previous field did not finish reading its data.
if state.stage == StreamingStage::ReadingFieldData {
match state
.buffer
.read_field_data(state.boundary.as_str(), state.curr_field_name.as_deref())?
{
Some((done, bytes)) => {
state.curr_field_size_counter += bytes.len() as u64;
if state.curr_field_size_counter > state.curr_field_size_limit {
return Poll::Ready(Err(Error::FieldSizeExceeded {
limit: state.curr_field_size_limit,
field_name: state.curr_field_name.clone(),
}));
}
if done {
state.stage = StreamingStage::ReadingBoundary;
} else {
return Poll::Pending;
}
}
None => {
return Poll::Pending;
}
}
}
if state.stage == StreamingStage::ReadingBoundary {
let boundary = &state.boundary;
let boundary_deriv_len = constants::BOUNDARY_EXT.len() + boundary.len();
let boundary_bytes = match state.buffer.read_exact(boundary_deriv_len) {
Some(bytes) => bytes,
None => {
return if state.buffer.eof {
Poll::Ready(Err(Error::IncompleteStream))
} else {
Poll::Pending
};
}
};
if &boundary_bytes[..] == format!("{}{}", constants::BOUNDARY_EXT, boundary).as_bytes() {
state.stage = StreamingStage::DeterminingBoundaryType;
} else {
return Poll::Ready(Err(Error::IncompleteStream));
}
}
if state.stage == StreamingStage::DeterminingBoundaryType {
let ext_len = constants::BOUNDARY_EXT.len();
let next_bytes = match state.buffer.peek_exact(ext_len) {
Some(bytes) => bytes,
None => {
return if state.buffer.eof {
Poll::Ready(Err(Error::IncompleteStream))
} else {
Poll::Pending
};
}
};
if next_bytes == constants::BOUNDARY_EXT.as_bytes() {
state.stage = StreamingStage::Eof;
return Poll::Ready(Ok(None));
} else {
state.stage = StreamingStage::ReadingTransportPadding;
}
}
if state.stage == StreamingStage::ReadingTransportPadding {
if !state.buffer.advance_past_transport_padding() {
return if state.buffer.eof {
Poll::Ready(Err(Error::IncompleteStream))
} else {
Poll::Pending
};
}
let crlf_len = constants::CRLF.len();
let crlf_bytes = match state.buffer.read_exact(crlf_len) {
Some(bytes) => bytes,
None => {
return if state.buffer.eof {
Poll::Ready(Err(Error::IncompleteStream))
} else {
Poll::Pending
};
}
};
if &crlf_bytes[..] == constants::CRLF.as_bytes() {
state.stage = StreamingStage::ReadingFieldHeaders;
} else {
return Poll::Ready(Err(Error::IncompleteStream));
}
}
if state.stage == StreamingStage::ReadingFieldHeaders {
let header_bytes = match state.buffer.read_until(constants::CRLF_CRLF.as_bytes()) {
Some(bytes) => bytes,
None => {
return if state.buffer.eof {
return Poll::Ready(Err(Error::IncompleteStream));
} else {
Poll::Pending
};
}
};
let mut headers = [httparse::EMPTY_HEADER; constants::MAX_HEADERS];
let headers = match httparse::parse_headers(&header_bytes, &mut headers).map_err(Error::ReadHeaderFailed)? {
httparse::Status::Complete((_, raw_headers)) => {
match helpers::convert_raw_headers_to_header_map(raw_headers) {
Ok(headers) => headers,
Err(err) => {
return Poll::Ready(Err(err));
}
}
}
httparse::Status::Partial => {
return Poll::Ready(Err(Error::IncompleteHeaders));
}
};
state.stage = StreamingStage::ReadingFieldData;
let field_idx = state.next_field_idx;
state.next_field_idx += 1;
let content_disposition = ContentDisposition::parse(&headers);
let field_size_limit = state
.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;
let field_name = content_disposition.field_name.as_deref();
if !state.constraints.is_it_allowed(field_name) {
return Poll::Ready(Err(Error::UnknownField {
field_name: field_name.map(str::to_owned),
}));
}
drop(lock); // The lock will be dropped anyway, but let's be explicit.
let field = Field::new(self.state.clone(), headers, field_idx, content_disposition);
return Poll::Ready(Ok(Some(field)));
}
Poll::Pending
}
/// Yields the next [`Field`] with their positioning index as a tuple
/// `(`[`usize`]`, `[`Field`]`)`.
///
/// Any previous `Field` returned by this method must be dropped before
/// calling this method or [`Multipart::next_field()`] again. See
/// [field-exclusivity](#field-exclusivity) for details.
///
/// # Examples
///
/// ```
/// use std::convert::Infallible;
///
/// use bytes::Bytes;
/// use futures_util::stream::once;
/// use multer::Multipart;
///
/// # 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 stream = once(async move { Result::<Bytes, Infallible>::Ok(Bytes::from(data)) });
/// let mut multipart = Multipart::new(stream, "X-BOUNDARY");
///
/// while let Some((idx, field)) = multipart.next_field_with_idx().await.unwrap() {
/// println!("Index: {:?}, Content: {:?}", idx, field.text().await)
/// }
/// # }
/// # tokio::runtime::Runtime::new().unwrap().block_on(run());
/// ```
pub async fn next_field_with_idx(&mut self) -> Result<Option<(usize, Field<'r>)>> {
self.next_field().await.map(|f| f.map(|field| (field.index(), field)))
}
}
================================================
FILE: src/size_limit.rs
================================================
use std::collections::HashMap;
use crate::constants;
/// Represents size limit of the stream to prevent DoS attacks.
///
/// Please refer [`Constraints`](crate::Constraints) for more info.
#[derive(Debug)]
pub struct SizeLimit {
pub(crate) whole_stream: u64,
pub(crate) per_field: u64,
pub(crate) field_map: HashMap<String, u64>,
}
impl SizeLimit {
/// Creates a default size limit which is [`u64::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: u64) -> SizeLimit {
self.whole_stream = limit;
self
}
/// Sets size limit for each field.
pub fn per_field(mut self, limit: u64) -> SizeLimit {
self.per_field = limit;
self
}
/// Sets size limit for a specific field, it overrides the
/// [`per_field`](Self::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 DoS attacks from
/// attackers running the server out of memory.
pub fn for_field<N: Into<String>>(mut self, field_name: N, limit: u64) -> SizeLimit {
self.field_map.insert(field_name.into(), limit);
self
}
pub(crate) fn extract_size_limit_for(&self, field: Option<&str>) -> u64 {
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(),
}
}
}
================================================
FILE: tests/integration.rs
================================================
use bytes::Bytes;
use futures_util::{stream, Stream};
use multer::{Constraints, Multipart, SizeLimit};
fn str_stream(string: &'static str) -> impl Stream<Item = multer::Result<Bytes>> {
stream::iter(
string
.chars()
.map(|ch| ch.to_string())
.map(|part| Ok(Bytes::copy_from_slice(part.as_bytes()))),
)
}
#[tokio::test]
async fn test_multipart_basic() {
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 = str_stream(data);
let mut m = Multipart::new(stream, "X-BOUNDARY");
while let Some((idx, field)) = m.next_field_with_idx().await.unwrap() {
if idx == 0 {
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("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);
assert_eq!(field.text().await, Ok("Hello world\nHello\r\nWorld\rAgain".to_owned()));
}
}
}
#[tokio::test]
async fn test_multipart_empty() {
let data = "--X-BOUNDARY--\r\n";
let stream = str_stream(data);
let mut m = Multipart::new(stream, "X-BOUNDARY");
assert!(m.next_field().await.unwrap().is_none());
assert!(m.next_field().await.unwrap().is_none());
}
#[tokio::test]
async fn test_multipart_clean_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 = str_stream(data);
let mut m = Multipart::new(stream, "X-BOUNDARY");
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_transport_padding() {
let data = "--X-BOUNDARY \t \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--\t\t\t\t\t\r\n";
let stream = str_stream(data);
let mut m = Multipart::new(stream, "X-BOUNDARY");
assert!(m.next_field().await.unwrap().is_some());
assert!(m.next_field().await.unwrap().is_some());
assert!(m.next_field().await.unwrap().is_none());
let bad_data = "--X-BOUNDARY \t \r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARYzz \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--\t\t\t\t\t\r\n";
let bad_stream = str_stream(bad_data);
let mut m = Multipart::new(bad_stream, "X-BOUNDARY");
assert!(m.next_field().await.unwrap().is_some());
assert!(m.next_field().await.is_err());
}
#[tokio::test]
async fn test_multipart_header() {
let should_pass = [
"ignored header\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n",
"\r\nignored header\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n",
"\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n",
"\r\n\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"my_text_field\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n",
];
for data in should_pass.iter() {
let stream = str_stream(data);
let mut m = Multipart::new(stream, "X-BOUNDARY");
assert_eq!(
m.next_field().await.unwrap().unwrap().text().await.unwrap(),
"abcd".to_owned()
);
}
}
#[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 = str_stream(data);
let constraints = Constraints::new().allowed_fields(vec!["my_text_field", "my_file_field"]);
let mut m = Multipart::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 = str_stream(data);
let constraints = Constraints::new().allowed_fields(vec!["my_text_field"]);
let mut m = Multipart::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 = str_stream(data);
let constraints = Constraints::new()
.allowed_fields(vec!["my_text_field", "my_file_field"])
.size_limit(SizeLimit::new().whole_stream(248));
let mut m = Multipart::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 = str_stream(data);
let constraints = Constraints::new()
.allowed_fields(vec!["my_text_field", "my_file_field"])
.size_limit(SizeLimit::new().whole_stream(100));
let mut m = Multipart::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 = str_stream(data);
let constraints = Constraints::new()
.allowed_fields(vec!["my_text_field", "my_file_field"])
.size_limit(SizeLimit::new().whole_stream(248).per_field(100));
let mut m = Multipart::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 = str_stream(data);
let constraints = Constraints::new()
.allowed_fields(vec!["my_text_field", "my_file_field"])
.size_limit(SizeLimit::new().whole_stream(248).per_field(10));
let mut m = Multipart::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 = str_stream(data);
let constraints = Constraints::new()
.allowed_fields(vec!["my_text_field", "my_file_field"])
.size_limit(
SizeLimit::new()
.whole_stream(248)
.per_field(100)
.for_field("my_text_field", 4)
.for_field("my_file_field", 30),
);
let mut m = Multipart::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 = str_stream(data);
let constraints = Constraints::new()
.allowed_fields(vec!["my_text_field", "my_file_field"])
.size_limit(
SizeLimit::new()
.whole_stream(248)
.per_field(100)
.for_field("my_text_field", 4)
.for_field("my_file_field", 10),
);
let mut m = Multipart::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_multiaccess_caught() {
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 = str_stream(data);
let mut m = Multipart::new(stream, "X-BOUNDARY");
let field1 = m.next_field().await;
let field2 = m.next_field().await;
assert!(matches!(field2.unwrap_err(), multer::Error::LockFailure));
assert!(field1.is_ok());
}
================================================
FILE: tusk.yml
================================================
options:
version:
usage: The next release version
short: v
required: true
tasks:
setup:
run:
- command: cargo install cargo-watch
- command: cargo install releez
check:dev:
run:
- command: cargo watch --watch ./src -x 'check --features="all"'
doc:dev:
run:
- command: cargo doc --open --features="all"
- command: cargo watch -x 'doc --features="all"'
test:dev:
run:
- command: cargo watch -x 'test --features="all"'
release:
run:
- command: releez "${version}"
gitextract_jlvbhjbj/ ├── .github/ │ └── workflows/ │ └── test.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── examples/ │ ├── README.md │ ├── hyper_server_example.rs │ ├── parse_async_read.rs │ ├── prevent_dos_attack.rs │ └── simple_example.rs ├── fuzz/ │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── corpus/ │ │ └── fuzz_multipart_bytes/ │ │ ├── multi.seed │ │ ├── multi2.seed │ │ ├── simple.seed │ │ ├── simple2.seed │ │ ├── simple3.seed │ │ └── single.seed │ └── fuzz_targets/ │ └── fuzz_multipart_bytes.rs ├── releez.yml ├── rustfmt.toml ├── src/ │ ├── buffer.rs │ ├── constants.rs │ ├── constraints.rs │ ├── content_disposition.rs │ ├── error.rs │ ├── field.rs │ ├── helpers.rs │ ├── lib.rs │ ├── multipart.rs │ └── size_limit.rs ├── tests/ │ └── integration.rs └── tusk.yml
SYMBOL INDEX (105 symbols across 17 files)
FILE: build.rs
function main (line 1) | fn main() {
FILE: examples/hyper_server_example.rs
function handle (line 11) | async fn handle(req: Request<Incoming>) -> Result<Response<Full<Bytes>>,...
function process_multipart (line 39) | async fn process_multipart(body: Incoming, boundary: String) -> multer::...
function main (line 78) | async fn main() {
FILE: examples/parse_async_read.rs
function main (line 5) | async fn main() -> Result<(), Box<dyn std::error::Error>> {
function get_async_reader_from_somewhere (line 37) | async fn get_async_reader_from_somewhere() -> (impl AsyncRead, &'static ...
FILE: examples/prevent_dos_attack.rs
function main (line 9) | async fn main() -> Result<(), Box<dyn std::error::Error>> {
function get_byte_stream_from_somewhere (line 51) | async fn get_byte_stream_from_somewhere() -> (impl Stream<Item = Result<...
FILE: examples/simple_example.rs
function main (line 9) | async fn main() -> Result<(), Box<dyn std::error::Error>> {
function get_byte_stream_from_somewhere (line 36) | async fn get_byte_stream_from_somewhere() -> (impl Stream<Item = Result<...
FILE: fuzz/fuzz_targets/fuzz_multipart_bytes.rs
constant FIELD_TIMEOUT (line 12) | const FIELD_TIMEOUT: Duration = Duration::from_millis(10);
FILE: src/buffer.rs
type StreamBuffer (line 10) | pub(crate) struct StreamBuffer<'r> {
function new (line 19) | pub fn new<S>(stream: S, whole_stream_size_limit: u64) -> Self
function poll_stream (line 32) | pub fn poll_stream(&mut self, cx: &mut Context<'_>) -> Result<(), crate:...
function read_exact (line 60) | pub fn read_exact(&mut self, size: usize) -> Option<Bytes> {
function peek_exact (line 68) | pub fn peek_exact(&mut self, size: usize) -> Option<&[u8]> {
function read_until (line 72) | pub fn read_until(&mut self, pattern: &[u8]) -> Option<Bytes> {
function read_to (line 76) | pub fn read_to(&mut self, pattern: &[u8]) -> Option<Bytes> {
function advance_past_transport_padding (line 80) | pub fn advance_past_transport_padding(&mut self) -> bool {
function read_field_data (line 93) | pub fn read_field_data(
function read_full_buf (line 160) | pub fn read_full_buf(&mut self) -> Bytes {
function fmt (line 166) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
FILE: src/constants.rs
constant DEFAULT_WHOLE_STREAM_SIZE_LIMIT (line 3) | pub(crate) const DEFAULT_WHOLE_STREAM_SIZE_LIMIT: u64 = std::u64::MAX;
constant DEFAULT_PER_FIELD_SIZE_LIMIT (line 4) | pub(crate) const DEFAULT_PER_FIELD_SIZE_LIMIT: u64 = std::u64::MAX;
constant MAX_HEADERS (line 6) | pub(crate) const MAX_HEADERS: usize = 32;
constant BOUNDARY_EXT (line 7) | pub(crate) const BOUNDARY_EXT: &str = "--";
constant CR (line 8) | pub(crate) const CR: &str = "\r";
constant LF (line 10) | pub(crate) const LF: &str = "\n";
constant CRLF (line 11) | pub(crate) const CRLF: &str = "\r\n";
constant CRLF_CRLF (line 12) | pub(crate) const CRLF_CRLF: &str = "\r\n\r\n";
type ContentDispositionAttr (line 15) | pub(crate) enum ContentDispositionAttr {
method extract_from (line 40) | pub fn extract_from<'h>(&self, mut header: &'h [u8]) -> Option<Cow<'h,...
function trim_ascii_ws_start (line 20) | fn trim_ascii_ws_start(bytes: &[u8]) -> &[u8] {
function trim_ascii_ws_then (line 27) | fn trim_ascii_ws_then(bytes: &[u8], char: u8) -> Option<&[u8]> {
function test_content_disposition_name_only (line 86) | fn test_content_disposition_name_only() {
function test_content_disposition_extraction (line 113) | fn test_content_disposition_extraction() {
function test_content_disposition_file_name_only (line 134) | fn test_content_disposition_file_name_only() {
function test_content_distribution_misordered_fields (line 151) | fn test_content_distribution_misordered_fields() {
function test_content_disposition_name_unquoted (line 172) | fn test_content_disposition_name_unquoted() {
function test_content_disposition_name_quoted (line 187) | fn test_content_disposition_name_quoted() {
function test_content_disposition_name_escaped_quote (line 214) | fn test_content_disposition_name_escaped_quote() {
FILE: src/constraints.rs
type Constraints (line 49) | pub struct Constraints {
method new (line 56) | pub fn new() -> Constraints {
method size_limit (line 61) | pub fn size_limit(self, size_limit: SizeLimit) -> Constraints {
method allowed_fields (line 70) | pub fn allowed_fields<N: Into<String>>(self, allowed_fields: Vec<N>) -...
method is_it_allowed (line 79) | pub(crate) fn is_it_allowed(&self, field: Option<&str>) -> bool {
FILE: src/content_disposition.rs
type ContentDisposition (line 6) | pub(crate) struct ContentDisposition {
method parse (line 12) | pub fn parse(headers: &HeaderMap) -> ContentDisposition {
FILE: src/error.rs
type BoxError (line 3) | type BoxError = Box<dyn std::error::Error + Send + Sync>;
type Error (line 8) | pub enum Error {
method source (line 106) | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
method fmt (line 62) | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
method fmt (line 68) | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
method eq (line 129) | fn eq(&self, other: &Self) -> bool {
FILE: src/field.rs
type Field (line 54) | pub struct Field<'r> {
function new (line 64) | pub(crate) fn new(
function name (line 82) | pub fn name(&self) -> Option<&str> {
function file_name (line 87) | pub fn file_name(&self) -> Option<&str> {
function content_type (line 92) | pub fn content_type(&self) -> Option<&mime::Mime> {
function headers (line 97) | pub fn headers(&self) -> &HeaderMap {
function bytes (line 125) | pub async fn bytes(self) -> crate::Result<Bytes> {
function chunk (line 163) | pub async fn chunk(&mut self) -> crate::Result<Option<Bytes>> {
function json (line 208) | pub async fn json<T: DeserializeOwned>(self) -> crate::Result<T> {
function text (line 241) | pub async fn text(self) -> crate::Result<String> {
function text_with_charset (line 276) | pub async fn text_with_charset(self, default_encoding: &str) -> crate::R...
function index (line 312) | pub fn index(&self) -> usize {
type Item (line 318) | type Item = Result<Bytes, Error>;
method poll_next (line 320) | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Opt...
FILE: src/helpers.rs
function convert_raw_headers_to_header_map (line 6) | pub(crate) fn convert_raw_headers_to_header_map(raw_headers: &[Header<'_...
function parse_content_type (line 26) | pub(crate) fn parse_content_type(headers: &HeaderMap) -> Option<mime::Mi...
FILE: src/lib.rs
type Result (line 151) | pub type Result<T, E = Error> = std::result::Result<T, E>;
function parse_boundary (line 168) | pub fn parse_boundary<T: AsRef<str>>(content_type: T) -> Result<String> {
function test_parse_boundary (line 188) | fn test_parse_boundary() {
FILE: src/multipart.rs
type Multipart (line 75) | pub struct Multipart<'r> {
type MultipartState (line 80) | pub(crate) struct MultipartState<'r> {
type StreamingStage (line 92) | pub(crate) enum StreamingStage {
function new (line 105) | pub fn new<S, O, E, B>(stream: S, boundary: B) -> Self
function with_constraints (line 117) | pub fn with_constraints<S, O, E, B>(stream: S, boundary: B, constraints:...
function with_reader (line 170) | pub fn with_reader<R, B>(reader: R, boundary: B) -> Self
function with_reader_with_constraints (line 207) | pub fn with_reader_with_constraints<R, B>(reader: R, boundary: B, constr...
function next_field (line 221) | pub async fn next_field(&mut self) -> Result<Option<Field<'r>>> {
function poll_next_field (line 232) | pub fn poll_next_field(&mut self, cx: &mut Context<'_>) -> Poll<Result<O...
function next_field_with_idx (line 456) | pub async fn next_field_with_idx(&mut self) -> Result<Option<(usize, Fie...
FILE: src/size_limit.rs
type SizeLimit (line 9) | pub struct SizeLimit {
method new (line 18) | pub fn new() -> SizeLimit {
method whole_stream (line 23) | pub fn whole_stream(mut self, limit: u64) -> SizeLimit {
method per_field (line 29) | pub fn per_field(mut self, limit: u64) -> SizeLimit {
method for_field (line 40) | pub fn for_field<N: Into<String>>(mut self, field_name: N, limit: u64)...
method extract_size_limit_for (line 45) | pub(crate) fn extract_size_limit_for(&self, field: Option<&str>) -> u64 {
method default (line 54) | fn default() -> Self {
FILE: tests/integration.rs
function str_stream (line 5) | fn str_stream(string: &'static str) -> impl Stream<Item = multer::Result...
function test_multipart_basic (line 15) | async fn test_multipart_basic() {
function test_multipart_empty (line 40) | async fn test_multipart_empty() {
function test_multipart_clean_field (line 50) | async fn test_multipart_clean_field() {
function test_multipart_transport_padding (line 62) | async fn test_multipart_transport_padding() {
function test_multipart_header (line 80) | async fn test_multipart_header() {
function test_multipart_constraint_allowed_fields_normal (line 100) | async fn test_multipart_constraint_allowed_fields_normal() {
function test_multipart_constraint_allowed_fields_unknown_field (line 119) | async fn test_multipart_constraint_allowed_fields_unknown_field() {
function test_multipart_constraint_size_limit_whole_stream (line 132) | async fn test_multipart_constraint_size_limit_whole_stream() {
function test_multipart_constraint_size_limit_whole_stream_size_exceeded (line 154) | async fn test_multipart_constraint_size_limit_whole_stream_size_exceeded...
function test_multipart_constraint_size_limit_per_field (line 170) | async fn test_multipart_constraint_size_limit_per_field() {
function test_multipart_constraint_size_limit_per_field_size_exceeded (line 192) | async fn test_multipart_constraint_size_limit_per_field_size_exceeded() {
function test_multipart_constraint_size_limit_for_field (line 208) | async fn test_multipart_constraint_size_limit_for_field() {
function test_multipart_constraint_size_limit_for_field_size_exceeded (line 236) | async fn test_multipart_constraint_size_limit_for_field_size_exceeded() {
function test_multiaccess_caught (line 258) | async fn test_multiaccess_caught() {
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (107K chars).
[
{
"path": ".github/workflows/test.yml",
"chars": 931,
"preview": "name: CI\n\non:\n pull_request:\n push:\n branches:\n - master\n - develop\n\nenv:\n RUSTFLAGS: -Dwarnings\n\njobs:\n"
},
{
"path": ".gitignore",
"chars": 964,
"preview": "# Created by https://www.gitignore.io/api/rust,macos\n# Edit at https://www.gitignore.io/?templates=rust,macos\n\n### macOS"
},
{
"path": "Cargo.toml",
"chars": 1612,
"preview": "[package]\nname = \"multer\"\nversion = \"3.1.0\"\ndescription = \"An async parser for `multipart/form-data` content-type in Rus"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2020 Rousan Ali\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 4315,
"preview": "[](https://github.com/"
},
{
"path": "build.rs",
"chars": 129,
"preview": "fn main() {\n if let Some(true) = version_check::is_feature_flaggable() {\n println!(\"cargo:rustc-cfg=nightly\");"
},
{
"path": "examples/README.md",
"chars": 753,
"preview": "# Examples of using multer-rs\n\nThese examples show of how to do common tasks using `multer-rs`.\n\nPlease visit: [Docs](ht"
},
{
"path": "examples/hyper_server_example.rs",
"chars": 3387,
"preview": "use std::{convert::Infallible, net::SocketAddr};\n\nuse bytes::Bytes;\nuse futures_util::StreamExt;\nuse http_body_util::{Bo"
},
{
"path": "examples/parse_async_read.rs",
"chars": 1687,
"preview": "use multer::Multipart;\nuse tokio::io::AsyncRead;\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error"
},
{
"path": "examples/prevent_dos_attack.rs",
"chars": 2520,
"preview": "use std::convert::Infallible;\n\nuse bytes::Bytes;\nuse futures_util::stream::Stream;\n// Import multer types.\nuse multer::{"
},
{
"path": "examples/simple_example.rs",
"chars": 1744,
"preview": "use std::convert::Infallible;\n\nuse bytes::Bytes;\nuse futures_util::stream::Stream;\n// Import multer types.\nuse multer::M"
},
{
"path": "fuzz/.gitignore",
"chars": 45,
"preview": "target\ncorpus/*/*\nartifacts\n!*.seed\ncoverage\n"
},
{
"path": "fuzz/Cargo.toml",
"chars": 622,
"preview": "[package]\nname = \"multer-fuzz\"\nversion = \"0.0.0\"\nauthors = [\"Automatically generated\"]\npublish = false\nedition = \"2018\"\n"
},
{
"path": "fuzz/README.md",
"chars": 269,
"preview": "# Fuzzing\n\nInstall `cargo-fuzz`:\n\n```sh\ncargo install -f cargo-fuzz\n```\n\nRun any available target where `$target` is the"
},
{
"path": "fuzz/corpus/fuzz_multipart_bytes/multi.seed",
"chars": 439,
"preview": "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"sometext\"\r\n\r\nsome text that you wrote in your html form ...\r\n--X-BOU"
},
{
"path": "fuzz/corpus/fuzz_multipart_bytes/multi2.seed",
"chars": 570,
"preview": "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"text1\"\r\n\r\ntext default\r\n--X-BOUNDARY\r\nContent-Disposition: form-data"
},
{
"path": "fuzz/corpus/fuzz_multipart_bytes/simple.seed",
"chars": 86,
"preview": "--X-BOUNDARY\r\nContent-Disposition: form-data; name=my_text_field\r\n\r\n\r\n--X-BOUNDARY--\r\n"
},
{
"path": "fuzz/corpus/fuzz_multipart_bytes/simple2.seed",
"chars": 182,
"preview": "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"field1\"\r\n\r\nvalue1\r\n--X-BOUNDARY\r\nContent-Disposition: form-data; nam"
},
{
"path": "fuzz/corpus/fuzz_multipart_bytes/simple3.seed",
"chars": 442,
"preview": "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\"text\"\r\nContent-Type: text/plain\r\nBook\r\n--X-BOUNDARY\r\nContent-Disposi"
},
{
"path": "fuzz/corpus/fuzz_multipart_bytes/single.seed",
"chars": 94,
"preview": "--X-BOUNDARY\r\nContent-Disposition: form-data; name=\\\"my_text_field\\\"\r\n\r\nabcd\r\n--X-BOUNDARY--\r\n"
},
{
"path": "fuzz/fuzz_targets/fuzz_multipart_bytes.rs",
"chars": 1000,
"preview": "#![no_main]\n\nuse std::convert::Infallible;\nuse std::time::Duration;\n\nuse multer::Multipart;\nuse multer::bytes::Bytes;\nus"
},
{
"path": "releez.yml",
"chars": 1229,
"preview": "version: 1.0.0\nchecklist:\n - name: Checkout master and sync with remote\n type: auto\n run:\n - git checkout ma"
},
{
"path": "rustfmt.toml",
"chars": 263,
"preview": "max_width = 120\ntab_spaces = 4\nwrap_comments = true\ncondense_wildcard_suffixes = true\nformat_code_in_doc_comments = true"
},
{
"path": "src/buffer.rs",
"chars": 5662,
"preview": "use std::fmt;\nuse std::pin::Pin;\nuse std::task::{Context, Poll};\n\nuse bytes::{Buf, Bytes, BytesMut};\nuse futures_util::s"
},
{
"path": "src/constants.rs",
"chars": 9239,
"preview": "use std::borrow::Cow;\n\npub(crate) const DEFAULT_WHOLE_STREAM_SIZE_LIMIT: u64 = std::u64::MAX;\npub(crate) const DEFAULT_P"
},
{
"path": "src/constraints.rs",
"chars": 3344,
"preview": "use crate::size_limit::SizeLimit;\n\n/// Represents some rules to be applied on the stream and field's content size\n/// to"
},
{
"path": "src/content_disposition.rs",
"chars": 816,
"preview": "use http::header::{self, HeaderMap};\n\nuse crate::constants::ContentDispositionAttr;\n\n#[derive(Debug)]\npub(crate) struct "
},
{
"path": "src/error.rs",
"chars": 5110,
"preview": "use std::fmt::{self, Debug, Display, Formatter};\n\ntype BoxError = Box<dyn std::error::Error + Send + Sync>;\n\n/// A set o"
},
{
"path": "src/field.rs",
"chars": 12322,
"preview": "use std::pin::Pin;\nuse std::sync::Arc;\nuse std::task::{Context, Poll};\n\nuse bytes::{Bytes, BytesMut};\nuse encoding_rs::{"
},
{
"path": "src/helpers.rs",
"chars": 1008,
"preview": "use std::convert::TryFrom;\n\nuse http::header::{self, HeaderMap, HeaderName, HeaderValue};\nuse httparse::Header;\n\npub(cra"
},
{
"path": "src/lib.rs",
"chars": 6871,
"preview": "//! An async parser for `multipart/form-data` content-type in Rust.\n//!\n//! It accepts a [`Stream`](futures_util::stream"
},
{
"path": "src/multipart.rs",
"chars": 16856,
"preview": "use std::sync::Arc;\nuse std::task::{Context, Poll};\n\nuse bytes::Bytes;\nuse futures_util::future;\nuse futures_util::strea"
},
{
"path": "src/size_limit.rs",
"chars": 1854,
"preview": "use std::collections::HashMap;\n\nuse crate::constants;\n\n/// Represents size limit of the stream to prevent DoS attacks.\n/"
},
{
"path": "tests/integration.rs",
"chars": 12395,
"preview": "use bytes::Bytes;\nuse futures_util::{stream, Stream};\nuse multer::{Constraints, Multipart, SizeLimit};\n\nfn str_stream(st"
},
{
"path": "tusk.yml",
"chars": 547,
"preview": "options:\n version:\n usage: The next release version\n short: v\n required: true\ntasks:\n setup:\n run:\n -"
}
]
About this extraction
This page contains the full source code of the rousan/multer-rs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (98.0 KB), approximately 25.2k tokens, and a symbol index with 105 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.