From 32b5544ad91d6b96d2640f37b1b41cda16c17e86 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 6 Mar 2018 00:43:25 -0800 Subject: [PATCH] port hyper header --- CHANGES.md | 2 + Cargo.toml | 3 +- src/fs.rs | 164 ++++++++-- src/header/common/accept.rs | 152 +++++++++ src/header/common/accept_charset.rs | 63 ++++ src/header/common/accept_encoding.rs | 72 +++++ src/header/common/accept_language.rs | 74 +++++ src/header/common/allow.rs | 81 +++++ src/header/common/cache_control.rs | 231 ++++++++++++++ src/header/common/content_disposition.rs | 264 ++++++++++++++++ src/header/common/content_language.rs | 66 ++++ src/header/common/content_range.rs | 205 ++++++++++++ src/header/common/content_type.rs | 115 +++++++ src/header/common/date.rs | 34 ++ src/header/common/etag.rs | 96 ++++++ src/header/common/expires.rs | 39 +++ src/header/common/if_match.rs | 70 ++++ src/header/common/if_modified_since.rs | 83 ++--- src/header/common/if_none_match.rs | 91 ++++++ src/header/common/if_range.rs | 107 +++++++ src/header/common/if_unmodified_since.rs | 84 +++-- src/header/common/last_modified.rs | 38 +++ src/header/common/mod.rs | 350 +++++++++++++++++++- src/header/common/range.rs | 387 +++++++++++++++++++++++ src/header/mod.rs | 153 +++++++-- src/header/shared/charset.rs | 154 +++++++++ src/header/shared/encoding.rs | 57 ++++ src/header/shared/entity.rs | 230 ++++++++++++++ src/header/{ => shared}/httpdate.rs | 2 +- src/header/shared/mod.rs | 14 + src/header/shared/quality_item.rs | 265 ++++++++++++++++ src/httpmessage.rs | 2 +- src/httpresponse.rs | 41 ++- src/lib.rs | 1 + src/server/encoding.rs | 3 +- src/test.rs | 12 +- 36 files changed, 3639 insertions(+), 166 deletions(-) create mode 100644 src/header/common/accept.rs create mode 100644 src/header/common/accept_charset.rs create mode 100644 src/header/common/accept_encoding.rs create mode 100644 src/header/common/accept_language.rs create mode 100644 src/header/common/allow.rs create mode 100644 src/header/common/cache_control.rs create mode 100644 src/header/common/content_disposition.rs create mode 100644 src/header/common/content_language.rs create mode 100644 src/header/common/content_range.rs create mode 100644 src/header/common/content_type.rs create mode 100644 src/header/common/date.rs create mode 100644 src/header/common/etag.rs create mode 100644 src/header/common/expires.rs create mode 100644 src/header/common/if_match.rs create mode 100644 src/header/common/if_none_match.rs create mode 100644 src/header/common/if_range.rs create mode 100644 src/header/common/last_modified.rs create mode 100644 src/header/common/range.rs create mode 100644 src/header/shared/charset.rs create mode 100644 src/header/shared/encoding.rs create mode 100644 src/header/shared/entity.rs rename src/header/{ => shared}/httpdate.rs (99%) create mode 100644 src/header/shared/mod.rs create mode 100644 src/header/shared/quality_item.rs diff --git a/CHANGES.md b/CHANGES.md index 278e4780e..76cda851c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ * Enable compression support for `NamedFile` +* Better support for `NamedFile` type + * Add `ResponseError` impl for `SendRequestError`. This improves ergonomics of http client. diff --git a/Cargo.toml b/Cargo.toml index 9b69e2560..c56589df4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ http-range = "0.1" libc = "0.2" log = "0.4" mime = "0.3" -mime_guess = "1.8" +mime_guess = "2.0.0-alpha" num_cpus = "1.0" percent-encoding = "1.0" rand = "0.4" @@ -59,6 +59,7 @@ sha1 = "0.6" smallvec = "0.6" time = "0.1" encoding = "0.2" +language-tags = "0.2" url = { version="1.7", features=["query_encoding"] } cookie = { version="0.10", features=["percent-encode", "secure"] } diff --git a/src/fs.rs b/src/fs.rs index 9b1bd810c..1a41d81a1 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -4,15 +4,21 @@ use std::io; use std::io::Read; use std::fmt::Write; -use std::fs::{File, DirEntry}; +use std::fs::{File, DirEntry, Metadata}; use std::path::{Path, PathBuf}; use std::ops::{Deref, DerefMut}; +use std::time::{SystemTime, UNIX_EPOCH}; -use http::{header, Method}; +#[cfg(unix)] +use std::os::unix::fs::MetadataExt; + +use http::{Method, StatusCode}; use mime_guess::get_mime_type; +use header; use param::FromParam; use handler::{Handler, Responder}; +use httpmessage::HttpMessage; use httprequest::HttpRequest; use httpresponse::HttpResponse; use httpcodes::{HttpOk, HttpFound, HttpMethodNotAllowed}; @@ -20,7 +26,12 @@ use httpcodes::{HttpOk, HttpFound, HttpMethodNotAllowed}; /// A file with an associated name; responds with the Content-Type based on the /// file extension. #[derive(Debug)] -pub struct NamedFile(PathBuf, File); +pub struct NamedFile { + path: PathBuf, + file: File, + md: Metadata, + modified: Option, +} impl NamedFile { /// Attempts to open a file in read-only mode. @@ -30,18 +41,20 @@ impl NamedFile { /// ```rust /// use actix_web::fs::NamedFile; /// - /// # #[allow(unused_variables)] /// let file = NamedFile::open("foo.txt"); /// ``` pub fn open>(path: P) -> io::Result { let file = File::open(path.as_ref())?; - Ok(NamedFile(path.as_ref().to_path_buf(), file)) + let md = file.metadata()?; + let path = path.as_ref().to_path_buf(); + let modified = md.modified().ok(); + Ok(NamedFile{path, file, md, modified}) } /// Returns reference to the underlying `File` object. #[inline] pub fn file(&self) -> &File { - &self.1 + &self.file } /// Retrieve the path of this file. @@ -52,7 +65,6 @@ impl NamedFile { /// # use std::io; /// use actix_web::fs::NamedFile; /// - /// # #[allow(dead_code)] /// # fn path() -> io::Result<()> { /// let file = NamedFile::open("test.txt")?; /// assert_eq!(file.path().as_os_str(), "foo.txt"); @@ -61,7 +73,30 @@ impl NamedFile { /// ``` #[inline] pub fn path(&self) -> &Path { - self.0.as_path() + self.path.as_path() + } + + fn etag(&self) -> Option { + // This etag format is similar to Apache's. + self.modified.as_ref().map(|mtime| { + let ino = { + #[cfg(unix)] + { self.md.ino() } + #[cfg(not(unix))] + { 0 } + }; + + let dur = mtime.duration_since(UNIX_EPOCH) + .expect("modification time must be after epoch"); + header::EntityTag::strong( + format!("{:x}:{:x}:{:x}:{:x}", + ino, self.md.len(), dur.as_secs(), + dur.subsec_nanos())) + }) + } + + fn last_modified(&self) -> Option { + self.modified.map(|mtime| mtime.into()) } } @@ -69,35 +104,112 @@ impl Deref for NamedFile { type Target = File; fn deref(&self) -> &File { - &self.1 + &self.file } } impl DerefMut for NamedFile { fn deref_mut(&mut self) -> &mut File { - &mut self.1 + &mut self.file } } +/// Returns true if `req` has no `If-Match` header or one which matches `etag`. +fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { + match req.get_header::() { + Err(_) | Ok(header::IfMatch::Any) => true, + Ok(header::IfMatch::Items(ref items)) => { + if let Some(some_etag) = etag { + for item in items { + if item.strong_eq(some_etag) { + return true; + } + } + } + false + } + } +} + +/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`. +fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { + match req.get_header::() { + Ok(header::IfNoneMatch::Any) => false, + Ok(header::IfNoneMatch::Items(ref items)) => { + if let Some(some_etag) = etag { + for item in items { + if item.weak_eq(some_etag) { + return false; + } + } + } + true + } + Err(_) => true, + } +} + + impl Responder for NamedFile { type Item = HttpResponse; type Error = io::Error; fn respond_to(mut self, req: HttpRequest) -> Result { if *req.method() != Method::GET && *req.method() != Method::HEAD { - Ok(HttpMethodNotAllowed.build() - .header(header::CONTENT_TYPE, "text/plain") - .header(header::ALLOW, "GET, HEAD") - .body("This resource only supports GET and HEAD.").unwrap()) + return Ok(HttpMethodNotAllowed.build() + .header(header::http::CONTENT_TYPE, "text/plain") + .header(header::http::ALLOW, "GET, HEAD") + .body("This resource only supports GET and HEAD.").unwrap()) + } + + let etag = self.etag(); + let last_modified = self.last_modified(); + + // check preconditions + let precondition_failed = if !any_match(etag.as_ref(), &req) { + true + } else if let (Some(ref m), Ok(header::IfUnmodifiedSince(ref since))) = + (last_modified, req.get_header()) + { + m > since } else { - let mut resp = HttpOk.build(); - if let Some(ext) = self.path().extension() { - let mime = get_mime_type(&ext.to_string_lossy()); - resp.content_type(format!("{}", mime).as_str()); - } + false + }; + + // check last modified + let not_modified = if !none_match(etag.as_ref(), &req) { + true + } else if let (Some(ref m), Ok(header::IfModifiedSince(ref since))) = + (last_modified, req.get_header()) + { + m <= since + } else { + false + }; + + let mut resp = HttpOk.build(); + + resp + .if_some(self.path().extension(), |ext, resp| { + resp.set(header::ContentType(get_mime_type(&ext.to_string_lossy()))); + }) + .if_some(last_modified, |lm, resp| {resp.set(header::LastModified(lm));}) + .if_some(etag, |etag, resp| {resp.set(header::ETag(etag));}); + + if precondition_failed { + return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish().unwrap()) + } else if not_modified { + return Ok(resp.status(StatusCode::NOT_MODIFIED).finish().unwrap()) + } + + resp.content_length(self.md.len()); + + if *req.method() == Method::GET { let mut data = Vec::new(); - let _ = self.1.read_to_end(&mut data); + let _ = self.file.read_to_end(&mut data); Ok(resp.body(data).unwrap()) + } else { + Ok(resp.finish().unwrap()) } } } @@ -314,7 +426,8 @@ impl Handler for StaticFiles { #[cfg(test)] mod tests { use super::*; - use http::{header, StatusCode}; + use test::TestRequest; + use http::{header, Method, StatusCode}; #[test] fn test_named_file() { @@ -328,6 +441,15 @@ mod tests { assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/x-toml") } + #[test] + fn test_named_file_not_allowed() { + let req = TestRequest::default().method(Method::POST).finish(); + let file = NamedFile::open("Cargo.toml").unwrap(); + + let resp = file.respond_to(req).unwrap(); + assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); + } + #[test] fn test_static_files() { let mut st = StaticFiles::new(".", true); diff --git a/src/header/common/accept.rs b/src/header/common/accept.rs new file mode 100644 index 000000000..84a1d800b --- /dev/null +++ b/src/header/common/accept.rs @@ -0,0 +1,152 @@ +use mime::{self, Mime}; +use header::{QualityItem, qitem, http}; + +header! { + /// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2) + /// + /// The `Accept` header field can be used by user agents to specify + /// response media types that are acceptable. Accept header fields can + /// be used to indicate that the request is specifically limited to a + /// small set of desired types, as in the case of a request for an + /// in-line image + /// + /// # ABNF + /// + /// ```text + /// Accept = #( media-range [ accept-params ] ) + /// + /// media-range = ( "*/*" + /// / ( type "/" "*" ) + /// / ( type "/" subtype ) + /// ) *( OWS ";" OWS parameter ) + /// accept-params = weight *( accept-ext ) + /// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] + /// ``` + /// + /// # Example values + /// * `audio/*; q=0.2, audio/basic` + /// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c` + /// + /// # Examples + /// ```rust + /// # extern crate actix_web; + /// extern crate mime; + /// use actix_web::httpcodes::HttpOk; + /// use actix_web::header::{Accept, qitem}; + /// + /// let mut builder = HttpOk.build(); + /// + /// builder.set( + /// Accept(vec![ + /// qitem(mime::TEXT_HTML), + /// ]) + /// ); + /// ``` + /// + /// ```rust + /// # extern crate actix_web; + /// extern crate mime; + /// use actix_web::httpcodes::HttpOk; + /// use actix_web::header::{Accept, qitem}; + /// + /// let mut builder = HttpOk.build(); + /// + /// builder.set( + /// Accept(vec![ + /// qitem(mime::APPLICATION_JSON), + /// ]) + /// ); + /// ``` + /// + /// ```rust + /// # extern crate actix_web; + /// extern crate mime; + /// use actix_web::httpcodes::HttpOk; + /// use actix_web::header::{Accept, QualityItem, q, qitem}; + /// + /// let mut builder = HttpOk.build(); + /// + /// builder.set( + /// Accept(vec![ + /// qitem(mime::TEXT_HTML), + /// qitem("application/xhtml+xml".parse().unwrap()), + /// QualityItem::new( + /// mime::TEXT_XML, + /// q(900) + /// ), + /// qitem("image/webp".parse().unwrap()), + /// QualityItem::new( + /// mime::STAR_STAR, + /// q(800) + /// ), + /// ]) + /// ); + /// ``` + (Accept, http::ACCEPT) => (QualityItem)+ + + test_accept { + // Tests from the RFC + test_header!( + test1, + vec![b"audio/*; q=0.2, audio/basic"], + Some(HeaderField(vec![ + QualityItem::new("audio/*".parse().unwrap(), q(200)), + qitem("audio/basic".parse().unwrap()), + ]))); + test_header!( + test2, + vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], + Some(HeaderField(vec![ + QualityItem::new(TEXT_PLAIN, q(500)), + qitem(TEXT_HTML), + QualityItem::new( + "text/x-dvi".parse().unwrap(), + q(800)), + qitem("text/x-c".parse().unwrap()), + ]))); + // Custom tests + test_header!( + test3, + vec![b"text/plain; charset=utf-8"], + Some(Accept(vec![ + qitem(TEXT_PLAIN_UTF_8), + ]))); + test_header!( + test4, + vec![b"text/plain; charset=utf-8; q=0.5"], + Some(Accept(vec![ + QualityItem::new(TEXT_PLAIN_UTF_8, + q(500)), + ]))); + + #[test] + fn test_fuzzing1() { + use test::TestRequest; + let req = TestRequest::with_header(http::ACCEPT, "chunk#;e").finish(); + let header = Accept::parse(&req); + assert!(header.is_ok()); + } + } +} + +impl Accept { + /// A constructor to easily create `Accept: */*`. + pub fn star() -> Accept { + Accept(vec![qitem(mime::STAR_STAR)]) + } + + /// A constructor to easily create `Accept: application/json`. + pub fn json() -> Accept { + Accept(vec![qitem(mime::APPLICATION_JSON)]) + } + + /// A constructor to easily create `Accept: text/*`. + pub fn text() -> Accept { + Accept(vec![qitem(mime::TEXT_STAR)]) + } + + /// A constructor to easily create `Accept: image/*`. + pub fn image() -> Accept { + Accept(vec![qitem(mime::IMAGE_STAR)]) + } +} diff --git a/src/header/common/accept_charset.rs b/src/header/common/accept_charset.rs new file mode 100644 index 000000000..781445ded --- /dev/null +++ b/src/header/common/accept_charset.rs @@ -0,0 +1,63 @@ +use header::{http, Charset, QualityItem}; + +header! { + /// `Accept-Charset` header, defined in + /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3) + /// + /// The `Accept-Charset` header field can be sent by a user agent to + /// indicate what charsets are acceptable in textual response content. + /// This field allows user agents capable of understanding more + /// comprehensive or special-purpose charsets to signal that capability + /// to an origin server that is capable of representing information in + /// those charsets. + /// + /// # ABNF + /// + /// ```text + /// Accept-Charset = 1#( ( charset / "*" ) [ weight ] ) + /// ``` + /// + /// # Example values + /// * `iso-8859-5, unicode-1-1;q=0.8` + /// + /// # Examples + /// ```rust + /// # extern crate actix_web; + /// use actix_web::httpcodes::HttpOk; + /// use actix_web::header::{AcceptCharset, Charset, qitem}; + /// + /// let mut builder = HttpOk.build(); + /// builder.set( + /// AcceptCharset(vec![qitem(Charset::Us_Ascii)]) + /// ); + /// ``` + /// ```rust + /// # extern crate actix_web; + /// use actix_web::httpcodes::HttpOk; + /// use actix_web::header::{AcceptCharset, Charset, q, QualityItem}; + /// + /// let mut builder = HttpOk.build(); + /// builder.set( + /// AcceptCharset(vec![ + /// QualityItem::new(Charset::Us_Ascii, q(900)), + /// QualityItem::new(Charset::Iso_8859_10, q(200)), + /// ]) + /// ); + /// ``` + /// ```rust + /// # extern crate actix_web; + /// use actix_web::httpcodes::HttpOk; + /// use actix_web::header::{AcceptCharset, Charset, qitem}; + /// + /// let mut builder = HttpOk.build(); + /// builder.set( + /// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))]) + /// ); + /// ``` + (AcceptCharset, http::ACCEPT_CHARSET) => (QualityItem)+ + + test_accept_charset { + /// Testcase from RFC + test_header!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); + } +} diff --git a/src/header/common/accept_encoding.rs b/src/header/common/accept_encoding.rs new file mode 100644 index 000000000..c90f529bc --- /dev/null +++ b/src/header/common/accept_encoding.rs @@ -0,0 +1,72 @@ +use header::{Encoding, QualityItem}; + +header! { + /// `Accept-Encoding` header, defined in + /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.4) + /// + /// The `Accept-Encoding` header field can be used by user agents to + /// indicate what response content-codings are + /// acceptable in the response. An `identity` token is used as a synonym + /// for "no encoding" in order to communicate when no encoding is + /// preferred. + /// + /// # ABNF + /// + /// ```text + /// Accept-Encoding = #( codings [ weight ] ) + /// codings = content-coding / "identity" / "*" + /// ``` + /// + /// # Example values + /// * `compress, gzip` + /// * `` + /// * `*` + /// * `compress;q=0.5, gzip;q=1` + /// * `gzip;q=1.0, identity; q=0.5, *;q=0` + /// + /// # Examples + /// ``` + /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem}; + /// + /// let mut headers = Headers::new(); + /// headers.set( + /// AcceptEncoding(vec![qitem(Encoding::Chunked)]) + /// ); + /// ``` + /// ``` + /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem}; + /// + /// let mut headers = Headers::new(); + /// headers.set( + /// AcceptEncoding(vec![ + /// qitem(Encoding::Chunked), + /// qitem(Encoding::Gzip), + /// qitem(Encoding::Deflate), + /// ]) + /// ); + /// ``` + /// ``` + /// use hyper::header::{Headers, AcceptEncoding, Encoding, QualityItem, q, qitem}; + /// + /// let mut headers = Headers::new(); + /// headers.set( + /// AcceptEncoding(vec![ + /// qitem(Encoding::Chunked), + /// QualityItem::new(Encoding::Gzip, q(600)), + /// QualityItem::new(Encoding::EncodingExt("*".to_owned()), q(0)), + /// ]) + /// ); + /// ``` + (AcceptEncoding, "Accept-Encoding") => (QualityItem)* + + test_accept_encoding { + // From the RFC + test_header!(test1, vec![b"compress, gzip"]); + test_header!(test2, vec![b""], Some(AcceptEncoding(vec![]))); + test_header!(test3, vec![b"*"]); + // Note: Removed quality 1 from gzip + test_header!(test4, vec![b"compress;q=0.5, gzip"]); + // Note: Removed quality 1 from gzip + test_header!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); + } +} diff --git a/src/header/common/accept_language.rs b/src/header/common/accept_language.rs new file mode 100644 index 000000000..916181b42 --- /dev/null +++ b/src/header/common/accept_language.rs @@ -0,0 +1,74 @@ +use language_tags::LanguageTag; +use header::{http, QualityItem}; + + +header! { + /// `Accept-Language` header, defined in + /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5) + /// + /// The `Accept-Language` header field can be used by user agents to + /// indicate the set of natural languages that are preferred in the + /// response. + /// + /// # ABNF + /// + /// ```text + /// Accept-Language = 1#( language-range [ weight ] ) + /// language-range = + /// ``` + /// + /// # Example values + /// * `da, en-gb;q=0.8, en;q=0.7` + /// * `en-us;q=1.0, en;q=0.5, fr` + /// + /// # Examples + /// + /// ```rust + /// # extern crate actix_web; + /// # extern crate language_tags; + /// use actix_web::httpcodes::HttpOk; + /// use actix_web::header::{AcceptLanguage, LanguageTag, qitem}; + /// + /// let mut builder = HttpOk.build(); + /// let mut langtag: LanguageTag = Default::default(); + /// langtag.language = Some("en".to_owned()); + /// langtag.region = Some("US".to_owned()); + /// builder.set( + /// AcceptLanguage(vec![ + /// qitem(langtag), + /// ]) + /// ); + /// ``` + /// + /// ```rust + /// # extern crate actix_web; + /// # #[macro_use] extern crate language_tags; + /// use actix_web::httpcodes::HttpOk; + /// use actix_web::header::{AcceptLanguage, QualityItem, q, qitem}; + /// # + /// # fn main() { + /// let mut builder = HttpOk.build(); + /// builder.set( + /// AcceptLanguage(vec![ + /// qitem(langtag!(da)), + /// QualityItem::new(langtag!(en;;;GB), q(800)), + /// QualityItem::new(langtag!(en), q(700)), + /// ]) + /// ); + /// # } + /// ``` + (AcceptLanguage, http::ACCEPT_LANGUAGE) => (QualityItem)+ + + test_accept_language { + // From the RFC + test_header!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]); + // Own test + test_header!( + test2, vec![b"en-US, en; q=0.5, fr"], + Some(AcceptLanguage(vec![ + qitem("en-US".parse().unwrap()), + QualityItem::new("en".parse().unwrap(), q(500)), + qitem("fr".parse().unwrap()), + ]))); + } +} diff --git a/src/header/common/allow.rs b/src/header/common/allow.rs new file mode 100644 index 000000000..274c691f6 --- /dev/null +++ b/src/header/common/allow.rs @@ -0,0 +1,81 @@ +use http::Method; +use header::http; + +header! { + /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) + /// + /// The `Allow` header field lists the set of methods advertised as + /// supported by the target resource. The purpose of this field is + /// strictly to inform the recipient of valid request methods associated + /// with the resource. + /// + /// # ABNF + /// + /// ```text + /// Allow = #method + /// ``` + /// + /// # Example values + /// * `GET, HEAD, PUT` + /// * `OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH, fOObAr` + /// * `` + /// + /// # Examples + /// + /// ```rust + /// # extern crate http; + /// # extern crate actix_web; + /// use actix_web::httpcodes::HttpOk; + /// use actix_web::header::Allow; + /// use http::Method; + /// + /// let mut builder = HttpOk.build(); + /// builder.set( + /// Allow(vec![Method::GET]) + /// ); + /// ``` + /// + /// ```rust + /// # extern crate http; + /// # extern crate actix_web; + /// use actix_web::httpcodes::HttpOk; + /// use actix_web::header::Allow; + /// use http::Method; + /// + /// let mut builder = HttpOk.build(); + /// builder.set( + /// Allow(vec![ + /// Method::GET, + /// Method::POST, + /// Method::PATCH, + /// ]) + /// ); + /// ``` + (Allow, http::ALLOW) => (Method)* + + test_allow { + // From the RFC + test_header!( + test1, + vec![b"GET, HEAD, PUT"], + Some(HeaderField(vec![Method::GET, Method::HEAD, Method::PUT]))); + // Own tests + test_header!( + test2, + vec![b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"], + Some(HeaderField(vec![ + Method::OPTIONS, + Method::GET, + Method::PUT, + Method::POST, + Method::DELETE, + Method::HEAD, + Method::TRACE, + Method::CONNECT, + Method::PATCH]))); + test_header!( + test3, + vec![b""], + Some(HeaderField(Vec::::new()))); + } +} diff --git a/src/header/common/cache_control.rs b/src/header/common/cache_control.rs new file mode 100644 index 000000000..a30c1134a --- /dev/null +++ b/src/header/common/cache_control.rs @@ -0,0 +1,231 @@ +use std::fmt::{self, Write}; +use std::str::FromStr; +use header::{Header, IntoHeaderValue, Writer}; +use header::{http, from_comma_delimited, fmt_comma_delimited}; + +/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2) +/// +/// The `Cache-Control` header field is used to specify directives for +/// caches along the request/response chain. Such cache directives are +/// unidirectional in that the presence of a directive in a request does +/// not imply that the same directive is to be given in the response. +/// +/// # ABNF +/// +/// ```text +/// Cache-Control = 1#cache-directive +/// cache-directive = token [ "=" ( token / quoted-string ) ] +/// ``` +/// +/// # Example values +/// +/// * `no-cache` +/// * `private, community="UCI"` +/// * `max-age=30` +/// +/// # Examples +/// ```rust +/// use actix_web::httpcodes::HttpOk; +/// use actix_web::header::{CacheControl, CacheDirective}; +/// +/// let mut builder = HttpOk.build(); +/// builder.set( +/// CacheControl(vec![CacheDirective::MaxAge(86400u32)]) +/// ); +/// ``` +/// +/// ```rust +/// use actix_web::httpcodes::HttpOk; +/// use actix_web::header::{CacheControl, CacheDirective}; +/// +/// let mut builder = HttpOk.build(); +/// builder.set( +/// CacheControl(vec![ +/// CacheDirective::NoCache, +/// CacheDirective::Private, +/// CacheDirective::MaxAge(360u32), +/// CacheDirective::Extension("foo".to_owned(), +/// Some("bar".to_owned())), +/// ]) +/// ); +/// ``` +#[derive(PartialEq, Clone, Debug)] +pub struct CacheControl(pub Vec); + +__hyper__deref!(CacheControl => Vec); + +//TODO: this could just be the header! macro +impl Header for CacheControl { + fn name() -> http::HeaderName { + http::CACHE_CONTROL + } + + #[inline] + fn parse(msg: &T) -> Result + where T: ::HttpMessage + { + let directives = from_comma_delimited(msg.headers().get_all(Self::name()))?; + if !directives.is_empty() { + Ok(CacheControl(directives)) + } else { + Err(::error::ParseError::Header) + } + } +} + +impl fmt::Display for CacheControl { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt_comma_delimited(f, &self[..]) + } +} + +impl IntoHeaderValue for CacheControl { + type Error = http::InvalidHeaderValueBytes; + + fn try_into(self) -> Result { + let mut writer = Writer::new(); + let _ = write!(&mut writer, "{}", self); + http::HeaderValue::from_shared(writer.take()) + } +} + +/// `CacheControl` contains a list of these directives. +#[derive(PartialEq, Clone, Debug)] +pub enum CacheDirective { + /// "no-cache" + NoCache, + /// "no-store" + NoStore, + /// "no-transform" + NoTransform, + /// "only-if-cached" + OnlyIfCached, + + // request directives + /// "max-age=delta" + MaxAge(u32), + /// "max-stale=delta" + MaxStale(u32), + /// "min-fresh=delta" + MinFresh(u32), + + // response directives + /// "must-revalidate" + MustRevalidate, + /// "public" + Public, + /// "private" + Private, + /// "proxy-revalidate" + ProxyRevalidate, + /// "s-maxage=delta" + SMaxAge(u32), + + /// Extension directives. Optionally include an argument. + Extension(String, Option) +} + +impl fmt::Display for CacheDirective { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::CacheDirective::*; + fmt::Display::fmt(match *self { + NoCache => "no-cache", + NoStore => "no-store", + NoTransform => "no-transform", + OnlyIfCached => "only-if-cached", + + MaxAge(secs) => return write!(f, "max-age={}", secs), + MaxStale(secs) => return write!(f, "max-stale={}", secs), + MinFresh(secs) => return write!(f, "min-fresh={}", secs), + + MustRevalidate => "must-revalidate", + Public => "public", + Private => "private", + ProxyRevalidate => "proxy-revalidate", + SMaxAge(secs) => return write!(f, "s-maxage={}", secs), + + Extension(ref name, None) => &name[..], + Extension(ref name, Some(ref arg)) => return write!(f, "{}={}", name, arg), + + }, f) + } +} + +impl FromStr for CacheDirective { + type Err = Option<::Err>; + fn from_str(s: &str) -> Result::Err>> { + use self::CacheDirective::*; + match s { + "no-cache" => Ok(NoCache), + "no-store" => Ok(NoStore), + "no-transform" => Ok(NoTransform), + "only-if-cached" => Ok(OnlyIfCached), + "must-revalidate" => Ok(MustRevalidate), + "public" => Ok(Public), + "private" => Ok(Private), + "proxy-revalidate" => Ok(ProxyRevalidate), + "" => Err(None), + _ => match s.find('=') { + Some(idx) if idx+1 < s.len() => match (&s[..idx], (&s[idx+1..]).trim_matches('"')) { + ("max-age" , secs) => secs.parse().map(MaxAge).map_err(Some), + ("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some), + ("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some), + ("s-maxage", secs) => secs.parse().map(SMaxAge).map_err(Some), + (left, right) => Ok(Extension(left.to_owned(), Some(right.to_owned()))) + }, + Some(_) => Err(None), + None => Ok(Extension(s.to_owned(), None)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use header::Header; + use test::TestRequest; + + #[test] + fn test_parse_multiple_headers() { + let req = TestRequest::with_header( + http::CACHE_CONTROL, "no-cache, private").finish(); + let cache = Header::parse(&req); + assert_eq!(cache.ok(), Some(CacheControl(vec![CacheDirective::NoCache, + CacheDirective::Private]))) + } + + #[test] + fn test_parse_argument() { + let req = TestRequest::with_header( + http::CACHE_CONTROL, "max-age=100, private").finish(); + let cache = Header::parse(&req); + assert_eq!(cache.ok(), Some(CacheControl(vec![CacheDirective::MaxAge(100), + CacheDirective::Private]))) + } + + #[test] + fn test_parse_quote_form() { + let req = TestRequest::with_header( + http::CACHE_CONTROL, "max-age=\"200\"").finish(); + let cache = Header::parse(&req); + assert_eq!(cache.ok(), Some(CacheControl(vec![CacheDirective::MaxAge(200)]))) + } + + #[test] + fn test_parse_extension() { + let req = TestRequest::with_header( + http::CACHE_CONTROL, "foo, bar=baz").finish(); + let cache = Header::parse(&req); + assert_eq!(cache.ok(), Some(CacheControl(vec![ + CacheDirective::Extension("foo".to_owned(), None), + CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned()))]))) + } + + #[test] + fn test_parse_bad_syntax() { + let req = TestRequest::with_header(http::CACHE_CONTROL, "foo=").finish(); + let cache: Result = Header::parse(&req); + assert_eq!(cache.ok(), None) + } +} diff --git a/src/header/common/content_disposition.rs b/src/header/common/content_disposition.rs new file mode 100644 index 000000000..0fcd6ee09 --- /dev/null +++ b/src/header/common/content_disposition.rs @@ -0,0 +1,264 @@ +// # References +// +// "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt +// "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt +// "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc2388.txt +// Browser conformance tests at: http://greenbytes.de/tech/tc2231/ +// 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::shared::Charset; + +/// The implied disposition of the content of the HTTP body. +#[derive(Clone, Debug, PartialEq)] +pub enum DispositionType { + /// Inline implies default processing + Inline, + /// Attachment implies that the recipient should prompt the user to save the response locally, + /// rather than process it normally (as per its media type). + Attachment, + /// Extension type. Should be handled by recipients the same way as Attachment + Ext(String) +} + +/// A parameter to the disposition type. +#[derive(Clone, Debug, PartialEq)] +pub enum DispositionParam { + /// A Filename consisting of a Charset, an optional LanguageTag, and finally a sequence of + /// bytes representing the filename + Filename(Charset, Option, Vec), + /// Extension type consisting of token and value. Recipients should ignore unrecognized + /// parameters. + Ext(String, String) +} + +/// A `Content-Disposition` header, (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266). +/// +/// The Content-Disposition response header field is used to convey +/// additional information about how to process the response payload, and +/// also can be used to attach additional metadata, such as the filename +/// to use when saving the response payload locally. +/// +/// # ABNF + +/// ```text +/// content-disposition = "Content-Disposition" ":" +/// disposition-type *( ";" disposition-parm ) +/// +/// disposition-type = "inline" | "attachment" | disp-ext-type +/// ; case-insensitive +/// +/// disp-ext-type = token +/// +/// disposition-parm = filename-parm | disp-ext-parm +/// +/// filename-parm = "filename" "=" value +/// | "filename*" "=" ext-value +/// +/// disp-ext-parm = token "=" value +/// | ext-token "=" ext-value +/// +/// ext-token = +/// ``` +/// +/// # Example +/// +/// ``` +/// use hyper::header::{Headers, ContentDisposition, DispositionType, DispositionParam, Charset}; +/// +/// let mut headers = Headers::new(); +/// headers.set(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 { + /// The disposition + pub disposition: DispositionType, + /// 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| { + let mut sections = s.split(';'); + let disposition = match sections.next() { + Some(s) => s.trim(), + None => return Err(::Error::Header), + }; + + let mut cd = ContentDisposition { + disposition: if unicase::eq_ascii(&*disposition, "inline") { + DispositionType::Inline + } else if unicase::eq_ascii(&*disposition, "attachment") { + DispositionType::Attachment + } else { + DispositionType::Ext(disposition.to_owned()) + }, + parameters: Vec::new(), + }; + + for section in sections { + let mut parts = section.splitn(2, '='); + + let key = if let Some(key) = parts.next() { + key.trim() + } else { + return Err(::Error::Header); + }; + + let val = if let Some(val) = parts.next() { + val.trim() + } else { + return Err(::Error::Header); + }; + + cd.parameters.push( + if unicase::eq_ascii(&*key, "filename") { + DispositionParam::Filename( + 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)); + DispositionParam::Filename(extended_value.charset, extended_value.language_tag, extended_value.value) + } else { + DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned()) + } + ); + } + + Ok(cd) + }) + } + + #[inline] + fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result { + f.fmt_line(self) + } +} + +impl fmt::Display for ContentDisposition { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.disposition { + DispositionType::Inline => try!(write!(f, "inline")), + DispositionType::Attachment => try!(write!(f, "attachment")), + DispositionType::Ext(ref s) => try!(write!(f, "{}", s)), + } + for param in &self.parameters { + match *param { + DispositionParam::Filename(ref charset, ref opt_lang, ref bytes) => { + 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") { + use_simple_format = true; + } + } + } + if use_simple_format { + try!(write!(f, "; filename=\"{}\"", + match String::from_utf8(bytes.clone()) { + Ok(s) => s, + Err(_) => return Err(fmt::Error), + })); + } else { + try!(write!(f, "; filename*={}'", charset)); + if let Some(ref lang) = *opt_lang { + try!(write!(f, "{}", lang)); + }; + try!(write!(f, "'")); + try!(http_percent_encode(f, bytes)) + } + }, + DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)), + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::{ContentDisposition,DispositionType,DispositionParam}; + use ::header::Header; + use ::header::shared::Charset; + + #[test] + fn test_parse_header() { + assert!(ContentDisposition::parse_header(&"".into()).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 b = ContentDisposition { + disposition: DispositionType::Ext("form-data".to_owned()), + parameters: vec![ + DispositionParam::Ext("dummy".to_owned(), "3".to_owned()), + DispositionParam::Ext("name".to_owned(), "upload".to_owned()), + DispositionParam::Filename( + Charset::Ext("UTF-8".to_owned()), + None, + "sample.png".bytes().collect()) ] + }; + assert_eq!(a, b); + + let a = "attachment; filename=\"image.jpg\"".into(); + let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap(); + let b = ContentDisposition { + disposition: DispositionType::Attachment, + parameters: vec![ + DispositionParam::Filename( + Charset::Ext("UTF-8".to_owned()), + None, + "image.jpg".bytes().collect()) ] + }; + 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 b = ContentDisposition { + disposition: DispositionType::Attachment, + parameters: vec![ + DispositionParam::Filename( + Charset::Ext("UTF-8".to_owned()), + None, + vec![0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, + 0xe2, 0x82, 0xac, 0x20, b'r', b'a', b't', b'e', b's']) ] + }; + assert_eq!(a, b); + } + + #[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 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 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 display_rendered = format!("{}",a); + assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered); + } +} diff --git a/src/header/common/content_language.rs b/src/header/common/content_language.rs new file mode 100644 index 000000000..5cb6a158f --- /dev/null +++ b/src/header/common/content_language.rs @@ -0,0 +1,66 @@ +use language_tags::LanguageTag; +use header::{http, QualityItem}; + + +header! { + /// `Content-Language` header, defined in + /// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2) + /// + /// The `Content-Language` header field describes the natural language(s) + /// of the intended audience for the representation. Note that this + /// might not be equivalent to all the languages used within the + /// representation. + /// + /// # ABNF + /// + /// ```text + /// Content-Language = 1#language-tag + /// ``` + /// + /// # Example values + /// + /// * `da` + /// * `mi, en` + /// + /// # Examples + /// + /// ```rust + /// # extern crate actix_web; + /// # #[macro_use] extern crate language_tags; + /// use actix_web::httpcodes::HttpOk; + /// # use actix_web::header::{ContentLanguage, qitem}; + /// # + /// # fn main() { + /// let mut builder = HttpOk.build(); + /// builder.set( + /// ContentLanguage(vec![ + /// qitem(langtag!(en)), + /// ]) + /// ); + /// # } + /// ``` + /// + /// ```rust + /// # extern crate actix_web; + /// # #[macro_use] extern crate language_tags; + /// use actix_web::httpcodes::HttpOk; + /// # use actix_web::header::{ContentLanguage, qitem}; + /// # + /// # fn main() { + /// + /// let mut builder = HttpOk.build(); + /// builder.set( + /// ContentLanguage(vec![ + /// qitem(langtag!(da)), + /// qitem(langtag!(en;;;GB)), + /// ]) + /// ); + /// # } + /// ``` + (ContentLanguage, http::CONTENT_LANGUAGE) => (QualityItem)+ + + test_content_language { + test_header!(test1, vec![b"da"]); + test_header!(test2, vec![b"mi, en"]); + } +} diff --git a/src/header/common/content_range.rs b/src/header/common/content_range.rs new file mode 100644 index 000000000..5e50fb7f0 --- /dev/null +++ b/src/header/common/content_range.rs @@ -0,0 +1,205 @@ +use std::fmt::{self, Display, Write}; +use std::str::FromStr; +use header::{http, IntoHeaderValue, Writer}; +use error::ParseError; + + +header! { + /// `Content-Range` header, defined in + /// [RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2) + (ContentRange, http::CONTENT_RANGE) => [ContentRangeSpec] + + test_content_range { + test_header!(test_bytes, + vec![b"bytes 0-499/500"], + Some(ContentRange(ContentRangeSpec::Bytes { + range: Some((0, 499)), + instance_length: Some(500) + }))); + + test_header!(test_bytes_unknown_len, + vec![b"bytes 0-499/*"], + Some(ContentRange(ContentRangeSpec::Bytes { + range: Some((0, 499)), + instance_length: None + }))); + + test_header!(test_bytes_unknown_range, + vec![b"bytes */500"], + Some(ContentRange(ContentRangeSpec::Bytes { + range: None, + instance_length: Some(500) + }))); + + test_header!(test_unregistered, + vec![b"seconds 1-2"], + Some(ContentRange(ContentRangeSpec::Unregistered { + unit: "seconds".to_owned(), + resp: "1-2".to_owned() + }))); + + test_header!(test_no_len, + vec![b"bytes 0-499"], + None::); + + test_header!(test_only_unit, + vec![b"bytes"], + None::); + + test_header!(test_end_less_than_start, + vec![b"bytes 499-0/500"], + None::); + + test_header!(test_blank, + vec![b""], + None::); + + test_header!(test_bytes_many_spaces, + vec![b"bytes 1-2/500 3"], + None::); + + test_header!(test_bytes_many_slashes, + vec![b"bytes 1-2/500/600"], + None::); + + test_header!(test_bytes_many_dashes, + vec![b"bytes 1-2-3/500"], + None::); + + } +} + + +/// Content-Range, described in [RFC7233](https://tools.ietf.org/html/rfc7233#section-4.2) +/// +/// # ABNF +/// +/// ```text +/// Content-Range = byte-content-range +/// / other-content-range +/// +/// byte-content-range = bytes-unit SP +/// ( byte-range-resp / unsatisfied-range ) +/// +/// byte-range-resp = byte-range "/" ( complete-length / "*" ) +/// byte-range = first-byte-pos "-" last-byte-pos +/// unsatisfied-range = "*/" complete-length +/// +/// complete-length = 1*DIGIT +/// +/// other-content-range = other-range-unit SP other-range-resp +/// other-range-resp = *CHAR +/// ``` +#[derive(PartialEq, Clone, Debug)] +pub enum ContentRangeSpec { + /// Byte range + Bytes { + /// First and last bytes of the range, omitted if request could not be + /// satisfied + range: Option<(u64, u64)>, + + /// Total length of the instance, can be omitted if unknown + instance_length: Option + }, + + /// Custom range, with unit not registered at IANA + Unregistered { + /// other-range-unit + unit: String, + + /// other-range-resp + resp: String + } +} + +fn split_in_two(s: &str, separator: char) -> Option<(&str, &str)> { + let mut iter = s.splitn(2, separator); + match (iter.next(), iter.next()) { + (Some(a), Some(b)) => Some((a, b)), + _ => None + } +} + +impl FromStr for ContentRangeSpec { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + let res = match split_in_two(s, ' ') { + Some(("bytes", resp)) => { + let (range, instance_length) = split_in_two( + resp, '/').ok_or(ParseError::Header)?; + + let instance_length = if instance_length == "*" { + None + } else { + Some(instance_length.parse() + .map_err(|_| ParseError::Header)?) + }; + + let range = if range == "*" { + None + } else { + let (first_byte, last_byte) = split_in_two( + range, '-').ok_or(ParseError::Header)?; + let first_byte = first_byte.parse() + .map_err(|_| ParseError::Header)?; + let last_byte = last_byte.parse() + .map_err(|_| ParseError::Header)?; + if last_byte < first_byte { + return Err(ParseError::Header); + } + Some((first_byte, last_byte)) + }; + + ContentRangeSpec::Bytes {range, instance_length} + } + Some((unit, resp)) => { + ContentRangeSpec::Unregistered { + unit: unit.to_owned(), + resp: resp.to_owned() + } + } + _ => return Err(ParseError::Header) + }; + Ok(res) + } +} + +impl Display for ContentRangeSpec { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ContentRangeSpec::Bytes { range, instance_length } => { + try!(f.write_str("bytes ")); + match range { + Some((first_byte, last_byte)) => { + try!(write!(f, "{}-{}", first_byte, last_byte)); + }, + None => { + try!(f.write_str("*")); + } + }; + try!(f.write_str("/")); + if let Some(v) = instance_length { + write!(f, "{}", v) + } else { + f.write_str("*") + } + } + ContentRangeSpec::Unregistered { ref unit, ref resp } => { + try!(f.write_str(unit)); + try!(f.write_str(" ")); + f.write_str(resp) + } + } + } +} + +impl IntoHeaderValue for ContentRangeSpec { + type Error = http::InvalidHeaderValueBytes; + + fn try_into(self) -> Result { + let mut writer = Writer::new(); + let _ = write!(&mut writer, "{}", self); + http::HeaderValue::from_shared(writer.take()) + } +} diff --git a/src/header/common/content_type.rs b/src/header/common/content_type.rs new file mode 100644 index 000000000..730bbd947 --- /dev/null +++ b/src/header/common/content_type.rs @@ -0,0 +1,115 @@ +use mime::{self, Mime}; +use header::http; + + +header! { + /// `Content-Type` header, defined in + /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-3.1.1.5) + /// + /// The `Content-Type` header field indicates the media type of the + /// associated representation: either the representation enclosed in the + /// message payload or the selected representation, as determined by the + /// message semantics. The indicated media type defines both the data + /// format and how that data is intended to be processed by a recipient, + /// within the scope of the received message semantics, after any content + /// codings indicated by Content-Encoding are decoded. + /// + /// Although the `mime` crate allows the mime options to be any slice, this crate + /// forces the use of Vec. This is to make sure the same header can't have more than 1 type. If + /// this is an issue, it's possible to implement `Header` on a custom struct. + /// + /// # ABNF + /// + /// ```text + /// Content-Type = media-type + /// ``` + /// + /// # Example values + /// + /// * `text/html; charset=utf-8` + /// * `application/json` + /// + /// # Examples + /// + /// ```rust + /// use actix_web::httpcodes::HttpOk; + /// use actix_web::header::ContentType; + /// + /// let mut builder = HttpOk.build(); + /// builder.set( + /// ContentType::json() + /// ); + /// ``` + /// + /// ```rust + /// # extern crate mime; + /// # extern crate actix_web; + /// use mime; + /// use actix_web::httpcodes::HttpOk; + /// use actix_web::header::ContentType; + /// + /// let mut builder = HttpOk.build(); + /// builder.set( + /// ContentType(mime::TEXT_HTML) + /// ); + /// ``` + (ContentType, http::CONTENT_TYPE) => [Mime] + + test_content_type { + test_header!( + test1, + vec![b"text/html"], + Some(HeaderField(TEXT_HTML))); + } +} + +impl ContentType { + /// A constructor to easily create a `Content-Type: application/json` header. + #[inline] + pub fn json() -> ContentType { + ContentType(mime::APPLICATION_JSON) + } + + /// A constructor to easily create a `Content-Type: text/plain; charset=utf-8` header. + #[inline] + pub fn plaintext() -> ContentType { + ContentType(mime::TEXT_PLAIN_UTF_8) + } + + /// A constructor to easily create a `Content-Type: text/html` header. + #[inline] + pub fn html() -> ContentType { + ContentType(mime::TEXT_HTML) + } + + /// A constructor to easily create a `Content-Type: text/xml` header. + #[inline] + pub fn xml() -> ContentType { + ContentType(mime::TEXT_XML) + } + + /// A constructor to easily create a `Content-Type: application/www-form-url-encoded` header. + #[inline] + pub fn form_url_encoded() -> ContentType { + ContentType(mime::APPLICATION_WWW_FORM_URLENCODED) + } + /// A constructor to easily create a `Content-Type: image/jpeg` header. + #[inline] + pub fn jpeg() -> ContentType { + ContentType(mime::IMAGE_JPEG) + } + + /// A constructor to easily create a `Content-Type: image/png` header. + #[inline] + pub fn png() -> ContentType { + ContentType(mime::IMAGE_PNG) + } + + /// A constructor to easily create a `Content-Type: application/octet-stream` header. + #[inline] + pub fn octet_stream() -> ContentType { + ContentType(mime::APPLICATION_OCTET_STREAM) + } +} + +impl Eq for ContentType {} diff --git a/src/header/common/date.rs b/src/header/common/date.rs new file mode 100644 index 000000000..33333f446 --- /dev/null +++ b/src/header/common/date.rs @@ -0,0 +1,34 @@ +use header::{http, HttpDate}; + +header! { + /// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2) + /// + /// The `Date` header field represents the date and time at which the + /// message was originated. + /// + /// # ABNF + /// + /// ```text + /// Date = HTTP-date + /// ``` + /// + /// # Example values + /// + /// * `Tue, 15 Nov 1994 08:12:31 GMT` + /// + /// # Example + /// + /// ```rust + /// use actix_web::httpcodes; + /// use actix_web::header::Date; + /// use std::time::SystemTime; + /// + /// let mut builder = httpcodes::HttpOk.build(); + /// builder.set(Date(SystemTime::now().into())); + /// ``` + (Date, http::DATE) => [HttpDate] + + test_date { + test_header!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]); + } +} diff --git a/src/header/common/etag.rs b/src/header/common/etag.rs new file mode 100644 index 000000000..68ec5d85f --- /dev/null +++ b/src/header/common/etag.rs @@ -0,0 +1,96 @@ +use header::{http, EntityTag}; + +header! { + /// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3) + /// + /// The `ETag` header field in a response provides the current entity-tag + /// for the selected representation, as determined at the conclusion of + /// handling the request. An entity-tag is an opaque validator for + /// differentiating between multiple representations of the same + /// resource, regardless of whether those multiple representations are + /// due to resource state changes over time, content negotiation + /// resulting in multiple representations being valid at the same time, + /// or both. An entity-tag consists of an opaque quoted string, possibly + /// prefixed by a weakness indicator. + /// + /// # ABNF + /// + /// ```text + /// ETag = entity-tag + /// ``` + /// + /// # Example values + /// + /// * `"xyzzy"` + /// * `W/"xyzzy"` + /// * `""` + /// + /// # Examples + /// + /// ```rust + /// use actix_web::httpcodes; + /// use actix_web::header::{ETag, EntityTag}; + /// + /// let mut builder = httpcodes::HttpOk.build(); + /// builder.set(ETag(EntityTag::new(false, "xyzzy".to_owned()))); + /// ``` + /// + /// ```rust + /// use actix_web::httpcodes; + /// use actix_web::header::{ETag, EntityTag}; + /// + /// let mut builder = httpcodes::HttpOk.build(); + /// builder.set(ETag(EntityTag::new(true, "xyzzy".to_owned()))); + /// ``` + (ETag, http::ETAG) => [EntityTag] + + test_etag { + // From the RFC + test_header!(test1, + vec![b"\"xyzzy\""], + Some(ETag(EntityTag::new(false, "xyzzy".to_owned())))); + test_header!(test2, + vec![b"W/\"xyzzy\""], + Some(ETag(EntityTag::new(true, "xyzzy".to_owned())))); + test_header!(test3, + vec![b"\"\""], + Some(ETag(EntityTag::new(false, "".to_owned())))); + // Own tests + test_header!(test4, + vec![b"\"foobar\""], + Some(ETag(EntityTag::new(false, "foobar".to_owned())))); + test_header!(test5, + vec![b"\"\""], + Some(ETag(EntityTag::new(false, "".to_owned())))); + test_header!(test6, + vec![b"W/\"weak-etag\""], + Some(ETag(EntityTag::new(true, "weak-etag".to_owned())))); + test_header!(test7, + vec![b"W/\"\x65\x62\""], + Some(ETag(EntityTag::new(true, "\u{0065}\u{0062}".to_owned())))); + test_header!(test8, + vec![b"W/\"\""], + Some(ETag(EntityTag::new(true, "".to_owned())))); + test_header!(test9, + vec![b"no-dquotes"], + None::); + test_header!(test10, + vec![b"w/\"the-first-w-is-case-sensitive\""], + None::); + test_header!(test11, + vec![b""], + None::); + test_header!(test12, + vec![b"\"unmatched-dquotes1"], + None::); + test_header!(test13, + vec![b"unmatched-dquotes2\""], + None::); + test_header!(test14, + vec![b"matched-\"dquotes\""], + None::); + test_header!(test15, + vec![b"\""], + None::); + } +} diff --git a/src/header/common/expires.rs b/src/header/common/expires.rs new file mode 100644 index 000000000..cc80cd241 --- /dev/null +++ b/src/header/common/expires.rs @@ -0,0 +1,39 @@ +use header::{http, HttpDate}; + +header! { + /// `Expires` header, defined in [RFC7234](http://tools.ietf.org/html/rfc7234#section-5.3) + /// + /// The `Expires` header field gives the date/time after which the + /// response is considered stale. + /// + /// The presence of an Expires field does not imply that the original + /// resource will change or cease to exist at, before, or after that + /// time. + /// + /// # ABNF + /// + /// ```text + /// Expires = HTTP-date + /// ``` + /// + /// # Example values + /// * `Thu, 01 Dec 1994 16:00:00 GMT` + /// + /// # Example + /// + /// ```rust + /// use actix_web::httpcodes; + /// use actix_web::header::Expires; + /// use std::time::{SystemTime, Duration}; + /// + /// let mut builder = httpcodes::HttpOk.build(); + /// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24); + /// builder.set(Expires(expiration.into())); + /// ``` + (Expires, http::EXPIRES) => [HttpDate] + + test_expires { + // Testcase from RFC + test_header!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]); + } +} diff --git a/src/header/common/if_match.rs b/src/header/common/if_match.rs new file mode 100644 index 000000000..6640376f0 --- /dev/null +++ b/src/header/common/if_match.rs @@ -0,0 +1,70 @@ +use header::{http, EntityTag}; + +header! { + /// `If-Match` header, defined in + /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1) + /// + /// The `If-Match` header field makes the request method conditional on + /// the recipient origin server either having at least one current + /// representation of the target resource, when the field-value is "*", + /// or having a current representation of the target resource that has an + /// entity-tag matching a member of the list of entity-tags provided in + /// the field-value. + /// + /// An origin server MUST use the strong comparison function when + /// comparing entity-tags for `If-Match`, since the client + /// intends this precondition to prevent the method from being applied if + /// there have been any changes to the representation data. + /// + /// # ABNF + /// + /// ```text + /// If-Match = "*" / 1#entity-tag + /// ``` + /// + /// # Example values + /// + /// * `"xyzzy"` + /// * "xyzzy", "r2d2xxxx", "c3piozzzz" + /// + /// # Examples + /// + /// ```rust + /// use actix_web::httpcodes; + /// use actix_web::header::IfMatch; + /// + /// let mut builder = httpcodes::HttpOk.build(); + /// builder.set(IfMatch::Any); + /// ``` + /// + /// ```rust + /// use actix_web::httpcodes; + /// use actix_web::header::{IfMatch, EntityTag}; + /// + /// let mut builder = httpcodes::HttpOk.build(); + /// builder.set( + /// IfMatch::Items(vec![ + /// EntityTag::new(false, "xyzzy".to_owned()), + /// EntityTag::new(false, "foobar".to_owned()), + /// EntityTag::new(false, "bazquux".to_owned()), + /// ]) + /// ); + /// ``` + (IfMatch, http::IF_MATCH) => {Any / (EntityTag)+} + + test_if_match { + test_header!( + test1, + vec![b"\"xyzzy\""], + Some(HeaderField::Items( + vec![EntityTag::new(false, "xyzzy".to_owned())]))); + test_header!( + test2, + vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""], + Some(HeaderField::Items( + vec![EntityTag::new(false, "xyzzy".to_owned()), + EntityTag::new(false, "r2d2xxxx".to_owned()), + EntityTag::new(false, "c3piozzzz".to_owned())]))); + test_header!(test3, vec![b"*"], Some(IfMatch::Any)); + } +} diff --git a/src/header/common/if_modified_since.rs b/src/header/common/if_modified_since.rs index 62fcd1bec..264fcac49 100644 --- a/src/header/common/if_modified_since.rs +++ b/src/header/common/if_modified_since.rs @@ -1,52 +1,39 @@ -use http::header; +use header::{http, HttpDate}; -use header::{Header, HttpDate, IntoHeaderValue}; -use error::ParseError; -use httpmessage::HttpMessage; +header! { + /// `If-Modified-Since` header, defined in + /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3) + /// + /// The `If-Modified-Since` header field makes a GET or HEAD request + /// method conditional on the selected representation's modification date + /// being more recent than the date provided in the field-value. + /// Transfer of the selected representation's data is avoided if that + /// data has not changed. + /// + /// # ABNF + /// + /// ```text + /// If-Unmodified-Since = HTTP-date + /// ``` + /// + /// # Example values + /// * `Sat, 29 Oct 1994 19:43:31 GMT` + /// + /// # Example + /// + /// ```rust + /// use actix_web::httpcodes; + /// use actix_web::header::IfModifiedSince; + /// use std::time::{SystemTime, Duration}; + /// + /// let mut builder = httpcodes::HttpOk.build(); + /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); + /// builder.set(IfModifiedSince(modified.into())); + /// ``` + (IfModifiedSince, http::IF_MODIFIED_SINCE) => [HttpDate] - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct IfModifiedSince(pub HttpDate); - -impl Header for IfModifiedSince { - fn name() -> header::HeaderName { - header::IF_MODIFIED_SINCE - } - - fn parse(msg: &T) -> Result { - let val = msg.headers().get(Self::name()) - .ok_or(ParseError::Header)?.to_str().map_err(|_| ParseError::Header)?; - Ok(IfModifiedSince(val.parse()?)) - } -} - -impl IntoHeaderValue for IfModifiedSince { - type Error = header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - self.0.try_into() - } -} - -#[cfg(test)] -mod tests { - use time::Tm; - use test::TestRequest; - use httpmessage::HttpMessage; - use super::HttpDate; - use super::IfModifiedSince; - - fn date() -> HttpDate { - Tm { - tm_nsec: 0, tm_sec: 37, tm_min: 48, tm_hour: 8, - tm_mday: 7, tm_mon: 10, tm_year: 94, - tm_wday: 0, tm_isdst: 0, tm_yday: 0, tm_utcoff: 0}.into() - } - - #[test] - fn test_if_mod_since() { - let req = TestRequest::with_hdr(IfModifiedSince(date())).finish(); - let h = req.get::().unwrap(); - assert_eq!(h.0, date()); + test_if_modified_since { + // Testcase from RFC + test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } diff --git a/src/header/common/if_none_match.rs b/src/header/common/if_none_match.rs new file mode 100644 index 000000000..6cb2a184d --- /dev/null +++ b/src/header/common/if_none_match.rs @@ -0,0 +1,91 @@ +use header::{http, EntityTag}; + +header! { + /// `If-None-Match` header, defined in + /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2) + /// + /// The `If-None-Match` header field makes the request method conditional + /// on a recipient cache or origin server either not having any current + /// representation of the target resource, when the field-value is "*", + /// or having a selected representation with an entity-tag that does not + /// match any of those listed in the field-value. + /// + /// A recipient MUST use the weak comparison function when comparing + /// entity-tags for If-None-Match (Section 2.3.2), since weak entity-tags + /// can be used for cache validation even if there have been changes to + /// the representation data. + /// + /// # ABNF + /// + /// ```text + /// If-None-Match = "*" / 1#entity-tag + /// ``` + /// + /// # Example values + /// + /// * `"xyzzy"` + /// * `W/"xyzzy"` + /// * `"xyzzy", "r2d2xxxx", "c3piozzzz"` + /// * `W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"` + /// * `*` + /// + /// # Examples + /// + /// ```rust + /// use actix_web::httpcodes; + /// use actix_web::header::IfNoneMatch; + /// + /// let mut builder = httpcodes::HttpOk.build(); + /// builder.set(IfNoneMatch::Any); + /// ``` + /// + /// ```rust + /// use actix_web::httpcodes; + /// use actix_web::header::{IfNoneMatch, EntityTag}; + /// + /// let mut builder = httpcodes::HttpOk.build(); + /// builder.set( + /// IfNoneMatch::Items(vec![ + /// EntityTag::new(false, "xyzzy".to_owned()), + /// EntityTag::new(false, "foobar".to_owned()), + /// EntityTag::new(false, "bazquux".to_owned()), + /// ]) + /// ); + /// ``` + (IfNoneMatch, http::IF_NONE_MATCH) => {Any / (EntityTag)+} + + test_if_none_match { + test_header!(test1, vec![b"\"xyzzy\""]); + test_header!(test2, vec![b"W/\"xyzzy\""]); + test_header!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); + test_header!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); + test_header!(test5, vec![b"*"]); + } +} + +#[cfg(test)] +mod tests { + use super::IfNoneMatch; + use test::TestRequest; + use header::{http, Header, EntityTag}; + + #[test] + fn test_if_none_match() { + let mut if_none_match: Result; + + let req = TestRequest::with_header(http::IF_NONE_MATCH, "*").finish(); + if_none_match = Header::parse(&req); + assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any)); + + let req = TestRequest::with_header( + http::IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..]).finish(); + + if_none_match = Header::parse(&req); + let mut entities: Vec = Vec::new(); + let foobar_etag = EntityTag::new(false, "foobar".to_owned()); + let weak_etag = EntityTag::new(true, "weak-etag".to_owned()); + entities.push(foobar_etag); + entities.push(weak_etag); + assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities))); + } +} diff --git a/src/header/common/if_range.rs b/src/header/common/if_range.rs new file mode 100644 index 000000000..435a5755c --- /dev/null +++ b/src/header/common/if_range.rs @@ -0,0 +1,107 @@ +use std::fmt::{self, Display, Write}; +use error::ParseError; +use httpmessage::HttpMessage; +use header::{http, from_one_raw_str}; +use header::{IntoHeaderValue, Header, EntityTag, HttpDate, Writer}; + +/// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2) +/// +/// If a client has a partial copy of a representation and wishes to have +/// an up-to-date copy of the entire representation, it could use the +/// Range header field with a conditional GET (using either or both of +/// If-Unmodified-Since and If-Match.) However, if the precondition +/// fails because the representation has been modified, the client would +/// then have to make a second request to obtain the entire current +/// representation. +/// +/// The `If-Range` header field allows a client to \"short-circuit\" the +/// second request. Informally, its meaning is as follows: if the +/// representation is unchanged, send me the part(s) that I am requesting +/// in Range; otherwise, send me the entire representation. +/// +/// # ABNF +/// +/// ```text +/// If-Range = entity-tag / HTTP-date +/// ``` +/// +/// # Example values +/// +/// * `Sat, 29 Oct 1994 19:43:31 GMT` +/// * `\"xyzzy\"` +/// +/// # Examples +/// +/// ```rust +/// use actix_web::httpcodes; +/// use actix_web::header::{IfRange, EntityTag}; +/// +/// let mut builder = httpcodes::HttpOk.build(); +/// builder.set(IfRange::EntityTag(EntityTag::new(false, "xyzzy".to_owned()))); +/// ``` +/// +/// ```rust +/// use actix_web::httpcodes; +/// use actix_web::header::IfRange; +/// use std::time::{SystemTime, Duration}; +/// +/// let mut builder = httpcodes::HttpOk.build(); +/// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24); +/// builder.set(IfRange::Date(fetched.into())); +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub enum IfRange { + /// The entity-tag the client has of the resource + EntityTag(EntityTag), + /// The date when the client retrieved the resource + Date(HttpDate), +} + +impl Header for IfRange { + fn name() -> http::HeaderName { + http::IF_RANGE + } + #[inline] + fn parse(msg: &T) -> Result where T: HttpMessage + { + let etag: Result = from_one_raw_str(msg.headers().get(http::IF_RANGE)); + if let Ok(etag) = etag { + return Ok(IfRange::EntityTag(etag)); + } + let date: Result = from_one_raw_str(msg.headers().get(http::IF_RANGE)); + if let Ok(date) = date { + return Ok(IfRange::Date(date)); + } + Err(ParseError::Header) + } +} + +impl Display for IfRange { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + IfRange::EntityTag(ref x) => Display::fmt(x, f), + IfRange::Date(ref x) => Display::fmt(x, f), + } + } +} + +impl IntoHeaderValue for IfRange { + type Error = http::InvalidHeaderValueBytes; + + fn try_into(self) -> Result { + let mut writer = Writer::new(); + let _ = write!(&mut writer, "{}", self); + http::HeaderValue::from_shared(writer.take()) + } +} + + +#[cfg(test)] +mod test_if_range { + use std::str; + use header::*; + use super::IfRange as HeaderField; + test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + test_header!(test2, vec![b"\"xyzzy\""]); + test_header!(test3, vec![b"this-is-invalid"], None::); +} diff --git a/src/header/common/if_unmodified_since.rs b/src/header/common/if_unmodified_since.rs index 6233a2040..d0fce4fcd 100644 --- a/src/header/common/if_unmodified_since.rs +++ b/src/header/common/if_unmodified_since.rs @@ -1,52 +1,40 @@ -use http::header; +use header::{http, HttpDate}; -use header::{Header, HttpDate, IntoHeaderValue}; -use error::ParseError; -use httpmessage::HttpMessage; +header! { + /// `If-Unmodified-Since` header, defined in + /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4) + /// + /// The `If-Unmodified-Since` header field makes the request method + /// conditional on the selected representation's last modification date + /// being earlier than or equal to the date provided in the field-value. + /// This field accomplishes the same purpose as If-Match for cases where + /// the user agent does not have an entity-tag for the representation. + /// + /// # ABNF + /// + /// ```text + /// If-Unmodified-Since = HTTP-date + /// ``` + /// + /// # Example values + /// + /// * `Sat, 29 Oct 1994 19:43:31 GMT` + /// + /// # Example + /// + /// ```rust + /// use actix_web::httpcodes; + /// use actix_web::header::IfUnmodifiedSince; + /// use std::time::{SystemTime, Duration}; + /// + /// let mut builder = httpcodes::HttpOk.build(); + /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); + /// builder.set(IfUnmodifiedSince(modified.into())); + /// ``` + (IfUnmodifiedSince, http::IF_UNMODIFIED_SINCE) => [HttpDate] - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct IfUnmodifiedSince(pub HttpDate); - -impl Header for IfUnmodifiedSince { - fn name() -> header::HeaderName { - header::IF_MODIFIED_SINCE - } - - fn parse(msg: &T) -> Result { - let val = msg.headers().get(Self::name()) - .ok_or(ParseError::Header)?.to_str().map_err(|_| ParseError::Header)?; - Ok(IfUnmodifiedSince(val.parse()?)) - } -} - -impl IntoHeaderValue for IfUnmodifiedSince { - type Error = header::InvalidHeaderValueBytes; - - fn try_into(self) -> Result { - self.0.try_into() - } -} - -#[cfg(test)] -mod tests { - use time::Tm; - use test::TestRequest; - use httpmessage::HttpMessage; - use super::HttpDate; - use super::IfUnmodifiedSince; - - fn date() -> HttpDate { - Tm { - tm_nsec: 0, tm_sec: 37, tm_min: 48, tm_hour: 8, - tm_mday: 7, tm_mon: 10, tm_year: 94, - tm_wday: 0, tm_isdst: 0, tm_yday: 0, tm_utcoff: 0}.into() - } - - #[test] - fn test_if_mod_since() { - let req = TestRequest::with_hdr(IfUnmodifiedSince(date())).finish(); - let h = req.get::().unwrap(); - assert_eq!(h.0, date()); + test_if_unmodified_since { + // Testcase from RFC + test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } diff --git a/src/header/common/last_modified.rs b/src/header/common/last_modified.rs new file mode 100644 index 000000000..402c73745 --- /dev/null +++ b/src/header/common/last_modified.rs @@ -0,0 +1,38 @@ +use header::{http, HttpDate}; + +header! { + /// `Last-Modified` header, defined in + /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2) + /// + /// The `Last-Modified` header field in a response provides a timestamp + /// indicating the date and time at which the origin server believes the + /// selected representation was last modified, as determined at the + /// conclusion of handling the request. + /// + /// # ABNF + /// + /// ```text + /// Expires = HTTP-date + /// ``` + /// + /// # Example values + /// + /// * `Sat, 29 Oct 1994 19:43:31 GMT` + /// + /// # Example + /// + /// ```rust + /// use actix_web::httpcodes; + /// use actix_web::header::LastModified; + /// use std::time::{SystemTime, Duration}; + /// + /// let mut builder = httpcodes::HttpOk.build(); + /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); + /// builder.set(LastModified(modified.into())); + /// ``` + (LastModified, http::LAST_MODIFIED) => [HttpDate] + + test_last_modified { + // Testcase from RFC + test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);} +} diff --git a/src/header/common/mod.rs b/src/header/common/mod.rs index 373fe07e3..12f7f4d76 100644 --- a/src/header/common/mod.rs +++ b/src/header/common/mod.rs @@ -1,5 +1,351 @@ -mod if_modified_since; -mod if_unmodified_since; +//! A Collection of Header implementations for common HTTP Headers. +//! +//! ## Mime +//! +//! Several header fields use MIME values for their contents. Keeping with the +//! strongly-typed theme, the [mime](https://docs.rs/mime) crate +//! is used, such as `ContentType(pub Mime)`. +pub use self::accept_charset::AcceptCharset; +//pub use self::accept_encoding::AcceptEncoding; +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_language::ContentLanguage; +pub use self::content_range::{ContentRange, ContentRangeSpec}; +pub use self::content_type::ContentType; +pub use self::date::Date; +pub use self::etag::ETag; +pub use self::expires::Expires; +pub use self::if_match::IfMatch; pub use self::if_modified_since::IfModifiedSince; +pub use self::if_none_match::IfNoneMatch; +pub use self::if_range::IfRange; pub use self::if_unmodified_since::IfUnmodifiedSince; +pub use self::last_modified::LastModified; +//pub use self::range::{Range, ByteRangeSpec}; + +#[doc(hidden)] +#[macro_export] +macro_rules! __hyper__deref { + ($from:ty => $to:ty) => { + impl ::std::ops::Deref for $from { + type Target = $to; + + #[inline] + fn deref(&self) -> &$to { + &self.0 + } + } + + impl ::std::ops::DerefMut for $from { + #[inline] + fn deref_mut(&mut self) -> &mut $to { + &mut self.0 + } + } + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __hyper__tm { + ($id:ident, $tm:ident{$($tf:item)*}) => { + #[allow(unused_imports)] + #[cfg(test)] + mod $tm{ + use std::str; + use http::Method; + use $crate::header::*; + use $crate::mime::*; + use super::$id as HeaderField; + $($tf)* + } + + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! test_header { + ($id:ident, $raw:expr) => { + #[test] + fn $id() { + #[allow(unused)] + use std::ascii::AsciiExt; + use test; + let raw = $raw; + let a: Vec> = raw.iter().map(|x| x.to_vec()).collect(); + let mut req = test::TestRequest::default(); + for item in a { + req = req.header(HeaderField::name(), item); + } + let req = req.finish(); + let value = HeaderField::parse(&req); + let result = format!("{}", value.unwrap()); + let expected = String::from_utf8(raw[0].to_vec()).unwrap(); + let result_cmp: Vec = result + .to_ascii_lowercase() + .split(' ') + .map(|x| x.to_owned()) + .collect(); + let expected_cmp: Vec = expected + .to_ascii_lowercase() + .split(' ') + .map(|x| x.to_owned()) + .collect(); + assert_eq!(result_cmp.concat(), expected_cmp.concat()); + } + }; + ($id:ident, $raw:expr, $typed:expr) => { + #[test] + fn $id() { + use $crate::test; + let a: Vec> = $raw.iter().map(|x| x.to_vec()).collect(); + let mut req = test::TestRequest::default(); + for item in a { + req = req.header(HeaderField::name(), item); + } + let req = req.finish(); + let val = HeaderField::parse(&req); + let typed: Option = $typed; + // Test parsing + assert_eq!(val.ok(), typed); + // Test formatting + if typed.is_some() { + let raw = &($raw)[..]; + let mut iter = raw.iter().map(|b|str::from_utf8(&b[..]).unwrap()); + let mut joined = String::new(); + joined.push_str(iter.next().unwrap()); + for s in iter { + joined.push_str(", "); + joined.push_str(s); + } + assert_eq!(format!("{}", typed.unwrap()), joined); + } + } + } +} + +#[macro_export] +macro_rules! header { + // $a:meta: Attributes associated with the header item (usually docs) + // $id:ident: Identifier of the header + // $n:expr: Lowercase name of the header + // $nn:expr: Nice name of the header + + // List header, zero or more items + ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)*) => { + $(#[$a])* + #[derive(Clone, Debug, PartialEq)] + pub struct $id(pub Vec<$item>); + __hyper__deref!($id => Vec<$item>); + impl $crate::header::Header for $id { + #[inline] + fn name() -> $crate::header::http::HeaderName { + $name + } + #[inline] + fn parse(msg: &T) -> Result + where T: $crate::HttpMessage + { + $crate::header::from_comma_delimited( + msg.headers().get_all(Self::name())).map($id) + } + } + impl ::std::fmt::Display for $id { + #[inline] + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + $crate::header::fmt_comma_delimited(f, &self.0[..]) + } + } + impl $crate::header::IntoHeaderValue for $id { + type Error = $crate::header::http::InvalidHeaderValueBytes; + + fn try_into(self) -> Result<$crate::header::http::HeaderValue, Self::Error> { + use std::fmt::Write; + let mut writer = $crate::header::Writer::new(); + let _ = write!(&mut writer, "{}", self); + $crate::header::http::HeaderValue::from_shared(writer.take()) + } + } + }; + // List header, one or more items + ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)+) => { + $(#[$a])* + #[derive(Clone, Debug, PartialEq)] + pub struct $id(pub Vec<$item>); + __hyper__deref!($id => Vec<$item>); + impl $crate::header::Header for $id { + #[inline] + fn name() -> $crate::header::http::HeaderName { + $name + } + #[inline] + fn parse(msg: &T) -> Result + where T: $crate::HttpMessage + { + $crate::header::from_comma_delimited( + msg.headers().get_all(Self::name())).map($id) + } + } + impl ::std::fmt::Display for $id { + #[inline] + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + $crate::header::fmt_comma_delimited(f, &self.0[..]) + } + } + impl $crate::header::IntoHeaderValue for $id { + type Error = $crate::header::http::InvalidHeaderValueBytes; + + fn try_into(self) -> Result<$crate::header::http::HeaderValue, Self::Error> { + use std::fmt::Write; + let mut writer = $crate::header::Writer::new(); + let _ = write!(&mut writer, "{}", self); + $crate::header::http::HeaderValue::from_shared(writer.take()) + } + } + }; + // Single value header + ($(#[$a:meta])*($id:ident, $name:expr) => [$value:ty]) => { + $(#[$a])* + #[derive(Clone, Debug, PartialEq)] + pub struct $id(pub $value); + __hyper__deref!($id => $value); + impl $crate::header::Header for $id { + #[inline] + fn name() -> $crate::header::http::HeaderName { + $name + } + #[inline] + fn parse(msg: &T) -> Result + where T: $crate::HttpMessage + { + $crate::header::from_one_raw_str( + msg.headers().get(Self::name())).map($id) + } + } + impl ::std::fmt::Display for $id { + #[inline] + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + ::std::fmt::Display::fmt(&self.0, f) + } + } + impl $crate::header::IntoHeaderValue for $id { + type Error = $crate::header::http::InvalidHeaderValueBytes; + + fn try_into(self) -> Result<$crate::header::http::HeaderValue, Self::Error> { + self.0.try_into() + } + } + }; + // List header, one or more items with "*" option + ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { + $(#[$a])* + #[derive(Clone, Debug, PartialEq)] + pub enum $id { + /// Any value is a match + Any, + /// Only the listed items are a match + Items(Vec<$item>), + } + impl $crate::header::Header for $id { + #[inline] + fn name() -> $crate::header::http::HeaderName { + $name + } + #[inline] + fn parse(msg: &T) -> Result + where T: $crate::header::HttpMessage + { + let any = msg.headers().get(Self::name()).and_then(|hdr| { + hdr.to_str().ok().and_then(|hdr| Some(hdr.trim() == "*"))}); + + if let Some(true) = any { + Ok($id::Any) + } else { + Ok($id::Items( + $crate::header::from_comma_delimited( + msg.headers().get_all(Self::name()))?)) + } + } + } + impl ::std::fmt::Display for $id { + #[inline] + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + match *self { + $id::Any => f.write_str("*"), + $id::Items(ref fields) => $crate::header::fmt_comma_delimited( + f, &fields[..]) + } + } + } + impl $crate::header::IntoHeaderValue for $id { + type Error = $crate::header::http::InvalidHeaderValueBytes; + + fn try_into(self) -> Result<$crate::header::http::HeaderValue, Self::Error> { + use std::fmt::Write; + let mut writer = $crate::header::Writer::new(); + let _ = write!(&mut writer, "{}", self); + $crate::header::http::HeaderValue::from_shared(writer.take()) + } + } + }; + + // optional test module + ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => { + header! { + $(#[$a])* + ($id, $name) => ($item)* + } + + __hyper__tm! { $id, $tm { $($tf)* }} + }; + ($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => { + header! { + $(#[$a])* + ($id, $n) => ($item)+ + } + + __hyper__tm! { $id, $tm { $($tf)* }} + }; + ($(#[$a:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => { + header! { + $(#[$a])* ($id, $name) => [$item] + } + + __hyper__tm! { $id, $tm { $($tf)* }} + }; + ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => { + header! { + $(#[$a])* + ($id, $name) => {Any / ($item)+} + } + + __hyper__tm! { $id, $tm { $($tf)* }} + }; +} + + +mod accept_charset; +//mod accept_encoding; +mod accept_language; +mod accept; +mod allow; +mod cache_control; +//mod content_disposition; +mod content_language; +mod content_range; +mod content_type; +mod date; +mod etag; +mod expires; +mod if_match; +mod if_modified_since; +mod if_none_match; +mod if_range; +mod if_unmodified_since; +mod last_modified; +//mod range; diff --git a/src/header/common/range.rs b/src/header/common/range.rs new file mode 100644 index 000000000..d0fca0f3e --- /dev/null +++ b/src/header/common/range.rs @@ -0,0 +1,387 @@ +use std::fmt::{self, Display}; +use std::str::FromStr; + +use header::{Header, Raw}; +use header::parsing::{from_one_raw_str}; + +/// `Range` header, defined in [RFC7233](https://tools.ietf.org/html/rfc7233#section-3.1) +/// +/// The "Range" header field on a GET request modifies the method +/// semantics to request transfer of only one or more subranges of the +/// selected representation data, rather than the entire selected +/// representation data. +/// +/// # ABNF +/// +/// ```text +/// Range = byte-ranges-specifier / other-ranges-specifier +/// other-ranges-specifier = other-range-unit "=" other-range-set +/// other-range-set = 1*VCHAR +/// +/// bytes-unit = "bytes" +/// +/// byte-ranges-specifier = bytes-unit "=" byte-range-set +/// byte-range-set = 1#(byte-range-spec / suffix-byte-range-spec) +/// byte-range-spec = first-byte-pos "-" [last-byte-pos] +/// first-byte-pos = 1*DIGIT +/// last-byte-pos = 1*DIGIT +/// ``` +/// +/// # Example values +/// +/// * `bytes=1000-` +/// * `bytes=-2000` +/// * `bytes=0-1,30-40` +/// * `bytes=0-10,20-90,-100` +/// * `custom_unit=0-123` +/// * `custom_unit=xxx-yyy` +/// +/// # Examples +/// +/// ``` +/// use hyper::header::{Headers, Range, ByteRangeSpec}; +/// +/// let mut headers = Headers::new(); +/// headers.set(Range::Bytes( +/// vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::AllFrom(200)] +/// )); +/// +/// headers.clear(); +/// headers.set(Range::Unregistered("letters".to_owned(), "a-f".to_owned())); +/// ``` +/// +/// ``` +/// use hyper::header::{Headers, Range}; +/// +/// let mut headers = Headers::new(); +/// headers.set(Range::bytes(1, 100)); +/// +/// headers.clear(); +/// headers.set(Range::bytes_multi(vec![(1, 100), (200, 300)])); +/// ``` +#[derive(PartialEq, Clone, Debug)] +pub enum Range { + /// Byte range + Bytes(Vec), + /// Custom range, with unit not registered at IANA + /// (`other-range-unit`: String , `other-range-set`: String) + Unregistered(String, String) +} + +/// Each `Range::Bytes` header can contain one or more `ByteRangeSpecs`. +/// Each `ByteRangeSpec` defines a range of bytes to fetch +#[derive(PartialEq, Clone, Debug)] +pub enum ByteRangeSpec { + /// Get all bytes between x and y ("x-y") + FromTo(u64, u64), + /// Get all bytes starting from x ("x-") + AllFrom(u64), + /// Get last x bytes ("-x") + Last(u64) +} + +impl ByteRangeSpec { + /// Given the full length of the entity, attempt to normalize the byte range + /// into an satisfiable end-inclusive (from, to) range. + /// + /// The resulting range is guaranteed to be a satisfiable range within the bounds + /// of `0 <= from <= to < full_length`. + /// + /// If the byte range is deemed unsatisfiable, `None` is returned. + /// An unsatisfiable range is generally cause for a server to either reject + /// the client request with a `416 Range Not Satisfiable` status code, or to + /// simply ignore the range header and serve the full entity using a `200 OK` + /// status code. + /// + /// This function closely follows [RFC 7233][1] section 2.1. + /// As such, it considers ranges to be satisfiable if they meet the following + /// conditions: + /// + /// > If a valid byte-range-set includes at least one byte-range-spec with + /// a first-byte-pos that is less than the current length of the + /// representation, or at least one suffix-byte-range-spec with a + /// non-zero suffix-length, then the byte-range-set is satisfiable. + /// Otherwise, the byte-range-set is unsatisfiable. + /// + /// The function also computes remainder ranges based on the RFC: + /// + /// > If the last-byte-pos value is + /// absent, or if the value is greater than or equal to the current + /// length of the representation data, the byte range is interpreted as + /// the remainder of the representation (i.e., the server replaces the + /// value of last-byte-pos with a value that is one less than the current + /// length of the selected representation). + /// + /// [1]: https://tools.ietf.org/html/rfc7233 + pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> { + // If the full length is zero, there is no satisfiable end-inclusive range. + if full_length == 0 { + return None; + } + match self { + &ByteRangeSpec::FromTo(from, to) => { + if from < full_length && from <= to { + Some((from, ::std::cmp::min(to, full_length - 1))) + } else { + None + } + }, + &ByteRangeSpec::AllFrom(from) => { + if from < full_length { + Some((from, full_length - 1)) + } else { + None + } + }, + &ByteRangeSpec::Last(last) => { + if last > 0 { + // From the RFC: If the selected representation is shorter + // than the specified suffix-length, + // the entire representation is used. + if last > full_length { + Some((0, full_length - 1)) + } else { + Some((full_length - last, full_length - 1)) + } + } else { + None + } + } + } + } +} + +impl Range { + /// Get the most common byte range header ("bytes=from-to") + pub fn bytes(from: u64, to: u64) -> Range { + Range::Bytes(vec![ByteRangeSpec::FromTo(from, to)]) + } + + /// Get byte range header with multiple subranges + /// ("bytes=from1-to1,from2-to2,fromX-toX") + pub fn bytes_multi(ranges: Vec<(u64, u64)>) -> Range { + Range::Bytes(ranges.iter().map(|r| ByteRangeSpec::FromTo(r.0, r.1)).collect()) + } +} + + +impl fmt::Display for ByteRangeSpec { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ByteRangeSpec::FromTo(from, to) => write!(f, "{}-{}", from, to), + ByteRangeSpec::Last(pos) => write!(f, "-{}", pos), + ByteRangeSpec::AllFrom(pos) => write!(f, "{}-", pos), + } + } +} + + +impl fmt::Display for Range { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Range::Bytes(ref ranges) => { + try!(write!(f, "bytes=")); + + for (i, range) in ranges.iter().enumerate() { + if i != 0 { + try!(f.write_str(",")); + } + try!(Display::fmt(range, f)); + } + Ok(()) + }, + Range::Unregistered(ref unit, ref range_str) => { + write!(f, "{}={}", unit, range_str) + }, + } + } +} + +impl FromStr for Range { + type Err = ::Error; + + fn from_str(s: &str) -> ::Result { + let mut iter = s.splitn(2, '='); + + match (iter.next(), iter.next()) { + (Some("bytes"), Some(ranges)) => { + let ranges = from_comma_delimited(ranges); + if ranges.is_empty() { + return Err(::Error::Header); + } + Ok(Range::Bytes(ranges)) + } + (Some(unit), Some(range_str)) if unit != "" && range_str != "" => { + Ok(Range::Unregistered(unit.to_owned(), range_str.to_owned())) + + }, + _ => Err(::Error::Header) + } + } +} + +impl FromStr for ByteRangeSpec { + type Err = ::Error; + + fn from_str(s: &str) -> ::Result { + let mut parts = s.splitn(2, '-'); + + match (parts.next(), parts.next()) { + (Some(""), Some(end)) => { + end.parse().or(Err(::Error::Header)).map(ByteRangeSpec::Last) + }, + (Some(start), Some("")) => { + start.parse().or(Err(::Error::Header)).map(ByteRangeSpec::AllFrom) + }, + (Some(start), Some(end)) => { + match (start.parse(), end.parse()) { + (Ok(start), Ok(end)) if start <= end => Ok(ByteRangeSpec::FromTo(start, end)), + _ => Err(::Error::Header) + } + }, + _ => Err(::Error::Header) + } + } +} + +fn from_comma_delimited(s: &str) -> Vec { + s.split(',') + .filter_map(|x| match x.trim() { + "" => None, + y => Some(y) + }) + .filter_map(|x| x.parse().ok()) + .collect() +} + +impl Header for Range { + + fn header_name() -> &'static str { + static NAME: &'static str = "Range"; + NAME + } + + fn parse_header(raw: &Raw) -> ::Result { + from_one_raw_str(raw) + } + + fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result { + f.fmt_line(self) + } + +} + +#[test] +fn test_parse_bytes_range_valid() { + let r: Range = Header::parse_header(&"bytes=1-100".into()).unwrap(); + let r2: Range = Header::parse_header(&"bytes=1-100,-".into()).unwrap(); + let r3 = Range::bytes(1, 100); + assert_eq!(r, r2); + assert_eq!(r2, r3); + + let r: Range = Header::parse_header(&"bytes=1-100,200-".into()).unwrap(); + let r2: Range = Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap(); + let r3 = Range::Bytes( + vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::AllFrom(200)] + ); + assert_eq!(r, r2); + assert_eq!(r2, r3); + + let r: Range = Header::parse_header(&"bytes=1-100,-100".into()).unwrap(); + let r2: Range = Header::parse_header(&"bytes=1-100, ,,-100".into()).unwrap(); + let r3 = Range::Bytes( + vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::Last(100)] + ); + assert_eq!(r, r2); + assert_eq!(r2, r3); + + let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); + let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); + assert_eq!(r, r2); + +} + +#[test] +fn test_parse_unregistered_range_valid() { + let r: Range = Header::parse_header(&"custom=1-100,-100".into()).unwrap(); + let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned()); + assert_eq!(r, r2); + + let r: Range = Header::parse_header(&"custom=abcd".into()).unwrap(); + let r2 = Range::Unregistered("custom".to_owned(), "abcd".to_owned()); + assert_eq!(r, r2); + + let r: Range = Header::parse_header(&"custom=xxx-yyy".into()).unwrap(); + let r2 = Range::Unregistered("custom".to_owned(), "xxx-yyy".to_owned()); + assert_eq!(r, r2); +} + +#[test] +fn test_parse_invalid() { + let r: ::Result = Header::parse_header(&"bytes=1-a,-".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"bytes=1-2-3".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"abc".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"bytes=1-100=".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"bytes=".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"custom=".into()); + assert_eq!(r.ok(), None); + + let r: ::Result = Header::parse_header(&"=1-100".into()); + assert_eq!(r.ok(), None); +} + +#[test] +fn test_fmt() { + use header::Headers; + + let mut headers = Headers::new(); + + headers.set( + Range::Bytes( + vec![ByteRangeSpec::FromTo(0, 1000), ByteRangeSpec::AllFrom(2000)] + )); + assert_eq!(&headers.to_string(), "Range: bytes=0-1000,2000-\r\n"); + + headers.clear(); + headers.set(Range::Bytes(vec![])); + + assert_eq!(&headers.to_string(), "Range: bytes=\r\n"); + + headers.clear(); + headers.set(Range::Unregistered("custom".to_owned(), "1-xxx".to_owned())); + + assert_eq!(&headers.to_string(), "Range: custom=1-xxx\r\n"); +} + +#[test] +fn test_byte_range_spec_to_satisfiable_range() { + assert_eq!(Some((0, 0)), ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(3)); + assert_eq!(Some((1, 2)), ByteRangeSpec::FromTo(1, 2).to_satisfiable_range(3)); + assert_eq!(Some((1, 2)), ByteRangeSpec::FromTo(1, 5).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::FromTo(3, 3).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::FromTo(2, 1).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(0)); + + assert_eq!(Some((0, 2)), ByteRangeSpec::AllFrom(0).to_satisfiable_range(3)); + assert_eq!(Some((2, 2)), ByteRangeSpec::AllFrom(2).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::AllFrom(3).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::AllFrom(5).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::AllFrom(0).to_satisfiable_range(0)); + + assert_eq!(Some((1, 2)), ByteRangeSpec::Last(2).to_satisfiable_range(3)); + assert_eq!(Some((2, 2)), ByteRangeSpec::Last(1).to_satisfiable_range(3)); + assert_eq!(Some((0, 2)), ByteRangeSpec::Last(5).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::Last(0).to_satisfiable_range(3)); + assert_eq!(None, ByteRangeSpec::Last(2).to_satisfiable_range(0)); +} + diff --git a/src/header/mod.rs b/src/header/mod.rs index 9c727935a..7c3ad7eb1 100644 --- a/src/header/mod.rs +++ b/src/header/mod.rs @@ -1,22 +1,32 @@ //! Various http headers -// A lot of code is inspired by hyper +// This is mostly copy of [hyper](https://github.com/hyperium/hyper/tree/master/src/header) -use bytes::Bytes; +use std::fmt; +use std::str::FromStr; + +use bytes::{Bytes, BytesMut}; use http::{Error as HttpError}; -use http::header::{InvalidHeaderValue, InvalidHeaderValueBytes}; +use http::header::GetAll; +use mime::Mime; pub use cookie::{Cookie, CookieBuilder}; pub use http_range::HttpRange; -pub use http::header::{HeaderName, HeaderValue}; + +#[doc(hidden)] +pub mod http { + pub use http::header::*; +} use error::ParseError; use httpmessage::HttpMessage; pub use httpresponse::ConnectionType; mod common; -mod httpdate; +mod shared; +#[doc(hidden)] pub use self::common::*; -pub use self::httpdate::HttpDate; +#[doc(hidden)] +pub use self::shared::*; #[doc(hidden)] @@ -24,7 +34,7 @@ pub use self::httpdate::HttpDate; pub trait Header where Self: IntoHeaderValue { /// Returns the name of the header field - fn name() -> HeaderName; + fn name() -> http::HeaderName; /// Parse a header fn parse(msg: &T) -> Result; @@ -37,42 +47,69 @@ pub trait IntoHeaderValue: Sized { type Error: Into; /// Cast from PyObject to a concrete Python object type. - fn try_into(self) -> Result; + fn try_into(self) -> Result; } -impl IntoHeaderValue for HeaderValue { - type Error = InvalidHeaderValue; +impl IntoHeaderValue for http::HeaderValue { + type Error = http::InvalidHeaderValue; #[inline] - fn try_into(self) -> Result { + fn try_into(self) -> Result { Ok(self) } } impl<'a> IntoHeaderValue for &'a str { - type Error = InvalidHeaderValue; + type Error = http::InvalidHeaderValue; #[inline] - fn try_into(self) -> Result { + fn try_into(self) -> Result { self.parse() } } impl<'a> IntoHeaderValue for &'a [u8] { - type Error = InvalidHeaderValue; + type Error = http::InvalidHeaderValue; #[inline] - fn try_into(self) -> Result { - HeaderValue::from_bytes(self) + fn try_into(self) -> Result { + http::HeaderValue::from_bytes(self) } } impl IntoHeaderValue for Bytes { - type Error = InvalidHeaderValueBytes; + type Error = http::InvalidHeaderValueBytes; #[inline] - fn try_into(self) -> Result { - HeaderValue::from_shared(self) + fn try_into(self) -> Result { + http::HeaderValue::from_shared(self) + } +} + +impl IntoHeaderValue for Vec { + type Error = http::InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + http::HeaderValue::from_shared(Bytes::from(self)) + } +} + +impl IntoHeaderValue for String { + type Error = http::InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + http::HeaderValue::from_shared(Bytes::from(self)) + } +} + +impl IntoHeaderValue for Mime { + type Error = http::InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + http::HeaderValue::from_shared(Bytes::from(format!("{}", self))) } } @@ -133,3 +170,81 @@ impl<'a> From<&'a str> for ContentEncoding { } } } + +#[doc(hidden)] +pub(crate) struct Writer { + buf: BytesMut, +} + +impl Writer { + fn new() -> Writer { + Writer{buf: BytesMut::new()} + } + fn take(&mut self) -> Bytes { + self.buf.take().freeze() + } +} + +impl fmt::Write for Writer { + #[inline] + fn write_str(&mut self, s: &str) -> fmt::Result { + self.buf.extend_from_slice(s.as_bytes()); + Ok(()) + } + + #[inline] + fn write_fmt(&mut self, args: fmt::Arguments) -> fmt::Result { + fmt::write(self, args) + } +} + +#[inline] +#[doc(hidden)] +/// Reads a comma-delimited raw header into a Vec. +pub fn from_comma_delimited(all: GetAll) + -> Result, ParseError> +{ + let mut result = Vec::new(); + for h in all { + let s = h.to_str().map_err(|_| ParseError::Header)?; + result.extend(s.split(',') + .filter_map(|x| match x.trim() { + "" => None, + y => Some(y) + }) + .filter_map(|x| x.trim().parse().ok())) + } + Ok(result) +} + +#[inline] +#[doc(hidden)] +/// Reads a single string when parsing a header. +pub fn from_one_raw_str(val: Option<&http::HeaderValue>) + -> Result +{ + if let Some(line) = val { + let line = line.to_str().map_err(|_| ParseError::Header)?; + if !line.is_empty() { + return T::from_str(line).or(Err(ParseError::Header)) + } + } + Err(ParseError::Header) +} + +#[inline] +#[doc(hidden)] +/// Format an array into a comma-delimited string. +pub fn fmt_comma_delimited(f: &mut fmt::Formatter, parts: &[T]) -> fmt::Result + where T: fmt::Display +{ + let mut iter = parts.iter(); + if let Some(part) = iter.next() { + fmt::Display::fmt(part, f)?; + } + for part in iter { + f.write_str(", ")?; + fmt::Display::fmt(part, f)?; + } + Ok(()) +} diff --git a/src/header/shared/charset.rs b/src/header/shared/charset.rs new file mode 100644 index 000000000..6a07fda59 --- /dev/null +++ b/src/header/shared/charset.rs @@ -0,0 +1,154 @@ +#![allow(unused)] +use std::fmt::{self, Display}; +use std::str::FromStr; +use std::ascii::AsciiExt; + +use self::Charset::*; + +/// A Mime charset. +/// +/// The string representation is normalised to upper case. +/// +/// See [http://www.iana.org/assignments/character-sets/character-sets.xhtml][url]. +/// +/// [url]: http://www.iana.org/assignments/character-sets/character-sets.xhtml +#[derive(Clone,Debug,PartialEq)] +#[allow(non_camel_case_types)] +pub enum Charset{ + /// US ASCII + Us_Ascii, + /// ISO-8859-1 + Iso_8859_1, + /// ISO-8859-2 + Iso_8859_2, + /// ISO-8859-3 + Iso_8859_3, + /// ISO-8859-4 + Iso_8859_4, + /// ISO-8859-5 + Iso_8859_5, + /// ISO-8859-6 + Iso_8859_6, + /// ISO-8859-7 + Iso_8859_7, + /// ISO-8859-8 + Iso_8859_8, + /// ISO-8859-9 + Iso_8859_9, + /// ISO-8859-10 + Iso_8859_10, + /// Shift_JIS + Shift_Jis, + /// EUC-JP + Euc_Jp, + /// ISO-2022-KR + Iso_2022_Kr, + /// EUC-KR + Euc_Kr, + /// ISO-2022-JP + Iso_2022_Jp, + /// ISO-2022-JP-2 + Iso_2022_Jp_2, + /// ISO-8859-6-E + Iso_8859_6_E, + /// ISO-8859-6-I + Iso_8859_6_I, + /// ISO-8859-8-E + Iso_8859_8_E, + /// ISO-8859-8-I + Iso_8859_8_I, + /// GB2312 + Gb2312, + /// Big5 + Big5, + /// KOI8-R + Koi8_R, + /// An arbitrary charset specified as a string + Ext(String) +} + +impl Charset { + fn name(&self) -> &str { + match *self { + Us_Ascii => "US-ASCII", + Iso_8859_1 => "ISO-8859-1", + Iso_8859_2 => "ISO-8859-2", + Iso_8859_3 => "ISO-8859-3", + Iso_8859_4 => "ISO-8859-4", + Iso_8859_5 => "ISO-8859-5", + Iso_8859_6 => "ISO-8859-6", + Iso_8859_7 => "ISO-8859-7", + Iso_8859_8 => "ISO-8859-8", + Iso_8859_9 => "ISO-8859-9", + Iso_8859_10 => "ISO-8859-10", + Shift_Jis => "Shift-JIS", + Euc_Jp => "EUC-JP", + Iso_2022_Kr => "ISO-2022-KR", + Euc_Kr => "EUC-KR", + Iso_2022_Jp => "ISO-2022-JP", + Iso_2022_Jp_2 => "ISO-2022-JP-2", + Iso_8859_6_E => "ISO-8859-6-E", + Iso_8859_6_I => "ISO-8859-6-I", + Iso_8859_8_E => "ISO-8859-8-E", + Iso_8859_8_I => "ISO-8859-8-I", + Gb2312 => "GB2312", + Big5 => "5", + Koi8_R => "KOI8-R", + Ext(ref s) => s + } + } +} + +impl Display for Charset { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.name()) + } +} + +impl FromStr for Charset { + type Err = ::Error; + fn from_str(s: &str) -> ::Result { + Ok(match s.to_ascii_uppercase().as_ref() { + "US-ASCII" => Us_Ascii, + "ISO-8859-1" => Iso_8859_1, + "ISO-8859-2" => Iso_8859_2, + "ISO-8859-3" => Iso_8859_3, + "ISO-8859-4" => Iso_8859_4, + "ISO-8859-5" => Iso_8859_5, + "ISO-8859-6" => Iso_8859_6, + "ISO-8859-7" => Iso_8859_7, + "ISO-8859-8" => Iso_8859_8, + "ISO-8859-9" => Iso_8859_9, + "ISO-8859-10" => Iso_8859_10, + "SHIFT-JIS" => Shift_Jis, + "EUC-JP" => Euc_Jp, + "ISO-2022-KR" => Iso_2022_Kr, + "EUC-KR" => Euc_Kr, + "ISO-2022-JP" => Iso_2022_Jp, + "ISO-2022-JP-2" => Iso_2022_Jp_2, + "ISO-8859-6-E" => Iso_8859_6_E, + "ISO-8859-6-I" => Iso_8859_6_I, + "ISO-8859-8-E" => Iso_8859_8_E, + "ISO-8859-8-I" => Iso_8859_8_I, + "GB2312" => Gb2312, + "5" => Big5, + "KOI8-R" => Koi8_R, + s => Ext(s.to_owned()) + }) + } +} + +#[test] +fn test_parse() { + assert_eq!(Us_Ascii,"us-ascii".parse().unwrap()); + assert_eq!(Us_Ascii,"US-Ascii".parse().unwrap()); + assert_eq!(Us_Ascii,"US-ASCII".parse().unwrap()); + assert_eq!(Shift_Jis,"Shift-JIS".parse().unwrap()); + assert_eq!(Ext("ABCD".to_owned()),"abcd".parse().unwrap()); +} + +#[test] +fn test_display() { + assert_eq!("US-ASCII", format!("{}", Us_Ascii)); + assert_eq!("ABCD", format!("{}", Ext("ABCD".to_owned()))); +} diff --git a/src/header/shared/encoding.rs b/src/header/shared/encoding.rs new file mode 100644 index 000000000..6381ac7eb --- /dev/null +++ b/src/header/shared/encoding.rs @@ -0,0 +1,57 @@ +use std::fmt; +use std::str; + +pub use self::Encoding::{Chunked, Brotli, Gzip, Deflate, Compress, Identity, EncodingExt, Trailers}; + +/// A value to represent an encoding used in `Transfer-Encoding` +/// or `Accept-Encoding` header. +#[derive(Clone, PartialEq, Debug)] +pub enum Encoding { + /// The `chunked` encoding. + Chunked, + /// The `br` encoding. + Brotli, + /// The `gzip` encoding. + Gzip, + /// The `deflate` encoding. + Deflate, + /// The `compress` encoding. + Compress, + /// The `identity` encoding. + Identity, + /// The `trailers` encoding. + Trailers, + /// Some other encoding that is less common, can be any String. + EncodingExt(String) +} + +impl fmt::Display for Encoding { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match *self { + Chunked => "chunked", + Brotli => "br", + Gzip => "gzip", + Deflate => "deflate", + Compress => "compress", + Identity => "identity", + Trailers => "trailers", + EncodingExt(ref s) => s.as_ref() + }) + } +} + +impl str::FromStr for Encoding { + type Err = ::error::ParseError; + fn from_str(s: &str) -> Result { + match s { + "chunked" => Ok(Chunked), + "br" => Ok(Brotli), + "deflate" => Ok(Deflate), + "gzip" => Ok(Gzip), + "compress" => Ok(Compress), + "identity" => Ok(Identity), + "trailers" => Ok(Trailers), + _ => Ok(EncodingExt(s.to_owned())) + } + } +} diff --git a/src/header/shared/entity.rs b/src/header/shared/entity.rs new file mode 100644 index 000000000..90c99e646 --- /dev/null +++ b/src/header/shared/entity.rs @@ -0,0 +1,230 @@ +use std::str::FromStr; +use std::fmt::{self, Display, Write}; +use header::{http, Writer, IntoHeaderValue}; + +/// check that each char in the slice is either: +/// 1. `%x21`, or +/// 2. in the range `%x23` to `%x7E`, or +/// 3. above `%x80` +fn check_slice_validity(slice: &str) -> bool { + slice.bytes().all(|c| + c == b'\x21' || (c >= b'\x23' && c <= b'\x7e') | (c >= b'\x80')) +} + +/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3) +/// +/// An entity tag consists of a string enclosed by two literal double quotes. +/// Preceding the first double quote is an optional weakness indicator, +/// which always looks like `W/`. Examples for valid tags are `"xyzzy"` and `W/"xyzzy"`. +/// +/// # ABNF +/// +/// ```text +/// entity-tag = [ weak ] opaque-tag +/// weak = %x57.2F ; "W/", case-sensitive +/// opaque-tag = DQUOTE *etagc DQUOTE +/// etagc = %x21 / %x23-7E / obs-text +/// ; VCHAR except double quotes, plus obs-text +/// ``` +/// +/// # Comparison +/// To check if two entity tags are equivalent in an application always use the `strong_eq` or +/// `weak_eq` methods based on the context of the Tag. Only use `==` to check if two tags are +/// identical. +/// +/// The example below shows the results for a set of entity-tag pairs and +/// both the weak and strong comparison function results: +/// +/// | `ETag 1`| `ETag 2`| Strong Comparison | Weak Comparison | +/// |---------|---------|-------------------|-----------------| +/// | `W/"1"` | `W/"1"` | no match | match | +/// | `W/"1"` | `W/"2"` | no match | no match | +/// | `W/"1"` | `"1"` | no match | match | +/// | `"1"` | `"1"` | match | match | +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EntityTag { + /// Weakness indicator for the tag + pub weak: bool, + /// The opaque string in between the DQUOTEs + tag: String +} + +impl EntityTag { + /// Constructs a new EntityTag. + /// # Panics + /// If the tag contains invalid characters. + pub fn new(weak: bool, tag: String) -> EntityTag { + assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); + EntityTag { weak, tag } + } + + /// Constructs a new weak EntityTag. + /// # Panics + /// If the tag contains invalid characters. + pub fn weak(tag: String) -> EntityTag { + EntityTag::new(true, tag) + } + + /// Constructs a new strong EntityTag. + /// # Panics + /// If the tag contains invalid characters. + pub fn strong(tag: String) -> EntityTag { + EntityTag::new(false, tag) + } + + /// Get the tag. + pub fn tag(&self) -> &str { + self.tag.as_ref() + } + + /// Set the tag. + /// # Panics + /// If the tag contains invalid characters. + pub fn set_tag(&mut self, tag: String) { + assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); + self.tag = tag + } + + /// For strong comparison two entity-tags are equivalent if both are not weak and their + /// opaque-tags match character-by-character. + pub fn strong_eq(&self, other: &EntityTag) -> bool { + !self.weak && !other.weak && self.tag == other.tag + } + + /// For weak comparison two entity-tags are equivalent if their + /// opaque-tags match character-by-character, regardless of either or + /// both being tagged as "weak". + pub fn weak_eq(&self, other: &EntityTag) -> bool { + self.tag == other.tag + } + + /// The inverse of `EntityTag.strong_eq()`. + pub fn strong_ne(&self, other: &EntityTag) -> bool { + !self.strong_eq(other) + } + + /// The inverse of `EntityTag.weak_eq()`. + pub fn weak_ne(&self, other: &EntityTag) -> bool { + !self.weak_eq(other) + } +} + +impl Display for EntityTag { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.weak { + write!(f, "W/\"{}\"", self.tag) + } else { + write!(f, "\"{}\"", self.tag) + } + } +} + +impl FromStr for EntityTag { + type Err = ::error::ParseError; + + fn from_str(s: &str) -> Result { + let length: usize = s.len(); + let slice = &s[..]; + // Early exits if it doesn't terminate in a DQUOTE. + if !slice.ends_with('"') || slice.len() < 2 { + return Err(::error::ParseError::Header); + } + // The etag is weak if its first char is not a DQUOTE. + if slice.len() >= 2 && slice.starts_with('"') + && check_slice_validity(&slice[1..length-1]) { + // No need to check if the last char is a DQUOTE, + // we already did that above. + return Ok(EntityTag { weak: false, tag: slice[1..length-1].to_owned() }); + } else if slice.len() >= 4 && slice.starts_with("W/\"") + && check_slice_validity(&slice[3..length-1]) { + return Ok(EntityTag { weak: true, tag: slice[3..length-1].to_owned() }); + } + Err(::error::ParseError::Header) + } +} + +impl IntoHeaderValue for EntityTag { + type Error = http::InvalidHeaderValueBytes; + + fn try_into(self) -> Result { + let mut wrt = Writer::new(); + write!(wrt, "{}", self).unwrap(); + unsafe{Ok(http::HeaderValue::from_shared_unchecked(wrt.take()))} + } +} + +#[cfg(test)] +mod tests { + use super::EntityTag; + + #[test] + fn test_etag_parse_success() { + // Expected success + assert_eq!("\"foobar\"".parse::().unwrap(), + EntityTag::strong("foobar".to_owned())); + assert_eq!("\"\"".parse::().unwrap(), + EntityTag::strong("".to_owned())); + assert_eq!("W/\"weaktag\"".parse::().unwrap(), + EntityTag::weak("weaktag".to_owned())); + assert_eq!("W/\"\x65\x62\"".parse::().unwrap(), + EntityTag::weak("\x65\x62".to_owned())); + assert_eq!("W/\"\"".parse::().unwrap(), EntityTag::weak("".to_owned())); + } + + #[test] + fn test_etag_parse_failures() { + // Expected failures + assert!("no-dquotes".parse::().is_err()); + assert!("w/\"the-first-w-is-case-sensitive\"".parse::().is_err()); + assert!("".parse::().is_err()); + assert!("\"unmatched-dquotes1".parse::().is_err()); + assert!("unmatched-dquotes2\"".parse::().is_err()); + assert!("matched-\"dquotes\"".parse::().is_err()); + } + + #[test] + fn test_etag_fmt() { + assert_eq!(format!("{}", EntityTag::strong("foobar".to_owned())), "\"foobar\""); + assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\""); + assert_eq!(format!("{}", EntityTag::weak("weak-etag".to_owned())), "W/\"weak-etag\""); + assert_eq!(format!("{}", EntityTag::weak("\u{0065}".to_owned())), "W/\"\x65\""); + assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\""); + } + + #[test] + fn test_cmp() { + // | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison | + // |---------|---------|-------------------|-----------------| + // | `W/"1"` | `W/"1"` | no match | match | + // | `W/"1"` | `W/"2"` | no match | no match | + // | `W/"1"` | `"1"` | no match | match | + // | `"1"` | `"1"` | match | match | + let mut etag1 = EntityTag::weak("1".to_owned()); + let mut etag2 = EntityTag::weak("1".to_owned()); + assert!(!etag1.strong_eq(&etag2)); + assert!(etag1.weak_eq(&etag2)); + assert!(etag1.strong_ne(&etag2)); + assert!(!etag1.weak_ne(&etag2)); + + etag1 = EntityTag::weak("1".to_owned()); + etag2 = EntityTag::weak("2".to_owned()); + assert!(!etag1.strong_eq(&etag2)); + assert!(!etag1.weak_eq(&etag2)); + assert!(etag1.strong_ne(&etag2)); + assert!(etag1.weak_ne(&etag2)); + + etag1 = EntityTag::weak("1".to_owned()); + etag2 = EntityTag::strong("1".to_owned()); + assert!(!etag1.strong_eq(&etag2)); + assert!(etag1.weak_eq(&etag2)); + assert!(etag1.strong_ne(&etag2)); + assert!(!etag1.weak_ne(&etag2)); + + etag1 = EntityTag::strong("1".to_owned()); + etag2 = EntityTag::strong("1".to_owned()); + assert!(etag1.strong_eq(&etag2)); + assert!(etag1.weak_eq(&etag2)); + assert!(!etag1.strong_ne(&etag2)); + assert!(!etag1.weak_ne(&etag2)); + } +} diff --git a/src/header/httpdate.rs b/src/header/shared/httpdate.rs similarity index 99% rename from src/header/httpdate.rs rename to src/header/shared/httpdate.rs index f5ac084b4..b2fcf5270 100644 --- a/src/header/httpdate.rs +++ b/src/header/shared/httpdate.rs @@ -8,7 +8,7 @@ use bytes::{BytesMut, BufMut}; use http::header::{HeaderValue, InvalidHeaderValueBytes}; use error::ParseError; -use super::IntoHeaderValue; +use header::IntoHeaderValue; /// A timestamp with HTTP formatting and parsing diff --git a/src/header/shared/mod.rs b/src/header/shared/mod.rs new file mode 100644 index 000000000..04ff7f41a --- /dev/null +++ b/src/header/shared/mod.rs @@ -0,0 +1,14 @@ +//! Copied for `hyper::header::shared`; + +pub use self::charset::Charset; +pub use self::encoding::Encoding; +pub use self::entity::EntityTag; +pub use self::httpdate::HttpDate; +pub use language_tags::LanguageTag; +pub use self::quality_item::{Quality, QualityItem, qitem, q}; + +mod charset; +mod entity; +mod encoding; +mod httpdate; +mod quality_item; diff --git a/src/header/shared/quality_item.rs b/src/header/shared/quality_item.rs new file mode 100644 index 000000000..efece72c8 --- /dev/null +++ b/src/header/shared/quality_item.rs @@ -0,0 +1,265 @@ +#![allow(unused)] +use std::ascii::AsciiExt; +use std::cmp; +use std::default::Default; +use std::fmt; +use std::str; + +use self::internal::IntoQuality; + +/// Represents a quality used in quality values. +/// +/// Can be created with the `q` function. +/// +/// # Implementation notes +/// +/// The quality value is defined as a number between 0 and 1 with three decimal places. This means +/// there are 1001 possible values. Since floating point numbers are not exact and the smallest +/// floating point data type (`f32`) consumes four bytes, hyper uses an `u16` value to store the +/// quality internally. For performance reasons you may set quality directly to a value between +/// 0 and 1000 e.g. `Quality(532)` matches the quality `q=0.532`. +/// +/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1) +/// gives more information on quality values in HTTP header fields. +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Quality(u16); + +impl Default for Quality { + fn default() -> Quality { + Quality(1000) + } +} + +/// Represents an item with a quality value as defined in +/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.1). +#[derive(Clone, PartialEq, Debug)] +pub struct QualityItem { + /// The actual contents of the field. + pub item: T, + /// The quality (client or server preference) for the value. + pub quality: Quality, +} + +impl QualityItem { + /// Creates a new `QualityItem` from an item and a quality. + /// The item can be of any type. + /// The quality should be a value in the range [0, 1]. + pub fn new(item: T, quality: Quality) -> QualityItem { + QualityItem { item, quality } + } +} + +impl cmp::PartialOrd for QualityItem { + fn partial_cmp(&self, other: &QualityItem) -> Option { + self.quality.partial_cmp(&other.quality) + } +} + +impl fmt::Display for QualityItem { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + try!(fmt::Display::fmt(&self.item, f)); + match self.quality.0 { + 1000 => Ok(()), + 0 => f.write_str("; q=0"), + x => write!(f, "; q=0.{}", format!("{:03}", x).trim_right_matches('0')) + } + } +} + +impl str::FromStr for QualityItem { + type Err = ::error::ParseError; + + fn from_str(s: &str) -> Result, ::error::ParseError> { + if !s.is_ascii() { + return Err(::error::ParseError::Header); + } + // Set defaults used if parsing fails. + let mut raw_item = s; + let mut quality = 1f32; + + let parts: Vec<&str> = s.rsplitn(2, ';').map(|x| x.trim()).collect(); + if parts.len() == 2 { + if parts[0].len() < 2 { + return Err(::error::ParseError::Header); + } + let start = &parts[0][0..2]; + if start == "q=" || start == "Q=" { + let q_part = &parts[0][2..parts[0].len()]; + if q_part.len() > 5 { + return Err(::error::ParseError::Header); + } + match q_part.parse::() { + Ok(q_value) => { + if 0f32 <= q_value && q_value <= 1f32 { + quality = q_value; + raw_item = parts[1]; + } else { + return Err(::error::ParseError::Header); + } + }, + Err(_) => return Err(::error::ParseError::Header), + } + } + } + match raw_item.parse::() { + // we already checked above that the quality is within range + Ok(item) => Ok(QualityItem::new(item, from_f32(quality))), + Err(_) => Err(::error::ParseError::Header), + } + } +} + +#[inline] +fn from_f32(f: f32) -> Quality { + // this function is only used internally. A check that `f` is within range + // should be done before calling this method. Just in case, this + // debug_assert should catch if we were forgetful + debug_assert!(f >= 0f32 && f <= 1f32, "q value must be between 0.0 and 1.0"); + Quality((f * 1000f32) as u16) +} + +/// Convenience function to wrap a value in a `QualityItem` +/// Sets `q` to the default 1.0 +pub fn qitem(item: T) -> QualityItem { + QualityItem::new(item, Default::default()) +} + +/// Convenience function to create a `Quality` from a float or integer. +/// +/// Implemented for `u16` and `f32`. Panics if value is out of range. +pub fn q(val: T) -> Quality { + val.into_quality() +} + +mod internal { + use super::Quality; + + // TryFrom is probably better, but it's not stable. For now, we want to + // keep the functionality of the `q` function, while allowing it to be + // generic over `f32` and `u16`. + // + // `q` would panic before, so keep that behavior. `TryFrom` can be + // introduced later for a non-panicking conversion. + + pub trait IntoQuality: Sealed + Sized { + fn into_quality(self) -> Quality; + } + + impl IntoQuality for f32 { + fn into_quality(self) -> Quality { + assert!(self >= 0f32 && self <= 1f32, "float must be between 0.0 and 1.0"); + super::from_f32(self) + } + } + + impl IntoQuality for u16 { + fn into_quality(self) -> Quality { + assert!(self <= 1000, "u16 must be between 0 and 1000"); + Quality(self) + } + } + + + pub trait Sealed {} + impl Sealed for u16 {} + impl Sealed for f32 {} +} + +#[cfg(test)] +mod tests { + use super::*; + use super::super::encoding::*; + + #[test] + fn test_quality_item_fmt_q_1() { + let x = qitem(Chunked); + assert_eq!(format!("{}", x), "chunked"); + } + #[test] + fn test_quality_item_fmt_q_0001() { + let x = QualityItem::new(Chunked, Quality(1)); + assert_eq!(format!("{}", x), "chunked; q=0.001"); + } + #[test] + fn test_quality_item_fmt_q_05() { + // Custom value + let x = QualityItem{ + item: EncodingExt("identity".to_owned()), + quality: Quality(500), + }; + assert_eq!(format!("{}", x), "identity; q=0.5"); + } + + #[test] + fn test_quality_item_fmt_q_0() { + // Custom value + let x = QualityItem{ + item: EncodingExt("identity".to_owned()), + quality: Quality(0), + }; + assert_eq!(x.to_string(), "identity; q=0"); + } + + #[test] + fn test_quality_item_from_str1() { + let x: Result, _> = "chunked".parse(); + assert_eq!(x.unwrap(), QualityItem{ item: Chunked, quality: Quality(1000), }); + } + #[test] + fn test_quality_item_from_str2() { + let x: Result, _> = "chunked; q=1".parse(); + assert_eq!(x.unwrap(), QualityItem{ item: Chunked, quality: Quality(1000), }); + } + #[test] + fn test_quality_item_from_str3() { + let x: Result, _> = "gzip; q=0.5".parse(); + assert_eq!(x.unwrap(), QualityItem{ item: Gzip, quality: Quality(500), }); + } + #[test] + fn test_quality_item_from_str4() { + let x: Result, _> = "gzip; q=0.273".parse(); + assert_eq!(x.unwrap(), QualityItem{ item: Gzip, quality: Quality(273), }); + } + #[test] + fn test_quality_item_from_str5() { + let x: Result, _> = "gzip; q=0.2739999".parse(); + assert!(x.is_err()); + } + #[test] + fn test_quality_item_from_str6() { + let x: Result, _> = "gzip; q=2".parse(); + assert!(x.is_err()); + } + #[test] + fn test_quality_item_ordering() { + let x: QualityItem = "gzip; q=0.5".parse().ok().unwrap(); + let y: QualityItem = "gzip; q=0.273".parse().ok().unwrap(); + let comparision_result: bool = x.gt(&y); + assert!(comparision_result) + } + + #[test] + fn test_quality() { + assert_eq!(q(0.5), Quality(500)); + } + + #[test] + #[should_panic] // FIXME - 32-bit msvc unwinding broken + #[cfg_attr(all(target_arch="x86", target_env="msvc"), ignore)] + fn test_quality_invalid() { + q(-1.0); + } + + #[test] + #[should_panic] // FIXME - 32-bit msvc unwinding broken + #[cfg_attr(all(target_arch="x86", target_env="msvc"), ignore)] + fn test_quality_invalid2() { + q(2.0); + } + + #[test] + fn test_fuzzing_bugs() { + assert!("99999;".parse::>().is_err()); + assert!("\x0d;;;=\u{d6aa}==".parse::>().is_err()) + } +} diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 89959e535..577074942 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -26,7 +26,7 @@ pub trait HttpMessage { #[doc(hidden)] /// Get a header - fn get(&self) -> Result where Self: Sized { + fn get_header(&self) -> Result where Self: Sized { H::parse(self) } diff --git a/src/httpresponse.rs b/src/httpresponse.rs index 724f1f1ae..154734c3d 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -230,6 +230,15 @@ pub struct HttpResponseBuilder { } impl HttpResponseBuilder { + /// Set HTTP status code of this response. + #[inline] + pub fn status(&mut self, status: StatusCode) -> &mut Self { + if let Some(parts) = parts(&mut self.response, &self.err) { + parts.status = status; + } + self + } + /// Set HTTP version of this response. /// /// By default response's http version depends on request's version. @@ -761,7 +770,7 @@ mod tests { use std::str::FromStr; use time::Duration; use http::{Method, Uri}; - use http::header::{COOKIE, CONTENT_TYPE}; + use http::header::{COOKIE, CONTENT_TYPE, HeaderValue}; use body::Binary; use {header, httpcodes}; @@ -776,7 +785,7 @@ mod tests { fn test_response_cookies() { let mut headers = HeaderMap::new(); headers.insert(COOKIE, - header::HeaderValue::from_static("cookie1=value1; cookie2=value2")); + HeaderValue::from_static("cookie1=value1; cookie2=value2")); let req = HttpRequest::new( Method::GET, Uri::from_str("/").unwrap(), Version::HTTP_11, headers, None); @@ -850,7 +859,7 @@ mod tests { let resp = HttpResponse::build(StatusCode::OK) .json(vec!["v1", "v2", "v3"]).unwrap(); let ct = resp.headers().get(CONTENT_TYPE).unwrap(); - assert_eq!(ct, header::HeaderValue::from_static("application/json")); + assert_eq!(ct, HeaderValue::from_static("application/json")); assert_eq!(*resp.body(), Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]"))); } @@ -860,7 +869,7 @@ mod tests { .header(CONTENT_TYPE, "text/json") .json(vec!["v1", "v2", "v3"]).unwrap(); let ct = resp.headers().get(CONTENT_TYPE).unwrap(); - assert_eq!(ct, header::HeaderValue::from_static("text/json")); + assert_eq!(ct, HeaderValue::from_static("text/json")); assert_eq!(*resp.body(), Body::from(Bytes::from_static(b"[\"v1\",\"v2\",\"v3\"]"))); } @@ -880,56 +889,56 @@ mod tests { let resp: HttpResponse = "test".into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), - header::HeaderValue::from_static("text/plain; charset=utf-8")); + HeaderValue::from_static("text/plain; charset=utf-8")); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().binary().unwrap(), &Binary::from("test")); let resp: HttpResponse = "test".respond_to(req.clone()).ok().unwrap(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), - header::HeaderValue::from_static("text/plain; charset=utf-8")); + HeaderValue::from_static("text/plain; charset=utf-8")); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().binary().unwrap(), &Binary::from("test")); let resp: HttpResponse = b"test".as_ref().into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), - header::HeaderValue::from_static("application/octet-stream")); + HeaderValue::from_static("application/octet-stream")); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().binary().unwrap(), &Binary::from(b"test".as_ref())); let resp: HttpResponse = b"test".as_ref().respond_to(req.clone()).ok().unwrap(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), - header::HeaderValue::from_static("application/octet-stream")); + HeaderValue::from_static("application/octet-stream")); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().binary().unwrap(), &Binary::from(b"test".as_ref())); let resp: HttpResponse = "test".to_owned().into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), - header::HeaderValue::from_static("text/plain; charset=utf-8")); + HeaderValue::from_static("text/plain; charset=utf-8")); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().binary().unwrap(), &Binary::from("test".to_owned())); let resp: HttpResponse = "test".to_owned().respond_to(req.clone()).ok().unwrap(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), - header::HeaderValue::from_static("text/plain; charset=utf-8")); + HeaderValue::from_static("text/plain; charset=utf-8")); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().binary().unwrap(), &Binary::from("test".to_owned())); let resp: HttpResponse = (&"test".to_owned()).into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), - header::HeaderValue::from_static("text/plain; charset=utf-8")); + HeaderValue::from_static("text/plain; charset=utf-8")); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().binary().unwrap(), &Binary::from(&"test".to_owned())); let resp: HttpResponse = (&"test".to_owned()).respond_to(req.clone()).ok().unwrap(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), - header::HeaderValue::from_static("text/plain; charset=utf-8")); + HeaderValue::from_static("text/plain; charset=utf-8")); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().binary().unwrap(), &Binary::from(&"test".to_owned())); @@ -937,7 +946,7 @@ mod tests { let resp: HttpResponse = b.into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), - header::HeaderValue::from_static("application/octet-stream")); + HeaderValue::from_static("application/octet-stream")); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().binary().unwrap(), &Binary::from(Bytes::from_static(b"test"))); @@ -945,7 +954,7 @@ mod tests { let resp: HttpResponse = b.respond_to(req.clone()).ok().unwrap(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), - header::HeaderValue::from_static("application/octet-stream")); + HeaderValue::from_static("application/octet-stream")); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().binary().unwrap(), &Binary::from(Bytes::from_static(b"test"))); @@ -953,7 +962,7 @@ mod tests { let resp: HttpResponse = b.into(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), - header::HeaderValue::from_static("application/octet-stream")); + HeaderValue::from_static("application/octet-stream")); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().binary().unwrap(), &Binary::from(BytesMut::from("test"))); @@ -961,7 +970,7 @@ mod tests { let resp: HttpResponse = b.respond_to(req.clone()).ok().unwrap(); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), - header::HeaderValue::from_static("application/octet-stream")); + HeaderValue::from_static("application/octet-stream")); assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.body().binary().unwrap(), &Binary::from(BytesMut::from("test"))); } diff --git a/src/lib.rs b/src/lib.rs index 076014039..378f45a5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,6 +71,7 @@ extern crate httparse; extern crate http_range; extern crate mime; extern crate mime_guess; +extern crate language_tags; extern crate rand; extern crate url; extern crate libc; diff --git a/src/server/encoding.rs b/src/server/encoding.rs index 84e48bc88..45dfff141 100644 --- a/src/server/encoding.rs +++ b/src/server/encoding.rs @@ -16,7 +16,6 @@ use bytes::{Bytes, BytesMut, BufMut}; use header::ContentEncoding; use body::{Body, Binary}; use error::PayloadError; -use helpers::convert_usize; use httprequest::HttpInnerMessage; use httpresponse::HttpResponse; use payload::{PayloadSender, PayloadWriter, PayloadStatus}; @@ -422,7 +421,7 @@ impl ContentEncoder { } if req.method == Method::HEAD { let mut b = BytesMut::new(); - convert_usize(bytes.len(), &mut b); + let _ = write!(b, "{}", bytes.len()); resp.headers_mut().insert( CONTENT_LENGTH, HeaderValue::try_from(b.freeze()).unwrap()); } else { diff --git a/src/test.rs b/src/test.rs index ee6c84777..7f400f947 100644 --- a/src/test.rs +++ b/src/test.rs @@ -8,7 +8,7 @@ use std::str::FromStr; use actix::{Arbiter, Addr, Syn, System, SystemRunner, msgs}; use cookie::Cookie; use http::{Uri, Method, Version, HeaderMap, HttpTryFrom}; -use http::header::{HeaderName, HeaderValue}; +use http::header::HeaderName; use futures::Future; use tokio_core::net::TcpListener; use tokio_core::reactor::Core; @@ -17,7 +17,7 @@ use net2::TcpBuilder; use ws; use body::Binary; use error::Error; -use header::Header; +use header::{Header, IntoHeaderValue}; use handler::{Handler, Responder, ReplyItem}; use middleware::Middleware; use application::{Application, HttpApplication}; @@ -341,8 +341,7 @@ impl TestRequest<()> { /// Create TestRequest and set header pub fn with_header(key: K, value: V) -> TestRequest<()> - where HeaderName: HttpTryFrom, - HeaderValue: HttpTryFrom + where HeaderName: HttpTryFrom, V: IntoHeaderValue, { TestRequest::default().header(key, value) } @@ -394,11 +393,10 @@ impl TestRequest { /// Set a header pub fn header(mut self, key: K, value: V) -> Self - where HeaderName: HttpTryFrom, - HeaderValue: HttpTryFrom + where HeaderName: HttpTryFrom, V: IntoHeaderValue { if let Ok(key) = HeaderName::try_from(key) { - if let Ok(value) = HeaderValue::try_from(value) { + if let Ok(value) = value.try_into() { self.headers.append(key, value); return self }