diff --git a/Cargo.toml b/Cargo.toml index 4cc9e86223..77314475b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ repository = "Metaswitch/swagger-rs" [features] default = ["serdejson"] multipart_form = ["mime"] -multipart_related = ["mime_multipart", "hyper_0_10", "mime_0_2"] +multipart_related = ["hyper_0_10", "mime_0_2"] serdejson = ["serde", "serde_json"] server = ["hyper/server"] http1 = ["hyper/http1"] @@ -49,7 +49,6 @@ mime = { version = "0.3", optional = true } # multipart/related hyper_0_10 = {package = "hyper", version = "0.10", default-features = false, optional=true} -mime_multipart = {version = "0.6", optional = true} mime_0_2 = { package = "mime", version = "0.2.6", optional = true } # UDS (Unix Domain Sockets) diff --git a/src/multipart/mime_multipart/error.rs b/src/multipart/mime_multipart/error.rs new file mode 100644 index 0000000000..b24536fa94 --- /dev/null +++ b/src/multipart/mime_multipart/error.rs @@ -0,0 +1,127 @@ +// Copyright 2016 mime-multipart Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use std::borrow::Cow; +use std::error::Error as StdError; +use std::fmt::{self, Display}; +use std::io; +use std::string::FromUtf8Error; + +use httparse; +use hyper; + +/// An error type for the `mime-multipart` crate. +pub enum Error { + /// The Hyper request did not have a Content-Type header. + NoRequestContentType, + /// The Hyper request Content-Type top-level Mime was not `Multipart`. + NotMultipart, + /// The Content-Type header failed to specify boundary token. + BoundaryNotSpecified, + /// A multipart section contained only partial headers. + PartialHeaders, + EofInMainHeaders, + EofBeforeFirstBoundary, + NoCrLfAfterBoundary, + EofInPartHeaders, + EofInFile, + EofInPart, + /// An HTTP parsing error from a multipart section. + Httparse(httparse::Error), + /// An I/O error. + Io(io::Error), + /// An error was returned from Hyper. + Hyper(hyper::Error), + /// An error occurred during UTF-8 processing. + Utf8(FromUtf8Error), + /// An error occurred during character decoding + Decoding(Cow<'static, str>), +} + +impl From for Error { + fn from(err: io::Error) -> Error { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: httparse::Error) -> Error { + Error::Httparse(err) + } +} + +impl From for Error { + fn from(err: hyper::Error) -> Error { + Error::Hyper(err) + } +} + +impl From for Error { + fn from(err: FromUtf8Error) -> Error { + Error::Utf8(err) + } +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Error::Httparse(ref e) => + format!("Httparse: {:?}", e).fmt(f), + Error::Io(ref e) => + format!("Io: {}", e).fmt(f), + Error::Hyper(ref e) => + format!("Hyper: {}", e).fmt(f), + Error::Utf8(ref e) => + format!("Utf8: {}", e).fmt(f), + Error::Decoding(ref e) => + format!("Decoding: {}", e).fmt(f), + _ => format!("{}", self).fmt(f), + } + } +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self)?; + if self.source().is_some() { + write!(f, ": {:?}", self.source().unwrap())?; // recurse + } + Ok(()) + } +} + +impl StdError for Error { + fn description(&self) -> &str{ + match *self { + Error::NoRequestContentType => "The Hyper request did not have a Content-Type header.", + Error::NotMultipart => + "The Hyper request Content-Type top-level Mime was not multipart.", + Error::BoundaryNotSpecified => + "The Content-Type header failed to specify a boundary token.", + Error::PartialHeaders => + "A multipart section contained only partial headers.", + Error::EofInMainHeaders => + "The request headers ended pre-maturely.", + Error::EofBeforeFirstBoundary => + "The request body ended prior to reaching the expected starting boundary.", + Error::NoCrLfAfterBoundary => + "Missing CRLF after boundary.", + Error::EofInPartHeaders => + "The request body ended prematurely while parsing headers of a multipart part.", + Error::EofInFile => + "The request body ended prematurely while streaming a file part.", + Error::EofInPart => + "The request body ended prematurely while reading a multipart part.", + Error::Httparse(_) => + "A parse error occurred while parsing the headers of a multipart section.", + Error::Io(_) => "An I/O error occurred.", + Error::Hyper(_) => "A Hyper error occurred.", + Error::Utf8(_) => "A UTF-8 error occurred.", + Error::Decoding(_) => "A decoding error occurred.", + } + } +} diff --git a/src/multipart/mime_multipart/mock.rs b/src/multipart/mime_multipart/mock.rs new file mode 100644 index 0000000000..99cb191251 --- /dev/null +++ b/src/multipart/mime_multipart/mock.rs @@ -0,0 +1,96 @@ +//! Code taken from Hyper, stripped down and with modification. +//! +//! See [https://github.com/hyperium/hyper](Hyper) for more information + +// Copyright (c) 2014 Sean McArthur +// +// 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. + +use std::fmt; +use std::io::{self, Read, Write, Cursor}; +use std::net::SocketAddr; +use std::time::Duration; + +use hyper::net::NetworkStream; + +pub struct MockStream { + pub read: Cursor>, + pub write: Vec, +} + +impl Clone for MockStream { + fn clone(&self) -> MockStream { + MockStream { + read: Cursor::new(self.read.get_ref().clone()), + write: self.write.clone(), + } + } +} + +impl fmt::Debug for MockStream { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "MockStream {{ read: {:?}, write: {:?} }}", self.read.get_ref(), self.write) + } +} + +impl PartialEq for MockStream { + fn eq(&self, other: &MockStream) -> bool { + self.read.get_ref() == other.read.get_ref() && self.write == other.write + } +} + +impl MockStream { + #[allow(dead_code)] + pub fn with_input(input: &[u8]) -> MockStream { + MockStream { + read: Cursor::new(input.to_vec()), + write: vec![], + } + } +} + +impl Read for MockStream { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.read.read(buf) + } +} + +impl Write for MockStream { + fn write(&mut self, msg: &[u8]) -> io::Result { + Write::write(&mut self.write, msg) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl NetworkStream for MockStream { + fn peer_addr(&mut self) -> io::Result { + Ok("127.0.0.1:1337".parse().unwrap()) + } + + fn set_read_timeout(&self, _: Option) -> io::Result<()> { + Ok(()) + } + + fn set_write_timeout(&self, _: Option) -> io::Result<()> { + Ok(()) + } +} diff --git a/src/multipart/mime_multipart/mod.rs b/src/multipart/mime_multipart/mod.rs new file mode 100644 index 0000000000..df6a8b0190 --- /dev/null +++ b/src/multipart/mime_multipart/mod.rs @@ -0,0 +1,498 @@ +// Copyright 2016-2020 mime-multipart Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +pub mod error; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +pub use crate::multipart::related::generate_boundary; +pub use error::Error; + +use buf_read_ext::BufReadExt; +use encoding::{all, DecoderTrap, Encoding}; +use hyper::header::{ + Charset, ContentDisposition, ContentType, DispositionParam, DispositionType, Headers, +}; +use mime::{Attr, Mime, TopLevel, Value}; +use std::borrow::Cow; +use std::fs::File; +use std::io::{BufRead, BufReader, Read, Write}; +use std::ops::Drop; +use std::path::{Path, PathBuf}; +use tempdir::TempDir; + +/// A multipart part which is not a file (stored in memory) +#[derive(Clone, Debug, PartialEq)] +pub struct Part { + pub headers: Headers, + pub body: Vec, +} +impl Part { + /// Mime content-type specified in the header + pub fn content_type(&self) -> Option { + let ct: Option<&ContentType> = self.headers.get(); + ct.map(|ref ct| ct.0.clone()) + } +} + +/// A file that is to be inserted into a `multipart/*` or alternatively an uploaded file that +/// was received as part of `multipart/*` parsing. +#[derive(Clone, Debug, PartialEq)] +pub struct FilePart { + /// The headers of the part + pub headers: Headers, + /// A temporary file containing the file content + pub path: PathBuf, + /// Optionally, the size of the file. This is filled when multiparts are parsed, but is + /// not necessary when they are generated. + pub size: Option, + // The temporary directory the upload was put into, saved for the Drop trait + tempdir: Option, +} +impl FilePart { + pub fn new(headers: Headers, path: &Path) -> FilePart { + FilePart { + headers: headers, + path: path.to_owned(), + size: None, + tempdir: None, + } + } + + /// If you do not want the file on disk to be deleted when Self drops, call this + /// function. It will become your responsability to clean up. + pub fn do_not_delete_on_drop(&mut self) { + self.tempdir = None; + } + + /// Create a new temporary FilePart (when created this way, the file will be + /// deleted once the FilePart object goes out of scope). + pub fn create(headers: Headers) -> Result { + // Setup a file to capture the contents. + let mut path = TempDir::new("mime_multipart")?.into_path(); + let tempdir = Some(path.clone()); + path.push(TextNonce::sized_urlsafe(32).unwrap().into_string()); + Ok(FilePart { + headers: headers, + path: path, + size: None, + tempdir: tempdir, + }) + } + + /// Filename that was specified when the file was uploaded. Returns `Ok` if there + /// was no content-disposition header supplied. + pub fn filename(&self) -> Result, Error> { + let cd: Option<&ContentDisposition> = self.headers.get(); + match cd { + Some(cd) => get_content_disposition_filename(cd), + None => Ok(None), + } + } + + /// Mime content-type specified in the header + pub fn content_type(&self) -> Option { + let ct: Option<&ContentType> = self.headers.get(); + ct.map(|ref ct| ct.0.clone()) + } +} +impl Drop for FilePart { + fn drop(&mut self) { + if self.tempdir.is_some() { + let _ = ::std::fs::remove_file(&self.path); + let _ = ::std::fs::remove_dir(&self.tempdir.as_ref().unwrap()); + } + } +} + +/// A multipart part which could be either a file, in memory, or another multipart +/// container containing nested parts. +#[derive(Clone, Debug)] +pub enum Node { + /// A part in memory + Part(Part), + /// A part streamed to a file + File(FilePart), + /// A container of nested multipart parts + Multipart((Headers, Vec)), +} + +/// Parse a MIME `multipart/*` from a `Read`able stream into a `Vec` of `Node`s, streaming +/// files to disk and keeping the rest in memory. Recursive `multipart/*` parts will are +/// parsed as well and returned within a `Node::Multipart` variant. +/// +/// If `always_use_files` is true, all parts will be streamed to files. If false, only parts +/// with a `ContentDisposition` header set to `Attachment` or otherwise containing a `Filename` +/// parameter will be streamed to files. +/// +/// It is presumed that the headers are still in the stream. If you have them separately, +/// use `read_multipart_body()` instead. +pub fn read_multipart(stream: &mut S, always_use_files: bool) -> Result, Error> { + let mut reader = BufReader::with_capacity(4096, stream); + let mut nodes: Vec = Vec::new(); + + let mut buf: Vec = Vec::new(); + + let (_, found) = reader.stream_until_token(b"\r\n\r\n", &mut buf)?; + if !found { + return Err(Error::EofInMainHeaders); + } + + // Keep the CRLFCRLF as httparse will expect it + buf.extend(b"\r\n\r\n".iter().cloned()); + + // Parse the headers + let mut header_memory = [httparse::EMPTY_HEADER; 64]; + let headers = match httparse::parse_headers(&buf, &mut header_memory) { + Ok(httparse::Status::Complete((_, raw_headers))) => { + Headers::from_raw(raw_headers).map_err(|e| From::from(e)) + } + Ok(httparse::Status::Partial) => Err(Error::PartialHeaders), + Err(err) => Err(From::from(err)), + }?; + + inner(&mut reader, &headers, &mut nodes, always_use_files)?; + Ok(nodes) +} + +/// Parse a MIME `multipart/*` from a `Read`able stream into a `Vec` of `Node`s, streaming +/// files to disk and keeping the rest in memory. Recursive `multipart/*` parts will are +/// parsed as well and returned within a `Node::Multipart` variant. +/// +/// If `always_use_files` is true, all parts will be streamed to files. If false, only parts +/// with a `ContentDisposition` header set to `Attachment` or otherwise containing a `Filename` +/// parameter will be streamed to files. +/// +/// It is presumed that you have the `Headers` already and the stream starts at the body. +/// If the headers are still in the stream, use `read_multipart()` instead. +pub fn read_multipart_body( + stream: &mut S, + headers: &Headers, + always_use_files: bool, +) -> Result, Error> { + let mut reader = BufReader::with_capacity(4096, stream); + let mut nodes: Vec = Vec::new(); + inner(&mut reader, headers, &mut nodes, always_use_files)?; + Ok(nodes) +} + +fn inner( + reader: &mut R, + headers: &Headers, + nodes: &mut Vec, + always_use_files: bool, +) -> Result<(), Error> { + let mut buf: Vec = Vec::new(); + + let boundary = get_multipart_boundary(headers)?; + + // Read past the initial boundary + let (_, found) = reader.stream_until_token(&boundary, &mut buf)?; + if !found { + return Err(Error::EofBeforeFirstBoundary); + } + + // Define the boundary, including the line terminator preceding it. + // Use their first line terminator to determine whether to use CRLF or LF. + let (lt, ltlt, lt_boundary) = { + let peeker = reader.fill_buf()?; + if peeker.len() > 1 && &peeker[..2] == b"\r\n" { + let mut output = Vec::with_capacity(2 + boundary.len()); + output.push(b'\r'); + output.push(b'\n'); + output.extend(boundary.clone()); + (vec![b'\r', b'\n'], vec![b'\r', b'\n', b'\r', b'\n'], output) + } else if peeker.len() > 0 && peeker[0] == b'\n' { + let mut output = Vec::with_capacity(1 + boundary.len()); + output.push(b'\n'); + output.extend(boundary.clone()); + (vec![b'\n'], vec![b'\n', b'\n'], output) + } else { + return Err(Error::NoCrLfAfterBoundary); + } + }; + + loop { + // If the next two lookahead characters are '--', parsing is finished. + { + let peeker = reader.fill_buf()?; + if peeker.len() >= 2 && &peeker[..2] == b"--" { + return Ok(()); + } + } + + // Read the line terminator after the boundary + let (_, found) = reader.stream_until_token(<, &mut buf)?; + if !found { + return Err(Error::NoCrLfAfterBoundary); + } + + // Read the headers (which end in 2 line terminators) + buf.truncate(0); // start fresh + let (_, found) = reader.stream_until_token(<lt, &mut buf)?; + if !found { + return Err(Error::EofInPartHeaders); + } + + // Keep the 2 line terminators as httparse will expect it + buf.extend(ltlt.iter().cloned()); + + // Parse the headers + let part_headers = { + let mut header_memory = [httparse::EMPTY_HEADER; 4]; + match httparse::parse_headers(&buf, &mut header_memory) { + Ok(httparse::Status::Complete((_, raw_headers))) => { + Headers::from_raw(raw_headers).map_err(|e| From::from(e)) + } + Ok(httparse::Status::Partial) => Err(Error::PartialHeaders), + Err(err) => Err(From::from(err)), + }? + }; + + // Check for a nested multipart + let nested = { + let ct: Option<&ContentType> = part_headers.get(); + if let Some(ct) = ct { + let &ContentType(Mime(ref top_level, _, _)) = ct; + *top_level == TopLevel::Multipart + } else { + false + } + }; + if nested { + // Recurse: + let mut inner_nodes: Vec = Vec::new(); + inner(reader, &part_headers, &mut inner_nodes, always_use_files)?; + nodes.push(Node::Multipart((part_headers, inner_nodes))); + continue; + } + + let is_file = always_use_files || { + let cd: Option<&ContentDisposition> = part_headers.get(); + if cd.is_some() { + if cd.unwrap().disposition == DispositionType::Attachment { + true + } else { + cd.unwrap().parameters.iter().any(|x| match x { + &DispositionParam::Filename(_, _, _) => true, + _ => false, + }) + } + } else { + false + } + }; + if is_file { + // Setup a file to capture the contents. + let mut filepart = FilePart::create(part_headers)?; + let mut file = File::create(filepart.path.clone())?; + + // Stream out the file. + let (read, found) = reader.stream_until_token(<_boundary, &mut file)?; + if !found { + return Err(Error::EofInFile); + } + filepart.size = Some(read); + + // TODO: Handle Content-Transfer-Encoding. RFC 7578 section 4.7 deprecated + // this, and the authors state "Currently, no deployed implementations that + // send such bodies have been discovered", so this is very low priority. + + nodes.push(Node::File(filepart)); + } else { + buf.truncate(0); // start fresh + let (_, found) = reader.stream_until_token(<_boundary, &mut buf)?; + if !found { + return Err(Error::EofInPart); + } + + nodes.push(Node::Part(Part { + headers: part_headers, + body: buf.clone(), + })); + } + } +} + +/// Get the `multipart/*` boundary string from `hyper::Headers` +pub fn get_multipart_boundary(headers: &Headers) -> Result, Error> { + // Verify that the request is 'Content-Type: multipart/*'. + let ct: &ContentType = match headers.get() { + Some(ct) => ct, + None => return Err(Error::NoRequestContentType), + }; + let ContentType(ref mime) = *ct; + let Mime(ref top_level, _, ref params) = *mime; + + if *top_level != TopLevel::Multipart { + return Err(Error::NotMultipart); + } + + for &(ref attr, ref val) in params.iter() { + if let (&Attr::Boundary, &Value::Ext(ref val)) = (attr, val) { + let mut boundary = Vec::with_capacity(2 + val.len()); + boundary.extend(b"--".iter().cloned()); + boundary.extend(val.as_bytes()); + return Ok(boundary); + } + } + Err(Error::BoundaryNotSpecified) +} + +#[inline] +fn get_content_disposition_filename(cd: &ContentDisposition) -> Result, Error> { + if let Some(&DispositionParam::Filename(ref charset, _, ref bytes)) = + cd.parameters.iter().find(|&x| match *x { + DispositionParam::Filename(_, _, _) => true, + _ => false, + }) + { + match charset_decode(charset, bytes) { + Ok(filename) => Ok(Some(filename)), + Err(e) => Err(Error::Decoding(e)), + } + } else { + Ok(None) + } +} + +// This decodes bytes encoded according to a hyper::header::Charset encoding, using the +// rust-encoding crate. Only supports encodings defined in both crates. +fn charset_decode(charset: &Charset, bytes: &[u8]) -> Result> { + Ok(match *charset { + Charset::Us_Ascii => all::ASCII.decode(bytes, DecoderTrap::Strict)?, + Charset::Iso_8859_1 => all::ISO_8859_1.decode(bytes, DecoderTrap::Strict)?, + Charset::Iso_8859_2 => all::ISO_8859_2.decode(bytes, DecoderTrap::Strict)?, + Charset::Iso_8859_3 => all::ISO_8859_3.decode(bytes, DecoderTrap::Strict)?, + Charset::Iso_8859_4 => all::ISO_8859_4.decode(bytes, DecoderTrap::Strict)?, + Charset::Iso_8859_5 => all::ISO_8859_5.decode(bytes, DecoderTrap::Strict)?, + Charset::Iso_8859_6 => all::ISO_8859_6.decode(bytes, DecoderTrap::Strict)?, + Charset::Iso_8859_7 => all::ISO_8859_7.decode(bytes, DecoderTrap::Strict)?, + Charset::Iso_8859_8 => all::ISO_8859_8.decode(bytes, DecoderTrap::Strict)?, + Charset::Iso_8859_9 => return Err("ISO_8859_9 is not supported".into()), + Charset::Iso_8859_10 => all::ISO_8859_10.decode(bytes, DecoderTrap::Strict)?, + Charset::Shift_Jis => return Err("Shift_Jis is not supported".into()), + Charset::Euc_Jp => all::EUC_JP.decode(bytes, DecoderTrap::Strict)?, + Charset::Iso_2022_Kr => return Err("Iso_2022_Kr is not supported".into()), + Charset::Euc_Kr => return Err("Euc_Kr is not supported".into()), + Charset::Iso_2022_Jp => all::ISO_2022_JP.decode(bytes, DecoderTrap::Strict)?, + Charset::Iso_2022_Jp_2 => return Err("Iso_2022_Jp_2 is not supported".into()), + Charset::Iso_8859_6_E => return Err("Iso_8859_6_E is not supported".into()), + Charset::Iso_8859_6_I => return Err("Iso_8859_6_I is not supported".into()), + Charset::Iso_8859_8_E => return Err("Iso_8859_8_E is not supported".into()), + Charset::Iso_8859_8_I => return Err("Iso_8859_8_I is not supported".into()), + Charset::Gb2312 => return Err("Gb2312 is not supported".into()), + Charset::Big5 => all::BIG5_2003.decode(bytes, DecoderTrap::Strict)?, + Charset::Koi8_R => all::KOI8_R.decode(bytes, DecoderTrap::Strict)?, + Charset::Ext(ref s) => match &**s { + "UTF-8" => all::UTF_8.decode(bytes, DecoderTrap::Strict)?, + _ => return Err("Encoding is not supported".into()), + }, + }) +} + +// Convenience method, like write_all(), but returns the count of bytes written. +trait WriteAllCount { + fn write_all_count(&mut self, buf: &[u8]) -> ::std::io::Result; +} +impl WriteAllCount for T { + fn write_all_count(&mut self, buf: &[u8]) -> ::std::io::Result { + self.write_all(buf)?; + Ok(buf.len()) + } +} + +/// Stream a multipart body to the output `stream` given, made up of the `parts` +/// given. Top-level headers are NOT included in this stream; the caller must send +/// those prior to calling write_multipart(). +/// Returns the number of bytes written, or an error. +pub fn write_multipart( + stream: &mut S, + boundary: &Vec, + nodes: &Vec, +) -> Result { + let mut count: usize = 0; + + for node in nodes { + // write a boundary + count += stream.write_all_count(b"--")?; + count += stream.write_all_count(&boundary)?; + count += stream.write_all_count(b"\r\n")?; + + match node { + &Node::Part(ref part) => { + // write the part's headers + for header in part.headers.iter() { + count += stream.write_all_count(header.name().as_bytes())?; + count += stream.write_all_count(b": ")?; + count += stream.write_all_count(header.value_string().as_bytes())?; + count += stream.write_all_count(b"\r\n")?; + } + + // write the blank line + count += stream.write_all_count(b"\r\n")?; + + // Write the part's content + count += stream.write_all_count(&part.body)?; + } + &Node::File(ref filepart) => { + // write the part's headers + for header in filepart.headers.iter() { + count += stream.write_all_count(header.name().as_bytes())?; + count += stream.write_all_count(b": ")?; + count += stream.write_all_count(header.value_string().as_bytes())?; + count += stream.write_all_count(b"\r\n")?; + } + + // write the blank line + count += stream.write_all_count(b"\r\n")?; + + // Write out the files's content + let mut file = File::open(&filepart.path)?; + count += std::io::copy(&mut file, stream)? as usize; + } + &Node::Multipart((ref headers, ref subnodes)) => { + // Get boundary + let boundary = get_multipart_boundary(headers)?; + + // write the multipart headers + for header in headers.iter() { + count += stream.write_all_count(header.name().as_bytes())?; + count += stream.write_all_count(b": ")?; + count += stream.write_all_count(header.value_string().as_bytes())?; + count += stream.write_all_count(b"\r\n")?; + } + + // write the blank line + count += stream.write_all_count(b"\r\n")?; + + // Recurse + count += write_multipart(stream, &boundary, &subnodes)?; + } + } + + // write a line terminator + count += stream.write_all_count(b"\r\n")?; + } + + // write a final boundary + count += stream.write_all_count(b"--")?; + count += stream.write_all_count(&boundary)?; + count += stream.write_all_count(b"--")?; + + Ok(count) +} + +pub fn write_chunk(stream: &mut S, chunk: &[u8]) -> Result<(), ::std::io::Error> { + write!(stream, "{:x}\r\n", chunk.len())?; + stream.write_all(chunk)?; + stream.write_all(b"\r\n")?; + Ok(()) +} diff --git a/src/multipart/mime_multipart/tests.rs b/src/multipart/mime_multipart/tests.rs new file mode 100644 index 0000000000..269c31d177 --- /dev/null +++ b/src/multipart/mime_multipart/tests.rs @@ -0,0 +1,332 @@ +// Copyright 2016 mime-multipart Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +use super::*; + +use std::net::SocketAddr; + +use hyper::buffer::BufReader; +use hyper::net::NetworkStream; +use hyper::server::Request as HyperRequest; + +use mock::MockStream; + +use hyper::header::{ContentDisposition, ContentType, DispositionParam, DispositionType, Headers}; +// This is required to import the old style macros +use mime::*; + +#[test] +fn parser() { + let input = b"POST / HTTP/1.1\r\n\ + Host: example.domain\r\n\ + Content-Type: multipart/mixed; boundary=\"abcdefg\"\r\n\ + Content-Length: 1000\r\n\ + \r\n\ + --abcdefg\r\n\ + Content-Type: application/json\r\n\ + \r\n\ + {\r\n\ + \"id\": 15\r\n\ + }\r\n\ + --abcdefg\r\n\ + Content-Disposition: Attachment; filename=\"image.gif\"\r\n\ + Content-Type: image/gif\r\n\ + \r\n\ + This is a file\r\n\ + with two lines\r\n\ + --abcdefg\r\n\ + Content-Disposition: Attachment; filename=\"file.txt\"\r\n\ + \r\n\ + This is a file\r\n\ + --abcdefg--"; + + let mut mock = MockStream::with_input(input); + + let mock: &mut dyn NetworkStream = &mut mock; + let mut stream = BufReader::new(mock); + let sock: SocketAddr = "127.0.0.1:80".parse().unwrap(); + let req = HyperRequest::new(&mut stream, sock).unwrap(); + let (_, _, headers, _, _, mut reader) = req.deconstruct(); + + match read_multipart_body(&mut reader, &headers, false) { + Ok(nodes) => { + assert_eq!(nodes.len(), 3); + + if let Node::Part(ref part) = nodes[0] { + assert_eq!( + part.body, + b"{\r\n\ + \"id\": 15\r\n\ + }" + ); + } else { + panic!("1st node of wrong type"); + } + + if let Node::File(ref filepart) = nodes[1] { + assert_eq!(filepart.size, Some(30)); + assert_eq!(filepart.filename().unwrap().unwrap(), "image.gif"); + assert_eq!(filepart.content_type().unwrap(), mime!(Image / Gif)); + + assert!(filepart.path.exists()); + assert!(filepart.path.is_file()); + } else { + panic!("2nd node of wrong type"); + } + + if let Node::File(ref filepart) = nodes[2] { + assert_eq!(filepart.size, Some(14)); + assert_eq!(filepart.filename().unwrap().unwrap(), "file.txt"); + assert!(filepart.content_type().is_none()); + + assert!(filepart.path.exists()); + assert!(filepart.path.is_file()); + } else { + panic!("3rd node of wrong type"); + } + } + Err(err) => panic!("{}", err), + } +} + +#[test] +fn mixed_parser() { + let input = b"POST / HTTP/1.1\r\n\ + Host: example.domain\r\n\ + Content-Type: multipart/form-data; boundary=AaB03x\r\n\ + Content-Length: 1000\r\n\ + \r\n\ + --AaB03x\r\n\ + Content-Disposition: form-data; name=\"submit-name\"\r\n\ + \r\n\ + Larry\r\n\ + --AaB03x\r\n\ + Content-Disposition: form-data; name=\"files\"\r\n\ + Content-Type: multipart/mixed; boundary=BbC04y\r\n\ + \r\n\ + --BbC04y\r\n\ + Content-Disposition: file; filename=\"file1.txt\"\r\n\ + \r\n\ + ... contents of file1.txt ...\r\n\ + --BbC04y\r\n\ + Content-Disposition: file; filename=\"awesome_image.gif\"\r\n\ + Content-Type: image/gif\r\n\ + Content-Transfer-Encoding: binary\r\n\ + \r\n\ + ... contents of awesome_image.gif ...\r\n\ + --BbC04y--\r\n\ + --AaB03x--"; + + let mut mock = MockStream::with_input(input); + + let mock: &mut dyn NetworkStream = &mut mock; + let mut stream = BufReader::new(mock); + let sock: SocketAddr = "127.0.0.1:80".parse().unwrap(); + let req = HyperRequest::new(&mut stream, sock).unwrap(); + let (_, _, headers, _, _, mut reader) = req.deconstruct(); + + match read_multipart_body(&mut reader, &headers, false) { + Ok(nodes) => { + assert_eq!(nodes.len(), 2); + + if let Node::Part(ref part) = nodes[0] { + let cd: &ContentDisposition = part.headers.get().unwrap(); + let cd_name: String = get_content_disposition_name(&cd).unwrap(); + assert_eq!(&*cd_name, "submit-name"); + assert_eq!(::std::str::from_utf8(&*part.body).unwrap(), "Larry"); + } else { + panic!("1st node of wrong type"); + } + + if let Node::Multipart((ref headers, ref subnodes)) = nodes[1] { + let cd: &ContentDisposition = headers.get().unwrap(); + let cd_name: String = get_content_disposition_name(&cd).unwrap(); + assert_eq!(&*cd_name, "files"); + + assert_eq!(subnodes.len(), 2); + + if let Node::File(ref filepart) = subnodes[0] { + assert_eq!(filepart.size, Some(29)); + assert_eq!(filepart.filename().unwrap().unwrap(), "file1.txt"); + assert!(filepart.content_type().is_none()); + + assert!(filepart.path.exists()); + assert!(filepart.path.is_file()); + } else { + panic!("1st subnode of wrong type"); + } + + if let Node::File(ref filepart) = subnodes[1] { + assert_eq!(filepart.size, Some(37)); + assert_eq!(filepart.filename().unwrap().unwrap(), "awesome_image.gif"); + assert_eq!(filepart.content_type().unwrap(), mime!(Image / Gif)); + + assert!(filepart.path.exists()); + assert!(filepart.path.is_file()); + } else { + panic!("2st subnode of wrong type"); + } + } else { + panic!("2st node of wrong type"); + } + } + Err(err) => panic!("{}", err), + } +} + +#[test] +fn test_line_feed() { + let input = b"POST /test HTTP/1.1\r\n\ + Host: example.domain\r\n\ + Cookie: session_id=a36ZVwAAAACDQ9gzBCzDVZ1VNrnZEI1U\r\n\ + Content-Type: multipart/form-data; boundary=\"ABCDEFG\"\r\n\ + Content-Length: 10000\r\n\ + \r\n\ + --ABCDEFG\n\ + Content-Disposition: form-data; name=\"consignment_id\"\n\ + \n\ + 4\n\ + --ABCDEFG\n\ + Content-Disposition: form-data; name=\"note\"\n\ + \n\ + Check out this file about genomes!\n\ + --ABCDEFG\n\ + Content-Type: text/plain\n\ + Content-Disposition: attachment; filename=genome.txt\n\ + \n\ + This is a text file about genomes, apparently.\n\ + Read on.\n\ + --ABCDEFG--"; + + let mut mock = MockStream::with_input(input); + + let mock: &mut dyn NetworkStream = &mut mock; + let mut stream = BufReader::new(mock); + let sock: SocketAddr = "127.0.0.1:80".parse().unwrap(); + let req = HyperRequest::new(&mut stream, sock).unwrap(); + let (_, _, headers, _, _, mut reader) = req.deconstruct(); + + if let Err(e) = read_multipart_body(&mut reader, &headers, false) { + panic!("{}", e); + } +} + +#[inline] +fn get_content_disposition_name(cd: &ContentDisposition) -> Option { + if let Some(&DispositionParam::Ext(_, ref value)) = cd.parameters.iter().find(|&x| match *x { + DispositionParam::Ext(ref token, _) => &*token == "name", + _ => false, + }) { + Some(value.clone()) + } else { + None + } +} + +#[test] +fn test_output() { + let mut output: Vec = Vec::new(); + let boundary = generate_boundary(); + + let first_name = Part { + headers: { + let mut h = Headers::new(); + h.set(ContentType(Mime(TopLevel::Text, SubLevel::Plain, vec![]))); + h.set(ContentDisposition { + disposition: DispositionType::Ext("form-data".to_owned()), + parameters: vec![DispositionParam::Ext( + "name".to_owned(), + "first_name".to_owned(), + )], + }); + h + }, + body: b"Michael".to_vec(), + }; + + let last_name = Part { + headers: { + let mut h = Headers::new(); + h.set(ContentType(Mime(TopLevel::Text, SubLevel::Plain, vec![]))); + h.set(ContentDisposition { + disposition: DispositionType::Ext("form-data".to_owned()), + parameters: vec![DispositionParam::Ext( + "name".to_owned(), + "last_name".to_owned(), + )], + }); + h + }, + body: b"Dilger".to_vec(), + }; + + let mut nodes: Vec = Vec::new(); + nodes.push(Node::Part(first_name)); + nodes.push(Node::Part(last_name)); + + let count = match write_multipart(&mut output, &boundary, &nodes) { + Ok(c) => c, + Err(e) => panic!("{:?}", e), + }; + assert_eq!(count, output.len()); + + let string = String::from_utf8_lossy(&output); + + // Hard to compare programmatically since the headers could come in any order. + println!("{}", string); + + assert_eq!(output.len(), 390); +} + +#[test] +fn test_chunked() { + let mut output: Vec = Vec::new(); + let boundary = generate_boundary(); + + let first_name = Part { + headers: { + let mut h = Headers::new(); + h.set(ContentType(Mime(TopLevel::Text, SubLevel::Plain, vec![]))); + h.set(ContentDisposition { + disposition: DispositionType::Ext("form-data".to_owned()), + parameters: vec![DispositionParam::Ext( + "name".to_owned(), + "first_name".to_owned(), + )], + }); + h + }, + body: b"Michael".to_vec(), + }; + + let last_name = Part { + headers: { + let mut h = Headers::new(); + h.set(ContentType(Mime(TopLevel::Text, SubLevel::Plain, vec![]))); + h.set(ContentDisposition { + disposition: DispositionType::Ext("form-data".to_owned()), + parameters: vec![DispositionParam::Ext( + "name".to_owned(), + "last_name".to_owned(), + )], + }); + h + }, + body: b"Dilger".to_vec(), + }; + + let mut nodes: Vec = Vec::new(); + nodes.push(Node::Part(first_name)); + nodes.push(Node::Part(last_name)); + + let string = String::from_utf8_lossy(&output); + + // Hard to compare programmatically since the headers could come in any order. + println!("{}", string); + + assert_eq!(output.len(), 557); +} diff --git a/src/multipart/mod.rs b/src/multipart/mod.rs index 7c9f99a0c0..fd2095d567 100644 --- a/src/multipart/mod.rs +++ b/src/multipart/mod.rs @@ -2,5 +2,7 @@ #[cfg(feature = "multipart_form")] pub mod form; +#[cfg(feature = "mime_multipart")] +pub mod mime_multipart; #[cfg(feature = "multipart_related")] pub mod related; diff --git a/src/multipart/related.rs b/src/multipart/related.rs index a502a4752d..67baef5360 100644 --- a/src/multipart/related.rs +++ b/src/multipart/related.rs @@ -3,18 +3,11 @@ use hyper_0_10::header::{ContentType, Headers}; use mime_0_2::Mime; -/// Construct the Body for a multipart/related request. The mime 0.2.6 library -/// does not parse quoted-string parameters correctly. The boundary doesn't -/// need to be a quoted string if it does not contain a '/', hence ensure -/// no such boundary is used. +/// Construct the boundary for a multipart/related request. +/// Requirement for the boundary is that it is statistically +/// unlikely to occur within the body of the message. pub fn generate_boundary() -> Vec { - let mut boundary = mime_multipart::generate_boundary(); - for b in boundary.iter_mut() { - if *b == b'/' { - *b = b'.'; - } - } - + let boundary: Vec = "MultipartBoundary".as_bytes(); boundary }