1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-12-01 02:44:37 +01:00

multipart: parse and validate Content-Disposition

This commit is contained in:
axon-q 2018-06-06 14:06:01 +00:00
parent 2d0b609c68
commit 936ba2a368
7 changed files with 193 additions and 44 deletions

View File

@ -77,6 +77,7 @@ time = "0.1"
encoding = "0.2" encoding = "0.2"
language-tags = "0.2" language-tags = "0.2"
lazy_static = "1.0" lazy_static = "1.0"
unicase = "2.1"
url = { version="1.7", features=["query_encoding"] } url = { version="1.7", features=["query_encoding"] }
cookie = { version="0.10", features=["percent-encode"] } cookie = { version="0.10", features=["percent-encode"] }
brotli2 = { version="^0.3.2", optional = true } brotli2 = { version="^0.3.2", optional = true }

View File

@ -353,6 +353,9 @@ pub enum MultipartError {
/// Can not parse Content-Type header /// Can not parse Content-Type header
#[fail(display = "Can not parse Content-Type header")] #[fail(display = "Can not parse Content-Type header")]
ParseContentType, ParseContentType,
/// Can not parse Content-Disposition header
#[fail(display = "Can not parse Content-Disposition header")]
ParseContentDisposition,
/// Multipart boundary is not found /// Multipart boundary is not found
#[fail(display = "Multipart boundary is not found")] #[fail(display = "Multipart boundary is not found")]
Boundary, Boundary,

View File

@ -7,13 +7,14 @@
// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml // IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml
use language_tags::LanguageTag; use language_tags::LanguageTag;
use std::fmt;
use unicase; use unicase;
use header::{Header, Raw, parsing}; use header;
use header::parsing::{parse_extended_value, http_percent_encode}; use header::{Header, IntoHeaderValue, Writer};
use header::shared::Charset; use header::shared::Charset;
use std::fmt::{self, Write};
/// The implied disposition of the content of the HTTP body. /// The implied disposition of the content of the HTTP body.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum DispositionType { pub enum DispositionType {
@ -88,19 +89,14 @@ pub struct ContentDisposition {
/// Disposition parameters /// Disposition parameters
pub parameters: Vec<DispositionParam>, pub parameters: Vec<DispositionParam>,
} }
impl ContentDisposition {
impl Header for ContentDisposition { /// Parse a raw Content-Disposition header value
fn header_name() -> &'static str { pub fn from_raw(hv: Option<&header::HeaderValue>) -> Result<Self, ::error::ParseError> {
static NAME: &'static str = "Content-Disposition"; header::from_one_raw_str(hv).and_then(|s: String| {
NAME
}
fn parse_header(raw: &Raw) -> ::Result<ContentDisposition> {
parsing::from_one_raw_str(raw).and_then(|s: String| {
let mut sections = s.split(';'); let mut sections = s.split(';');
let disposition = match sections.next() { let disposition = match sections.next() {
Some(s) => s.trim(), Some(s) => s.trim(),
None => return Err(::Error::Header), None => return Err(::error::ParseError::Header),
}; };
let mut cd = ContentDisposition { let mut cd = ContentDisposition {
@ -120,13 +116,13 @@ impl Header for ContentDisposition {
let key = if let Some(key) = parts.next() { let key = if let Some(key) = parts.next() {
key.trim() key.trim()
} else { } else {
return Err(::Error::Header); return Err(::error::ParseError::Header);
}; };
let val = if let Some(val) = parts.next() { let val = if let Some(val) = parts.next() {
val.trim() val.trim()
} else { } else {
return Err(::Error::Header); return Err(::error::ParseError::Header);
}; };
cd.parameters.push( cd.parameters.push(
@ -135,7 +131,7 @@ impl Header for ContentDisposition {
Charset::Ext("UTF-8".to_owned()), None, Charset::Ext("UTF-8".to_owned()), None,
val.trim_matches('"').as_bytes().to_owned()) val.trim_matches('"').as_bytes().to_owned())
} else if unicase::eq_ascii(&*key, "filename*") { } 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) DispositionParam::Filename(extended_value.charset, extended_value.language_tag, extended_value.value)
} else { } else {
DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned()) DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned())
@ -146,10 +142,25 @@ impl Header for ContentDisposition {
Ok(cd) Ok(cd)
}) })
} }
}
#[inline] impl IntoHeaderValue for ContentDisposition {
fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result { type Error = header::InvalidHeaderValueBytes;
f.fmt_line(self)
fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
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<T: ::HttpMessage>(msg: &T) -> Result<Self, ::error::ParseError> {
Self::from_raw(msg.headers().get(Self::name()))
} }
} }
@ -183,7 +194,7 @@ impl fmt::Display for ContentDisposition {
try!(write!(f, "{}", lang)); try!(write!(f, "{}", lang));
}; };
try!(write!(f, "'")); 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)), DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)),
@ -196,15 +207,14 @@ impl fmt::Display for ContentDisposition {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ContentDisposition,DispositionType,DispositionParam}; use super::{ContentDisposition,DispositionType,DispositionParam};
use ::header::Header; use header::HeaderValue;
use ::header::shared::Charset; use header::shared::Charset;
#[test] #[test]
fn test_parse_header() { fn test_from_raw() {
assert!(ContentDisposition::parse_header(&"".into()).is_err()); 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 = HeaderValue::from_static("form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"");
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap();
let b = ContentDisposition { let b = ContentDisposition {
disposition: DispositionType::Ext("form-data".to_owned()), disposition: DispositionType::Ext("form-data".to_owned()),
parameters: vec![ parameters: vec![
@ -217,8 +227,8 @@ mod tests {
}; };
assert_eq!(a, b); assert_eq!(a, b);
let a = "attachment; filename=\"image.jpg\"".into(); let a = HeaderValue::from_static("attachment; filename=\"image.jpg\"");
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap();
let b = ContentDisposition { let b = ContentDisposition {
disposition: DispositionType::Attachment, disposition: DispositionType::Attachment,
parameters: vec![ parameters: vec![
@ -229,8 +239,8 @@ mod tests {
}; };
assert_eq!(a, b); assert_eq!(a, b);
let a = "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates".into(); let a = HeaderValue::from_static("attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates");
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap();
let b = ContentDisposition { let b = ContentDisposition {
disposition: DispositionType::Attachment, disposition: DispositionType::Attachment,
parameters: vec![ parameters: vec![
@ -246,18 +256,18 @@ mod tests {
#[test] #[test]
fn test_display() { fn test_display() {
let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates"; let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates";
let a = as_string.into(); let a = HeaderValue::from_static(as_string);
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap();
let display_rendered = format!("{}",a); let display_rendered = format!("{}",a);
assert_eq!(as_string, display_rendered); assert_eq!(as_string, display_rendered);
let a = "attachment; filename*=UTF-8''black%20and%20white.csv".into(); let a = HeaderValue::from_static("attachment; filename*=UTF-8''black%20and%20white.csv");
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap();
let display_rendered = format!("{}",a); let display_rendered = format!("{}",a);
assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered); assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered);
let a = "attachment; filename=colourful.csv".into(); let a = HeaderValue::from_static("attachment; filename=colourful.csv");
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(Some(&a)).unwrap();
let display_rendered = format!("{}",a); let display_rendered = format!("{}",a);
assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered); assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered);
} }

View File

@ -13,7 +13,7 @@ pub use self::accept_language::AcceptLanguage;
pub use self::accept::Accept; pub use self::accept::Accept;
pub use self::allow::Allow; pub use self::allow::Allow;
pub use self::cache_control::{CacheControl, CacheDirective}; 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_language::ContentLanguage;
pub use self::content_range::{ContentRange, ContentRangeSpec}; pub use self::content_range::{ContentRange, ContentRangeSpec};
pub use self::content_type::ContentType; pub use self::content_type::ContentType;
@ -334,7 +334,7 @@ mod accept_language;
mod accept; mod accept;
mod allow; mod allow;
mod cache_control; mod cache_control;
//mod content_disposition; mod content_disposition;
mod content_language; mod content_language;
mod content_range; mod content_range;
mod content_type; mod content_type;

View File

@ -8,6 +8,7 @@ use bytes::{Bytes, BytesMut};
use mime::Mime; use mime::Mime;
use modhttp::header::GetAll; use modhttp::header::GetAll;
use modhttp::Error as HttpError; use modhttp::Error as HttpError;
use percent_encoding;
pub use modhttp::header::*; pub use modhttp::header::*;
@ -259,3 +260,122 @@ where
} }
Ok(()) 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<LanguageTag>,
/// The parameter value, as expressed in octets.
pub value: Vec<u8>,
}
/// 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 <extended-initial-value>
/// ; (see [RFC2231], Section 7)
///
/// charset = "UTF-8" / "ISO-8859-1" / mime-charset
///
/// mime-charset = 1*mime-charsetc
/// mime-charsetc = ALPHA / DIGIT
/// / "!" / "#" / "$" / "%" / "&"
/// / "+" / "-" / "^" / "_" / "`"
/// / "{" / "}" / "~"
/// ; as <mime-charset> in Section 2.3 of [RFC2978]
/// ; except that the single quote is not included
/// ; SHOULD be registered in the IANA charset registry
///
/// language = <Language-Tag, defined in [RFC5646], Section 2.1>
///
/// 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<ExtendedValue, ::error::ParseError> {
// 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<LanguageTag> = 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<u8> = 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] | {
' ', '"', '%', '\'', '(', ')', '*', ',', '/', ':', ';', '<', '-', '>', '?',
'[', '\\', ']', '{', '}'
}
}
}

View File

@ -118,6 +118,7 @@ extern crate tokio_io;
extern crate tokio_reactor; extern crate tokio_reactor;
extern crate tokio_tcp; extern crate tokio_tcp;
extern crate tokio_timer; extern crate tokio_timer;
extern crate unicase;
extern crate url; extern crate url;
#[macro_use] #[macro_use]
extern crate serde; extern crate serde;
@ -128,6 +129,7 @@ extern crate encoding;
extern crate flate2; extern crate flate2;
extern crate h2 as http2; extern crate h2 as http2;
extern crate num_cpus; extern crate num_cpus;
#[macro_use]
extern crate percent_encoding; extern crate percent_encoding;
extern crate serde_json; extern crate serde_json;
extern crate serde_urlencoded; extern crate serde_urlencoded;

View File

@ -7,7 +7,7 @@ use std::{cmp, fmt};
use bytes::Bytes; use bytes::Bytes;
use futures::task::{current as current_task, Task}; use futures::task::{current as current_task, Task};
use futures::{Async, Poll, Stream}; use futures::{Async, Poll, Stream};
use http::header::{self, HeaderMap, HeaderName, HeaderValue}; use http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue};
use http::HttpTryFrom; use http::HttpTryFrom;
use httparse; use httparse;
use mime; use mime;
@ -362,7 +362,7 @@ where
headers, headers,
mt, mt,
field, field,
))))) )?))))
} }
} }
} }
@ -378,6 +378,7 @@ impl<S> Drop for InnerMultipart<S> {
/// A single field in a multipart stream /// A single field in a multipart stream
pub struct Field<S> { pub struct Field<S> {
ct: mime::Mime, ct: mime::Mime,
cd: ContentDisposition,
headers: HeaderMap, headers: HeaderMap,
inner: Rc<RefCell<InnerField<S>>>, inner: Rc<RefCell<InnerField<S>>>,
safety: Safety, safety: Safety,
@ -390,13 +391,20 @@ where
fn new( fn new(
safety: Safety, headers: HeaderMap, ct: mime::Mime, safety: Safety, headers: HeaderMap, ct: mime::Mime,
inner: Rc<RefCell<InnerField<S>>>, inner: Rc<RefCell<InnerField<S>>>,
) -> Self { ) -> Result<Self, MultipartError> {
Field { // 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, ct,
cd,
headers, headers,
inner, inner,
safety, safety,
} })
} }
/// Get a map of headers /// Get a map of headers
@ -408,6 +416,11 @@ where
pub fn content_type(&self) -> &mime::Mime { pub fn content_type(&self) -> &mime::Mime {
&self.ct &self.ct
} }
/// Get the content disposition of the field
pub fn content_disposition(&self) -> &ContentDisposition {
&self.cd
}
} }
impl<S> Stream for Field<S> impl<S> Stream for Field<S>