diff --git a/Cargo.toml b/Cargo.toml index 3cdae3f..23b85bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ hyper = "0.11" hyper-tls = "0.1.2" libflate = "0.1.11" log = "0.3" +mime_guess = "2.0.0-alpha.2" native-tls = "0.1.3" serde = "1.0" serde_json = "1.0" @@ -24,11 +25,12 @@ tokio-core = "0.1.6" tokio-io = "0.1" tokio-tls = "0.1" url = "1.2" +uuid = { version = "0.5", features = ["v4"] } [dev-dependencies] env_logger = "0.4" -serde_derive = "1.0" error-chain = "0.10" +serde_derive = "1.0" [features] unstable = [] diff --git a/src/lib.rs b/src/lib.rs index 947d0ec..1b7ec6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ //! to do for them. //! //! - Uses system-native TLS -//! - Plain bodies, JSON, urlencoded, (TODO: multipart) +//! - Plain bodies, JSON, urlencoded, multipart //! - Customizable redirect policy //! - Proxies //! - (TODO: Cookies) @@ -127,6 +127,7 @@ extern crate hyper_tls; #[macro_use] extern crate log; extern crate libflate; +extern crate mime_guess; extern crate native_tls; extern crate serde; extern crate serde_json; @@ -135,6 +136,7 @@ extern crate tokio_core; extern crate tokio_io; extern crate tokio_tls; extern crate url; +extern crate uuid; pub use hyper::header; pub use hyper::mime; @@ -158,6 +160,21 @@ pub use self::tls::Certificate; #[macro_use] mod error; +mod async_impl; +mod connect; +mod body; +mod client; +mod into_url; +mod multipart_; +mod proxy; +mod redirect; +mod request; +mod response; +mod tls; +mod wait; + +pub mod multipart; + /// A set of unstable functionality. /// /// This module is only available when the `unstable` feature is enabled. @@ -182,20 +199,6 @@ pub mod unstable { } } - -mod async_impl; -mod connect; -mod body; -mod client; -mod into_url; -mod proxy; -mod redirect; -mod request; -mod response; -mod tls; -mod wait; - - /// Shortcut method to quickly make a `GET` request. /// /// See also the methods on the [`reqwest::Response`](./struct.Response.html) diff --git a/src/multipart.rs b/src/multipart.rs new file mode 100644 index 0000000..bd0321e --- /dev/null +++ b/src/multipart.rs @@ -0,0 +1,2 @@ +//! # multipart/form-data +pub use ::multipart_::{Form, Part}; diff --git a/src/multipart_.rs b/src/multipart_.rs new file mode 100644 index 0000000..d49bd84 --- /dev/null +++ b/src/multipart_.rs @@ -0,0 +1,424 @@ +use std::borrow::Cow; +use std::fmt; +use std::fs::File; +use std::io::{self, Cursor, Read}; +use std::path::Path; + +use mime::Mime; +use mime_guess; +use url::percent_encoding; +use uuid::Uuid; + +/// A multipart/form-data request. +#[derive(Debug)] +pub struct Form { + boundary: String, + fields: Vec<(Cow<'static, str>, Part)>, + headers: Vec, +} + +impl Form { + /// Creates a new Form without any content. + pub fn new() -> Form { + Form { + boundary: format!("{}", Uuid::new_v4().simple()), + fields: Vec::new(), + headers: Vec::new(), + } + } + + /// Add a data field with supplied name and value. + /// + /// # Examples + /// + /// ``` + /// let form = reqwest::multipart::Form::new() + /// .text("username", "seanmonstar") + /// .text("password", "secret"); + /// ``` + pub fn text(self, name: T, value: U) -> Form + where T: Into>, + U: AsRef<[u8]> + Send + 'static + { + self.part(name, Part::text(value)) + } + + /// Adds a file field. + /// + /// The path will be used to try to guess the filename and mime. + /// + /// # Examples + /// + /// ```no_run + /// # fn run() -> ::std::io::Result<()> { + /// let files = reqwest::multipart::Form::new() + /// .file("key", "/path/to/file")?; + /// # Ok(()) + /// # } + /// ``` + /// + /// # Errors + /// + /// Errors when the file cannot be opened. + pub fn file(self, name: T, path: U) -> io::Result
+ where T: Into>, + U: AsRef + { + Ok(self.part(name, Part::file(path)?)) + } + + /// Adds a customized Part. + pub fn part(mut self, name: T, part: Part) -> Form + where T: Into>, + { + self.fields.push((name.into(), part)); + self + } +} + +impl Form { + fn reader(self) -> Reader { + Reader::new(self) + } + + fn compute_length(&mut self) -> Option { + let mut length = 0u64; + for &(ref name, ref field) in self.fields.iter() { + match field.value_length { + Some(value_length) => { + // We are constructing the header just to get its length. To not have to + // construct it again when the request is sent we cache these headers. + let header = header(name, field); + let header_length = header.len(); + self.headers.push(header); + // The additions mimick the format string out of which the field is constructed + // in Reader. Not the cleanest solution because if that format string is + // ever changed then this formula needs to be changed too which is not an + // obvious dependency in the code. + length += 2 + self.boundary.len() as u64 + 2 + header_length as u64 + 4 + value_length + 2 + } + _ => return None, + } + } + // If there is a at least one field there is a special boundary for the very last field. + if self.fields.len() != 0 { + length += 2 + self.boundary.len() as u64 + 2 + } + Some(length) + } +} + + +/// A field in a multipart form. +pub struct Part { + value: Box, + value_length: Option, + mime: Option, + file_name: Option>, +} + +impl Part { + /// Makes a text parameter. + pub fn text + Send + 'static>(value: T) -> Part { + let value_length = Some(value.as_ref().len() as u64); + let mut field = Part::new(Box::new(Cursor::new(value))); + field.value_length = value_length; + field + } + + /// Adds a generic reader. + /// + /// Does not set filename or mime. + pub fn reader(value: T) -> Part { + Part::new(Box::from(value)) + } + + /// Adds a generic reader with known length. + /// + /// Does not set filename or mime. + pub fn reader_with_length(value: T, length: u64) -> Part { + let mut field = Part::new(Box::new(value)); + field.value_length = Some(length); + field + } + + /// Makes a file parameter. + /// + /// # Errors + /// + /// Errors when the file cannot be opened. + pub fn file>(path: T) -> io::Result { + let path = path.as_ref(); + let file_name = path.file_name().and_then(|filename| { + Some(Cow::from(filename.to_string_lossy().into_owned())) + }); + let ext = path.extension() + .and_then(|ext| ext.to_str()) + .unwrap_or(""); + let mime = mime_guess::get_mime_type(ext); + let file = File::open(path)?; + let file_length = file.metadata().ok().map(|meta| meta.len()); + let mut field = Part::new(Box::new(file)); + field.value_length = file_length; + field.mime = Some(mime); + field.file_name = file_name; + Ok(field) + } + + fn new(value: Box) -> Part { + Part { + value: value, + value_length: None, + mime: None, + file_name: None, + } + } + + /// Sets the mime, builder style. + pub fn mime(mut self, mime: Mime) -> Part { + self.mime = Some(mime); + self + } + + /// Sets the filename, builder style. + pub fn file_name>>(mut self, filename: T) -> Part { + self.file_name = Some(filename.into()); + self + } +} + +impl fmt::Debug for Part { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Part") + .field("value_length", &self.value_length) + .field("mime", &self.mime) + .field("file_name", &self.file_name) + .finish() + } +} + +// pub(crate) + +// Turns this Form into a Reader which implements the Read trait. +pub fn reader(form: Form) -> Reader { + form.reader() +} + +// If predictable, computes the length the request will have +// The length should be preditable if only String and file fields have been added, +// but not if a generic reader has been added; +pub fn compute_length(form: &mut Form) -> Option { + form.compute_length() +} + +pub fn boundary(form: &Form) -> &str { + &form.boundary +} + +pub struct Reader { + form: Form, + active_reader: Option>, +} + +impl fmt::Debug for Reader { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Reader") + .field("form", &self.form) + .finish() + } +} + +impl Reader { + fn new(form: Form) -> Reader { + let mut reader = Reader { + form: form, + active_reader: None, + }; + reader.next_reader(); + reader + } + + fn next_reader(&mut self) { + self.active_reader = if self.form.fields.len() != 0 { + // We need to move out of the vector here because we are consuming the field's reader + let (name, field) = self.form.fields.remove(0); + let reader = Cursor::new(format!( + "--{}\r\n{}\r\n\r\n", + self.form.boundary, + // Try to use cached headers created by compute_length + if self.form.headers.len() > 0 { + self.form.headers.remove(0) + } else { + header(&name, &field) + } + )).chain(field.value) + .chain(Cursor::new("\r\n")); + // According to https://tools.ietf.org/html/rfc2046#section-5.1.1 + // the very last field has a special boundary + if self.form.fields.len() != 0 { + Some(Box::new(reader)) + } else { + Some(Box::new(reader.chain(Cursor::new( + format!("--{}--", self.form.boundary), + )))) + } + } else { + None + } + } +} + +impl Read for Reader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let mut total_bytes_read = 0usize; + let mut last_read_bytes; + loop { + match self.active_reader { + Some(ref mut reader) => { + last_read_bytes = reader.read(&mut buf[total_bytes_read..])?; + total_bytes_read += last_read_bytes; + if total_bytes_read == buf.len() { + return Ok(total_bytes_read); + } + } + None => return Ok(total_bytes_read), + }; + if last_read_bytes == 0 && buf.len() != 0 { + self.next_reader(); + } + } + } +} + + +fn header(name: &str, field: &Part) -> String { + format!( + "Content-Disposition: form-data; {}{}{}", + format_parameter("name", name), + match field.file_name { + Some(ref file_name) => format!("; {}", format_parameter("filename", file_name)), + None => String::new(), + }, + match field.mime { + Some(ref mime) => format!("\r\nContent-Type: {}", mime), + None => "".to_string(), + } + ) +} + +fn format_parameter(name: &str, value: &str) -> String { + let legal_value = + percent_encoding::utf8_percent_encode(value, percent_encoding::PATH_SEGMENT_ENCODE_SET) + .to_string(); + if value.len() == legal_value.len() { + // nothing has been percent encoded + format!("{}=\"{}\"", name, value) + } else { + // something has been percent encoded + format!("{}*=utf-8''{}", name, legal_value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn form_empty() { + let mut output = Vec::new(); + let mut request = Form::new(); + let length = request.compute_length(); + request.reader().read_to_end(&mut output).unwrap(); + assert_eq!(output, b""); + assert_eq!(length.unwrap(), 0); + } + + #[test] + fn read_to_end() { + let mut output = Vec::new(); + let mut request = Form::new() + .part("reader1", Part::reader(::std::io::empty())) + .part("key1", Part::text("value1")) + .part( + "key2", + Part::text("value2").mime(::mime::IMAGE_BMP), + ) + .part("reader2", Part::reader(::std::io::empty())) + .part( + "key3", + Part::text("value3").file_name("filename"), + ); + request.boundary = "boundary".to_string(); + let length = request.compute_length(); + let expected = "--boundary\r\n\ + Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\ + \r\n\ + --boundary\r\n\ + Content-Disposition: form-data; name=\"key1\"\r\n\r\n\ + value1\r\n\ + --boundary\r\n\ + Content-Disposition: form-data; name=\"key2\"\r\n\ + Content-Type: image/bmp\r\n\r\n\ + value2\r\n\ + --boundary\r\n\ + Content-Disposition: form-data; name=\"reader2\"\r\n\r\n\ + \r\n\ + --boundary\r\n\ + Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\ + value3\r\n--boundary--"; + request.reader().read_to_end(&mut output).unwrap(); + // These prints are for debug purposes in case the test fails + println!( + "START REAL\n{}\nEND REAL", + ::std::str::from_utf8(&output).unwrap() + ); + println!("START EXPECTED\n{}\nEND EXPECTED", expected); + assert_eq!(::std::str::from_utf8(&output).unwrap(), expected); + assert!(length.is_none()); + } + + #[test] + fn read_to_end_with_length() { + let mut output = Vec::new(); + let mut request = Form::new() + .text("key1", "value1") + .part( + "key2", + Part::text("value2").mime(::mime::IMAGE_BMP), + ) + .part( + "key3", + Part::text("value3").file_name("filename"), + ); + request.boundary = "boundary".to_string(); + let length = request.compute_length(); + let expected = "--boundary\r\n\ + Content-Disposition: form-data; name=\"key1\"\r\n\r\n\ + value1\r\n\ + --boundary\r\n\ + Content-Disposition: form-data; name=\"key2\"\r\n\ + Content-Type: image/bmp\r\n\r\n\ + value2\r\n\ + --boundary\r\n\ + Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\ + value3\r\n--boundary--"; + request.reader().read_to_end(&mut output).unwrap(); + // These prints are for debug purposes in case the test fails + println!( + "START REAL\n{}\nEND REAL", + ::std::str::from_utf8(&output).unwrap() + ); + println!("START EXPECTED\n{}\nEND EXPECTED", expected); + assert_eq!(::std::str::from_utf8(&output).unwrap(), expected); + assert_eq!(length.unwrap(), expected.len() as u64); + } + + #[test] + fn header_percent_encoding() { + let name = "start%'\"\r\nßend"; + let field = Part::text(""); + let expected = "Content-Disposition: form-data; name*=utf-8''start%25\'%22%0D%0A%C3%9Fend"; + + assert_eq!(header(name, &field), expected); + } +} diff --git a/src/request.rs b/src/request.rs index 878ee8a..0e7d651 100644 --- a/src/request.rs +++ b/src/request.rs @@ -277,6 +277,42 @@ impl RequestBuilder { Ok(self) } + /// Sends a multipart/form-data body. + /// + /// ``` + /// use reqwest::mime; + /// # use reqwest::Error; + /// + /// # fn run() -> Result<(), Box> { + /// let client = reqwest::Client::new()?; + /// let form = reqwest::multipart::Form::new() + /// .text("key3", "value3") + /// .file("file", "/path/to/field")?; + /// + /// let response = client.post("your url")? + /// .multipart(form) + /// .send()?; + /// # Ok(()) + /// # } + /// ``` + /// + /// See [`multipart`](multipart/) for more examples. + pub fn multipart(&mut self, mut multipart: ::multipart::Form) -> &mut RequestBuilder { + { + let req = self.request_mut(); + req.headers_mut().set( + ::header::ContentType(format!("multipart/form-data; boundary={}", ::multipart_::boundary(&multipart)) + .parse().unwrap() + ) + ); + *req.body_mut() = Some(match ::multipart_::compute_length(&mut multipart) { + Some(length) => Body::sized(::multipart_::reader(multipart), length), + None => Body::new(::multipart_::reader(multipart)), + }) + } + self + } + /// Build a `Request`, which can be inspected, modified and executed with /// `Client::execute()`. ///