diff --git a/Cargo.toml b/Cargo.toml index 4cc9e86223..51997222b3 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", "httparse", "tempdir", "log", "encoding", "buf-read-ext"] serdejson = ["serde", "serde_json"] server = ["hyper/server"] http1 = ["hyper/http1"] @@ -49,8 +49,12 @@ 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 } +httparse = { version = "1.3", optional = true } +tempdir = { version = "0.3", optional = true } +log = { version = "0.4", optional = true } +encoding = { version = "0.2", optional = true } +buf-read-ext = { version = "0.4", optional = true } # UDS (Unix Domain Sockets) tokio = { version = "1.0", default-features = false, optional = true } diff --git a/src/multipart/related.rs b/src/multipart/related.rs deleted file mode 100644 index a502a4752d..0000000000 --- a/src/multipart/related.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Helper functions for multipart/related support - -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. -pub fn generate_boundary() -> Vec { - let mut boundary = mime_multipart::generate_boundary(); - for b in boundary.iter_mut() { - if *b == b'/' { - *b = b'.'; - } - } - - boundary -} - -/// Create the multipart headers from a request so that we can parse the -/// body using `mime_multipart::read_multipart_body`. -pub fn create_multipart_headers( - content_type: Option<&hyper::header::HeaderValue>, -) -> Result { - let content_type = content_type - .ok_or_else(|| "Missing Content-Type header".to_string())? - .to_str() - .map_err(|e| format!("Couldn't read Content-Type header value: {}", e))? - .parse::() - .map_err(|_e| "Couldn't parse Content-Type header value".to_string())?; - - // Insert top-level content type header into a Headers object. - let mut multipart_headers = Headers::new(); - multipart_headers.set(ContentType(content_type)); - - Ok(multipart_headers) -} diff --git a/src/multipart/related/error.rs b/src/multipart/related/error.rs new file mode 100644 index 0000000000..b007826532 --- /dev/null +++ b/src/multipart/related/error.rs @@ -0,0 +1,132 @@ +//! 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. +// +// File defining Error enum used in the module. + +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_0_10; + +/// 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, + /// The request headers ended pre-maturely. + EofInMainHeaders, + /// The request body ended prior to reaching the expected starting boundary. + EofBeforeFirstBoundary, + /// Missing CRLF after boundary. + NoCrLfAfterBoundary, + /// The request body ended prematurely while parsing headers of a multipart part. + EofInPartHeaders, + /// The request body ended prematurely while streaming a file part. + EofInFile, + /// The request body ended prematurely while reading a multipart part. + 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_0_10::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_0_10::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/related/mock.rs b/src/multipart/related/mock.rs new file mode 100644 index 0000000000..41b3ad6684 --- /dev/null +++ b/src/multipart/related/mock.rs @@ -0,0 +1,101 @@ +//! 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, Cursor, Read, Write}; +use std::net::SocketAddr; +use std::time::Duration; + +use hyper_0_10::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/related/mod.rs b/src/multipart/related/mod.rs new file mode 100644 index 0000000000..d6cefec8b7 --- /dev/null +++ b/src/multipart/related/mod.rs @@ -0,0 +1,387 @@ +//! 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. +// +// Content taken from mime_multipart crate: https://github.com/mikedilger/mime-multipart +// +///////////////////////////////////////////////////////////////////////////////////////// +// +// Apache License +// Version 2.0, January 2004 +// http://www.apache.org/licenses/ +// +// TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +// +// 1. Definitions. +// +// "License" shall mean the terms and conditions for use, reproduction, +// and distribution as defined by Sections 1 through 9 of this document. +// +// "Licensor" shall mean the copyright owner or entity authorized by +// the copyright owner that is granting the License. +// +// "Legal Entity" shall mean the union of the acting entity and all +// other entities that control, are controlled by, or are under common +// control with that entity. For the purposes of this definition, +// "control" means (i) the power, direct or indirect, to cause the +// direction or management of such entity, whether by contract or +// otherwise, or (ii) ownership of fifty percent (50%) or more of the +// outstanding shares, or (iii) beneficial ownership of such entity. +// +// "You" (or "Your") shall mean an individual or Legal Entity +// exercising permissions granted by this License. +// +// "Source" form shall mean the preferred form for making modifications, +// including but not limited to software source code, documentation +// source, and configuration files. +// +// "Object" form shall mean any form resulting from mechanical +// transformation or translation of a Source form, including but +// not limited to compiled object code, generated documentation, +// and conversions to other media types. +// +// "Work" shall mean the work of authorship, whether in Source or +// Object form, made available under the License, as indicated by a +// copyright notice that is included in or attached to the work +// (an example is provided in the Appendix below). +// +// "Derivative Works" shall mean any work, whether in Source or Object +// form, that is based on (or derived from) the Work and for which the +// editorial revisions, annotations, elaborations, or other modifications +// represent, as a whole, an original work of authorship. For the purposes +// of this License, Derivative Works shall not include works that remain +// separable from, or merely link (or bind by name) to the interfaces of, +// the Work and Derivative Works thereof. +// +// "Contribution" shall mean any work of authorship, including +// the original version of the Work and any modifications or additions +// to that Work or Derivative Works thereof, that is intentionally +// submitted to Licensor for inclusion in the Work by the copyright owner +// or by an individual or Legal Entity authorized to submit on behalf of +// the copyright owner. For the purposes of this definition, "submitted" +// means any form of electronic, verbal, or written communication sent +// to the Licensor or its representatives, including but not limited to +// communication on electronic mailing lists, source code control systems, +// and issue tracking systems that are managed by, or on behalf of, the +// Licensor for the purpose of discussing and improving the Work, but +// excluding communication that is conspicuously marked or otherwise +// designated in writing by the copyright owner as "Not a Contribution." +// +// "Contributor" shall mean Licensor and any individual or Legal Entity +// on behalf of whom a Contribution has been received by Licensor and +// subsequently incorporated within the Work. +// +// 2. Grant of Copyright License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// copyright license to reproduce, prepare Derivative Works of, +// publicly display, publicly perform, sublicense, and distribute the +// Work and such Derivative Works in Source or Object form. +// +// 3. Grant of Patent License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// (except as stated in this section) patent license to make, have made, +// use, offer to sell, sell, import, and otherwise transfer the Work, +// where such license applies only to those patent claims licensable +// by such Contributor that are necessarily infringed by their +// Contribution(s) alone or by combination of their Contribution(s) +// with the Work to which such Contribution(s) was submitted. If You +// institute patent litigation against any entity (including a +// cross-claim or counterclaim in a lawsuit) alleging that the Work +// or a Contribution incorporated within the Work constitutes direct +// or contributory patent infringement, then any patent licenses +// granted to You under this License for that Work shall terminate +// as of the date such litigation is filed. +// +// 4. Redistribution. You may reproduce and distribute copies of the +// Work or Derivative Works thereof in any medium, with or without +// modifications, and in Source or Object form, provided that You +// meet the following conditions: +// +// (a) You must give any other recipients of the Work or +// Derivative Works a copy of this License; and +// +// (b) You must cause any modified files to carry prominent notices +// stating that You changed the files; and +// +// (c) You must retain, in the Source form of any Derivative Works +// that You distribute, all copyright, patent, trademark, and +// attribution notices from the Source form of the Work, +// excluding those notices that do not pertain to any part of +// the Derivative Works; and +// +// (d) If the Work includes a "NOTICE" text file as part of its +// distribution, then any Derivative Works that You distribute must +// include a readable copy of the attribution notices contained +// within such NOTICE file, excluding those notices that do not +// pertain to any part of the Derivative Works, in at least one +// of the following places: within a NOTICE text file distributed +// as part of the Derivative Works; within the Source form or +// documentation, if provided along with the Derivative Works; or, +// within a display generated by the Derivative Works, if and +// wherever such third-party notices normally appear. The contents +// of the NOTICE file are for informational purposes only and +// do not modify the License. You may add Your own attribution +// notices within Derivative Works that You distribute, alongside +// or as an addendum to the NOTICE text from the Work, provided +// that such additional attribution notices cannot be construed +// as modifying the License. +// +// You may add Your own copyright statement to Your modifications and +// may provide additional or different license terms and conditions +// for use, reproduction, or distribution of Your modifications, or +// for any such Derivative Works as a whole, provided Your use, +// reproduction, and distribution of the Work otherwise complies with +// the conditions stated in this License. +// +// 5. Submission of Contributions. Unless You explicitly state otherwise, +// any Contribution intentionally submitted for inclusion in the Work +// by You to the Licensor shall be under the terms and conditions of +// this License, without any additional terms or conditions. +// Notwithstanding the above, nothing herein shall supersede or modify +// the terms of any separate license agreement you may have executed +// with Licensor regarding such Contributions. +// +// 6. Trademarks. This License does not grant permission to use the trade +// names, trademarks, service marks, or product names of the Licensor, +// except as required for reasonable and customary use in describing the +// origin of the Work and reproducing the content of the NOTICE file. +// +// 7. Disclaimer of Warranty. Unless required by applicable law or +// agreed to in writing, Licensor provides the Work (and each +// Contributor provides its Contributions) on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied, including, without limitation, any warranties or conditions +// of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +// PARTICULAR PURPOSE. You are solely responsible for determining the +// appropriateness of using or redistributing the Work and assume any +// risks associated with Your exercise of permissions under this License. +// +// 8. Limitation of Liability. In no event and under no legal theory, +// whether in tort (including negligence), contract, or otherwise, +// unless required by applicable law (such as deliberate and grossly +// negligent acts) or agreed to in writing, shall any Contributor be +// liable to You for damages, including any direct, indirect, special, +// incidental, or consequential damages of any character arising as a +// result of this License or out of the use or inability to use the +// Work (including but not limited to damages for loss of goodwill, +// work stoppage, computer failure or malfunction, or any and all +// other commercial damages or losses), even if such Contributor +// has been advised of the possibility of such damages. +// +// 9. Accepting Warranty or Additional Liability. While redistributing +// the Work or Derivative Works thereof, You may choose to offer, +// and charge a fee for, acceptance of support, warranty, indemnity, +// or other liability obligations and/or rights consistent with this +// License. However, in accepting such obligations, You may act only +// on Your own behalf and on Your sole responsibility, not on behalf +// of any other Contributor, and only if You agree to indemnify, +// defend, and hold each Contributor harmless for any liability +// incurred by, or claims asserted against, such Contributor by reason +// of your accepting any such warranty or additional liability. +// +// END OF TERMS AND CONDITIONS +// +// APPENDIX: How to apply the Apache License to your work. +// +// To apply the Apache License to your work, attach the following +// boilerplate notice, with the fields enclosed by brackets "[]" +// replaced with your own identifying information. (Don't include +// the brackets!) The text should be enclosed in the appropriate +// comment syntax for the file format. We also recommend that a +// file or class name and description of purpose be included on the +// same "printed page" as the copyright notice for easier +// identification within third-party archives. +// +// Copyright 2023 libraries.core +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +///////////////////////////////////////////////////////////////////////// +// +// MIT License +// +// Copyright (c) 2010 The Rust Project Developers +// +// 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. +// +// Declare objects used in the module. + +pub mod error; +pub mod readwrite; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +pub use error::Error; + +use hyper::header::HeaderValue; +use hyper_0_10::header::{ContentDisposition, ContentType, Headers}; +use mime_0_2::Mime; +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 { + /// The headers of the part + pub headers: Headers, + /// The body of the part + 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 { + /// Create a new FilePart object + 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. + // The tempdir is always unique, with a random part added to the directory name. + let mut path = TempDir::new("mime_multipart")?.into_path(); + let tempdir = Some(path.clone()); + // since the directory is always unique, the temporary filename can be the same. + path.push("NewTemporaryFile".to_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) => readwrite::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)), +} + +/// 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 boundary: Vec = "MultipartBoundary".as_bytes().to_vec(); + boundary +} + +/// Create the multipart headers from a request so that we can parse the +/// body using `mime_multipart::read_multipart_body`. +pub fn create_multipart_headers(content_type: Option<&HeaderValue>) -> Result { + let content_type = content_type + .ok_or_else(|| "Missing Content-Type header".to_string())? + .to_str() + .map_err(|e| format!("Couldn't read Content-Type header value: {}", e))? + .parse::() + .map_err(|_e| "Couldn't parse Content-Type header value".to_string())?; + + // Insert top-level content type header into a Headers object. + let mut multipart_headers = Headers::new(); + multipart_headers.set(ContentType(content_type)); + + Ok(multipart_headers) +} diff --git a/src/multipart/related/readwrite.rs b/src/multipart/related/readwrite.rs new file mode 100644 index 0000000000..e1d6e0a0dc --- /dev/null +++ b/src/multipart/related/readwrite.rs @@ -0,0 +1,350 @@ +//! 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. +// +// Content taken from mime_multipart crate: https://github.com/mikedilger/mime-multipart + +pub use crate::multipart::related::{error::Error, FilePart, Node, Part}; + +use buf_read_ext::BufReadExt; +use encoding::{all, DecoderTrap, Encoding}; + +use hyper_0_10::header::{ + Charset, ContentDisposition, ContentType, DispositionParam, DispositionType, Headers, +}; +use mime_0_2::{Attr, Mime, TopLevel, Value}; +use std::borrow::Cow; +use std::fs::File; +use std::io::{BufRead, BufReader, Read, Write}; + +/// 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) +} + +/// Get filename for content disposition. +#[inline] +pub 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) +} diff --git a/src/multipart/related/tests.rs b/src/multipart/related/tests.rs new file mode 100644 index 0000000000..941c9e145a --- /dev/null +++ b/src/multipart/related/tests.rs @@ -0,0 +1,285 @@ +// 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_0_10::buffer::BufReader; +use hyper_0_10::net::NetworkStream; +use hyper_0_10::server::Request as HyperRequest; + +use crate::multipart::related::readwrite::read_multipart_body; +use crate::multipart::related::readwrite::write_multipart; +use mock::MockStream; + +use hyper_0_10::header::{ + ContentDisposition, ContentType, DispositionParam, DispositionType, Headers, +}; +// This is required to import the old style macros +use mime_0_2::*; + +#[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); +}