1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-27 17:22:57 +01:00

split http request; add HttpRequest::range()

This commit is contained in:
Nikolay Kim 2017-10-14 22:52:38 -07:00
parent f30aef404d
commit 41f1e6cdc9
16 changed files with 285 additions and 178 deletions

View File

@ -30,10 +30,13 @@ nightly = []
time = "0.1" time = "0.1"
http = "0.1" http = "0.1"
httparse = "0.1" httparse = "0.1"
http-range = "0.1"
cookie = { version="0.10", features=["percent-encode"] } cookie = { version="0.10", features=["percent-encode"] }
regex = "0.2"
slab = "0.4" slab = "0.4"
sha1 = "0.2" sha1 = "0.2"
url = "1.5" url = "1.5"
lazy_static = "0.2"
route-recognizer = "0.1" route-recognizer = "0.1"
# tokio # tokio

View File

@ -9,7 +9,7 @@ use route::RouteHandler;
use router::Handler; use router::Handler;
use resource::Resource; use resource::Resource;
use payload::Payload; use payload::Payload;
use httpmessage::HttpRequest; use httprequest::HttpRequest;
/// Application /// Application

View File

@ -9,13 +9,23 @@
//! ``` //! ```
pub use ws; pub use ws;
pub use httpcodes; pub use httpcodes;
pub use error::ParseError;
pub use application::Application; pub use application::Application;
pub use httpmessage::{Body, Builder, HttpRequest, HttpResponse}; pub use httprequest::HttpRequest;
pub use payload::{Payload, PayloadItem}; pub use httpmessage::{Body, Builder, HttpResponse};
pub use payload::{Payload, PayloadItem, PayloadError};
pub use router::RoutingMap; pub use router::RoutingMap;
pub use resource::{Reply, Resource}; pub use resource::{Reply, Resource};
pub use route::{Route, RouteFactory, RouteHandler}; pub use route::{Route, RouteFactory, RouteHandler};
pub use server::HttpServer; pub use server::HttpServer;
pub use context::HttpContext; 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 route_recognizer::Params;
pub use http_range::{HttpRange, HttpRangeParseError};
// dev specific
pub use task::Task;

View File

@ -9,8 +9,10 @@ use cookie;
use httparse; use httparse;
use http::{StatusCode, Error as HttpError}; use http::{StatusCode, Error as HttpError};
use HttpRangeParseError;
use httpmessage::{Body, HttpResponse}; use httpmessage::{Body, HttpResponse};
/// A set of errors that can occur during parsing HTTP streams. /// A set of errors that can occur during parsing HTTP streams.
#[derive(Debug)] #[derive(Debug)]
pub enum ParseError { pub enum ParseError {
@ -129,6 +131,14 @@ impl From<cookie::ParseError> for HttpResponse {
} }
} }
/// Return `BadRequest` for `HttpRangeParseError`
impl From<HttpRangeParseError> for HttpResponse {
fn from(_: HttpRangeParseError) -> Self {
HttpResponse::new(StatusCode::BAD_REQUEST,
Body::Binary("Invalid Range header provided".into()))
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::error::Error as StdError; use std::error::Error as StdError;

View File

@ -6,13 +6,15 @@ use http::StatusCode;
use task::Task; use task::Task;
use route::RouteHandler; use route::RouteHandler;
use payload::Payload; 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 HTTPOk: StaticResponse = StaticResponse(StatusCode::OK);
pub const HTTPCreated: StaticResponse = StaticResponse(StatusCode::CREATED); pub const HTTPCreated: StaticResponse = StaticResponse(StatusCode::CREATED);
pub const HTTPNoContent: StaticResponse = StaticResponse(StatusCode::NO_CONTENT); pub const HTTPNoContent: StaticResponse = StaticResponse(StatusCode::NO_CONTENT);
pub const HTTPBadRequest: StaticResponse = StaticResponse(StatusCode::BAD_REQUEST); pub const HTTPBadRequest: StaticResponse = StaticResponse(StatusCode::BAD_REQUEST);
pub const HTTPNotFound: StaticResponse = StaticResponse(StatusCode::NOT_FOUND); 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 HTTPMethodNotAllowed: StaticResponse = StaticResponse(StatusCode::METHOD_NOT_ALLOWED);
pub const HTTPInternalServerError: StaticResponse = pub const HTTPInternalServerError: StaticResponse =
StaticResponse(StatusCode::INTERNAL_SERVER_ERROR); StaticResponse(StatusCode::INTERNAL_SERVER_ERROR);

View File

@ -4,177 +4,23 @@ use std::convert::Into;
use cookie::CookieJar; use cookie::CookieJar;
use bytes::Bytes; 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 http::header::{self, HeaderName, HeaderValue};
use Params; use Cookie;
use {Cookie, CookieParseError};
/// Represents various types of connection
#[derive(Copy, Clone, PartialEq, Debug)] #[derive(Copy, Clone, PartialEq, Debug)]
pub enum ConnectionType { pub enum ConnectionType {
/// Close connection after response
Close, Close,
/// Keep connection alive after response
KeepAlive, KeepAlive,
/// Connection is upgraded to different type
Upgrade, Upgrade,
} }
#[derive(Debug)]
/// An HTTP Request
pub struct HttpRequest {
version: Version,
method: Method,
uri: Uri,
headers: HeaderMap,
params: Params,
cookies: Vec<Cookie<'static>>,
}
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<SocketAddr> { 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<Cookie<'static>> {
&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<Cookie>, 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<bool, io::Error> {
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. /// Represents various types of http message body.
#[derive(Debug)] #[derive(Debug)]
pub enum Body { pub enum Body {

178
src/httprequest.rs Normal file
View File

@ -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<Cookie<'static>>,
}
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<SocketAddr> { 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<Cookie<'static>> {
&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<Cookie>, 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<bool, io::Error> {
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<Vec<HttpRange>, 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())
}
}
}

View File

@ -9,17 +9,21 @@ extern crate log;
extern crate time; extern crate time;
extern crate bytes; extern crate bytes;
extern crate sha1; extern crate sha1;
extern crate url; extern crate regex;
extern crate cookie; #[macro_use]
extern crate lazy_static;
#[macro_use] #[macro_use]
extern crate futures; extern crate futures;
extern crate tokio_core; extern crate tokio_core;
extern crate tokio_io; extern crate tokio_io;
extern crate tokio_proto; extern crate tokio_proto;
extern crate cookie;
extern crate http; extern crate http;
extern crate httparse; extern crate httparse;
extern crate http_range;
extern crate route_recognizer; extern crate route_recognizer;
extern crate url;
extern crate actix; extern crate actix;
mod application; mod application;
@ -27,6 +31,7 @@ mod context;
mod error; mod error;
mod date; mod date;
mod decode; mod decode;
mod httprequest;
mod httpmessage; mod httpmessage;
mod payload; mod payload;
mod resource; mod resource;
@ -34,6 +39,7 @@ mod route;
mod router; mod router;
mod task; mod task;
mod reader; mod reader;
mod staticfiles;
mod server; mod server;
mod wsframe; mod wsframe;
mod wsproto; mod wsproto;
@ -41,16 +47,20 @@ mod wsproto;
pub mod ws; pub mod ws;
pub mod dev; pub mod dev;
pub mod httpcodes; pub mod httpcodes;
pub use error::ParseError;
pub use application::Application; 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 payload::{Payload, PayloadItem, PayloadError};
pub use router::RoutingMap; pub use router::RoutingMap;
pub use resource::{Reply, Resource}; pub use resource::{Reply, Resource};
pub use route::{Route, RouteFactory, RouteHandler}; pub use route::{Route, RouteFactory, RouteHandler};
pub use server::HttpServer; pub use server::HttpServer;
pub use context::HttpContext; pub use context::HttpContext;
pub use staticfiles::StaticFiles;
// re-exports // re-exports
pub use cookie::{Cookie, CookieBuilder}; pub use cookie::{Cookie, CookieBuilder};
pub use cookie::{ParseError as CookieParseError}; pub use cookie::{ParseError as CookieParseError};
pub use route_recognizer::Params; pub use route_recognizer::Params;
pub use http_range::{HttpRange, HttpRangeParseError};

View File

@ -9,7 +9,7 @@ use tokio_io::AsyncRead;
use error::ParseError; use error::ParseError;
use decode::Decoder; use decode::Decoder;
use httpmessage::HttpRequest; use httprequest::HttpRequest;
use payload::{Payload, PayloadError, PayloadSender}; use payload::{Payload, PayloadError, PayloadSender};
const MAX_HEADERS: usize = 100; const MAX_HEADERS: usize = 100;

View File

@ -10,8 +10,9 @@ use task::Task;
use route::{Route, RouteHandler}; use route::{Route, RouteHandler};
use payload::Payload; use payload::Payload;
use context::HttpContext; use context::HttpContext;
use httprequest::HttpRequest;
use httpmessage::HttpResponse;
use httpcodes::HTTPMethodNotAllowed; use httpcodes::HTTPMethodNotAllowed;
use httpmessage::{HttpRequest, HttpResponse};
/// Http resource /// Http resource
/// ///
@ -27,9 +28,8 @@ use httpmessage::{HttpRequest, HttpResponse};
/// fn main() { /// fn main() {
/// let mut routes = RoutingMap::default(); /// let mut routes = RoutingMap::default();
/// ///
/// routes /// routes.add_resource("/")
/// .add_resource("/") /// .post::<MyRoute>();
/// .post::<MyRoute>();
/// } /// }
pub struct Resource<S=()> { pub struct Resource<S=()> {
state: PhantomData<S>, state: PhantomData<S>,

View File

@ -8,7 +8,8 @@ use task::Task;
use context::HttpContext; use context::HttpContext;
use resource::Reply; use resource::Reply;
use payload::Payload; use payload::Payload;
use httpmessage::{HttpRequest, HttpResponse}; use httprequest::HttpRequest;
use httpmessage::HttpResponse;
#[doc(hidden)] #[doc(hidden)]
#[derive(Debug)] #[derive(Debug)]

View File

@ -9,7 +9,7 @@ use route::RouteHandler;
use resource::Resource; use resource::Resource;
use application::Application; use application::Application;
use httpcodes::HTTPNotFound; use httpcodes::HTTPNotFound;
use httpmessage::HttpRequest; use httprequest::HttpRequest;
pub(crate) trait Handler: 'static { pub(crate) trait Handler: 'static {
fn handle(&self, req: HttpRequest, payload: Payload) -> Task; fn handle(&self, req: HttpRequest, payload: Payload) -> Task;

23
src/staticfiles.rs Normal file
View File

@ -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<S: 'static> RouteHandler<S> for StaticFiles {
fn handle(&self, req: HttpRequest, payload: Payload, state: Rc<S>) -> Task {
Task::reply(HTTPOk)
}
}

View File

@ -12,7 +12,8 @@ use tokio_core::net::TcpStream;
use date; use date;
use route::Frame; use route::Frame;
use httpmessage::{Body, HttpRequest, HttpResponse}; use httprequest::HttpRequest;
use httpmessage::{Body, HttpResponse};
type FrameStream = Stream<Item=Frame, Error=io::Error>; type FrameStream = Stream<Item=Frame, Error=io::Error>;
const AVERAGE_HEADER_SIZE: usize = 30; // totally scientific const AVERAGE_HEADER_SIZE: usize = 30; // totally scientific

View File

@ -74,7 +74,8 @@ use context::HttpContext;
use route::Route; use route::Route;
use payload::Payload; use payload::Payload;
use httpcodes::{HTTPBadRequest, HTTPMethodNotAllowed}; use httpcodes::{HTTPBadRequest, HTTPMethodNotAllowed};
use httpmessage::{Body, ConnectionType, HttpRequest, HttpResponse}; use httprequest::HttpRequest;
use httpmessage::{Body, ConnectionType, HttpResponse};
use wsframe; use wsframe;
use wsproto::*; use wsproto::*;

View File

@ -72,3 +72,25 @@ fn test_response_cookies() {
assert_eq!( assert_eq!(
val[1],"name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400"); 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);
}