Skip to main content

actix_multipart/
field.rs

1use std::{
2    cell::RefCell,
3    cmp, fmt,
4    future::poll_fn,
5    mem,
6    pin::Pin,
7    rc::Rc,
8    task::{ready, Context, Poll},
9};
10
11use actix_web::{
12    error::PayloadError,
13    http::header::{self, ContentDisposition, HeaderMap},
14    web::{Bytes, BytesMut},
15};
16use derive_more::{Display, Error};
17use futures_core::Stream;
18use mime::Mime;
19
20use crate::{
21    error::Error,
22    payload::{PayloadBuffer, PayloadRef},
23    safety::Safety,
24};
25
26/// Error type returned from [`Field::bytes()`] when field data is larger than limit.
27#[derive(Debug, Display, Error)]
28#[display("size limit exceeded while collecting field data")]
29#[non_exhaustive]
30pub struct LimitExceeded;
31
32/// A single field in a multipart stream.
33pub struct Field {
34    /// Field's Content-Type.
35    content_type: Option<Mime>,
36
37    /// Field's Content-Disposition.
38    content_disposition: Option<ContentDisposition>,
39
40    /// Form field name.
41    ///
42    /// A non-optional storage for form field names to avoid unwraps in `form` module. Will be an
43    /// empty string in non-form contexts.
44    ///
45    // INVARIANT: always non-empty when request content-type is multipart/form-data.
46    pub(crate) form_field_name: String,
47
48    /// Field's header map.
49    headers: HeaderMap,
50
51    safety: Safety,
52    inner: Rc<RefCell<InnerField>>,
53}
54
55impl Field {
56    pub(crate) fn new(
57        content_type: Option<Mime>,
58        content_disposition: Option<ContentDisposition>,
59        form_field_name: Option<String>,
60        headers: HeaderMap,
61        safety: Safety,
62        inner: Rc<RefCell<InnerField>>,
63    ) -> Self {
64        Field {
65            content_type,
66            content_disposition,
67            form_field_name: form_field_name.unwrap_or_default(),
68            headers,
69            inner,
70            safety,
71        }
72    }
73
74    /// Returns a reference to the field's header map.
75    pub fn headers(&self) -> &HeaderMap {
76        &self.headers
77    }
78
79    /// Returns a reference to the field's content (mime) type, if it is supplied by the client.
80    ///
81    /// According to [RFC 7578](https://www.rfc-editor.org/rfc/rfc7578#section-4.4), if it is not
82    /// present, it should default to "text/plain". Note it is the responsibility of the client to
83    /// provide the appropriate content type, there is no attempt to validate this by the server.
84    pub fn content_type(&self) -> Option<&Mime> {
85        self.content_type.as_ref()
86    }
87
88    /// Returns this field's parsed Content-Disposition header, if set.
89    ///
90    /// # Validation
91    ///
92    /// Per [RFC 7578 §4.2], the parts of a multipart/form-data payload MUST contain a
93    /// Content-Disposition header field where the disposition type is `form-data` and MUST also
94    /// contain an additional parameter of `name` with its value being the original field name from
95    /// the form. This requirement is enforced during extraction for multipart/form-data requests,
96    /// but not other kinds of multipart requests (such as multipart/related).
97    ///
98    /// As such, it is safe to `.unwrap()` calls `.content_disposition()` if you've verified.
99    ///
100    /// The [`name()`](Self::name) method is also provided as a convenience for obtaining the
101    /// aforementioned name parameter.
102    ///
103    /// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2
104    pub fn content_disposition(&self) -> Option<&ContentDisposition> {
105        self.content_disposition.as_ref()
106    }
107
108    /// Returns the field's name, if set.
109    ///
110    /// See [`content_disposition()`](Self::content_disposition) regarding guarantees on presence of
111    /// the "name" field.
112    pub fn name(&self) -> Option<&str> {
113        self.content_disposition()?.get_name()
114    }
115
116    /// Collects the raw field data, up to `limit` bytes.
117    ///
118    /// # Errors
119    ///
120    /// Any errors produced by the data stream are returned as `Ok(Err(Error))` immediately.
121    ///
122    /// If the buffered data size would exceed `limit`, an `Err(LimitExceeded)` is returned. Note
123    /// that, in this case, the full data stream is exhausted before returning the error so that
124    /// subsequent fields can still be read. To better defend against malicious/infinite requests,
125    /// it is advisable to also put a timeout on this call.
126    pub async fn bytes(&mut self, limit: usize) -> Result<Result<Bytes, Error>, LimitExceeded> {
127        /// Sensible default (2kB) for initial, bounded allocation when collecting body bytes.
128        const INITIAL_ALLOC_BYTES: usize = 2 * 1024;
129
130        let mut exceeded_limit = false;
131        let mut buf = BytesMut::with_capacity(INITIAL_ALLOC_BYTES);
132
133        let mut field = Pin::new(self);
134
135        match poll_fn(|cx| loop {
136            match ready!(field.as_mut().poll_next(cx)) {
137                // if already over limit, discard chunk to advance multipart request
138                Some(Ok(_chunk)) if exceeded_limit => {}
139
140                // if limit is exceeded set flag to true and continue
141                Some(Ok(chunk)) if buf.len() + chunk.len() > limit => {
142                    exceeded_limit = true;
143                    // eagerly de-allocate field data buffer
144                    let _ = mem::take(&mut buf);
145                }
146
147                Some(Ok(chunk)) => buf.extend_from_slice(&chunk),
148
149                None => return Poll::Ready(Ok(())),
150                Some(Err(err)) => return Poll::Ready(Err(err)),
151            }
152        })
153        .await
154        {
155            // propagate error returned from body poll
156            Err(err) => Ok(Err(err)),
157
158            // limit was exceeded while reading body
159            Ok(()) if exceeded_limit => Err(LimitExceeded),
160
161            // otherwise return body buffer
162            Ok(()) => Ok(Ok(buf.freeze())),
163        }
164    }
165}
166
167impl Stream for Field {
168    type Item = Result<Bytes, Error>;
169
170    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
171        let this = self.get_mut();
172        let mut inner = this.inner.borrow_mut();
173
174        if let Some(mut buffer) = inner
175            .payload
176            .as_ref()
177            .expect("Field should not be polled after completion")
178            .get_mut(&this.safety)
179        {
180            // check safety and poll read payload to buffer.
181            buffer.poll_stream(cx)?;
182        } else if !this.safety.is_clean() {
183            // safety violation
184            return Poll::Ready(Some(Err(Error::NotConsumed)));
185        } else {
186            return Poll::Pending;
187        }
188
189        inner.poll(&this.safety)
190    }
191}
192
193impl fmt::Debug for Field {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        if let Some(ct) = &self.content_type {
196            writeln!(f, "\nField: {}", ct)?;
197        } else {
198            writeln!(f, "\nField:")?;
199        }
200        writeln!(f, "  boundary: {}", self.inner.borrow().boundary)?;
201        writeln!(f, "  headers:")?;
202        for (key, val) in self.headers.iter() {
203            writeln!(f, "    {:?}: {:?}", key, val)?;
204        }
205        Ok(())
206    }
207}
208
209pub(crate) struct InnerField {
210    /// Payload is initialized as Some and is `take`n when the field stream finishes.
211    payload: Option<PayloadRef>,
212
213    /// Field boundary (without "--" prefix).
214    boundary: String,
215
216    /// True if request payload has been exhausted.
217    eof: bool,
218
219    /// Field data's stated size according to it's Content-Length header.
220    length: Option<u64>,
221}
222
223impl InnerField {
224    pub(crate) fn new_in_rc(
225        payload: PayloadRef,
226        boundary: String,
227        headers: &HeaderMap,
228    ) -> Result<Rc<RefCell<InnerField>>, PayloadError> {
229        Self::new(payload, boundary, headers).map(|this| Rc::new(RefCell::new(this)))
230    }
231
232    pub(crate) fn new(
233        payload: PayloadRef,
234        boundary: String,
235        headers: &HeaderMap,
236    ) -> Result<InnerField, PayloadError> {
237        let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) {
238            match len.to_str().ok().and_then(|len| len.parse::<u64>().ok()) {
239                Some(len) => Some(len),
240                None => return Err(PayloadError::Incomplete(None)),
241            }
242        } else {
243            None
244        };
245
246        Ok(InnerField {
247            boundary,
248            payload: Some(payload),
249            eof: false,
250            length: len,
251        })
252    }
253
254    /// Reads body part content chunk of the specified size.
255    ///
256    /// The body part must has `Content-Length` header with proper value.
257    pub(crate) fn read_len(
258        payload: &mut PayloadBuffer,
259        size: &mut u64,
260    ) -> Poll<Option<Result<Bytes, Error>>> {
261        if *size == 0 {
262            Poll::Ready(None)
263        } else {
264            match payload.read_max(*size)? {
265                Some(mut chunk) => {
266                    let len = cmp::min(chunk.len() as u64, *size);
267                    *size -= len;
268                    let ch = chunk.split_to(len as usize);
269                    if !chunk.is_empty() {
270                        payload.unprocessed(chunk);
271                    }
272                    Poll::Ready(Some(Ok(ch)))
273                }
274                None => {
275                    if payload.eof && (*size != 0) {
276                        Poll::Ready(Some(Err(Error::Incomplete)))
277                    } else {
278                        Poll::Pending
279                    }
280                }
281            }
282        }
283    }
284
285    /// Reads content chunk of body part with unknown length.
286    ///
287    /// The `Content-Length` header for body part is not necessary.
288    pub(crate) fn read_stream(
289        payload: &mut PayloadBuffer,
290        boundary: &str,
291    ) -> Poll<Option<Result<Bytes, Error>>> {
292        let mut pos = 0;
293
294        let len = payload.buf.len();
295
296        if len == 0 {
297            return if payload.eof {
298                Poll::Ready(Some(Err(Error::Incomplete)))
299            } else {
300                Poll::Pending
301            };
302        }
303
304        // check boundary
305        if len >= 4 && payload.buf[0] == b'\r' {
306            let b_len = if payload.buf.starts_with(b"\r\n") && &payload.buf[2..4] == b"--" {
307                Some(4)
308            } else if &payload.buf[1..3] == b"--" {
309                Some(3)
310            } else {
311                None
312            };
313
314            if let Some(b_len) = b_len {
315                let b_size = boundary.len() + b_len;
316                let available = len - b_len;
317                let check_len = cmp::min(available, boundary.len());
318
319                if payload.buf[b_len..b_len + check_len] == boundary.as_bytes()[..check_len] {
320                    match (len >= b_size, payload.eof) {
321                        // full boundary delimiter found
322                        (true, _) => return Poll::Ready(None),
323                        // partial boundary prefix with stream still open; wait for more chunks
324                        (false, false) => return Poll::Pending,
325                        // partial boundary prefix at EOF indicates truncated payload
326                        (false, true) => return Poll::Ready(Some(Err(Error::Incomplete))),
327                    }
328                }
329            }
330        }
331
332        loop {
333            return if let Some(idx) = memchr::memmem::find(&payload.buf[pos..], b"\r") {
334                let cur = pos + idx;
335
336                // check if we have enough data for boundary detection
337                if cur + 4 > len {
338                    if cur > 0 {
339                        Poll::Ready(Some(Ok(payload.buf.split_to(cur).freeze())))
340                    } else if payload.eof {
341                        Poll::Ready(Some(Err(Error::Incomplete)))
342                    } else {
343                        Poll::Pending
344                    }
345                } else {
346                    // check boundary
347                    if (&payload.buf[cur..cur + 2] == b"\r\n"
348                        && &payload.buf[cur + 2..cur + 4] == b"--")
349                        || (&payload.buf[cur..=cur] == b"\r"
350                            && &payload.buf[cur + 1..cur + 3] == b"--")
351                    {
352                        if cur != 0 {
353                            // return buffer
354                            Poll::Ready(Some(Ok(payload.buf.split_to(cur).freeze())))
355                        } else {
356                            pos = cur + 1;
357                            continue;
358                        }
359                    } else {
360                        // not boundary
361                        pos = cur + 1;
362                        continue;
363                    }
364                }
365            } else {
366                Poll::Ready(Some(Ok(payload.buf.split().freeze())))
367            };
368        }
369    }
370
371    pub(crate) fn poll(&mut self, safety: &Safety) -> Poll<Option<Result<Bytes, Error>>> {
372        if self.payload.is_none() {
373            return Poll::Ready(None);
374        }
375
376        let Some(mut payload) = self
377            .payload
378            .as_ref()
379            .expect("Field should not be polled after completion")
380            .get_mut(safety)
381        else {
382            return Poll::Pending;
383        };
384
385        if !self.eof {
386            let res = if let Some(ref mut len) = self.length {
387                Self::read_len(&mut payload, len)
388            } else {
389                Self::read_stream(&mut payload, &self.boundary)
390            };
391
392            match ready!(res) {
393                Some(Ok(bytes)) => return Poll::Ready(Some(Ok(bytes))),
394                Some(Err(err)) => return Poll::Ready(Some(Err(err))),
395                None => self.eof = true,
396            }
397        }
398
399        let result = match payload.readline() {
400            Ok(None) => Poll::Pending,
401            Ok(Some(line)) => {
402                if line.as_ref() != b"\r\n" {
403                    log::warn!("multipart field did not read all the data or it is malformed");
404                }
405                Poll::Ready(None)
406            }
407            Err(err) => Poll::Ready(Some(Err(err))),
408        };
409
410        drop(payload);
411
412        if let Poll::Ready(None) = result {
413            // drop payload buffer and make future un-poll-able
414            let _ = self.payload.take();
415        }
416
417        result
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use actix_http::h1;
424    use futures_util::{stream, FutureExt as _, StreamExt as _};
425
426    use super::*;
427    use crate::Multipart;
428
429    // TODO: use test utility when multi-file support is introduced
430    fn create_double_request_with_header() -> (Bytes, HeaderMap) {
431        let bytes = Bytes::from(
432            "testasdadsad\r\n\
433             --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
434             Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
435             Content-Type: text/plain; charset=utf-8\r\n\
436             \r\n\
437             one+one+one\r\n\
438             --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
439             Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
440             Content-Type: text/plain; charset=utf-8\r\n\
441             \r\n\
442             two+two+two\r\n\
443             --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n",
444        );
445        let mut headers = HeaderMap::new();
446        headers.insert(
447            header::CONTENT_TYPE,
448            header::HeaderValue::from_static(
449                "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
450            ),
451        );
452        (bytes, headers)
453    }
454
455    #[actix_rt::test]
456    async fn bytes_unlimited() {
457        let (body, headers) = create_double_request_with_header();
458
459        let mut multipart = Multipart::new(&headers, stream::iter([Ok(body)]));
460
461        let field = multipart
462            .next()
463            .await
464            .expect("multipart should have two fields")
465            .expect("multipart body should be well formatted")
466            .bytes(usize::MAX)
467            .await
468            .expect("field data should not be size limited")
469            .expect("reading field data should not error");
470        assert_eq!(field, "one+one+one");
471
472        let field = multipart
473            .next()
474            .await
475            .expect("multipart should have two fields")
476            .expect("multipart body should be well formatted")
477            .bytes(usize::MAX)
478            .await
479            .expect("field data should not be size limited")
480            .expect("reading field data should not error");
481        assert_eq!(field, "two+two+two");
482    }
483
484    #[actix_rt::test]
485    async fn bytes_limited() {
486        let (body, headers) = create_double_request_with_header();
487
488        let mut multipart = Multipart::new(&headers, stream::iter([Ok(body)]));
489
490        multipart
491            .next()
492            .await
493            .expect("multipart should have two fields")
494            .expect("multipart body should be well formatted")
495            .bytes(8) // smaller than data size
496            .await
497            .expect_err("field data should be size limited");
498
499        // next field still readable
500        let field = multipart
501            .next()
502            .await
503            .expect("multipart should have two fields")
504            .expect("multipart body should be well formatted")
505            .bytes(usize::MAX)
506            .await
507            .expect("field data should not be size limited")
508            .expect("reading field data should not error");
509        assert_eq!(field, "two+two+two");
510    }
511
512    #[actix_rt::test]
513    async fn boundary_marker_split_across_chunks() {
514        let (body, headers) = create_double_request_with_header();
515        let boundary_start = memchr::memmem::find_iter(&body, b"\r\n--")
516            .nth(1)
517            .expect("body should contain a boundary between its fields");
518        let boundary_marker_end = boundary_start + 4;
519
520        let (mut tx, rx) = h1::Payload::create(false);
521        tx.feed_data(body.slice(..boundary_start));
522        tx.feed_data(body.slice(boundary_start..boundary_marker_end));
523
524        let mut multipart = Multipart::new(&headers, rx);
525
526        let mut field = multipart
527            .next()
528            .await
529            .expect("multipart should have two fields")
530            .expect("multipart body should be well formatted");
531        assert_eq!(field.next().await.unwrap().unwrap(), "one+one+one");
532        let next = field.next().now_or_never();
533        assert!(
534            next.is_none(),
535            "partial boundary marker should not be emitted as field data: {next:?}",
536        );
537
538        tx.feed_data(body.slice(boundary_marker_end..));
539        assert!(field.next().await.is_none());
540        drop(field);
541
542        let field = multipart
543            .next()
544            .await
545            .expect("multipart should have two fields")
546            .expect("multipart body should be well formatted")
547            .bytes(usize::MAX)
548            .await
549            .expect("field data should not be size limited")
550            .expect("reading field data should not error");
551        assert_eq!(field, "two+two+two");
552    }
553
554    #[test]
555    fn mismatching_boundary_prefix_is_yielded_without_more_input() {
556        let mut headers = HeaderMap::new();
557        headers.insert(
558            header::CONTENT_TYPE,
559            header::HeaderValue::from_static("multipart/form-data; boundary=abc"),
560        );
561
562        let (mut tx, rx) = h1::Payload::create(false);
563        tx.feed_data(Bytes::from_static(
564            b"--abc\r\nContent-Disposition: form-data; name=\"field\"\r\n\r\n\r\n--ax",
565        ));
566
567        let mut multipart = Multipart::new(&headers, rx);
568        let mut field = multipart
569            .next()
570            .now_or_never()
571            .expect("field headers should be ready")
572            .expect("multipart should contain a field")
573            .expect("field headers should be valid");
574
575        let next = field.next().now_or_never();
576        assert!(
577            matches!(&next, Some(Some(Ok(bytes))) if bytes.as_ref() == b"\r\n--ax"),
578            "a mismatching boundary prefix should be yielded without more input: {next:?}",
579        );
580
581        tx.feed_eof();
582
583        let next = field.next().now_or_never();
584        assert!(
585            matches!(next, Some(Some(Err(crate::error::Error::Incomplete)))),
586            "EOF without a closing boundary should report an incomplete message: {next:?}",
587        );
588    }
589
590    #[test]
591    fn mismatching_boundary_prefix_is_yielded_before_eof_incomplete() {
592        let mut headers = HeaderMap::new();
593        headers.insert(
594            header::CONTENT_TYPE,
595            header::HeaderValue::from_static("multipart/form-data; boundary=abc"),
596        );
597
598        let (mut tx, rx) = h1::Payload::create(false);
599        tx.feed_data(Bytes::from_static(
600            b"--abc\r\nContent-Disposition: form-data; name=\"field\"\r\n\r\n\r\n--ax",
601        ));
602        tx.feed_eof();
603
604        let mut multipart = Multipart::new(&headers, rx);
605        let mut field = multipart
606            .next()
607            .now_or_never()
608            .expect("field headers should be ready")
609            .expect("multipart should contain a field")
610            .expect("field headers should be valid");
611
612        let next = field.next().now_or_never();
613        assert!(
614            matches!(&next, Some(Some(Ok(bytes))) if bytes.as_ref() == b"\r\n--ax"),
615            "a mismatching boundary prefix should be yielded before Incomplete: {next:?}",
616        );
617
618        let next = field.next().now_or_never();
619        assert!(
620            matches!(next, Some(Some(Err(crate::error::Error::Incomplete)))),
621            "EOF after yielding mismatch should report an incomplete message: {next:?}",
622        );
623    }
624}