From 936ba2a3682f9c7becd287392fcc66014c16dd32 Mon Sep 17 00:00:00 2001 From: axon-q Date: Wed, 6 Jun 2018 14:06:01 +0000 Subject: [PATCH 01/10] multipart: parse and validate Content-Disposition --- Cargo.toml | 1 + src/error.rs | 3 + src/header/common/content_disposition.rs | 84 +++++++++------- src/header/common/mod.rs | 4 +- src/header/mod.rs | 120 +++++++++++++++++++++++ src/lib.rs | 2 + src/multipart.rs | 23 ++++- 7 files changed, 193 insertions(+), 44 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9cd3304fd..9d080f53b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,6 +77,7 @@ time = "0.1" encoding = "0.2" language-tags = "0.2" lazy_static = "1.0" +unicase = "2.1" url = { version="1.7", features=["query_encoding"] } cookie = { version="0.10", features=["percent-encode"] } brotli2 = { version="^0.3.2", optional = true } diff --git a/src/error.rs b/src/error.rs index cfb6a0287..f4de36813 100644 --- a/src/error.rs +++ b/src/error.rs @@ -353,6 +353,9 @@ pub enum MultipartError { /// Can not parse Content-Type header #[fail(display = "Can not parse Content-Type header")] ParseContentType, + /// Can not parse Content-Disposition header + #[fail(display = "Can not parse Content-Disposition header")] + ParseContentDisposition, /// Multipart boundary is not found #[fail(display = "Multipart boundary is not found")] Boundary, diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs index 0fcd6ee09..4d1a0c6d2 100644 --- a/src/header/common/content_disposition.rs +++ b/src/header/common/content_disposition.rs @@ -7,13 +7,14 @@ // IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml use language_tags::LanguageTag; -use std::fmt; use unicase; -use header::{Header, Raw, parsing}; -use header::parsing::{parse_extended_value, http_percent_encode}; +use header; +use header::{Header, IntoHeaderValue, Writer}; use header::shared::Charset; +use std::fmt::{self, Write}; + /// The implied disposition of the content of the HTTP body. #[derive(Clone, Debug, PartialEq)] pub enum DispositionType { @@ -88,19 +89,14 @@ pub struct ContentDisposition { /// Disposition parameters pub parameters: Vec, } - -impl Header for ContentDisposition { - fn header_name() -> &'static str { - static NAME: &'static str = "Content-Disposition"; - NAME - } - - fn parse_header(raw: &Raw) -> ::Result { - parsing::from_one_raw_str(raw).and_then(|s: String| { +impl ContentDisposition { + /// Parse a raw Content-Disposition header value + pub fn from_raw(hv: Option<&header::HeaderValue>) -> Result { + header::from_one_raw_str(hv).and_then(|s: String| { let mut sections = s.split(';'); let disposition = match sections.next() { Some(s) => s.trim(), - None => return Err(::Error::Header), + None => return Err(::error::ParseError::Header), }; let mut cd = ContentDisposition { @@ -120,13 +116,13 @@ impl Header for ContentDisposition { let key = if let Some(key) = parts.next() { key.trim() } else { - return Err(::Error::Header); + return Err(::error::ParseError::Header); }; let val = if let Some(val) = parts.next() { val.trim() } else { - return Err(::Error::Header); + return Err(::error::ParseError::Header); }; cd.parameters.push( @@ -135,7 +131,7 @@ impl Header for ContentDisposition { Charset::Ext("UTF-8".to_owned()), None, val.trim_matches('"').as_bytes().to_owned()) } else if unicase::eq_ascii(&*key, "filename*") { - let extended_value = try!(parse_extended_value(val)); + let extended_value = try!(header::parse_extended_value(val)); DispositionParam::Filename(extended_value.charset, extended_value.language_tag, extended_value.value) } else { DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned()) @@ -146,10 +142,25 @@ impl Header for ContentDisposition { Ok(cd) }) } +} - #[inline] - fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result { - f.fmt_line(self) +impl IntoHeaderValue for ContentDisposition { + type Error = header::InvalidHeaderValueBytes; + + fn try_into(self) -> Result { + let mut writer = Writer::new(); + let _ = write!(&mut writer, "{}", self); + header::HeaderValue::from_shared(writer.take()) + } +} + +impl Header for ContentDisposition { + fn name() -> header::HeaderName { + header::CONTENT_DISPOSITION + } + + fn parse(msg: &T) -> Result { + Self::from_raw(msg.headers().get(Self::name())) } } @@ -183,7 +194,7 @@ impl fmt::Display for ContentDisposition { try!(write!(f, "{}", lang)); }; try!(write!(f, "'")); - try!(http_percent_encode(f, bytes)) + try!(header::http_percent_encode(f, bytes)) } }, DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)), @@ -196,15 +207,14 @@ impl fmt::Display for ContentDisposition { #[cfg(test)] mod tests { use super::{ContentDisposition,DispositionType,DispositionParam}; - use ::header::Header; - use ::header::shared::Charset; - + use header::HeaderValue; + use header::shared::Charset; #[test] - fn test_parse_header() { - assert!(ContentDisposition::parse_header(&"".into()).is_err()); + fn test_from_raw() { + assert!(ContentDisposition::from_raw(Some(&HeaderValue::from_static(""))).is_err()); - let a = "form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"".into(); - let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); + let a = HeaderValue::from_static("form-data; dummy=3; name=upload;\r\n filename=\"sample.png\""); + let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap(); let b = ContentDisposition { disposition: DispositionType::Ext("form-data".to_owned()), parameters: vec![ @@ -217,8 +227,8 @@ mod tests { }; assert_eq!(a, b); - let a = "attachment; filename=\"image.jpg\"".into(); - let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); + let a = HeaderValue::from_static("attachment; filename=\"image.jpg\""); + let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap(); let b = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![ @@ -229,8 +239,8 @@ mod tests { }; assert_eq!(a, b); - let a = "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates".into(); - let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); + let a = HeaderValue::from_static("attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates"); + let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap(); let b = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![ @@ -246,18 +256,18 @@ mod tests { #[test] fn test_display() { let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates"; - let a = as_string.into(); - let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); + let a = HeaderValue::from_static(as_string); + let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap(); let display_rendered = format!("{}",a); assert_eq!(as_string, display_rendered); - let a = "attachment; filename*=UTF-8''black%20and%20white.csv".into(); - let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); + let a = HeaderValue::from_static("attachment; filename*=UTF-8''black%20and%20white.csv"); + let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap(); let display_rendered = format!("{}",a); assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered); - let a = "attachment; filename=colourful.csv".into(); - let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); + let a = HeaderValue::from_static("attachment; filename=colourful.csv"); + let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap(); let display_rendered = format!("{}",a); assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered); } diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs index 08f8e0cc4..e6185b5a7 100644 --- a/src/header/common/mod.rs +++ b/src/header/common/mod.rs @@ -13,7 +13,7 @@ pub use self::accept_language::AcceptLanguage; pub use self::accept::Accept; pub use self::allow::Allow; pub use self::cache_control::{CacheControl, CacheDirective}; -//pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam}; +pub use self::content_disposition::{ContentDisposition, DispositionType, DispositionParam}; pub use self::content_language::ContentLanguage; pub use self::content_range::{ContentRange, ContentRangeSpec}; pub use self::content_type::ContentType; @@ -334,7 +334,7 @@ mod accept_language; mod accept; mod allow; mod cache_control; -//mod content_disposition; +mod content_disposition; mod content_language; mod content_range; mod content_type; diff --git a/src/header/mod.rs b/src/header/mod.rs index a9c42e29c..e4d4e0491 100644 --- a/src/header/mod.rs +++ b/src/header/mod.rs @@ -8,6 +8,7 @@ use bytes::{Bytes, BytesMut}; use mime::Mime; use modhttp::header::GetAll; use modhttp::Error as HttpError; +use percent_encoding; pub use modhttp::header::*; @@ -259,3 +260,122 @@ where } Ok(()) } + +// From hyper v0.11.27 src/header/parsing.rs + +/// An extended header parameter value (i.e., tagged with a character set and optionally, +/// a language), as defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). +#[derive(Clone, Debug, PartialEq)] +pub struct ExtendedValue { + /// The character set that is used to encode the `value` to a string. + pub charset: Charset, + /// The human language details of the `value`, if available. + pub language_tag: Option, + /// The parameter value, as expressed in octets. + pub value: Vec, +} + +/// Parses extended header parameter values (`ext-value`), as defined in +/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). +/// +/// Extended values are denoted by parameter names that end with `*`. +/// +/// ## ABNF +/// +/// ```text +/// ext-value = charset "'" [ language ] "'" value-chars +/// ; like RFC 2231's +/// ; (see [RFC2231], Section 7) +/// +/// charset = "UTF-8" / "ISO-8859-1" / mime-charset +/// +/// mime-charset = 1*mime-charsetc +/// mime-charsetc = ALPHA / DIGIT +/// / "!" / "#" / "$" / "%" / "&" +/// / "+" / "-" / "^" / "_" / "`" +/// / "{" / "}" / "~" +/// ; as in Section 2.3 of [RFC2978] +/// ; except that the single quote is not included +/// ; SHOULD be registered in the IANA charset registry +/// +/// language = +/// +/// value-chars = *( pct-encoded / attr-char ) +/// +/// pct-encoded = "%" HEXDIG HEXDIG +/// ; see [RFC3986], Section 2.1 +/// +/// attr-char = ALPHA / DIGIT +/// / "!" / "#" / "$" / "&" / "+" / "-" / "." +/// / "^" / "_" / "`" / "|" / "~" +/// ; token except ( "*" / "'" / "%" ) +/// ``` +pub fn parse_extended_value(val: &str) -> Result { + + // Break into three pieces separated by the single-quote character + let mut parts = val.splitn(3,'\''); + + // Interpret the first piece as a Charset + let charset: Charset = match parts.next() { + None => return Err(::error::ParseError::Header), + Some(n) => FromStr::from_str(n).map_err(|_| ::error::ParseError::Header)?, + }; + + // Interpret the second piece as a language tag + let lang: Option = match parts.next() { + None => return Err(::error::ParseError::Header), + Some("") => None, + Some(s) => match s.parse() { + Ok(lt) => Some(lt), + Err(_) => return Err(::error::ParseError::Header), + } + }; + + // Interpret the third piece as a sequence of value characters + let value: Vec = match parts.next() { + None => return Err(::error::ParseError::Header), + Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(), + }; + + Ok(ExtendedValue { + charset: charset, + language_tag: lang, + value: value, + }) +} + + +impl fmt::Display for ExtendedValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let encoded_value = + percent_encoding::percent_encode(&self.value[..], self::percent_encoding_http::HTTP_VALUE); + if let Some(ref lang) = self.language_tag { + write!(f, "{}'{}'{}", self.charset, lang, encoded_value) + } else { + write!(f, "{}''{}", self.charset, encoded_value) + } + } +} + +/// Percent encode a sequence of bytes with a character set defined in +/// [https://tools.ietf.org/html/rfc5987#section-3.2][url] +/// +/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2 +pub fn http_percent_encode(f: &mut fmt::Formatter, bytes: &[u8]) -> fmt::Result { + let encoded = percent_encoding::percent_encode(bytes, self::percent_encoding_http::HTTP_VALUE); + fmt::Display::fmt(&encoded, f) +} +mod percent_encoding_http { + use percent_encoding; + + // internal module because macro is hard-coded to make a public item + // but we don't want to public export this item + define_encode_set! { + // This encode set is used for HTTP header values and is defined at + // https://tools.ietf.org/html/rfc5987#section-3.2 + pub HTTP_VALUE = [percent_encoding::SIMPLE_ENCODE_SET] | { + ' ', '"', '%', '\'', '(', ')', '*', ',', '/', ':', ';', '<', '-', '>', '?', + '[', '\\', ']', '{', '}' + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 5d3767a29..25b4ef776 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,6 +118,7 @@ extern crate tokio_io; extern crate tokio_reactor; extern crate tokio_tcp; extern crate tokio_timer; +extern crate unicase; extern crate url; #[macro_use] extern crate serde; @@ -128,6 +129,7 @@ extern crate encoding; extern crate flate2; extern crate h2 as http2; extern crate num_cpus; +#[macro_use] extern crate percent_encoding; extern crate serde_json; extern crate serde_urlencoded; diff --git a/src/multipart.rs b/src/multipart.rs index f310327f4..632a40c24 100644 --- a/src/multipart.rs +++ b/src/multipart.rs @@ -7,7 +7,7 @@ use std::{cmp, fmt}; use bytes::Bytes; use futures::task::{current as current_task, Task}; use futures::{Async, Poll, Stream}; -use http::header::{self, HeaderMap, HeaderName, HeaderValue}; +use http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}; use http::HttpTryFrom; use httparse; use mime; @@ -362,7 +362,7 @@ where headers, mt, field, - ))))) + )?)))) } } } @@ -378,6 +378,7 @@ impl Drop for InnerMultipart { /// A single field in a multipart stream pub struct Field { ct: mime::Mime, + cd: ContentDisposition, headers: HeaderMap, inner: Rc>>, safety: Safety, @@ -390,13 +391,20 @@ where fn new( safety: Safety, headers: HeaderMap, ct: mime::Mime, inner: Rc>>, - ) -> Self { - Field { + ) -> Result { + // RFC 7578: 'Each part MUST contain a Content-Disposition header field + // where the disposition type is "form-data".' + let cd = ContentDisposition::from_raw( + headers.get(::http::header::CONTENT_DISPOSITION) + ).map_err(|_| MultipartError::ParseContentDisposition)?; + + Ok(Field { ct, + cd, headers, inner, safety, - } + }) } /// Get a map of headers @@ -408,6 +416,11 @@ where pub fn content_type(&self) -> &mime::Mime { &self.ct } + + /// Get the content disposition of the field + pub fn content_disposition(&self) -> &ContentDisposition { + &self.cd + } } impl Stream for Field From 82c888df22d4a7edefbfdef14f80bb8715b84d17 Mon Sep 17 00:00:00 2001 From: axon-q Date: Thu, 7 Jun 2018 09:10:46 +0000 Subject: [PATCH 02/10] fix test --- src/header/common/content_disposition.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs index 4d1a0c6d2..b26644e58 100644 --- a/src/header/common/content_disposition.rs +++ b/src/header/common/content_disposition.rs @@ -213,7 +213,7 @@ mod tests { fn test_from_raw() { assert!(ContentDisposition::from_raw(Some(&HeaderValue::from_static(""))).is_err()); - let a = HeaderValue::from_static("form-data; dummy=3; name=upload;\r\n filename=\"sample.png\""); + let a = HeaderValue::from_static("form-data; dummy=3; name=upload; filename=\"sample.png\""); let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap(); let b = ContentDisposition { disposition: DispositionType::Ext("form-data".to_owned()), From c0c1817b5c9cbcdd4c74c1671dd20bdd3eda7486 Mon Sep 17 00:00:00 2001 From: axon-q Date: Thu, 7 Jun 2018 10:33:00 +0000 Subject: [PATCH 03/10] remove unicase dependency --- Cargo.toml | 1 - src/header/common/content_disposition.rs | 12 +++++------- src/lib.rs | 1 - 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9d080f53b..9cd3304fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,6 @@ time = "0.1" encoding = "0.2" language-tags = "0.2" lazy_static = "1.0" -unicase = "2.1" url = { version="1.7", features=["query_encoding"] } cookie = { version="0.10", features=["percent-encode"] } brotli2 = { version="^0.3.2", optional = true } diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs index b26644e58..b2563c0d9 100644 --- a/src/header/common/content_disposition.rs +++ b/src/header/common/content_disposition.rs @@ -7,8 +7,6 @@ // IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml use language_tags::LanguageTag; -use unicase; - use header; use header::{Header, IntoHeaderValue, Writer}; use header::shared::Charset; @@ -100,9 +98,9 @@ impl ContentDisposition { }; let mut cd = ContentDisposition { - disposition: if unicase::eq_ascii(&*disposition, "inline") { + disposition: if disposition.eq_ignore_ascii_case("inline") { DispositionType::Inline - } else if unicase::eq_ascii(&*disposition, "attachment") { + } else if disposition.eq_ignore_ascii_case("attachment") { DispositionType::Attachment } else { DispositionType::Ext(disposition.to_owned()) @@ -126,11 +124,11 @@ impl ContentDisposition { }; cd.parameters.push( - if unicase::eq_ascii(&*key, "filename") { + if key.eq_ignore_ascii_case("filename") { DispositionParam::Filename( Charset::Ext("UTF-8".to_owned()), None, val.trim_matches('"').as_bytes().to_owned()) - } else if unicase::eq_ascii(&*key, "filename*") { + } else if key.eq_ignore_ascii_case("filename*") { let extended_value = try!(header::parse_extended_value(val)); DispositionParam::Filename(extended_value.charset, extended_value.language_tag, extended_value.value) } else { @@ -177,7 +175,7 @@ impl fmt::Display for ContentDisposition { let mut use_simple_format: bool = false; if opt_lang.is_none() { if let Charset::Ext(ref ext) = *charset { - if unicase::eq_ascii(&**ext, "utf-8") { + if ext.eq_ignore_ascii_case("utf-8") { use_simple_format = true; } } diff --git a/src/lib.rs b/src/lib.rs index 25b4ef776..5b1420305 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,7 +118,6 @@ extern crate tokio_io; extern crate tokio_reactor; extern crate tokio_tcp; extern crate tokio_timer; -extern crate unicase; extern crate url; #[macro_use] extern crate serde; From 5a37a8b813a6aa606a3269bf5b141125812cce42 Mon Sep 17 00:00:00 2001 From: axon-q Date: Thu, 7 Jun 2018 10:55:36 +0000 Subject: [PATCH 04/10] restore hyper tests --- src/header/mod.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/header/mod.rs b/src/header/mod.rs index e4d4e0491..8a7dd5bde 100644 --- a/src/header/mod.rs +++ b/src/header/mod.rs @@ -379,3 +379,78 @@ mod percent_encoding_http { } } } + +#[cfg(test)] +mod tests { + use header::shared::Charset; + use super::{ExtendedValue, parse_extended_value}; + use language_tags::LanguageTag; + + #[test] + fn test_parse_extended_value_with_encoding_and_language_tag() { + let expected_language_tag = "en".parse::().unwrap(); + // RFC 5987, Section 3.2.2 + // Extended notation, using the Unicode character U+00A3 (POUND SIGN) + let result = parse_extended_value("iso-8859-1'en'%A3%20rates"); + assert!(result.is_ok()); + let extended_value = result.unwrap(); + assert_eq!(Charset::Iso_8859_1, extended_value.charset); + assert!(extended_value.language_tag.is_some()); + assert_eq!(expected_language_tag, extended_value.language_tag.unwrap()); + assert_eq!(vec![163, b' ', b'r', b'a', b't', b'e', b's'], extended_value.value); + } + + #[test] + fn test_parse_extended_value_with_encoding() { + // RFC 5987, Section 3.2.2 + // Extended notation, using the Unicode characters U+00A3 (POUND SIGN) + // and U+20AC (EURO SIGN) + let result = parse_extended_value("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates"); + assert!(result.is_ok()); + let extended_value = result.unwrap(); + assert_eq!(Charset::Ext("UTF-8".to_string()), extended_value.charset); + assert!(extended_value.language_tag.is_none()); + assert_eq!(vec![194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', b't', b'e', b's'], extended_value.value); + } + + #[test] + fn test_parse_extended_value_missing_language_tag_and_encoding() { + // From: https://greenbytes.de/tech/tc2231/#attwithfn2231quot2 + let result = parse_extended_value("foo%20bar.html"); + assert!(result.is_err()); + } + + #[test] + fn test_parse_extended_value_partially_formatted() { + let result = parse_extended_value("UTF-8'missing third part"); + assert!(result.is_err()); + } + + #[test] + fn test_parse_extended_value_partially_formatted_blank() { + let result = parse_extended_value("blank second part'"); + assert!(result.is_err()); + } + + #[test] + fn test_fmt_extended_value_with_encoding_and_language_tag() { + let extended_value = ExtendedValue { + charset: Charset::Iso_8859_1, + language_tag: Some("en".parse().expect("Could not parse language tag")), + value: vec![163, b' ', b'r', b'a', b't', b'e', b's'], + }; + assert_eq!("ISO-8859-1'en'%A3%20rates", format!("{}", extended_value)); + } + + #[test] + fn test_fmt_extended_value_with_encoding() { + let extended_value = ExtendedValue { + charset: Charset::Ext("UTF-8".to_string()), + language_tag: None, + value: vec![194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a', + b't', b'e', b's'], + }; + assert_eq!("UTF-8''%C2%A3%20and%20%E2%82%AC%20rates", + format!("{}", extended_value)); + } +} From 31a301c9a6e39ec1995ebdbefefb1a79c4a84754 Mon Sep 17 00:00:00 2001 From: axon-q Date: Thu, 7 Jun 2018 11:38:35 +0000 Subject: [PATCH 05/10] fix multipart test --- src/multipart.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/multipart.rs b/src/multipart.rs index 632a40c24..9727ec0a1 100644 --- a/src/multipart.rs +++ b/src/multipart.rs @@ -736,9 +736,11 @@ mod tests { let bytes = Bytes::from( "testasdadsad\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ test\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ data\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n"); @@ -751,6 +753,12 @@ mod tests { match multipart.poll() { Ok(Async::Ready(Some(item))) => match item { MultipartItem::Field(mut field) => { + { + use http::header::{DispositionType, DispositionParam}; + let cd = field.content_disposition(); + assert_eq!(cd.disposition, DispositionType::Ext("form-data".into())); + assert_eq!(cd.parameters[0], DispositionParam::Ext("name".into(), "file".into())); + } assert_eq!(field.content_type().type_(), mime::TEXT); assert_eq!(field.content_type().subtype(), mime::PLAIN); From a6e07c06b6ecd80ddd53cd9f2fb100b2486a8452 Mon Sep 17 00:00:00 2001 From: axon-q Date: Thu, 7 Jun 2018 12:32:49 +0000 Subject: [PATCH 06/10] move CD parsing to Content-Type parsing location --- src/multipart.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/multipart.rs b/src/multipart.rs index 9727ec0a1..a92c235a8 100644 --- a/src/multipart.rs +++ b/src/multipart.rs @@ -317,6 +317,13 @@ where return Ok(Async::NotReady); }; + // content disposition + // RFC 7578: 'Each part MUST contain a Content-Disposition header field + // where the disposition type is "form-data".' + let cd = ContentDisposition::from_raw( + headers.get(::http::header::CONTENT_DISPOSITION) + ).map_err(|_| MultipartError::ParseContentDisposition)?; + // content type let mut mt = mime::APPLICATION_OCTET_STREAM; if let Some(content_type) = headers.get(header::CONTENT_TYPE) { @@ -360,9 +367,10 @@ where Ok(Async::Ready(Some(MultipartItem::Field(Field::new( safety.clone(), headers, + cd, mt, field, - )?)))) + ))))) } } } @@ -377,8 +385,8 @@ impl Drop for InnerMultipart { /// A single field in a multipart stream pub struct Field { - ct: mime::Mime, cd: ContentDisposition, + ct: mime::Mime, headers: HeaderMap, inner: Rc>>, safety: Safety, @@ -389,22 +397,16 @@ where S: Stream, { fn new( - safety: Safety, headers: HeaderMap, ct: mime::Mime, + safety: Safety, headers: HeaderMap, cd: ContentDisposition, ct: mime::Mime, inner: Rc>>, - ) -> Result { - // RFC 7578: 'Each part MUST contain a Content-Disposition header field - // where the disposition type is "form-data".' - let cd = ContentDisposition::from_raw( - headers.get(::http::header::CONTENT_DISPOSITION) - ).map_err(|_| MultipartError::ParseContentDisposition)?; - - Ok(Field { - ct, + ) -> Self { + Field { cd, + ct, headers, inner, safety, - }) + } } /// Get a map of headers From 97b5410aad12890b3bc4b3bfd90967d35399c524 Mon Sep 17 00:00:00 2001 From: axon-q Date: Thu, 7 Jun 2018 12:55:35 +0000 Subject: [PATCH 07/10] remove Option from ContentDisposition::from_raw() argument --- src/header/common/content_disposition.rs | 24 ++++++++++++++---------- src/multipart.rs | 8 +++++--- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs index b2563c0d9..bc5014f56 100644 --- a/src/header/common/content_disposition.rs +++ b/src/header/common/content_disposition.rs @@ -89,8 +89,8 @@ pub struct ContentDisposition { } impl ContentDisposition { /// Parse a raw Content-Disposition header value - pub fn from_raw(hv: Option<&header::HeaderValue>) -> Result { - header::from_one_raw_str(hv).and_then(|s: String| { + pub fn from_raw(hv: &header::HeaderValue) -> Result { + header::from_one_raw_str(Some(hv)).and_then(|s: String| { let mut sections = s.split(';'); let disposition = match sections.next() { Some(s) => s.trim(), @@ -158,7 +158,11 @@ impl Header for ContentDisposition { } fn parse(msg: &T) -> Result { - Self::from_raw(msg.headers().get(Self::name())) + if let Some(h) = msg.headers().get(Self::name()) { + Self::from_raw(&h) + } else { + Err(::error::ParseError::Header) + } } } @@ -209,10 +213,10 @@ mod tests { use header::shared::Charset; #[test] fn test_from_raw() { - assert!(ContentDisposition::from_raw(Some(&HeaderValue::from_static(""))).is_err()); + assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err()); let a = HeaderValue::from_static("form-data; dummy=3; name=upload; filename=\"sample.png\""); - let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap(); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::Ext("form-data".to_owned()), parameters: vec![ @@ -226,7 +230,7 @@ mod tests { assert_eq!(a, b); let a = HeaderValue::from_static("attachment; filename=\"image.jpg\""); - let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap(); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![ @@ -238,7 +242,7 @@ mod tests { assert_eq!(a, b); let a = HeaderValue::from_static("attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates"); - let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap(); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![ @@ -255,17 +259,17 @@ mod tests { fn test_display() { let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates"; let a = HeaderValue::from_static(as_string); - let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap(); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let display_rendered = format!("{}",a); assert_eq!(as_string, display_rendered); let a = HeaderValue::from_static("attachment; filename*=UTF-8''black%20and%20white.csv"); - let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap(); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let display_rendered = format!("{}",a); assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered); let a = HeaderValue::from_static("attachment; filename=colourful.csv"); - let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap(); + let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let display_rendered = format!("{}",a); assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered); } diff --git a/src/multipart.rs b/src/multipart.rs index a92c235a8..542c6c3df 100644 --- a/src/multipart.rs +++ b/src/multipart.rs @@ -320,9 +320,11 @@ where // content disposition // RFC 7578: 'Each part MUST contain a Content-Disposition header field // where the disposition type is "form-data".' - let cd = ContentDisposition::from_raw( - headers.get(::http::header::CONTENT_DISPOSITION) - ).map_err(|_| MultipartError::ParseContentDisposition)?; + let cd = match headers.get(::http::header::CONTENT_DISPOSITION) { + Some(content_disposition) => ContentDisposition::from_raw(content_disposition) + .map_err(|_| MultipartError::ParseContentDisposition)?, + None => return Err(MultipartError::ParseContentDisposition) + }; // content type let mut mt = mime::APPLICATION_OCTET_STREAM; From 56e0dc06c16bba83b78116a4b18c9b7c0040cc1f Mon Sep 17 00:00:00 2001 From: axon-q Date: Thu, 7 Jun 2018 17:29:46 +0000 Subject: [PATCH 08/10] defer parsing until user method call --- src/error.rs | 3 --- src/multipart.rs | 29 +++++++++++------------------ 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/error.rs b/src/error.rs index f4de36813..cfb6a0287 100644 --- a/src/error.rs +++ b/src/error.rs @@ -353,9 +353,6 @@ pub enum MultipartError { /// Can not parse Content-Type header #[fail(display = "Can not parse Content-Type header")] ParseContentType, - /// Can not parse Content-Disposition header - #[fail(display = "Can not parse Content-Disposition header")] - ParseContentDisposition, /// Multipart boundary is not found #[fail(display = "Multipart boundary is not found")] Boundary, diff --git a/src/multipart.rs b/src/multipart.rs index 542c6c3df..9c5c0380c 100644 --- a/src/multipart.rs +++ b/src/multipart.rs @@ -317,15 +317,6 @@ where return Ok(Async::NotReady); }; - // content disposition - // RFC 7578: 'Each part MUST contain a Content-Disposition header field - // where the disposition type is "form-data".' - let cd = match headers.get(::http::header::CONTENT_DISPOSITION) { - Some(content_disposition) => ContentDisposition::from_raw(content_disposition) - .map_err(|_| MultipartError::ParseContentDisposition)?, - None => return Err(MultipartError::ParseContentDisposition) - }; - // content type let mut mt = mime::APPLICATION_OCTET_STREAM; if let Some(content_type) = headers.get(header::CONTENT_TYPE) { @@ -369,7 +360,6 @@ where Ok(Async::Ready(Some(MultipartItem::Field(Field::new( safety.clone(), headers, - cd, mt, field, ))))) @@ -387,7 +377,6 @@ impl Drop for InnerMultipart { /// A single field in a multipart stream pub struct Field { - cd: ContentDisposition, ct: mime::Mime, headers: HeaderMap, inner: Rc>>, @@ -399,11 +388,10 @@ where S: Stream, { fn new( - safety: Safety, headers: HeaderMap, cd: ContentDisposition, ct: mime::Mime, + safety: Safety, headers: HeaderMap, ct: mime::Mime, inner: Rc>>, ) -> Self { Field { - cd, ct, headers, inner, @@ -421,9 +409,15 @@ where &self.ct } - /// Get the content disposition of the field - pub fn content_disposition(&self) -> &ContentDisposition { - &self.cd + /// Get the content disposition of the field, if it exists + pub fn content_disposition(&self) -> Option { + // RFC 7578: 'Each part MUST contain a Content-Disposition header field + // where the disposition type is "form-data".' + if let Some(content_disposition) = self.headers.get(::http::header::CONTENT_DISPOSITION) { + ContentDisposition::from_raw(content_disposition).ok() + } else { + None + } } } @@ -744,7 +738,6 @@ mod tests { Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ test\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ - Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ data\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n"); @@ -759,7 +752,7 @@ mod tests { MultipartItem::Field(mut field) => { { use http::header::{DispositionType, DispositionParam}; - let cd = field.content_disposition(); + let cd = field.content_disposition().unwrap(); assert_eq!(cd.disposition, DispositionType::Ext("form-data".into())); assert_eq!(cd.parameters[0], DispositionParam::Ext("name".into(), "file".into())); } From e970846167a80942d4d957f46fa5c9df069fac97 Mon Sep 17 00:00:00 2001 From: axon-q Date: Thu, 7 Jun 2018 17:59:35 +0000 Subject: [PATCH 09/10] update changelog --- CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index f46fa9cbf..bf33040ec 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ ### Added +* Add `.content_disposition()` method to parse Content-Disposition of + multipart fields + * Re-export `actix::prelude::*` as `actix_web::actix` module. * `HttpRequest::url_for_static()` for a named route with no variables segments From a11f3c112f7e9da8fc3f1dce4577d1da7f35fe88 Mon Sep 17 00:00:00 2001 From: axon-q Date: Thu, 7 Jun 2018 21:16:28 +0000 Subject: [PATCH 10/10] fix doc test --- src/header/common/content_disposition.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs index bc5014f56..93102d464 100644 --- a/src/header/common/content_disposition.rs +++ b/src/header/common/content_disposition.rs @@ -68,17 +68,16 @@ pub enum DispositionParam { /// # Example /// /// ``` -/// use hyper::header::{Headers, ContentDisposition, DispositionType, DispositionParam, Charset}; +/// use actix_web::http::header::{ContentDisposition, DispositionType, DispositionParam, Charset}; /// -/// let mut headers = Headers::new(); -/// headers.set(ContentDisposition { +/// let cd = ContentDisposition { /// disposition: DispositionType::Attachment, /// parameters: vec![DispositionParam::Filename( /// Charset::Iso_8859_1, // The character set for the bytes of the filename /// None, // The optional language tag (see `language-tag` crate) /// b"\xa9 Copyright 1989.txt".to_vec() // the actual bytes of the filename /// )] -/// }); +/// }; /// ``` #[derive(Clone, Debug, PartialEq)] pub struct ContentDisposition {