From 0c30057c8cc2a0d54637c3ddf13918615b2a3b6c Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Mon, 5 Mar 2018 16:45:54 -0800 Subject: [PATCH] move headers to separate module; allow custom HeaderValue conversion --- src/error.rs | 15 +++- src/header.rs | 197 +++++++++++++++++++++++++++++++++++++++++ src/httpresponse.rs | 6 +- src/lib.rs | 19 +--- src/server/encoding.rs | 50 +---------- 5 files changed, 219 insertions(+), 68 deletions(-) create mode 100644 src/header.rs diff --git a/src/error.rs b/src/error.rs index 6abbf7a0f..40ecf7045 100644 --- a/src/error.rs +++ b/src/error.rs @@ -129,8 +129,19 @@ impl ResponseError for io::Error { } } -/// `InternalServerError` for `InvalidHeaderValue` -impl ResponseError for header::InvalidHeaderValue {} +/// `BadRequest` for `InvalidHeaderValue` +impl ResponseError for header::InvalidHeaderValue { + fn error_response(&self) -> HttpResponse { + HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty) + } +} + +/// `BadRequest` for `InvalidHeaderValue` +impl ResponseError for header::InvalidHeaderValueBytes { + fn error_response(&self) -> HttpResponse { + HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty) + } +} /// `InternalServerError` for `futures::Canceled` impl ResponseError for Canceled {} diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 000000000..3715e1b18 --- /dev/null +++ b/src/header.rs @@ -0,0 +1,197 @@ +use std::fmt::{self, Display}; +use std::io::Write; +use std::str::FromStr; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use time; +use bytes::{Bytes, BytesMut, BufMut}; +use http::{Error as HttpError}; +use http::header::{HeaderValue, InvalidHeaderValue, InvalidHeaderValueBytes}; + +pub use httpresponse::ConnectionType; +pub use cookie::{Cookie, CookieBuilder}; +pub use http_range::HttpRange; + +use error::ParseError; + + +pub trait IntoHeaderValue: Sized { + /// The type returned in the event of a conversion error. + type Error: Into; + + /// Cast from PyObject to a concrete Python object type. + fn try_into(self) -> Result; +} + +impl IntoHeaderValue for HeaderValue { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + Ok(self) + } +} + +impl<'a> IntoHeaderValue for &'a str { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + self.parse() + } +} + +impl<'a> IntoHeaderValue for &'a [u8] { + type Error = InvalidHeaderValue; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_bytes(self) + } +} + +impl IntoHeaderValue for Bytes { + type Error = InvalidHeaderValueBytes; + + #[inline] + fn try_into(self) -> Result { + HeaderValue::from_shared(self) + } +} + +/// Represents supported types of content encodings +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum ContentEncoding { + /// Automatically select encoding based on encoding negotiation + Auto, + /// A format using the Brotli algorithm + Br, + /// A format using the zlib structure with deflate algorithm + Deflate, + /// Gzip algorithm + Gzip, + /// Indicates the identity function (i.e. no compression, nor modification) + Identity, +} + +impl ContentEncoding { + + #[inline] + pub fn is_compression(&self) -> bool { + match *self { + ContentEncoding::Identity | ContentEncoding::Auto => false, + _ => true + } + } + + #[inline] + pub fn as_str(&self) -> &'static str { + match *self { + ContentEncoding::Br => "br", + ContentEncoding::Gzip => "gzip", + ContentEncoding::Deflate => "deflate", + ContentEncoding::Identity | ContentEncoding::Auto => "identity", + } + } + /// default quality value + pub fn quality(&self) -> f64 { + match *self { + ContentEncoding::Br => 1.1, + ContentEncoding::Gzip => 1.0, + ContentEncoding::Deflate => 0.9, + ContentEncoding::Identity | ContentEncoding::Auto => 0.1, + } + } +} + +// TODO: remove memory allocation +impl<'a> From<&'a str> for ContentEncoding { + fn from(s: &'a str) -> ContentEncoding { + match s.trim().to_lowercase().as_ref() { + "br" => ContentEncoding::Br, + "gzip" => ContentEncoding::Gzip, + "deflate" => ContentEncoding::Deflate, + "identity" => ContentEncoding::Identity, + _ => ContentEncoding::Auto, + } + } +} + +/// A timestamp with HTTP formatting and parsing +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Date(time::Tm); + +impl FromStr for Date { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + match time::strptime(s, "%a, %d %b %Y %T %Z").or_else(|_| { + time::strptime(s, "%A, %d-%b-%y %T %Z") + }).or_else(|_| { + time::strptime(s, "%c") + }) { + Ok(t) => Ok(Date(t)), + Err(_) => Err(ParseError::Header), + } + } +} + +impl Display for Date { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0.to_utc().rfc822(), f) + } +} + +impl From for Date { + fn from(sys: SystemTime) -> Date { + let tmspec = match sys.duration_since(UNIX_EPOCH) { + Ok(dur) => { + time::Timespec::new(dur.as_secs() as i64, dur.subsec_nanos() as i32) + }, + Err(err) => { + let neg = err.duration(); + time::Timespec::new(-(neg.as_secs() as i64), -(neg.subsec_nanos() as i32)) + }, + }; + Date(time::at_utc(tmspec)) + } +} + +impl IntoHeaderValue for Date { + type Error = InvalidHeaderValueBytes; + + fn try_into(self) -> Result { + let mut wrt = BytesMut::with_capacity(29).writer(); + write!(wrt, "{}", self.0.rfc822()).unwrap(); + HeaderValue::from_shared(wrt.get_mut().take().freeze()) + } +} + +impl From for SystemTime { + fn from(date: Date) -> SystemTime { + let spec = date.0.to_timespec(); + if spec.sec >= 0 { + UNIX_EPOCH + Duration::new(spec.sec as u64, spec.nsec as u32) + } else { + UNIX_EPOCH - Duration::new(spec.sec as u64, spec.nsec as u32) + } + } +} + +#[cfg(test)] +mod tests { + use time::Tm; + use super::Date; + + const NOV_07: HttpDate = 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}); + + #[test] + fn test_date() { + assert_eq!("Sun, 07 Nov 1994 08:48:37 GMT".parse::().unwrap(), NOV_07); + assert_eq!("Sunday, 07-Nov-94 08:48:37 GMT".parse::().unwrap(), NOV_07); + assert_eq!("Sun Nov 7 08:48:37 1994".parse::().unwrap(), NOV_07); + assert!("this-is-no-date".parse::().is_err()); + } +} diff --git a/src/httpresponse.rs b/src/httpresponse.rs index 9af932b12..fdb142040 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -14,7 +14,7 @@ use serde::Serialize; use body::Body; use error::Error; use handler::Responder; -use headers::ContentEncoding; +use header::{IntoHeaderValue, ContentEncoding}; use httprequest::HttpRequest; /// Represents various types of connection @@ -261,12 +261,12 @@ impl HttpResponseBuilder { /// ``` pub fn header(&mut self, key: K, value: V) -> &mut Self where HeaderName: HttpTryFrom, - HeaderValue: HttpTryFrom + V: IntoHeaderValue, { if let Some(parts) = parts(&mut self.response, &self.err) { match HeaderName::try_from(key) { Ok(key) => { - match HeaderValue::try_from(value) { + match value.try_into() { Ok(value) => { parts.headers.append(key, value); } Err(e) => self.err = Some(e.into()), } diff --git a/src/lib.rs b/src/lib.rs index f3decb149..076014039 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,6 +120,7 @@ pub mod client; pub mod fs; pub mod ws; pub mod error; +pub mod header; pub mod httpcodes; pub mod multipart; pub mod middleware; @@ -153,28 +154,14 @@ pub(crate) const HAS_OPENSSL: bool = false; // pub(crate) const HAS_TLS: bool = false; +#[deprecated(since="0.4.4", note="please use `actix::header` module")] pub mod headers { //! Headers implementation pub use httpresponse::ConnectionType; - pub use cookie::{Cookie, CookieBuilder}; pub use http_range::HttpRange; - - /// Represents supported types of content encodings - #[derive(Copy, Clone, PartialEq, Debug)] - pub enum ContentEncoding { - /// Automatically select encoding based on encoding negotiation - Auto, - /// A format using the Brotli algorithm - Br, - /// A format using the zlib structure with deflate algorithm - Deflate, - /// Gzip algorithm - Gzip, - /// Indicates the identity function (i.e. no compression, nor modification) - Identity, - } + pub use header::ContentEncoding; } pub mod dev { diff --git a/src/server/encoding.rs b/src/server/encoding.rs index 23f4aef7f..84e48bc88 100644 --- a/src/server/encoding.rs +++ b/src/server/encoding.rs @@ -13,60 +13,16 @@ use flate2::write::{GzEncoder, DeflateDecoder, DeflateEncoder}; use brotli2::write::{BrotliDecoder, BrotliEncoder}; use bytes::{Bytes, BytesMut, BufMut}; -use headers::ContentEncoding; +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}; use super::shared::SharedBytes; - -impl ContentEncoding { - - #[inline] - pub fn is_compression(&self) -> bool { - match *self { - ContentEncoding::Identity | ContentEncoding::Auto => false, - _ => true - } - } - - #[inline] - pub fn as_str(&self) -> &'static str { - match *self { - ContentEncoding::Br => "br", - ContentEncoding::Gzip => "gzip", - ContentEncoding::Deflate => "deflate", - ContentEncoding::Identity | ContentEncoding::Auto => "identity", - } - } - /// default quality value - fn quality(&self) -> f64 { - match *self { - ContentEncoding::Br => 1.1, - ContentEncoding::Gzip => 1.0, - ContentEncoding::Deflate => 0.9, - ContentEncoding::Identity | ContentEncoding::Auto => 0.1, - } - } -} - -// TODO: remove memory allocation -impl<'a> From<&'a str> for ContentEncoding { - fn from(s: &'a str) -> ContentEncoding { - match s.trim().to_lowercase().as_ref() { - "br" => ContentEncoding::Br, - "gzip" => ContentEncoding::Gzip, - "deflate" => ContentEncoding::Deflate, - "identity" => ContentEncoding::Identity, - _ => ContentEncoding::Auto, - } - } -} - - pub(crate) enum PayloadType { Sender(PayloadSender), Encoding(Box), @@ -466,7 +422,7 @@ impl ContentEncoder { } if req.method == Method::HEAD { let mut b = BytesMut::new(); - let _ = write!(b, "{}", bytes.len()); + convert_usize(bytes.len(), &mut b); resp.headers_mut().insert( CONTENT_LENGTH, HeaderValue::try_from(b.freeze()).unwrap()); } else {