reqwest/async_impl/
multipart.rs

1//! multipart/form-data
2use std::borrow::Cow;
3use std::fmt;
4use std::pin::Pin;
5
6use bytes::Bytes;
7use mime_guess::Mime;
8use percent_encoding::{self, AsciiSet, NON_ALPHANUMERIC};
9
10use futures_core::Stream;
11use futures_util::{future, stream, StreamExt};
12
13use super::Body;
14use crate::header::HeaderMap;
15
16/// An async multipart/form-data request.
17pub struct Form {
18    inner: FormParts<Part>,
19}
20
21/// A field in a multipart form.
22pub struct Part {
23    meta: PartMetadata,
24    value: Body,
25    body_length: Option<u64>,
26}
27
28pub(crate) struct FormParts<P> {
29    pub(crate) boundary: String,
30    pub(crate) computed_headers: Vec<Vec<u8>>,
31    pub(crate) fields: Vec<(Cow<'static, str>, P)>,
32    pub(crate) percent_encoding: PercentEncoding,
33}
34
35pub(crate) struct PartMetadata {
36    mime: Option<Mime>,
37    file_name: Option<Cow<'static, str>>,
38    pub(crate) headers: HeaderMap,
39}
40
41pub(crate) trait PartProps {
42    fn value_len(&self) -> Option<u64>;
43    fn metadata(&self) -> &PartMetadata;
44}
45
46// ===== impl Form =====
47
48impl Default for Form {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54impl Form {
55    /// Creates a new async Form without any content.
56    pub fn new() -> Form {
57        Form {
58            inner: FormParts::new(),
59        }
60    }
61
62    /// Get the boundary that this form will use.
63    #[inline]
64    pub fn boundary(&self) -> &str {
65        self.inner.boundary()
66    }
67
68    /// Add a data field with supplied name and value.
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// let form = reqwest::multipart::Form::new()
74    ///     .text("username", "seanmonstar")
75    ///     .text("password", "secret");
76    /// ```
77    pub fn text<T, U>(self, name: T, value: U) -> Form
78    where
79        T: Into<Cow<'static, str>>,
80        U: Into<Cow<'static, str>>,
81    {
82        self.part(name, Part::text(value))
83    }
84
85    /// Adds a customized Part.
86    pub fn part<T>(self, name: T, part: Part) -> Form
87    where
88        T: Into<Cow<'static, str>>,
89    {
90        self.with_inner(move |inner| inner.part(name, part))
91    }
92
93    /// Configure this `Form` to percent-encode using the `path-segment` rules.
94    pub fn percent_encode_path_segment(self) -> Form {
95        self.with_inner(|inner| inner.percent_encode_path_segment())
96    }
97
98    /// Configure this `Form` to percent-encode using the `attr-char` rules.
99    pub fn percent_encode_attr_chars(self) -> Form {
100        self.with_inner(|inner| inner.percent_encode_attr_chars())
101    }
102
103    /// Configure this `Form` to skip percent-encoding
104    pub fn percent_encode_noop(self) -> Form {
105        self.with_inner(|inner| inner.percent_encode_noop())
106    }
107
108    /// Consume this instance and transform into an instance of Body for use in a request.
109    pub(crate) fn stream(mut self) -> Body {
110        if self.inner.fields.is_empty() {
111            return Body::empty();
112        }
113
114        // create initial part to init reduce chain
115        let (name, part) = self.inner.fields.remove(0);
116        let start = Box::pin(self.part_stream(name, part))
117            as Pin<Box<dyn Stream<Item = crate::Result<Bytes>> + Send + Sync>>;
118
119        let fields = self.inner.take_fields();
120        // for each field, chain an additional stream
121        let stream = fields.into_iter().fold(start, |memo, (name, part)| {
122            let part_stream = self.part_stream(name, part);
123            Box::pin(memo.chain(part_stream))
124                as Pin<Box<dyn Stream<Item = crate::Result<Bytes>> + Send + Sync>>
125        });
126        // append special ending boundary
127        let last = stream::once(future::ready(Ok(
128            format!("--{}--\r\n", self.boundary()).into()
129        )));
130        Body::stream(stream.chain(last))
131    }
132
133    /// Generate a hyper::Body stream for a single Part instance of a Form request.
134    pub(crate) fn part_stream<T>(
135        &mut self,
136        name: T,
137        part: Part,
138    ) -> impl Stream<Item = Result<Bytes, crate::Error>>
139    where
140        T: Into<Cow<'static, str>>,
141    {
142        // start with boundary
143        let boundary = stream::once(future::ready(Ok(
144            format!("--{}\r\n", self.boundary()).into()
145        )));
146        // append headers
147        let header = stream::once(future::ready(Ok({
148            let mut h = self
149                .inner
150                .percent_encoding
151                .encode_headers(&name.into(), &part.meta);
152            h.extend_from_slice(b"\r\n\r\n");
153            h.into()
154        })));
155        // then append form data followed by terminating CRLF
156        boundary
157            .chain(header)
158            .chain(part.value.into_stream())
159            .chain(stream::once(future::ready(Ok("\r\n".into()))))
160    }
161
162    pub(crate) fn compute_length(&mut self) -> Option<u64> {
163        self.inner.compute_length()
164    }
165
166    fn with_inner<F>(self, func: F) -> Self
167    where
168        F: FnOnce(FormParts<Part>) -> FormParts<Part>,
169    {
170        Form {
171            inner: func(self.inner),
172        }
173    }
174}
175
176impl fmt::Debug for Form {
177    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
178        self.inner.fmt_fields("Form", f)
179    }
180}
181
182// ===== impl Part =====
183
184impl Part {
185    /// Makes a text parameter.
186    pub fn text<T>(value: T) -> Part
187    where
188        T: Into<Cow<'static, str>>,
189    {
190        let body = match value.into() {
191            Cow::Borrowed(slice) => Body::from(slice),
192            Cow::Owned(string) => Body::from(string),
193        };
194        Part::new(body, None)
195    }
196
197    /// Makes a new parameter from arbitrary bytes.
198    pub fn bytes<T>(value: T) -> Part
199    where
200        T: Into<Cow<'static, [u8]>>,
201    {
202        let body = match value.into() {
203            Cow::Borrowed(slice) => Body::from(slice),
204            Cow::Owned(vec) => Body::from(vec),
205        };
206        Part::new(body, None)
207    }
208
209    /// Makes a new parameter from an arbitrary stream.
210    pub fn stream<T: Into<Body>>(value: T) -> Part {
211        Part::new(value.into(), None)
212    }
213
214    /// Makes a new parameter from an arbitrary stream with a known length. This is particularly
215    /// useful when adding something like file contents as a stream, where you can know the content
216    /// length beforehand.
217    pub fn stream_with_length<T: Into<Body>>(value: T, length: u64) -> Part {
218        Part::new(value.into(), Some(length))
219    }
220
221    fn new(value: Body, body_length: Option<u64>) -> Part {
222        Part {
223            meta: PartMetadata::new(),
224            value,
225            body_length,
226        }
227    }
228
229    /// Tries to set the mime of this part.
230    pub fn mime_str(self, mime: &str) -> crate::Result<Part> {
231        Ok(self.mime(mime.parse().map_err(crate::error::builder)?))
232    }
233
234    // Re-export when mime 0.4 is available, with split MediaType/MediaRange.
235    fn mime(self, mime: Mime) -> Part {
236        self.with_inner(move |inner| inner.mime(mime))
237    }
238
239    /// Sets the filename, builder style.
240    pub fn file_name<T>(self, filename: T) -> Part
241    where
242        T: Into<Cow<'static, str>>,
243    {
244        self.with_inner(move |inner| inner.file_name(filename))
245    }
246
247    /// Sets custom headers for the part.
248    pub fn headers(self, headers: HeaderMap) -> Part {
249        self.with_inner(move |inner| inner.headers(headers))
250    }
251
252    fn with_inner<F>(self, func: F) -> Self
253    where
254        F: FnOnce(PartMetadata) -> PartMetadata,
255    {
256        Part {
257            meta: func(self.meta),
258            ..self
259        }
260    }
261}
262
263impl fmt::Debug for Part {
264    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
265        let mut dbg = f.debug_struct("Part");
266        dbg.field("value", &self.value);
267        self.meta.fmt_fields(&mut dbg);
268        dbg.finish()
269    }
270}
271
272impl PartProps for Part {
273    fn value_len(&self) -> Option<u64> {
274        if self.body_length.is_some() {
275            self.body_length
276        } else {
277            self.value.content_length()
278        }
279    }
280
281    fn metadata(&self) -> &PartMetadata {
282        &self.meta
283    }
284}
285
286// ===== impl FormParts =====
287
288impl<P: PartProps> FormParts<P> {
289    pub(crate) fn new() -> Self {
290        FormParts {
291            boundary: gen_boundary(),
292            computed_headers: Vec::new(),
293            fields: Vec::new(),
294            percent_encoding: PercentEncoding::PathSegment,
295        }
296    }
297
298    pub(crate) fn boundary(&self) -> &str {
299        &self.boundary
300    }
301
302    /// Adds a customized Part.
303    pub(crate) fn part<T>(mut self, name: T, part: P) -> Self
304    where
305        T: Into<Cow<'static, str>>,
306    {
307        self.fields.push((name.into(), part));
308        self
309    }
310
311    /// Configure this `Form` to percent-encode using the `path-segment` rules.
312    pub(crate) fn percent_encode_path_segment(mut self) -> Self {
313        self.percent_encoding = PercentEncoding::PathSegment;
314        self
315    }
316
317    /// Configure this `Form` to percent-encode using the `attr-char` rules.
318    pub(crate) fn percent_encode_attr_chars(mut self) -> Self {
319        self.percent_encoding = PercentEncoding::AttrChar;
320        self
321    }
322
323    /// Configure this `Form` to skip percent-encoding
324    pub(crate) fn percent_encode_noop(mut self) -> Self {
325        self.percent_encoding = PercentEncoding::NoOp;
326        self
327    }
328
329    // If predictable, computes the length the request will have
330    // The length should be preditable if only String and file fields have been added,
331    // but not if a generic reader has been added;
332    pub(crate) fn compute_length(&mut self) -> Option<u64> {
333        let mut length = 0u64;
334        for &(ref name, ref field) in self.fields.iter() {
335            match field.value_len() {
336                Some(value_length) => {
337                    // We are constructing the header just to get its length. To not have to
338                    // construct it again when the request is sent we cache these headers.
339                    let header = self.percent_encoding.encode_headers(name, field.metadata());
340                    let header_length = header.len();
341                    self.computed_headers.push(header);
342                    // The additions mimic the format string out of which the field is constructed
343                    // in Reader. Not the cleanest solution because if that format string is
344                    // ever changed then this formula needs to be changed too which is not an
345                    // obvious dependency in the code.
346                    length += 2
347                        + self.boundary().len() as u64
348                        + 2
349                        + header_length as u64
350                        + 4
351                        + value_length
352                        + 2
353                }
354                _ => return None,
355            }
356        }
357        // If there is a at least one field there is a special boundary for the very last field.
358        if !self.fields.is_empty() {
359            length += 2 + self.boundary().len() as u64 + 4
360        }
361        Some(length)
362    }
363
364    /// Take the fields vector of this instance, replacing with an empty vector.
365    fn take_fields(&mut self) -> Vec<(Cow<'static, str>, P)> {
366        std::mem::replace(&mut self.fields, Vec::new())
367    }
368}
369
370impl<P: fmt::Debug> FormParts<P> {
371    pub(crate) fn fmt_fields(&self, ty_name: &'static str, f: &mut fmt::Formatter) -> fmt::Result {
372        f.debug_struct(ty_name)
373            .field("boundary", &self.boundary)
374            .field("parts", &self.fields)
375            .finish()
376    }
377}
378
379// ===== impl PartMetadata =====
380
381impl PartMetadata {
382    pub(crate) fn new() -> Self {
383        PartMetadata {
384            mime: None,
385            file_name: None,
386            headers: HeaderMap::default(),
387        }
388    }
389
390    pub(crate) fn mime(mut self, mime: Mime) -> Self {
391        self.mime = Some(mime);
392        self
393    }
394
395    pub(crate) fn file_name<T>(mut self, filename: T) -> Self
396    where
397        T: Into<Cow<'static, str>>,
398    {
399        self.file_name = Some(filename.into());
400        self
401    }
402
403    pub(crate) fn headers<T>(mut self, headers: T) -> Self
404    where
405        T: Into<HeaderMap>,
406    {
407        self.headers = headers.into();
408        self
409    }
410}
411
412impl PartMetadata {
413    pub(crate) fn fmt_fields<'f, 'fa, 'fb>(
414        &self,
415        debug_struct: &'f mut fmt::DebugStruct<'fa, 'fb>,
416    ) -> &'f mut fmt::DebugStruct<'fa, 'fb> {
417        debug_struct
418            .field("mime", &self.mime)
419            .field("file_name", &self.file_name)
420            .field("headers", &self.headers)
421    }
422}
423
424// https://url.spec.whatwg.org/#fragment-percent-encode-set
425const FRAGMENT_ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS
426    .add(b' ')
427    .add(b'"')
428    .add(b'<')
429    .add(b'>')
430    .add(b'`');
431
432// https://url.spec.whatwg.org/#path-percent-encode-set
433const PATH_ENCODE_SET: &AsciiSet = &FRAGMENT_ENCODE_SET.add(b'#').add(b'?').add(b'{').add(b'}');
434
435const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &PATH_ENCODE_SET.add(b'/').add(b'%');
436
437// https://tools.ietf.org/html/rfc8187#section-3.2.1
438const ATTR_CHAR_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
439    .remove(b'!')
440    .remove(b'#')
441    .remove(b'$')
442    .remove(b'&')
443    .remove(b'+')
444    .remove(b'-')
445    .remove(b'.')
446    .remove(b'^')
447    .remove(b'_')
448    .remove(b'`')
449    .remove(b'|')
450    .remove(b'~');
451
452pub(crate) enum PercentEncoding {
453    PathSegment,
454    AttrChar,
455    NoOp,
456}
457
458impl PercentEncoding {
459    pub(crate) fn encode_headers(&self, name: &str, field: &PartMetadata) -> Vec<u8> {
460        let mut buf = Vec::new();
461        buf.extend_from_slice(b"Content-Disposition: form-data; ");
462
463        match self.percent_encode(name) {
464            Cow::Borrowed(value) => {
465                // nothing has been percent encoded
466                buf.extend_from_slice(b"name=\"");
467                buf.extend_from_slice(value.as_bytes());
468                buf.extend_from_slice(b"\"");
469            }
470            Cow::Owned(value) => {
471                // something has been percent encoded
472                buf.extend_from_slice(b"name*=utf-8''");
473                buf.extend_from_slice(value.as_bytes());
474            }
475        }
476
477        // According to RFC7578 Section 4.2, `filename*=` syntax is invalid.
478        // See https://github.com/seanmonstar/reqwest/issues/419.
479        if let Some(filename) = &field.file_name {
480            buf.extend_from_slice(b"; filename=\"");
481            let legal_filename = filename
482                .replace('\\', "\\\\")
483                .replace('"', "\\\"")
484                .replace('\r', "\\\r")
485                .replace('\n', "\\\n");
486            buf.extend_from_slice(legal_filename.as_bytes());
487            buf.extend_from_slice(b"\"");
488        }
489
490        if let Some(mime) = &field.mime {
491            buf.extend_from_slice(b"\r\nContent-Type: ");
492            buf.extend_from_slice(mime.as_ref().as_bytes());
493        }
494
495        for (k, v) in field.headers.iter() {
496            buf.extend_from_slice(b"\r\n");
497            buf.extend_from_slice(k.as_str().as_bytes());
498            buf.extend_from_slice(b": ");
499            buf.extend_from_slice(v.as_bytes());
500        }
501        buf
502    }
503
504    fn percent_encode<'a>(&self, value: &'a str) -> Cow<'a, str> {
505        use percent_encoding::utf8_percent_encode as percent_encode;
506
507        match self {
508            Self::PathSegment => percent_encode(value, PATH_SEGMENT_ENCODE_SET).into(),
509            Self::AttrChar => percent_encode(value, ATTR_CHAR_ENCODE_SET).into(),
510            Self::NoOp => value.into(),
511        }
512    }
513}
514
515fn gen_boundary() -> String {
516    use crate::util::fast_random as random;
517
518    let a = random();
519    let b = random();
520    let c = random();
521    let d = random();
522
523    format!("{a:016x}-{b:016x}-{c:016x}-{d:016x}")
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529    use futures_util::TryStreamExt;
530    use futures_util::{future, stream};
531    use tokio::{self, runtime};
532
533    #[test]
534    fn form_empty() {
535        let form = Form::new();
536
537        let rt = runtime::Builder::new_current_thread()
538            .enable_all()
539            .build()
540            .expect("new rt");
541        let body = form.stream().into_stream();
542        let s = body.map_ok(|try_c| try_c.to_vec()).try_concat();
543
544        let out = rt.block_on(s);
545        assert!(out.unwrap().is_empty());
546    }
547
548    #[test]
549    fn stream_to_end() {
550        let mut form = Form::new()
551            .part(
552                "reader1",
553                Part::stream(Body::stream(stream::once(future::ready::<
554                    Result<String, crate::Error>,
555                >(Ok(
556                    "part1".to_owned()
557                ))))),
558            )
559            .part("key1", Part::text("value1"))
560            .part("key2", Part::text("value2").mime(mime::IMAGE_BMP))
561            .part(
562                "reader2",
563                Part::stream(Body::stream(stream::once(future::ready::<
564                    Result<String, crate::Error>,
565                >(Ok(
566                    "part2".to_owned()
567                ))))),
568            )
569            .part("key3", Part::text("value3").file_name("filename"));
570        form.inner.boundary = "boundary".to_string();
571        let expected = "--boundary\r\n\
572             Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\
573             part1\r\n\
574             --boundary\r\n\
575             Content-Disposition: form-data; name=\"key1\"\r\n\r\n\
576             value1\r\n\
577             --boundary\r\n\
578             Content-Disposition: form-data; name=\"key2\"\r\n\
579             Content-Type: image/bmp\r\n\r\n\
580             value2\r\n\
581             --boundary\r\n\
582             Content-Disposition: form-data; name=\"reader2\"\r\n\r\n\
583             part2\r\n\
584             --boundary\r\n\
585             Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\
586             value3\r\n--boundary--\r\n";
587        let rt = runtime::Builder::new_current_thread()
588            .enable_all()
589            .build()
590            .expect("new rt");
591        let body = form.stream().into_stream();
592        let s = body.map(|try_c| try_c.map(|r| r.to_vec())).try_concat();
593
594        let out = rt.block_on(s).unwrap();
595        // These prints are for debug purposes in case the test fails
596        println!(
597            "START REAL\n{}\nEND REAL",
598            std::str::from_utf8(&out).unwrap()
599        );
600        println!("START EXPECTED\n{expected}\nEND EXPECTED");
601        assert_eq!(std::str::from_utf8(&out).unwrap(), expected);
602    }
603
604    #[test]
605    fn stream_to_end_with_header() {
606        let mut part = Part::text("value2").mime(mime::IMAGE_BMP);
607        let mut headers = HeaderMap::new();
608        headers.insert("Hdr3", "/a/b/c".parse().unwrap());
609        part = part.headers(headers);
610        let mut form = Form::new().part("key2", part);
611        form.inner.boundary = "boundary".to_string();
612        let expected = "--boundary\r\n\
613                        Content-Disposition: form-data; name=\"key2\"\r\n\
614                        Content-Type: image/bmp\r\n\
615                        hdr3: /a/b/c\r\n\
616                        \r\n\
617                        value2\r\n\
618                        --boundary--\r\n";
619        let rt = runtime::Builder::new_current_thread()
620            .enable_all()
621            .build()
622            .expect("new rt");
623        let body = form.stream().into_stream();
624        let s = body.map(|try_c| try_c.map(|r| r.to_vec())).try_concat();
625
626        let out = rt.block_on(s).unwrap();
627        // These prints are for debug purposes in case the test fails
628        println!(
629            "START REAL\n{}\nEND REAL",
630            std::str::from_utf8(&out).unwrap()
631        );
632        println!("START EXPECTED\n{expected}\nEND EXPECTED");
633        assert_eq!(std::str::from_utf8(&out).unwrap(), expected);
634    }
635
636    #[test]
637    fn correct_content_length() {
638        // Setup an arbitrary data stream
639        let stream_data = b"just some stream data";
640        let stream_len = stream_data.len();
641        let stream_data = stream_data
642            .chunks(3)
643            .map(|c| Ok::<_, std::io::Error>(Bytes::from(c)));
644        let the_stream = futures_util::stream::iter(stream_data);
645
646        let bytes_data = b"some bytes data".to_vec();
647        let bytes_len = bytes_data.len();
648
649        let stream_part = Part::stream_with_length(Body::stream(the_stream), stream_len as u64);
650        let body_part = Part::bytes(bytes_data);
651
652        // A simple check to make sure we get the configured body length
653        assert_eq!(stream_part.value_len().unwrap(), stream_len as u64);
654
655        // Make sure it delegates to the underlying body if length is not specified
656        assert_eq!(body_part.value_len().unwrap(), bytes_len as u64);
657    }
658
659    #[test]
660    fn header_percent_encoding() {
661        let name = "start%'\"\r\nßend";
662        let field = Part::text("");
663
664        assert_eq!(
665            PercentEncoding::PathSegment.encode_headers(name, &field.meta),
666            &b"Content-Disposition: form-data; name*=utf-8''start%25'%22%0D%0A%C3%9Fend"[..]
667        );
668
669        assert_eq!(
670            PercentEncoding::AttrChar.encode_headers(name, &field.meta),
671            &b"Content-Disposition: form-data; name*=utf-8''start%25%27%22%0D%0A%C3%9Fend"[..]
672        );
673    }
674}