use std::convert::Infallible;
use std::fmt;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use axum::extract::Multipart;
use axum::extract::multipart::Field;
use axum::http::{Extensions, Request, Response, StatusCode};
use bytes::Bytes;
use tower::{Layer, Service};
use crate::api::{Problem, ProblemKind};
pub const DEFAULT_TOTAL_BYTES: u64 = 16 * 1024 * 1024;
pub const DEFAULT_FIELD_BYTES: u64 = 8 * 1024 * 1024;
pub const DEFAULT_FIELDS: usize = 32;
pub const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(30);
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MultipartLimits {
total_bytes: u64,
field_bytes: u64,
fields: usize,
read_timeout: Duration,
}
impl MultipartLimits {
#[must_use]
pub const fn new() -> Self {
Self {
total_bytes: DEFAULT_TOTAL_BYTES,
field_bytes: DEFAULT_FIELD_BYTES,
fields: DEFAULT_FIELDS,
read_timeout: DEFAULT_READ_TIMEOUT,
}
}
#[must_use]
pub const fn with_total_bytes(mut self, bytes: u64) -> Self {
self.total_bytes = bytes;
self
}
#[must_use]
pub const fn with_field_bytes(mut self, bytes: u64) -> Self {
self.field_bytes = bytes;
self
}
#[must_use]
pub const fn with_fields(mut self, fields: usize) -> Self {
self.fields = fields;
self
}
#[must_use]
pub const fn with_read_timeout(mut self, timeout: Duration) -> Self {
self.read_timeout = timeout;
self
}
#[must_use]
pub const fn total_bytes(&self) -> u64 {
self.total_bytes
}
#[must_use]
pub const fn field_bytes(&self) -> u64 {
self.field_bytes
}
#[must_use]
pub const fn fields(&self) -> usize {
self.fields
}
#[must_use]
pub const fn read_timeout(&self) -> Duration {
self.read_timeout
}
#[must_use]
pub fn from_extensions(extensions: &Extensions) -> Self {
extensions
.get::<MultipartLimits>()
.copied()
.unwrap_or_default()
}
}
impl Default for MultipartLimits {
fn default() -> Self {
Self::new()
}
}
impl<S> Layer<S> for MultipartLimits {
type Service = MultipartLimitsService<S>;
fn layer(&self, inner: S) -> Self::Service {
MultipartLimitsService {
inner,
limits: *self,
}
}
}
#[derive(Clone, Debug)]
pub struct MultipartLimitsService<S> {
inner: S,
limits: MultipartLimits,
}
impl<S, B> Service<Request<B>> for MultipartLimitsService<S>
where
S: Service<Request<B>, Response = Response<axum::body::Body>, Error = Infallible>
+ Clone
+ Send
+ 'static,
S::Future: Send + 'static,
B: Send + 'static,
{
type Response = Response<axum::body::Body>;
type Error = Infallible;
type Future =
Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut request: Request<B>) -> Self::Future {
let clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, clone);
request.extensions_mut().insert(self.limits);
Box::pin(async move { inner.call(request).await })
}
}
#[non_exhaustive]
#[derive(Debug)]
pub enum MultipartError {
TooManyFields {
limit: usize,
},
FieldTooLarge {
limit: u64,
},
BodyTooLarge {
limit: u64,
},
ReadTimeout {
after: Duration,
},
Parse {
source: axum::extract::multipart::MultipartError,
},
}
impl MultipartError {
#[must_use]
pub fn status(&self) -> StatusCode {
match self {
Self::TooManyFields { .. } | Self::FieldTooLarge { .. } | Self::BodyTooLarge { .. } => {
StatusCode::PAYLOAD_TOO_LARGE
}
Self::ReadTimeout { .. } => StatusCode::REQUEST_TIMEOUT,
Self::Parse { source } => source.status(),
}
}
#[must_use]
pub fn problem(&self) -> Problem {
let kind = match self.status() {
StatusCode::PAYLOAD_TOO_LARGE => ProblemKind::PayloadTooLarge,
StatusCode::REQUEST_TIMEOUT => ProblemKind::Timeout,
StatusCode::BAD_REQUEST => ProblemKind::BadRequest,
_ => ProblemKind::Internal,
};
let detail = match self {
Self::TooManyFields { .. } => "Request has too many multipart fields",
Self::FieldTooLarge { .. } => "A multipart field is too large",
Self::BodyTooLarge { .. } => "Request body is too large",
Self::ReadTimeout { .. } => "Timed out reading the request body",
Self::Parse { .. } => "Request body is not a well-formed multipart/form-data body",
};
Problem::of(kind).with_detail(detail)
}
}
impl fmt::Display for MultipartError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooManyFields { limit } => {
write!(formatter, "multipart body has more than {limit} fields")
}
Self::FieldTooLarge { limit } => {
write!(formatter, "a multipart field exceeded {limit} bytes")
}
Self::BodyTooLarge { limit } => {
write!(formatter, "the multipart body exceeded {limit} bytes")
}
Self::ReadTimeout { after } => write!(
formatter,
"reading the multipart body blocked for more than {after:?}"
),
Self::Parse { .. } => write!(formatter, "the multipart body could not be parsed"),
}
}
}
impl std::error::Error for MultipartError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Parse { source } => Some(source),
_ => None,
}
}
}
#[derive(Debug, Default)]
struct Counters {
fields: usize,
total: u64,
}
pub struct BoundedMultipart {
inner: Multipart,
limits: MultipartLimits,
counters: Counters,
}
impl fmt::Debug for BoundedMultipart {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("BoundedMultipart")
.field("limits", &self.limits)
.field("counters", &self.counters)
.finish_non_exhaustive()
}
}
impl BoundedMultipart {
#[must_use]
pub fn new(inner: Multipart, limits: MultipartLimits) -> Self {
Self {
inner,
limits,
counters: Counters::default(),
}
}
#[must_use]
pub fn limits(&self) -> MultipartLimits {
self.limits
}
#[must_use]
pub fn fields_read(&self) -> usize {
self.counters.fields
}
#[must_use]
pub fn bytes_read(&self) -> u64 {
self.counters.total
}
pub async fn next_field(&mut self) -> Result<Option<BoundedField<'_>>, MultipartError> {
let Self {
inner,
limits,
counters,
} = self;
let limits = *limits;
if counters.fields >= limits.fields {
return Err(MultipartError::TooManyFields {
limit: limits.fields,
});
}
let field = tokio::time::timeout(limits.read_timeout, inner.next_field())
.await
.map_err(|_| MultipartError::ReadTimeout {
after: limits.read_timeout,
})?
.map_err(|source| MultipartError::Parse { source })?;
match field {
None => Ok(None),
Some(field) => {
counters.fields += 1;
Ok(Some(BoundedField {
field,
limits,
counters,
field_bytes: 0,
}))
}
}
}
}
pub struct BoundedField<'a> {
field: Field<'a>,
limits: MultipartLimits,
counters: &'a mut Counters,
field_bytes: u64,
}
impl fmt::Debug for BoundedField<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("BoundedField")
.field("name", &self.field.name())
.field("bytes_read", &self.field_bytes)
.finish_non_exhaustive()
}
}
impl BoundedField<'_> {
#[must_use]
pub fn name(&self) -> Option<&str> {
self.field.name()
}
#[must_use]
pub fn file_name(&self) -> Option<&str> {
self.field.file_name()
}
#[must_use]
pub fn declared_content_type(&self) -> Option<&str> {
self.field.content_type()
}
#[must_use]
pub fn byte_len(&self) -> u64 {
self.field_bytes
}
pub async fn chunk(&mut self) -> Result<Option<Bytes>, MultipartError> {
let chunk = tokio::time::timeout(self.limits.read_timeout, self.field.chunk())
.await
.map_err(|_| MultipartError::ReadTimeout {
after: self.limits.read_timeout,
})?
.map_err(|source| MultipartError::Parse { source })?;
let Some(chunk) = chunk else { return Ok(None) };
let len = chunk.len() as u64;
self.field_bytes = self.field_bytes.saturating_add(len);
if self.field_bytes > self.limits.field_bytes {
return Err(MultipartError::FieldTooLarge {
limit: self.limits.field_bytes,
});
}
self.counters.total = self.counters.total.saturating_add(len);
if self.counters.total > self.limits.total_bytes {
return Err(MultipartError::BodyTooLarge {
limit: self.limits.total_bytes,
});
}
Ok(Some(chunk))
}
pub async fn bytes(mut self) -> Result<Bytes, MultipartError> {
let mut buffer = Vec::new();
while let Some(chunk) = self.chunk().await? {
buffer.extend_from_slice(&chunk);
}
Ok(Bytes::from(buffer))
}
pub async fn text(self) -> Result<Option<String>, MultipartError> {
let bytes = self.bytes().await?;
Ok(String::from_utf8(bytes.to_vec()).ok())
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::extract::FromRequest;
const BOUNDARY: &str = "XbCaRcAtUrE";
async fn multipart(parts: &[(&str, Option<&str>, &[u8])]) -> Multipart {
let mut body: Vec<u8> = Vec::new();
for (name, filename, content) in parts {
body.extend_from_slice(format!("--{BOUNDARY}\r\n").as_bytes());
match filename {
Some(filename) => body.extend_from_slice(
format!(
"Content-Disposition: form-data; name=\"{name}\"; filename=\"{filename}\"\r\n\r\n"
)
.as_bytes(),
),
None => body.extend_from_slice(
format!("Content-Disposition: form-data; name=\"{name}\"\r\n\r\n").as_bytes(),
),
}
body.extend_from_slice(content);
body.extend_from_slice(b"\r\n");
}
body.extend_from_slice(format!("--{BOUNDARY}--\r\n").as_bytes());
let request = Request::builder()
.method("POST")
.header(
axum::http::header::CONTENT_TYPE,
format!("multipart/form-data; boundary={BOUNDARY}"),
)
.body(Body::from(body))
.expect("the hand-built multipart request is well-formed");
Multipart::from_request(request, &())
.await
.expect("the boundary is valid")
}
async fn drain(bounded: &mut BoundedMultipart) -> Result<usize, MultipartError> {
let mut seen = 0;
while let Some(mut field) = bounded.next_field().await? {
while field.chunk().await?.is_some() {}
seen += 1;
}
Ok(seen)
}
#[test]
fn the_defaults_are_the_documented_ones() {
let limits = MultipartLimits::new();
assert_eq!(limits.total_bytes(), DEFAULT_TOTAL_BYTES);
assert_eq!(limits.field_bytes(), DEFAULT_FIELD_BYTES);
assert_eq!(limits.fields(), DEFAULT_FIELDS);
assert_eq!(limits.read_timeout(), DEFAULT_READ_TIMEOUT);
assert_eq!(limits, MultipartLimits::default());
}
#[test]
fn a_request_with_no_layer_is_still_bounded() {
let extensions = Extensions::new();
assert_eq!(
MultipartLimits::from_extensions(&extensions),
MultipartLimits::new()
);
}
#[test]
fn the_route_override_is_read_back_from_the_extensions() {
let configured = MultipartLimits::new().with_fields(3).with_total_bytes(99);
let mut extensions = Extensions::new();
extensions.insert(configured);
assert_eq!(MultipartLimits::from_extensions(&extensions), configured);
}
#[tokio::test]
async fn an_ordinary_form_passes_every_bound() {
let mut bounded = BoundedMultipart::new(
multipart(&[
("title", None, b"holiday"),
("file", Some("photo.png"), b"\x89PNG\r\n\x1a\n"),
])
.await,
MultipartLimits::new(),
);
assert_eq!(drain(&mut bounded).await.unwrap(), 2);
assert_eq!(bounded.fields_read(), 2);
assert_eq!(bounded.bytes_read(), 7 + 8);
}
#[tokio::test]
async fn a_thousand_tiny_fields_are_refused_on_the_count() {
let parts: Vec<(String, Vec<u8>)> = (0..1000)
.map(|index| (format!("f{index}"), b"ab".to_vec()))
.collect();
let borrowed: Vec<(&str, Option<&str>, &[u8])> = parts
.iter()
.map(|(name, content)| (name.as_str(), None, content.as_slice()))
.collect();
let mut bounded = BoundedMultipart::new(
multipart(&borrowed).await,
MultipartLimits::new()
.with_total_bytes(u64::MAX)
.with_field_bytes(u64::MAX)
.with_fields(4),
);
let error = drain(&mut bounded).await.unwrap_err();
assert!(matches!(error, MultipartError::TooManyFields { limit: 4 }));
assert_eq!(error.status(), StatusCode::PAYLOAD_TOO_LARGE);
assert_eq!(bounded.fields_read(), 4);
}
#[tokio::test]
async fn one_oversized_field_is_refused_on_the_per_field_cap() {
let big = vec![b'x'; 4096];
let mut bounded = BoundedMultipart::new(
multipart(&[("file", Some("big.bin"), &big)]).await,
MultipartLimits::new()
.with_total_bytes(u64::MAX)
.with_field_bytes(1024),
);
let error = drain(&mut bounded).await.unwrap_err();
assert!(matches!(
error,
MultipartError::FieldTooLarge { limit: 1024 }
));
assert_eq!(error.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
#[tokio::test]
async fn many_legal_fields_together_are_refused_on_the_total() {
let chunk = vec![b'y'; 512];
let parts: Vec<(&str, Option<&str>, &[u8])> = (0..8)
.map(|_| ("f", None::<&str>, chunk.as_slice()))
.collect();
let mut bounded = BoundedMultipart::new(
multipart(&parts).await,
MultipartLimits::new()
.with_field_bytes(1024)
.with_total_bytes(2048),
);
let error = drain(&mut bounded).await.unwrap_err();
assert!(matches!(
error,
MultipartError::BodyTooLarge { limit: 2048 }
));
}
#[tokio::test]
async fn a_body_that_stops_mid_part_times_out_rather_than_hanging() {
use futures::StreamExt as _;
let head = format!(
"--{BOUNDARY}\r\nContent-Disposition: form-data; name=\"file\"; \
filename=\"slow.bin\"\r\n\r\npartial"
);
let stream =
futures::stream::once(async move { Ok::<Bytes, std::io::Error>(Bytes::from(head)) })
.chain(futures::stream::pending::<Result<Bytes, std::io::Error>>());
let body = Body::from_stream(stream);
let request = Request::builder()
.method("POST")
.header(
axum::http::header::CONTENT_TYPE,
format!("multipart/form-data; boundary={BOUNDARY}"),
)
.body(body)
.expect("the hand-built multipart request is well-formed");
let inner = Multipart::from_request(request, &())
.await
.expect("the boundary is valid");
let mut bounded = BoundedMultipart::new(
inner,
MultipartLimits::new().with_read_timeout(Duration::from_millis(50)),
);
let error = drain(&mut bounded).await.unwrap_err();
assert!(matches!(error, MultipartError::ReadTimeout { .. }));
assert_eq!(error.status(), StatusCode::REQUEST_TIMEOUT);
}
#[tokio::test]
async fn the_declared_content_type_is_carried_but_not_believed() {
let mut bounded = BoundedMultipart::new(
multipart(&[("file", Some("evil.jpg"), b"<?php echo 1; ?>")]).await,
MultipartLimits::new(),
);
let field = bounded.next_field().await.unwrap().unwrap();
assert_eq!(field.file_name(), Some("evil.jpg"));
assert_eq!(field.declared_content_type(), None);
}
#[test]
fn every_bound_reports_a_problem_that_never_quotes_the_request() {
for error in [
MultipartError::TooManyFields { limit: 1 },
MultipartError::FieldTooLarge { limit: 1 },
MultipartError::BodyTooLarge { limit: 1 },
MultipartError::ReadTimeout {
after: Duration::from_secs(1),
},
] {
let problem = error.problem();
assert_eq!(problem.status(), error.status());
let json = problem.to_json();
let detail = json["detail"].as_str().unwrap_or_default();
assert!(!detail.is_empty());
assert!(!detail.contains('1'));
}
}
}