From 41f1e6cdc97632bf9daafb950a396d15df40a747 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Sat, 14 Oct 2017 22:52:38 -0700 Subject: [PATCH] split http request; add HttpRequest::range() --- Cargo.toml | 3 + src/application.rs | 2 +- src/dev.rs | 16 +++- src/error.rs | 10 +++ src/httpcodes.rs | 4 +- src/httpmessage.rs | 168 ++--------------------------------- src/httprequest.rs | 178 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 16 +++- src/reader.rs | 2 +- src/resource.rs | 8 +- src/route.rs | 3 +- src/router.rs | 2 +- src/staticfiles.rs | 23 +++++ src/task.rs | 3 +- src/ws.rs | 3 +- tests/test_httpmessage.rs | 22 +++++ 16 files changed, 285 insertions(+), 178 deletions(-) create mode 100644 src/httprequest.rs create mode 100644 src/staticfiles.rs diff --git a/Cargo.toml b/Cargo.toml index f940803da..92cdf1801 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,10 +30,13 @@ nightly = [] time = "0.1" http = "0.1" httparse = "0.1" +http-range = "0.1" cookie = { version="0.10", features=["percent-encode"] } +regex = "0.2" slab = "0.4" sha1 = "0.2" url = "1.5" +lazy_static = "0.2" route-recognizer = "0.1" # tokio diff --git a/src/application.rs b/src/application.rs index 8df6c1482..130a548e0 100644 --- a/src/application.rs +++ b/src/application.rs @@ -9,7 +9,7 @@ use route::RouteHandler; use router::Handler; use resource::Resource; use payload::Payload; -use httpmessage::HttpRequest; +use httprequest::HttpRequest; /// Application diff --git a/src/dev.rs b/src/dev.rs index 32942fe74..c9fd7d215 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -9,13 +9,23 @@ //! ``` pub use ws; pub use httpcodes; +pub use error::ParseError; pub use application::Application; -pub use httpmessage::{Body, Builder, HttpRequest, HttpResponse}; -pub use payload::{Payload, PayloadItem}; +pub use httprequest::HttpRequest; +pub use httpmessage::{Body, Builder, HttpResponse}; +pub use payload::{Payload, PayloadItem, PayloadError}; pub use router::RoutingMap; pub use resource::{Reply, Resource}; pub use route::{Route, RouteFactory, RouteHandler}; pub use server::HttpServer; pub use context::HttpContext; -pub use task::Task; +pub use staticfiles::StaticFiles; + +// re-exports +pub use cookie::{Cookie, CookieBuilder}; +pub use cookie::{ParseError as CookieParseError}; pub use route_recognizer::Params; +pub use http_range::{HttpRange, HttpRangeParseError}; + +// dev specific +pub use task::Task; diff --git a/src/error.rs b/src/error.rs index 145bd34d3..e0625debb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,8 +9,10 @@ use cookie; use httparse; use http::{StatusCode, Error as HttpError}; +use HttpRangeParseError; use httpmessage::{Body, HttpResponse}; + /// A set of errors that can occur during parsing HTTP streams. #[derive(Debug)] pub enum ParseError { @@ -129,6 +131,14 @@ impl From for HttpResponse { } } +/// Return `BadRequest` for `HttpRangeParseError` +impl From for HttpResponse { + fn from(_: HttpRangeParseError) -> Self { + HttpResponse::new(StatusCode::BAD_REQUEST, + Body::Binary("Invalid Range header provided".into())) + } +} + #[cfg(test)] mod tests { use std::error::Error as StdError; diff --git a/src/httpcodes.rs b/src/httpcodes.rs index 77d149176..b6c4ee78f 100644 --- a/src/httpcodes.rs +++ b/src/httpcodes.rs @@ -6,13 +6,15 @@ use http::StatusCode; use task::Task; use route::RouteHandler; use payload::Payload; -use httpmessage::{Body, Builder, HttpRequest, HttpResponse}; +use httprequest::HttpRequest; +use httpmessage::{Body, Builder, HttpResponse}; pub const HTTPOk: StaticResponse = StaticResponse(StatusCode::OK); pub const HTTPCreated: StaticResponse = StaticResponse(StatusCode::CREATED); pub const HTTPNoContent: StaticResponse = StaticResponse(StatusCode::NO_CONTENT); pub const HTTPBadRequest: StaticResponse = StaticResponse(StatusCode::BAD_REQUEST); pub const HTTPNotFound: StaticResponse = StaticResponse(StatusCode::NOT_FOUND); +pub const HTTPForbidden: StaticResponse = StaticResponse(StatusCode::FORBIDDEN); pub const HTTPMethodNotAllowed: StaticResponse = StaticResponse(StatusCode::METHOD_NOT_ALLOWED); pub const HTTPInternalServerError: StaticResponse = StaticResponse(StatusCode::INTERNAL_SERVER_ERROR); diff --git a/src/httpmessage.rs b/src/httpmessage.rs index 2e0d426c9..07d93c258 100644 --- a/src/httpmessage.rs +++ b/src/httpmessage.rs @@ -4,177 +4,23 @@ use std::convert::Into; use cookie::CookieJar; use bytes::Bytes; -use http::{Method, StatusCode, Version, Uri, HeaderMap, HttpTryFrom, Error}; +use http::{StatusCode, Version, HeaderMap, HttpTryFrom, Error}; use http::header::{self, HeaderName, HeaderValue}; -use Params; -use {Cookie, CookieParseError}; +use Cookie; + +/// Represents various types of connection #[derive(Copy, Clone, PartialEq, Debug)] pub enum ConnectionType { + /// Close connection after response Close, + /// Keep connection alive after response KeepAlive, + /// Connection is upgraded to different type Upgrade, } -#[derive(Debug)] -/// An HTTP Request -pub struct HttpRequest { - version: Version, - method: Method, - uri: Uri, - headers: HeaderMap, - params: Params, - cookies: Vec>, -} - -impl HttpRequest { - /// Construct a new Request. - #[inline] - pub fn new(method: Method, uri: Uri, version: Version, headers: HeaderMap) -> Self { - HttpRequest { - method: method, - uri: uri, - version: version, - headers: headers, - params: Params::new(), - cookies: Vec::new(), - } - } - - /// Read the Request Uri. - #[inline] - pub fn uri(&self) -> &Uri { &self.uri } - - /// Read the Request method. - #[inline] - pub fn method(&self) -> &Method { &self.method } - - /// Read the Request Version. - pub fn version(&self) -> Version { - self.version - } - - /// Read the Request Headers. - pub fn headers(&self) -> &HeaderMap { - &self.headers - } - - // /// The remote socket address of this request - // /// - // /// This is an `Option`, because some underlying transports may not have - // /// a socket address, such as Unix Sockets. - // /// - // /// This field is not used for outgoing requests. - // #[inline] - // pub fn remote_addr(&self) -> Option { self.remote_addr } - - /// The target path of this Request. - #[inline] - pub fn path(&self) -> &str { - self.uri.path() - } - - /// The query string of this Request. - #[inline] - pub fn query(&self) -> Option<&str> { - self.uri.query() - } - - /// Return request cookies. - pub fn cookies(&self) -> &Vec> { - &self.cookies - } - - /// Return request cookie. - pub fn cookie(&self, name: &str) -> Option<&Cookie> { - for cookie in &self.cookies { - if cookie.name() == name { - return Some(&cookie) - } - } - None - } - - /// Load cookies - pub fn load_cookies(&mut self) -> Result<&Vec, CookieParseError> - { - if let Some(val) = self.headers.get(header::COOKIE) { - let s = str::from_utf8(val.as_bytes()) - .map_err(CookieParseError::from)?; - for cookie in s.split("; ") { - self.cookies.push(Cookie::parse_encoded(cookie)?.into_owned()); - } - } - Ok(&self.cookies) - } - - /// Get a mutable reference to the Request headers. - #[inline] - pub fn headers_mut(&mut self) -> &mut HeaderMap { - &mut self.headers - } - - /// Get a reference to the Params object. - /// Params is a container for url parameters. - /// Route supports glob patterns: * for a single wildcard segment and :param - /// for matching storing that segment of the request url in the Params object. - #[inline] - pub fn params(&self) -> &Params { &self.params } - - /// Create new request with Params object. - pub fn with_params(self, params: Params) -> Self { - HttpRequest { - method: self.method, - uri: self.uri, - version: self.version, - headers: self.headers, - params: params, - cookies: self.cookies, - } - } - - /// Checks if a connection should be kept alive. - pub fn keep_alive(&self) -> bool { - if let Some(conn) = self.headers.get(header::CONNECTION) { - if let Ok(conn) = conn.to_str() { - if self.version == Version::HTTP_10 && conn.contains("keep-alive") { - true - } else { - self.version == Version::HTTP_11 && - !(conn.contains("close") || conn.contains("upgrade")) - } - } else { - false - } - } else { - self.version != Version::HTTP_10 - } - } - - pub(crate) fn upgrade(&self) -> bool { - if let Some(conn) = self.headers().get(header::CONNECTION) { - if let Ok(s) = conn.to_str() { - return s.to_lowercase().contains("upgrade") - } - } - self.method == Method::CONNECT - } - - pub fn chunked(&self) -> Result { - if let Some(encodings) = self.headers().get(header::TRANSFER_ENCODING) { - if let Ok(s) = encodings.to_str() { - Ok(s.to_lowercase().contains("chunked")) - } else { - Err(io::Error::new( - io::ErrorKind::Other, "Can not read transfer-encoding header")) - } - } else { - Ok(false) - } - } -} - /// Represents various types of http message body. #[derive(Debug)] pub enum Body { diff --git a/src/httprequest.rs b/src/httprequest.rs new file mode 100644 index 000000000..5e8fe9bf4 --- /dev/null +++ b/src/httprequest.rs @@ -0,0 +1,178 @@ +//! Pieces pertaining to the HTTP message protocol. +use std::{io, str}; +use http::{header, Method, Version, Uri, HeaderMap}; + +use Params; +use {Cookie, CookieParseError}; +use {HttpRange, HttpRangeParseError}; + + +#[derive(Debug)] +/// An HTTP Request +pub struct HttpRequest { + version: Version, + method: Method, + uri: Uri, + headers: HeaderMap, + params: Params, + cookies: Vec>, +} + +impl HttpRequest { + /// Construct a new Request. + #[inline] + pub fn new(method: Method, uri: Uri, version: Version, headers: HeaderMap) -> Self { + HttpRequest { + method: method, + uri: uri, + version: version, + headers: headers, + params: Params::new(), + cookies: Vec::new(), + } + } + + /// Read the Request Uri. + #[inline] + pub fn uri(&self) -> &Uri { &self.uri } + + /// Read the Request method. + #[inline] + pub fn method(&self) -> &Method { &self.method } + + /// Read the Request Version. + pub fn version(&self) -> Version { + self.version + } + + /// Read the Request Headers. + pub fn headers(&self) -> &HeaderMap { + &self.headers + } + + // /// The remote socket address of this request + // /// + // /// This is an `Option`, because some underlying transports may not have + // /// a socket address, such as Unix Sockets. + // /// + // /// This field is not used for outgoing requests. + // #[inline] + // pub fn remote_addr(&self) -> Option { self.remote_addr } + + /// The target path of this Request. + #[inline] + pub fn path(&self) -> &str { + self.uri.path() + } + + /// The query string of this Request. + #[inline] + pub fn query(&self) -> Option<&str> { + self.uri.query() + } + + /// Return request cookies. + pub fn cookies(&self) -> &Vec> { + &self.cookies + } + + /// Return request cookie. + pub fn cookie(&self, name: &str) -> Option<&Cookie> { + for cookie in &self.cookies { + if cookie.name() == name { + return Some(&cookie) + } + } + None + } + + /// Load cookies + pub fn load_cookies(&mut self) -> Result<&Vec, CookieParseError> + { + if let Some(val) = self.headers.get(header::COOKIE) { + let s = str::from_utf8(val.as_bytes()) + .map_err(CookieParseError::from)?; + for cookie in s.split("; ") { + self.cookies.push(Cookie::parse_encoded(cookie)?.into_owned()); + } + } + Ok(&self.cookies) + } + + /// Get a mutable reference to the Request headers. + #[inline] + pub fn headers_mut(&mut self) -> &mut HeaderMap { + &mut self.headers + } + + /// Get a reference to the Params object. + /// Params is a container for url parameters. + /// Route supports glob patterns: * for a single wildcard segment and :param + /// for matching storing that segment of the request url in the Params object. + #[inline] + pub fn params(&self) -> &Params { &self.params } + + /// Create new request with Params object. + pub fn with_params(self, params: Params) -> Self { + HttpRequest { + method: self.method, + uri: self.uri, + version: self.version, + headers: self.headers, + params: params, + cookies: self.cookies, + } + } + + /// Checks if a connection should be kept alive. + pub fn keep_alive(&self) -> bool { + if let Some(conn) = self.headers.get(header::CONNECTION) { + if let Ok(conn) = conn.to_str() { + if self.version == Version::HTTP_10 && conn.contains("keep-alive") { + true + } else { + self.version == Version::HTTP_11 && + !(conn.contains("close") || conn.contains("upgrade")) + } + } else { + false + } + } else { + self.version != Version::HTTP_10 + } + } + + /// Check if request requires connection upgrade + pub(crate) fn upgrade(&self) -> bool { + if let Some(conn) = self.headers().get(header::CONNECTION) { + if let Ok(s) = conn.to_str() { + return s.to_lowercase().contains("upgrade") + } + } + self.method == Method::CONNECT + } + + /// Check if request has chunked transfer encoding + pub fn chunked(&self) -> Result { + if let Some(encodings) = self.headers().get(header::TRANSFER_ENCODING) { + if let Ok(s) = encodings.to_str() { + Ok(s.to_lowercase().contains("chunked")) + } else { + Err(io::Error::new( + io::ErrorKind::Other, "Can not read transfer-encoding header")) + } + } else { + Ok(false) + } + } + + /// Parses Range HTTP header string as per RFC 2616. + /// `size` is full size of response (file). + pub fn range(&self, size: u64) -> Result, HttpRangeParseError> { + if let Some(range) = self.headers().get(header::RANGE) { + HttpRange::parse(unsafe{str::from_utf8_unchecked(range.as_bytes())}, size) + } else { + Ok(Vec::new()) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index c2f7d69fd..33829b2f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,17 +9,21 @@ extern crate log; extern crate time; extern crate bytes; extern crate sha1; -extern crate url; -extern crate cookie; +extern crate regex; +#[macro_use] +extern crate lazy_static; #[macro_use] extern crate futures; extern crate tokio_core; extern crate tokio_io; extern crate tokio_proto; +extern crate cookie; extern crate http; extern crate httparse; +extern crate http_range; extern crate route_recognizer; +extern crate url; extern crate actix; mod application; @@ -27,6 +31,7 @@ mod context; mod error; mod date; mod decode; +mod httprequest; mod httpmessage; mod payload; mod resource; @@ -34,6 +39,7 @@ mod route; mod router; mod task; mod reader; +mod staticfiles; mod server; mod wsframe; mod wsproto; @@ -41,16 +47,20 @@ mod wsproto; pub mod ws; pub mod dev; pub mod httpcodes; +pub use error::ParseError; pub use application::Application; -pub use httpmessage::{Body, Builder, HttpRequest, HttpResponse}; +pub use httprequest::HttpRequest; +pub use httpmessage::{Body, Builder, HttpResponse}; pub use payload::{Payload, PayloadItem, PayloadError}; pub use router::RoutingMap; pub use resource::{Reply, Resource}; pub use route::{Route, RouteFactory, RouteHandler}; pub use server::HttpServer; pub use context::HttpContext; +pub use staticfiles::StaticFiles; // re-exports pub use cookie::{Cookie, CookieBuilder}; pub use cookie::{ParseError as CookieParseError}; pub use route_recognizer::Params; +pub use http_range::{HttpRange, HttpRangeParseError}; diff --git a/src/reader.rs b/src/reader.rs index 21d657d8e..10be74131 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -9,7 +9,7 @@ use tokio_io::AsyncRead; use error::ParseError; use decode::Decoder; -use httpmessage::HttpRequest; +use httprequest::HttpRequest; use payload::{Payload, PayloadError, PayloadSender}; const MAX_HEADERS: usize = 100; diff --git a/src/resource.rs b/src/resource.rs index 7ecf7c5e8..5a26de9f0 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -10,8 +10,9 @@ use task::Task; use route::{Route, RouteHandler}; use payload::Payload; use context::HttpContext; +use httprequest::HttpRequest; +use httpmessage::HttpResponse; use httpcodes::HTTPMethodNotAllowed; -use httpmessage::{HttpRequest, HttpResponse}; /// Http resource /// @@ -27,9 +28,8 @@ use httpmessage::{HttpRequest, HttpResponse}; /// fn main() { /// let mut routes = RoutingMap::default(); /// -/// routes -/// .add_resource("/") -/// .post::(); +/// routes.add_resource("/") +/// .post::(); /// } pub struct Resource { state: PhantomData, diff --git a/src/route.rs b/src/route.rs index 8e38c554a..bff5aa44a 100644 --- a/src/route.rs +++ b/src/route.rs @@ -8,7 +8,8 @@ use task::Task; use context::HttpContext; use resource::Reply; use payload::Payload; -use httpmessage::{HttpRequest, HttpResponse}; +use httprequest::HttpRequest; +use httpmessage::HttpResponse; #[doc(hidden)] #[derive(Debug)] diff --git a/src/router.rs b/src/router.rs index e86e962f5..98965b92b 100644 --- a/src/router.rs +++ b/src/router.rs @@ -9,7 +9,7 @@ use route::RouteHandler; use resource::Resource; use application::Application; use httpcodes::HTTPNotFound; -use httpmessage::HttpRequest; +use httprequest::HttpRequest; pub(crate) trait Handler: 'static { fn handle(&self, req: HttpRequest, payload: Payload) -> Task; diff --git a/src/staticfiles.rs b/src/staticfiles.rs new file mode 100644 index 000000000..ff60ff8d1 --- /dev/null +++ b/src/staticfiles.rs @@ -0,0 +1,23 @@ +#![allow(dead_code, unused_variables)] +use std::rc::Rc; + +use task::Task; +use route::RouteHandler; +use payload::Payload; +use httpcodes::HTTPOk; +use httprequest::HttpRequest; + + +pub struct StaticFiles { + directory: String, + show_index: bool, + chunk_size: usize, + follow_synlinks: bool, +} + +impl RouteHandler for StaticFiles { + + fn handle(&self, req: HttpRequest, payload: Payload, state: Rc) -> Task { + Task::reply(HTTPOk) + } +} diff --git a/src/task.rs b/src/task.rs index d22914284..189b803c9 100644 --- a/src/task.rs +++ b/src/task.rs @@ -12,7 +12,8 @@ use tokio_core::net::TcpStream; use date; use route::Frame; -use httpmessage::{Body, HttpRequest, HttpResponse}; +use httprequest::HttpRequest; +use httpmessage::{Body, HttpResponse}; type FrameStream = Stream; const AVERAGE_HEADER_SIZE: usize = 30; // totally scientific diff --git a/src/ws.rs b/src/ws.rs index 58130430d..96c6cead1 100644 --- a/src/ws.rs +++ b/src/ws.rs @@ -74,7 +74,8 @@ use context::HttpContext; use route::Route; use payload::Payload; use httpcodes::{HTTPBadRequest, HTTPMethodNotAllowed}; -use httpmessage::{Body, ConnectionType, HttpRequest, HttpResponse}; +use httprequest::HttpRequest; +use httpmessage::{Body, ConnectionType, HttpResponse}; use wsframe; use wsproto::*; diff --git a/tests/test_httpmessage.rs b/tests/test_httpmessage.rs index 5f5b923f2..4d4530d26 100644 --- a/tests/test_httpmessage.rs +++ b/tests/test_httpmessage.rs @@ -72,3 +72,25 @@ fn test_response_cookies() { assert_eq!( val[1],"name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400"); } + +#[test] +fn test_no_request_range_header() { + let req = HttpRequest::new(Method::GET, Uri::try_from("/").unwrap(), + Version::HTTP_11, HeaderMap::new()); + let ranges = req.range(100).unwrap(); + assert!(ranges.is_empty()); +} + +#[test] +fn test_request_range_header() { + let mut headers = HeaderMap::new(); + headers.insert(header::RANGE, + header::HeaderValue::from_static("bytes=0-4")); + + let req = HttpRequest::new(Method::GET, Uri::try_from("/").unwrap(), + Version::HTTP_11, headers); + let ranges = req.range(100).unwrap(); + assert_eq!(ranges.len(), 1); + assert_eq!(ranges[0].start, 0); + assert_eq!(ranges[0].length, 5); +}