From b87bb20f0c25891c30ef2399da2721596fbc1fcf Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Thu, 12 Mar 2015 14:01:52 -0700 Subject: [PATCH] perf(http): changes http parsing to use httparse crate httparse is a http1 stateless push parser. This not only speeds up parsing right now with sync io, but will also be useful for when we get async io, since it's push based instead of pull. BREAKING CHANGE: Several public functions and types in the `http` module have been removed. They have been replaced with 2 methods that handle all of the http1 parsing. --- Cargo.toml | 5 + examples/client.rs | 4 + examples/hello.rs | 2 + examples/server.rs | 5 +- src/client/response.rs | 16 +- src/error.rs | 88 ++++ src/header/common/authorization.rs | 20 +- src/header/mod.rs | 73 ++-- src/http.rs | 633 ++++------------------------- src/lib.rs | 72 +--- src/method.rs | 7 +- src/net.rs | 1 - src/server/mod.rs | 66 +-- src/server/request.rs | 48 ++- src/uri.rs | 41 ++ 15 files changed, 354 insertions(+), 727 deletions(-) create mode 100644 src/error.rs diff --git a/Cargo.toml b/Cargo.toml index 39dd05f515..86821ee203 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["http", "hyper", "hyperium"] [dependencies] cookie = "*" +httparse = "*" log = ">= 0.2.0" mime = "*" openssl = "*" @@ -20,3 +21,7 @@ rustc-serialize = "*" time = "*" unicase = "*" url = "*" + +[dev-dependencies] +env_logger = "*" + diff --git a/examples/client.rs b/examples/client.rs index c3b505d9af..98b1155980 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -1,11 +1,15 @@ #![deny(warnings)] extern crate hyper; +extern crate env_logger; + use std::env; use hyper::Client; fn main() { + env_logger::init().unwrap(); + let url = match env::args().nth(1) { Some(url) => url, None => { diff --git a/examples/hello.rs b/examples/hello.rs index f9bb03ca43..d2bd437a58 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,6 +1,7 @@ #![deny(warnings)] #![feature(io, net)] extern crate hyper; +extern crate env_logger; use std::io::Write; use std::net::IpAddr; @@ -15,6 +16,7 @@ fn hello(_: Request, res: Response) { } fn main() { + env_logger::init().unwrap(); let _listening = hyper::Server::http(hello) .listen(IpAddr::new_v4(127, 0, 0, 1), 3000).unwrap(); println!("Listening on http://127.0.0.1:3000"); diff --git a/examples/server.rs b/examples/server.rs index 06d375967a..f2d806e578 100644 --- a/examples/server.rs +++ b/examples/server.rs @@ -1,7 +1,7 @@ #![deny(warnings)] #![feature(io, net)] extern crate hyper; -#[macro_use] extern crate log; +extern crate env_logger; use std::io::{Write, copy}; use std::net::IpAddr; @@ -15,7 +15,7 @@ macro_rules! try_return( ($e:expr) => {{ match $e { Ok(v) => v, - Err(e) => { error!("Error: {}", e); return; } + Err(e) => { println!("Error: {}", e); return; } } }} ); @@ -51,6 +51,7 @@ fn echo(mut req: Request, mut res: Response) { } fn main() { + env_logger::init().unwrap(); let server = Server::http(echo); let _guard = server.listen(IpAddr::new_v4(127, 0, 0, 1), 1337).unwrap(); println!("Listening on http://127.0.0.1:1337"); diff --git a/src/client/response.rs b/src/client/response.rs index 5d6898c5a7..d9b170a3d7 100644 --- a/src/client/response.rs +++ b/src/client/response.rs @@ -7,7 +7,7 @@ use header; use header::{ContentLength, TransferEncoding}; use header::Encoding::Chunked; use net::{NetworkStream, HttpStream}; -use http::{read_status_line, HttpReader, RawStatus}; +use http::{self, HttpReader, RawStatus}; use http::HttpReader::{SizedReader, ChunkedReader, EofReader}; use status; use version; @@ -36,15 +36,17 @@ impl Response { /// Creates a new response from a server. pub fn new(stream: Box) -> HttpResult { let mut stream = BufReader::new(stream); - let (version, raw_status) = try!(read_status_line(&mut stream)); + + let head = try!(http::parse_response(&mut stream)); + let raw_status = head.subject; + let headers = head.headers; + let status = match FromPrimitive::from_u16(raw_status.0) { Some(status) => status, None => return Err(HttpStatusError) }; - debug!("{:?} {:?}", version, status); - - let headers = try!(header::Headers::from_raw(&mut stream)); - debug!("Headers: [\n{:?}]", headers); + debug!("version={:?}, status={:?}", head.version, status); + debug!("headers={:?}", headers); let body = if headers.has::() { match headers.get::() { @@ -74,7 +76,7 @@ impl Response { Ok(Response { status: status, - version: version, + version: head.version, headers: headers, body: body, status_raw: raw_status, diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000000..893b866122 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,88 @@ +//! HttpError and HttpResult module. +use std::error::{Error, FromError}; +use std::fmt; +use std::io::Error as IoError; + +use httparse; +use url; + +use self::HttpError::{HttpMethodError, HttpUriError, HttpVersionError, + HttpHeaderError, HttpStatusError, HttpIoError, + HttpTooLargeError}; + + +/// Result type often returned from methods that can have `HttpError`s. +pub type HttpResult = Result; + +/// A set of errors that can occur parsing HTTP streams. +#[derive(Debug, PartialEq, Clone)] +pub enum HttpError { + /// An invalid `Method`, such as `GE,T`. + HttpMethodError, + /// An invalid `RequestUri`, such as `exam ple.domain`. + HttpUriError(url::ParseError), + /// An invalid `HttpVersion`, such as `HTP/1.1` + HttpVersionError, + /// An invalid `Header`. + HttpHeaderError, + /// A message head is too large to be reasonable. + HttpTooLargeError, + /// An invalid `Status`, such as `1337 ELITE`. + HttpStatusError, + /// An `IoError` that occured while trying to read or write to a network stream. + HttpIoError(IoError), +} + +impl fmt::Display for HttpError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.description()) + } +} + +impl Error for HttpError { + fn description(&self) -> &str { + match *self { + HttpMethodError => "Invalid Method specified", + HttpUriError(_) => "Invalid Request URI specified", + HttpVersionError => "Invalid HTTP version specified", + HttpHeaderError => "Invalid Header provided", + HttpTooLargeError => "Message head is too large", + HttpStatusError => "Invalid Status provided", + HttpIoError(_) => "An IoError occurred while connecting to the specified network", + } + } + + fn cause(&self) -> Option<&Error> { + match *self { + HttpIoError(ref error) => Some(error as &Error), + HttpUriError(ref error) => Some(error as &Error), + _ => None, + } + } +} + +impl FromError for HttpError { + fn from_error(err: IoError) -> HttpError { + HttpIoError(err) + } +} + +impl FromError for HttpError { + fn from_error(err: url::ParseError) -> HttpError { + HttpUriError(err) + } +} + +impl FromError for HttpError { + fn from_error(err: httparse::Error) -> HttpError { + match err { + httparse::Error::HeaderName => HttpHeaderError, + httparse::Error::HeaderValue => HttpHeaderError, + httparse::Error::NewLine => HttpHeaderError, + httparse::Error::Status => HttpStatusError, + httparse::Error::Token => HttpHeaderError, + httparse::Error::TooManyHeaders => HttpTooLargeError, + httparse::Error::Version => HttpVersionError, + } + } +} diff --git a/src/header/common/authorization.rs b/src/header/common/authorization.rs index 78ba8a3f7d..af8efbdd5a 100644 --- a/src/header/common/authorization.rs +++ b/src/header/common/authorization.rs @@ -32,9 +32,9 @@ impl Header for Authorization where ::Err: match (from_utf8(unsafe { &raw.get_unchecked(0)[..] }), Scheme::scheme(None::)) { (Ok(header), Some(scheme)) if header.starts_with(scheme) && header.len() > scheme.len() + 1 => { - header[scheme.len() + 1..].parse::().map(|s| Authorization(s)).ok() + header[scheme.len() + 1..].parse::().map(Authorization).ok() }, - (Ok(header), None) => header.parse::().map(|s| Authorization(s)).ok(), + (Ok(header), None) => header.parse::().map(Authorization).ok(), _ => None } } else { @@ -143,7 +143,7 @@ impl FromStr for Basic { #[cfg(test)] mod tests { use super::{Authorization, Basic}; - use super::super::super::{Headers}; + use super::super::super::{Headers, Header}; #[test] fn test_raw_auth() { @@ -154,8 +154,8 @@ mod tests { #[test] fn test_raw_auth_parse() { - let headers = Headers::from_raw(&mut b"Authorization: foo bar baz\r\n\r\n").unwrap(); - assert_eq!(&headers.get::>().unwrap().0[..], "foo bar baz"); + let header: Authorization = Header::parse_header(&[b"foo bar baz".to_vec()]).unwrap(); + assert_eq!(header.0, "foo bar baz"); } #[test] @@ -174,17 +174,15 @@ mod tests { #[test] fn test_basic_auth_parse() { - let headers = Headers::from_raw(&mut b"Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==\r\n\r\n").unwrap(); - let auth = headers.get::>().unwrap(); - assert_eq!(&auth.0.username[..], "Aladdin"); + let auth: Authorization = Header::parse_header(&[b"Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==".to_vec()]).unwrap(); + assert_eq!(auth.0.username, "Aladdin"); assert_eq!(auth.0.password, Some("open sesame".to_string())); } #[test] fn test_basic_auth_parse_no_password() { - let headers = Headers::from_raw(&mut b"Authorization: Basic QWxhZGRpbjo=\r\n\r\n").unwrap(); - let auth = headers.get::>().unwrap(); - assert_eq!(auth.0.username.as_slice(), "Aladdin"); + let auth: Authorization = Header::parse_header(&[b"Basic QWxhZGRpbjo=".to_vec()]).unwrap(); + assert_eq!(auth.0.username, "Aladdin"); assert_eq!(auth.0.password, Some("".to_string())); } diff --git a/src/header/mod.rs b/src/header/mod.rs index ff4f4ce190..2f32c37d76 100644 --- a/src/header/mod.rs +++ b/src/header/mod.rs @@ -5,9 +5,9 @@ //! must implement the `Header` trait from this module. Several common headers //! are already provided, such as `Host`, `ContentType`, `UserAgent`, and others. use std::any::Any; -use std::borrow::Cow::{Borrowed, Owned}; +use std::borrow::Cow::{Borrowed}; +use std::borrow::ToOwned; use std::fmt; -use std::io::Read; use std::raw::TraitObject; use std::collections::HashMap; use std::collections::hash_map::{Iter, Entry}; @@ -15,10 +15,11 @@ use std::iter::{FromIterator, IntoIterator}; use std::borrow::{Cow, IntoCow}; use std::{mem, raw}; +use httparse; use unicase::UniCase; use self::internals::Item; -use {http, HttpResult, HttpError}; +use error::HttpResult; pub use self::shared::{Charset, Encoding, EntityTag, Quality, QualityItem, qitem, q}; pub use self::common::*; @@ -105,10 +106,6 @@ pub struct Headers { data: HashMap } -// To prevent DOS from a server sending a never ending header. -// The value was copied from curl. -const MAX_HEADERS_LENGTH: u32 = 100 * 1024; - impl Headers { /// Creates a new, empty headers map. @@ -119,27 +116,18 @@ impl Headers { } #[doc(hidden)] - pub fn from_raw(rdr: &mut R) -> HttpResult { + pub fn from_raw<'a>(raw: &[httparse::Header<'a>]) -> HttpResult { let mut headers = Headers::new(); - let mut count = 0u32; - loop { - match try!(http::read_header(rdr)) { - Some((name, value)) => { - debug!("raw header: {:?}={:?}", name, &value[..]); - count += (name.len() + value.len()) as u32; - if count > MAX_HEADERS_LENGTH { - debug!("Max header size reached, aborting"); - return Err(HttpError::HttpHeaderError) - } - let name = UniCase(Owned(name)); - let mut item = match headers.data.entry(name) { - Entry::Vacant(entry) => entry.insert(Item::new_raw(vec![])), - Entry::Occupied(entry) => entry.into_mut() - }; - item.mut_raw().push(value); - }, - None => break, - } + for header in raw { + debug!("raw header: {:?}={:?}", header.name, &header.value[..]); + let name = UniCase(header.name.to_owned().into_cow()); + let mut item = match headers.data.entry(name) { + Entry::Vacant(entry) => entry.insert(Item::new_raw(vec![])), + Entry::Occupied(entry) => entry.into_mut() + }; + let trim = header.value.iter().rev().take_while(|&&x| x == b' ').count(); + let value = &header.value[.. header.value.len() - trim]; + item.mut_raw().push(value.to_vec()); } Ok(headers) } @@ -364,12 +352,26 @@ mod tests { use mime::SubLevel::Plain; use super::{Headers, Header, HeaderFormat, ContentLength, ContentType, Accept, Host, qitem}; + use httparse; use test::Bencher; + macro_rules! raw { + ($($line:expr),*) => ({ + [$({ + let line = $line; + let pos = line.position_elem(&b':').expect("raw splits on :, not found"); + httparse::Header { + name: ::std::str::from_utf8(&line[..pos]).unwrap(), + value: &line[pos + 2..] + } + }),*] + }) + } + #[test] fn test_from_raw() { - let headers = Headers::from_raw(&mut b"Content-Length: 10\r\n\r\n").unwrap(); + let headers = Headers::from_raw(&raw!(b"Content-Length: 10")).unwrap(); assert_eq!(headers.get(), Some(&ContentLength(10))); } @@ -422,20 +424,20 @@ mod tests { #[test] fn test_different_structs_for_same_header() { - let headers = Headers::from_raw(&mut b"Content-Length: 10\r\n\r\n").unwrap(); + let headers = Headers::from_raw(&raw!(b"Content-Length: 10")).unwrap(); assert_eq!(headers.get::(), Some(&ContentLength(10))); assert_eq!(headers.get::(), Some(&CrazyLength(Some(false), 10))); } #[test] fn test_trailing_whitespace() { - let headers = Headers::from_raw(&mut b"Content-Length: 10 \r\n\r\n").unwrap(); + let headers = Headers::from_raw(&raw!(b"Content-Length: 10 ")).unwrap(); assert_eq!(headers.get::(), Some(&ContentLength(10))); } #[test] fn test_multiple_reads() { - let headers = Headers::from_raw(&mut b"Content-Length: 10\r\n\r\n").unwrap(); + let headers = Headers::from_raw(&raw!(b"Content-Length: 10")).unwrap(); let ContentLength(one) = *headers.get::().unwrap(); let ContentLength(two) = *headers.get::().unwrap(); assert_eq!(one, two); @@ -443,14 +445,14 @@ mod tests { #[test] fn test_different_reads() { - let headers = Headers::from_raw(&mut b"Content-Length: 10\r\nContent-Type: text/plain\r\n\r\n").unwrap(); + let headers = Headers::from_raw(&raw!(b"Content-Length: 10", b"Content-Type: text/plain")).unwrap(); let ContentLength(_) = *headers.get::().unwrap(); let ContentType(_) = *headers.get::().unwrap(); } #[test] fn test_get_mutable() { - let mut headers = Headers::from_raw(&mut b"Content-Length: 10\r\nContent-Type: text/plain\r\n\r\n").unwrap(); + let mut headers = Headers::from_raw(&raw!(b"Content-Length: 10")).unwrap(); *headers.get_mut::().unwrap() = ContentLength(20); assert_eq!(*headers.get::().unwrap(), ContentLength(20)); } @@ -471,7 +473,7 @@ mod tests { #[test] fn test_headers_show_raw() { - let headers = Headers::from_raw(&mut b"Content-Length: 10\r\n\r\n").unwrap(); + let headers = Headers::from_raw(&raw!(b"Content-Length: 10")).unwrap(); let s = headers.to_string(); assert_eq!(s, "Content-Length: 10\r\n"); } @@ -538,7 +540,8 @@ mod tests { #[bench] fn bench_headers_from_raw(b: &mut Bencher) { - b.iter(|| Headers::from_raw(&mut b"Content-Length: 10\r\n\r\n").unwrap()) + let raw = raw!(b"Content-Length: 10"); + b.iter(|| Headers::from_raw(&raw).unwrap()) } #[bench] diff --git a/src/http.rs b/src/http.rs index 7b9b3efbbe..a63d2a7219 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,22 +1,15 @@ //! Pieces pertaining to the HTTP message protocol. -use std::borrow::Cow::{self, Borrowed, Owned}; -use std::borrow::IntoCow; +use std::borrow::{Cow, IntoCow, ToOwned}; use std::cmp::min; -use std::io::{self, Read, Write, Cursor}; -use std::num::from_u16; -use std::str; - -use url::Url; -use url::ParseError as UrlError; - -use method; -use status::StatusCode; -use uri; -use uri::RequestUri::{AbsolutePath, AbsoluteUri, Authority, Star}; -use version::HttpVersion; -use version::HttpVersion::{Http09, Http10, Http11}; -use HttpError::{HttpHeaderError, HttpMethodError, HttpStatusError, - HttpUriError, HttpVersionError}; +use std::io::{self, Read, Write, BufRead}; + +use httparse; + +use header::Headers; +use method::Method; +use uri::RequestUri; +use version::HttpVersion::{self, Http10, Http11}; +use HttpError:: HttpTooLargeError; use HttpResult; use self::HttpReader::{SizedReader, ChunkedReader, EofReader, EmptyReader}; @@ -314,337 +307,73 @@ impl Write for HttpWriter { } } -pub const SP: u8 = b' '; -pub const CR: u8 = b'\r'; -pub const LF: u8 = b'\n'; -pub const STAR: u8 = b'*'; -pub const LINE_ENDING: &'static str = "\r\n"; - -/// Determines if byte is a token char. -/// -/// > ```notrust -/// > token = 1*tchar -/// > -/// > tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" -/// > / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" -/// > / DIGIT / ALPHA -/// > ; any VCHAR, except delimiters -/// > ``` -#[inline] -pub fn is_token(b: u8) -> bool { - match b { - b'a'...b'z' | - b'A'...b'Z' | - b'0'...b'9' | - b'!' | - b'#' | - b'$' | - b'%' | - b'&' | - b'\''| - b'*' | - b'+' | - b'-' | - b'.' | - b'^' | - b'_' | - b'`' | - b'|' | - b'~' => true, - _ => false - } -} - -/// Read token bytes from `stream` into `buf` until a space is encountered. -/// Returns `Ok(true)` if we read until a space, -/// `Ok(false)` if we got to the end of `buf` without encountering a space, -/// otherwise returns any error encountered reading the stream. -/// -/// The remaining contents of `buf` are left untouched. -fn read_method_token_until_space(stream: &mut R, buf: &mut [u8]) -> HttpResult { - macro_rules! byte ( - ($rdr:ident) => ({ - let mut slot = [0]; - match try!($rdr.read(&mut slot)) { - 1 => slot[0], - _ => return Err(HttpMethodError), - } - }) - ); - - let mut cursor = Cursor::new(buf); - - loop { - let b = byte!(stream); - - if b == SP { - break; - } else if !is_token(b) { - return Err(HttpMethodError); - // Read to end but there's still more - } else { - match cursor.write(&[b]) { - Ok(1) => (), - _ => return Ok(false) +/// Parses a request into an Incoming message head. +pub fn parse_request(buf: &mut T) -> HttpResult> { + let (inc, len) = { + let slice = try!(buf.fill_buf()); + let mut headers = [httparse::Header { name: "", value: b"" }; 64]; + let mut req = httparse::Request::new(&mut headers); + match try!(req.parse(slice)) { + httparse::Status::Complete(len) => { + (Incoming { + version: if req.version.unwrap() == 1 { Http11 } else { Http10 }, + subject: ( + try!(req.method.unwrap().parse()), + try!(req.path.unwrap().parse()) + ), + headers: try!(Headers::from_raw(req.headers)) + }, len) + }, + _ => { + // request head is bigger than a BufRead's buffer? 400 that! + return Err(HttpTooLargeError) } } - } - - if cursor.position() == 0 { - return Err(HttpMethodError); - } - - Ok(true) -} - -/// Read a `Method` from a raw stream, such as `GET`. -/// ### Note: -/// Extension methods are only parsed to 16 characters. -pub fn read_method(stream: &mut R) -> HttpResult { - let mut buf = [SP; 16]; - - if !try!(read_method_token_until_space(stream, &mut buf)) { - return Err(HttpMethodError); - } - - let maybe_method = match &buf[0..7] { - b"GET " => Some(method::Method::Get), - b"PUT " => Some(method::Method::Put), - b"POST " => Some(method::Method::Post), - b"HEAD " => Some(method::Method::Head), - b"PATCH " => Some(method::Method::Patch), - b"TRACE " => Some(method::Method::Trace), - b"DELETE " => Some(method::Method::Delete), - b"CONNECT" => Some(method::Method::Connect), - b"OPTIONS" => Some(method::Method::Options), - _ => None, }; - - debug!("maybe_method = {:?}", maybe_method); - - match (maybe_method, &buf[..]) { - (Some(method), _) => Ok(method), - (None, ext) => { - // We already checked that the buffer is ASCII - Ok(method::Method::Extension(unsafe { str::from_utf8_unchecked(ext) }.trim().to_string())) - }, - } + buf.consume(len); + Ok(inc) } -/// Read a `RequestUri` from a raw stream. -pub fn read_uri(stream: &mut R) -> HttpResult { - macro_rules! byte ( - ($rdr:ident) => ({ - let mut buf = [0]; - match try!($rdr.read(&mut buf)) { - 1 => buf[0], - _ => return Err(HttpUriError(UrlError::InvalidCharacter)), - } - }) - ); - let mut b = byte!(stream); - while b == SP { - b = byte!(stream); - } - - let mut s = String::new(); - if b == STAR { - try!(expect(byte!(stream), SP)); - return Ok(Star) - } else { - s.push(b as char); - loop { - match byte!(stream) { - SP => { - break; - }, - CR | LF => { - return Err(HttpUriError(UrlError::InvalidCharacter)) - }, - b => s.push(b as char) +/// Parses a response into an Incoming message head. +pub fn parse_response(buf: &mut T) -> HttpResult> { + let (inc, len) = { + let mut headers = [httparse::Header { name: "", value: b"" }; 64]; + let mut res = httparse::Response::new(&mut headers); + match try!(res.parse(try!(buf.fill_buf()))) { + httparse::Status::Complete(len) => { + (Incoming { + version: if res.version.unwrap() == 1 { Http11 } else { Http10 }, + subject: RawStatus( + res.code.unwrap(), res.reason.unwrap().to_owned().into_cow() + ), + headers: try!(Headers::from_raw(res.headers)) + }, len) + }, + _ => { + // response head is bigger than a BufRead's buffer? + return Err(HttpTooLargeError) } } - } - - debug!("uri buf = {:?}", s); - - if s.as_slice().starts_with("/") { - Ok(AbsolutePath(s)) - } else if s.as_slice().contains("/") { - Ok(AbsoluteUri(try!(Url::parse(s.as_slice())))) - } else { - let mut temp = "http://".to_string(); - temp.push_str(s.as_slice()); - try!(Url::parse(temp.as_slice())); - todo!("compare vs u.authority()"); - Ok(Authority(s)) - } - - + }; + buf.consume(len); + Ok(inc) } - -/// Read the `HttpVersion` from a raw stream, such as `HTTP/1.1`. -pub fn read_http_version(stream: &mut R) -> HttpResult { - macro_rules! byte ( - ($rdr:ident) => ({ - let mut buf = [0]; - match try!($rdr.read(&mut buf)) { - 1 => buf[0], - _ => return Err(HttpVersionError), - } - }) - ); - - try!(expect(byte!(stream), b'H')); - try!(expect(byte!(stream), b'T')); - try!(expect(byte!(stream), b'T')); - try!(expect(byte!(stream), b'P')); - try!(expect(byte!(stream), b'/')); - - match byte!(stream) { - b'0' => { - try!(expect(byte!(stream), b'.')); - try!(expect(byte!(stream), b'9')); - Ok(Http09) - }, - b'1' => { - try!(expect(byte!(stream), b'.')); - match byte!(stream) { - b'0' => Ok(Http10), - b'1' => Ok(Http11), - _ => Err(HttpVersionError) - } - }, - _ => Err(HttpVersionError) - } +/// An Incoming Message head. Includes request/status line, and headers. +pub struct Incoming { + /// HTTP version of the message. + pub version: HttpVersion, + /// Subject (request line or status line) of Incoming message. + pub subject: S, + /// Headers of the Incoming message. + pub headers: Headers } -const MAX_HEADER_NAME_LENGTH: usize = 100; -const MAX_HEADER_FIELD_LENGTH: usize = 4096; - -/// The raw bytes when parsing a header line. -/// -/// A String and Vec, divided by COLON (`:`). The String is guaranteed -/// to be all `token`s. See `is_token` source for all valid characters. -pub type RawHeaderLine = (String, Vec); - -/// Read a RawHeaderLine from a Reader. -/// -/// From [spec](https://tools.ietf.org/html/http#section-3.2): -/// -/// > Each header field consists of a case-insensitive field name followed -/// > by a colon (":"), optional leading whitespace, the field value, and -/// > optional trailing whitespace. -/// > -/// > ```notrust -/// > header-field = field-name ":" OWS field-value OWS -/// > -/// > field-name = token -/// > field-value = *( field-content / obs-fold ) -/// > field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] -/// > field-vchar = VCHAR / obs-text -/// > -/// > obs-fold = CRLF 1*( SP / HTAB ) -/// > ; obsolete line folding -/// > ; see Section 3.2.4 -/// > ``` -pub fn read_header(stream: &mut R) -> HttpResult> { - macro_rules! byte ( - ($rdr:ident) => ({ - let mut buf = [0]; - match try!($rdr.read(&mut buf)) { - 1 => buf[0], - _ => return Err(HttpHeaderError), - } - }) - ); - - let mut name = String::new(); - let mut value = vec![]; - - loop { - match byte!(stream) { - CR if name.len() == 0 => { - match byte!(stream) { - LF => return Ok(None), - _ => return Err(HttpHeaderError) - } - }, - b':' => break, - b if is_token(b) => { - if name.len() > MAX_HEADER_NAME_LENGTH { return Err(HttpHeaderError); } - name.push(b as char) - }, - _nontoken => return Err(HttpHeaderError) - }; - } - - debug!("header name = {:?}", name); - - let mut ows = true; //optional whitespace - - todo!("handle obs-folding (gross!)"); - loop { - match byte!(stream) { - CR => break, - LF => return Err(HttpHeaderError), - b' ' if ows => {}, - b => { - ows = false; - if value.len() > MAX_HEADER_FIELD_LENGTH { return Err(HttpHeaderError); } - value.push(b) - } - }; - } - // Remove optional trailing whitespace - let real_len = value.len() - value.iter().rev().take_while(|&&x| b' ' == x).count(); - value.truncate(real_len); - - match byte!(stream) { - LF => Ok(Some((name, value))), - _ => Err(HttpHeaderError) - } - -} - -/// `request-line = method SP request-target SP HTTP-version CRLF` -pub type RequestLine = (method::Method, uri::RequestUri, HttpVersion); - -/// Read the `RequestLine`, such as `GET / HTTP/1.1`. -pub fn read_request_line(stream: &mut R) -> HttpResult { - macro_rules! byte ( - ($rdr:ident) => ({ - let mut buf = [0]; - match try!($rdr.read(&mut buf)) { - 1 => buf[0], - _ => return Err(HttpVersionError), - } - }) - ); - - debug!("read request line"); - let method = try!(read_method(stream)); - debug!("method = {:?}", method); - let uri = try!(read_uri(stream)); - debug!("uri = {:?}", uri); - let version = try!(read_http_version(stream)); - debug!("version = {:?}", version); - - if byte!(stream) != CR { - return Err(HttpVersionError); - } - if byte!(stream) != LF { - return Err(HttpVersionError); - } - - Ok((method, uri, version)) -} - -/// `status-line = HTTP-version SP status-code SP reason-phrase CRLF` -/// -/// However, reason-phrase is absolutely useless, so its tossed. -pub type StatusLine = (HttpVersion, RawStatus); +pub const SP: u8 = b' '; +pub const CR: u8 = b'\r'; +pub const LF: u8 = b'\n'; +pub const STAR: u8 = b'*'; +pub const LINE_ENDING: &'static str = "\r\n"; /// The raw status code and reason-phrase. #[derive(PartialEq, Debug)] @@ -656,219 +385,12 @@ impl Clone for RawStatus { } } -/// Read the StatusLine, such as `HTTP/1.1 200 OK`. -/// -/// > The first line of a response message is the status-line, consisting -/// > of the protocol version, a space (SP), the status code, another -/// > space, a possibly empty textual phrase describing the status code, -/// > and ending with CRLF. -/// > -/// >```notrust -/// > status-line = HTTP-version SP status-code SP reason-phrase CRLF -/// > status-code = 3DIGIT -/// > reason-phrase = *( HTAB / SP / VCHAR / obs-text ) -/// >``` -pub fn read_status_line(stream: &mut R) -> HttpResult { - macro_rules! byte ( - ($rdr:ident) => ({ - let mut buf = [0]; - match try!($rdr.read(&mut buf)) { - 1 => buf[0], - _ => return Err(HttpVersionError), - } - }) - ); - - let version = try!(read_http_version(stream)); - if byte!(stream) != SP { - return Err(HttpVersionError); - } - let code = try!(read_status(stream)); - - Ok((version, code)) -} - -/// Read the StatusCode from a stream. -pub fn read_status(stream: &mut R) -> HttpResult { - macro_rules! byte ( - ($rdr:ident) => ({ - let mut buf = [0]; - match try!($rdr.read(&mut buf)) { - 1 => buf[0], - _ => return Err(HttpStatusError), - } - }) - ); - - let code = [ - byte!(stream), - byte!(stream), - byte!(stream), - ]; - - let code = match str::from_utf8(code.as_slice()).ok().and_then(|x| x.parse().ok()) { - Some(num) => num, - None => return Err(HttpStatusError) - }; - - match byte!(stream) { - b' ' => (), - _ => return Err(HttpStatusError) - } - - let mut buf = [SP; 32]; - let mut cursor = Cursor::new(&mut buf[..]); - { - 'read: loop { - match byte!(stream) { - CR => match byte!(stream) { - LF => break, - _ => return Err(HttpStatusError) - }, - b => match cursor.write(&[b]) { - Ok(0) | Err(_) => { - for _ in 0u8..128 { - match byte!(stream) { - CR => match byte!(stream) { - LF => break 'read, - _ => return Err(HttpStatusError) - }, - _ => { /* ignore */ } - } - } - return Err(HttpStatusError) - } - Ok(_) => (), - } - } - } - } - - let reason = match str::from_utf8(cursor.into_inner()) { - Ok(s) => s.trim(), - Err(_) => return Err(HttpStatusError) - }; - - let reason = match from_u16::(code) { - Some(status) => match status.canonical_reason() { - Some(phrase) => { - if phrase == reason { - Borrowed(phrase) - } else { - Owned(reason.to_string()) - } - } - _ => Owned(reason.to_string()) - }, - None => return Err(HttpStatusError) - }; - - Ok(RawStatus(code, reason)) -} - -#[inline] -fn expect(actual: u8, expected: u8) -> HttpResult<()> { - if actual == expected { - Ok(()) - } else { - Err(HttpVersionError) - } -} - #[cfg(test)] mod tests { use std::io::{self, Write}; - use std::borrow::Cow::{Borrowed, Owned}; - use test::Bencher; - use uri::RequestUri; - use uri::RequestUri::{Star, AbsoluteUri, AbsolutePath, Authority}; - use method; - use version::HttpVersion; - use version::HttpVersion::{Http10, Http11}; - use HttpError::{HttpVersionError, HttpMethodError}; - use HttpResult; - use url::Url; - - use super::{read_method, read_uri, read_http_version, read_header, - RawHeaderLine, read_status, RawStatus, read_chunk_size}; - #[test] - fn test_read_method() { - fn read(s: &str, result: HttpResult) { - assert_eq!(read_method(&mut s.as_bytes()), result); - } - - read("GET /", Ok(method::Method::Get)); - read("POST /", Ok(method::Method::Post)); - read("PUT /", Ok(method::Method::Put)); - read("HEAD /", Ok(method::Method::Head)); - read("OPTIONS /", Ok(method::Method::Options)); - read("CONNECT /", Ok(method::Method::Connect)); - read("TRACE /", Ok(method::Method::Trace)); - read("PATCH /", Ok(method::Method::Patch)); - read("FOO /", Ok(method::Method::Extension("FOO".to_string()))); - read("akemi!~#HOMURA /", Ok(method::Method::Extension("akemi!~#HOMURA".to_string()))); - read(" ", Err(HttpMethodError)); - } - - #[test] - fn test_read_uri() { - fn read(s: &str, result: HttpResult) { - assert_eq!(read_uri(&mut s.as_bytes()), result); - } - - read("* ", Ok(Star)); - read("http://hyper.rs/ ", Ok(AbsoluteUri(Url::parse("http://hyper.rs/").unwrap()))); - read("hyper.rs ", Ok(Authority("hyper.rs".to_string()))); - read("/ ", Ok(AbsolutePath("/".to_string()))); - } - - #[test] - fn test_read_http_version() { - fn read(s: &str, result: HttpResult) { - assert_eq!(read_http_version(&mut s.as_bytes()), result); - } - - read("HTTP/1.0", Ok(Http10)); - read("HTTP/1.1", Ok(Http11)); - read("HTP/2.0", Err(HttpVersionError)); - read("HTTP.2.0", Err(HttpVersionError)); - read("HTTP 2.0", Err(HttpVersionError)); - read("TTP 2.0", Err(HttpVersionError)); - } - - #[test] - fn test_read_status() { - fn read(s: &str, result: HttpResult) { - assert_eq!(read_status(&mut s.as_bytes()), result); - } - fn read_ignore_string(s: &str, result: HttpResult) { - match (read_status(&mut s.as_bytes()), result) { - (Ok(RawStatus(ref c1, _)), Ok(RawStatus(ref c2, _))) => { - assert_eq!(c1, c2); - }, - (r1, r2) => assert_eq!(r1, r2) - } - } - - read("200 OK\r\n", Ok(RawStatus(200, Borrowed("OK")))); - read("404 Not Found\r\n", Ok(RawStatus(404, Borrowed("Not Found")))); - read("200 crazy pants\r\n", Ok(RawStatus(200, Owned("crazy pants".to_string())))); - read("301 Moved Permanently\r\n", Ok(RawStatus(301, Owned("Moved Permanently".to_string())))); - read_ignore_string("301 Unreasonably long header that should not happen, \ - but some men just want to watch the world burn\r\n", - Ok(RawStatus(301, Owned("Ignored".to_string())))); - } - - #[test] - fn test_read_header() { - fn read(s: &str, result: HttpResult>) { - assert_eq!(read_header(&mut s.as_bytes()), result); - } + use super::{read_chunk_size}; - read("Host: rust-lang.org\r\n", Ok(Some(("Host".to_string(), - b"rust-lang.org".to_vec())))); - } #[test] fn test_write_chunked() { @@ -934,16 +456,19 @@ mod tests { read_err("1;no CRLF"); } - #[bench] - fn bench_read_method(b: &mut Bencher) { - b.bytes = b"CONNECT ".len() as u64; - b.iter(|| assert_eq!(read_method(&mut b"CONNECT "), Ok(method::Method::Connect))); - } + use test::Bencher; #[bench] - fn bench_read_status(b: &mut Bencher) { - b.bytes = b"404 Not Found\r\n".len() as u64; - b.iter(|| assert_eq!(read_status(&mut b"404 Not Found\r\n"), Ok(RawStatus(404, Borrowed("Not Found"))))); + fn bench_parse_incoming(b: &mut Bencher) { + use std::io::BufReader; + use mock::MockStream; + use net::NetworkStream; + use super::parse_request; + b.iter(|| { + let mut raw = MockStream::with_input(b"GET /echo HTTP/1.1\r\nHost: hyper.rs\r\n\r\n"); + let mut buf = BufReader::new(&mut raw as &mut NetworkStream); + + parse_request(&mut buf).unwrap(); + }); } - } diff --git a/src/lib.rs b/src/lib.rs index f6fe69a132..ed937852b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ #![feature(core, collections, io, net, os, path, std_misc, box_syntax, unsafe_destructor)] -#![cfg_attr(test, deny(missing_docs))] +#![deny(missing_docs)] #![cfg_attr(test, deny(warnings))] #![cfg_attr(test, feature(alloc, test))] @@ -132,6 +132,7 @@ extern crate url; extern crate openssl; extern crate cookie; extern crate unicase; +extern crate httparse; #[macro_use] extern crate log; @@ -143,17 +144,11 @@ extern crate test; pub use mimewrapper::mime; pub use url::Url; pub use client::Client; +pub use error::{HttpResult, HttpError}; pub use method::Method::{Get, Head, Post, Delete}; pub use status::StatusCode::{Ok, BadRequest, NotFound}; pub use server::Server; -use std::error::{Error, FromError}; -use std::fmt; -use std::io::Error as IoError; - -use self::HttpError::{HttpMethodError, HttpUriError, HttpVersionError, - HttpHeaderError, HttpStatusError, HttpIoError}; - macro_rules! todo( ($($arg:tt)*) => (if cfg!(not(ndebug)) { trace!("TODO: {:?}", format_args!($($arg)*)) @@ -173,6 +168,7 @@ macro_rules! inspect( mod mock; pub mod client; +pub mod error; pub mod method; pub mod header; pub mod http; @@ -188,66 +184,6 @@ mod mimewrapper { extern crate mime; } - -/// Result type often returned from methods that can have `HttpError`s. -pub type HttpResult = Result; - -/// A set of errors that can occur parsing HTTP streams. -#[derive(Debug, PartialEq, Clone)] -pub enum HttpError { - /// An invalid `Method`, such as `GE,T`. - HttpMethodError, - /// An invalid `RequestUri`, such as `exam ple.domain`. - HttpUriError(url::ParseError), - /// An invalid `HttpVersion`, such as `HTP/1.1` - HttpVersionError, - /// An invalid `Header`. - HttpHeaderError, - /// An invalid `Status`, such as `1337 ELITE`. - HttpStatusError, - /// An `IoError` that occured while trying to read or write to a network stream. - HttpIoError(IoError), -} - -impl fmt::Display for HttpError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(self.description()) - } -} - -impl Error for HttpError { - fn description(&self) -> &str { - match *self { - HttpMethodError => "Invalid Method specified", - HttpUriError(_) => "Invalid Request URI specified", - HttpVersionError => "Invalid HTTP version specified", - HttpHeaderError => "Invalid Header provided", - HttpStatusError => "Invalid Status provided", - HttpIoError(_) => "An IoError occurred while connecting to the specified network", - } - } - - fn cause(&self) -> Option<&Error> { - match *self { - HttpIoError(ref error) => Some(error as &Error), - HttpUriError(ref error) => Some(error as &Error), - _ => None, - } - } -} - -impl FromError for HttpError { - fn from_error(err: IoError) -> HttpError { - HttpIoError(err) - } -} - -impl FromError for HttpError { - fn from_error(err: url::ParseError) -> HttpError { - HttpUriError(err) - } -} - #[allow(unconditional_recursion)] fn _assert_send() { _assert_send::>(); diff --git a/src/method.rs b/src/method.rs index 6fc62ff82b..0ff368b72b 100644 --- a/src/method.rs +++ b/src/method.rs @@ -2,6 +2,7 @@ use std::fmt; use std::str::FromStr; +use error::HttpError; use self::Method::{Options, Get, Post, Put, Delete, Head, Trace, Connect, Patch, Extension}; @@ -68,10 +69,10 @@ impl Method { } impl FromStr for Method { - type Err = (); - fn from_str(s: &str) -> Result { + type Err = HttpError; + fn from_str(s: &str) -> Result { if s == "" { - Err(()) + Err(HttpError::HttpMethodError) } else { Ok(match s { "OPTIONS" => Options, diff --git a/src/net.rs b/src/net.rs index 9f9af4e6b6..5ed153c13e 100644 --- a/src/net.rs +++ b/src/net.rs @@ -66,7 +66,6 @@ pub trait NetworkStream: Read + Write + Any + StreamClone + Send { fn peer_addr(&mut self) -> io::Result; } - #[doc(hidden)] pub trait StreamClone { fn clone_box(&self) -> Box; diff --git a/src/server/mod.rs b/src/server/mod.rs index c7ba8a854b..008af8c41a 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -45,6 +45,7 @@ macro_rules! try_option( ); impl<'a, H: Handler, L: NetworkListener> Server<'a, H, L> { + /// Creates a new server with the provided handler. pub fn new(handler: H) -> Server<'a, H, L> { Server { handler: handler, @@ -97,7 +98,7 @@ S: NetworkStream + Clone + Send> Server<'a, H, L> { debug!("threads = {:?}", threads); let pool = ListenerPool::new(listener.clone()); - let work = move |stream| handle_connection(stream, &handler); + let work = move |stream| keep_alive_loop(stream, &handler); let guard = thread::scoped(move || pool.accept(work, threads)); @@ -109,7 +110,7 @@ S: NetworkStream + Clone + Send> Server<'a, H, L> { } -fn handle_connection(mut stream: S, handler: &H) +fn keep_alive_loop<'h, S, H>(mut stream: S, handler: &'h H) where S: NetworkStream + Clone, H: Handler { debug!("Incoming stream"); let addr = match stream.peer_addr() { @@ -120,36 +121,47 @@ where S: NetworkStream + Clone, H: Handler { } }; - let mut rdr = BufReader::new(stream.clone()); + let mut stream_clone = stream.clone(); + let mut rdr = BufReader::new(&mut stream_clone as &mut NetworkStream); let mut wrt = BufWriter::new(stream); let mut keep_alive = true; while keep_alive { - let mut res = Response::new(&mut wrt); - let req = match Request::new(&mut rdr, addr) { - Ok(req) => req, - Err(e@HttpIoError(_)) => { - debug!("ioerror in keepalive loop = {:?}", e); - return; - } - Err(e) => { - //TODO: send a 400 response - error!("request error = {:?}", e); - return; - } - }; - - keep_alive = match (req.version, req.headers.get::()) { - (Http10, Some(conn)) if !conn.contains(&KeepAlive) => false, - (Http11, Some(conn)) if conn.contains(&Close) => false, - _ => true - }; - res.version = req.version; - handler.handle(req, res); + keep_alive = handle_connection(addr, &mut rdr, &mut wrt, handler); debug!("keep_alive = {:?}", keep_alive); } } +fn handle_connection<'a, 'aa, 'h, S, H>( + addr: SocketAddr, + rdr: &'a mut BufReader<&'aa mut NetworkStream>, + wrt: &mut BufWriter, + handler: &'h H +) -> bool where 'aa: 'a, S: NetworkStream, H: Handler { + let mut res = Response::new(wrt); + let req = match Request::<'a, 'aa>::new(rdr, addr) { + Ok(req) => req, + Err(e@HttpIoError(_)) => { + debug!("ioerror in keepalive loop = {:?}", e); + return false; + } + Err(e) => { + //TODO: send a 400 response + error!("request error = {:?}", e); + return false; + } + }; + + let keep_alive = match (req.version, req.headers.get::()) { + (Http10, Some(conn)) if !conn.contains(&KeepAlive) => false, + (Http11, Some(conn)) if conn.contains(&Close) => false, + _ => true + }; + res.version = req.version; + handler.handle(req, res); + keep_alive +} + /// A listening server, which can later be closed. pub struct Listening { _guard: JoinGuard<'static, ()>, @@ -171,11 +183,11 @@ pub trait Handler: Sync + Send { /// Receives a `Request`/`Response` pair, and should perform some action on them. /// /// This could reading from the request, and writing to the response. - fn handle<'a>(&'a self, Request<'a>, Response<'a, Fresh>); + fn handle<'a, 'aa, 'b, 's>(&'s self, Request<'aa, 'a>, Response<'b, Fresh>); } impl Handler for F where F: Fn(Request, Response), F: Sync + Send { - fn handle(&self, req: Request, res: Response) { - (*self)(req, res) + fn handle<'a, 'aa, 'b, 's>(&'s self, req: Request<'a, 'aa>, res: Response<'b, Fresh>) { + self(req, res) } } diff --git a/src/server/request.rs b/src/server/request.rs index 6647d2a068..aa56c3441a 100644 --- a/src/server/request.rs +++ b/src/server/request.rs @@ -2,20 +2,20 @@ //! //! These are requests that a `hyper::Server` receives, and include its method, //! target URI, headers, and message body. -use std::io::{self, Read}; +use std::io::{self, Read, BufReader}; use std::net::SocketAddr; use {HttpResult}; +use net::NetworkStream; use version::{HttpVersion}; use method::Method::{self, Get, Head}; use header::{Headers, ContentLength, TransferEncoding}; -use http::{read_request_line}; -use http::HttpReader; +use http::{self, Incoming, HttpReader}; use http::HttpReader::{SizedReader, ChunkedReader, EmptyReader}; use uri::RequestUri; /// A request bundles several parts of an incoming `NetworkStream`, given to a `Handler`. -pub struct Request<'a> { +pub struct Request<'a, 'b: 'a> { /// The IP address of the remote connection. pub remote_addr: SocketAddr, /// The `Method`, such as `Get`, `Post`, etc. @@ -26,17 +26,18 @@ pub struct Request<'a> { pub uri: RequestUri, /// The version of HTTP for this request. pub version: HttpVersion, - body: HttpReader<&'a mut (Read + 'a)> + body: HttpReader<&'a mut BufReader<&'b mut NetworkStream>> } -impl<'a> Request<'a> { +impl<'a, 'b: 'a> Request<'a, 'b> { /// Create a new Request, reading the StartLine and Headers so they are /// immediately useful. - pub fn new(mut stream: &'a mut (Read + 'a), addr: SocketAddr) -> HttpResult> { - let (method, uri, version) = try!(read_request_line(&mut stream)); + pub fn new(mut stream: &'a mut BufReader<&'b mut NetworkStream>, addr: SocketAddr) + -> HttpResult> { + + let Incoming { version, subject: (method, uri), headers } = try!(http::parse_request(stream)); debug!("Request Line: {:?} {:?} {:?}", method, uri, version); - let headers = try!(Headers::from_raw(&mut stream)); debug!("{:?}", headers); let body = if method == Get || method == Head { @@ -66,13 +67,13 @@ impl<'a> Request<'a> { /// Deconstruct a Request into its constituent parts. pub fn deconstruct(self) -> (SocketAddr, Method, Headers, RequestUri, HttpVersion, - HttpReader<&'a mut (Read + 'a)>,) { + HttpReader<&'a mut BufReader<&'b mut NetworkStream>>) { (self.remote_addr, self.method, self.headers, self.uri, self.version, self.body) } } -impl<'a> Read for Request<'a> { +impl<'a, 'b> Read for Request<'a, 'b> { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.body.read(buf) } @@ -81,10 +82,11 @@ impl<'a> Read for Request<'a> { #[cfg(test)] mod tests { use header::{Host, TransferEncoding, Encoding}; + use net::NetworkStream; use mock::MockStream; use super::Request; - use std::io::{self, Read}; + use std::io::{self, Read, BufReader}; use std::net::SocketAddr; fn sock(s: &str) -> SocketAddr { @@ -99,25 +101,28 @@ mod tests { #[test] fn test_get_empty_body() { - let mut stream = MockStream::with_input(b"\ + let mut mock = MockStream::with_input(b"\ GET / HTTP/1.1\r\n\ Host: example.domain\r\n\ \r\n\ I'm a bad request.\r\n\ "); + let mut stream = BufReader::new(&mut mock as &mut NetworkStream); + let req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap(); assert_eq!(read_to_string(req), Ok("".to_string())); } #[test] fn test_head_empty_body() { - let mut stream = MockStream::with_input(b"\ + let mut mock = MockStream::with_input(b"\ HEAD / HTTP/1.1\r\n\ Host: example.domain\r\n\ \r\n\ I'm a bad request.\r\n\ "); + let mut stream = BufReader::new(&mut mock as &mut NetworkStream); let req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap(); assert_eq!(read_to_string(req), Ok("".to_string())); @@ -125,12 +130,13 @@ mod tests { #[test] fn test_post_empty_body() { - let mut stream = MockStream::with_input(b"\ + let mut mock = MockStream::with_input(b"\ POST / HTTP/1.1\r\n\ Host: example.domain\r\n\ \r\n\ I'm a bad request.\r\n\ "); + let mut stream = BufReader::new(&mut mock as &mut NetworkStream); let req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap(); assert_eq!(read_to_string(req), Ok("".to_string())); @@ -138,7 +144,7 @@ mod tests { #[test] fn test_parse_chunked_request() { - let mut stream = MockStream::with_input(b"\ + let mut mock = MockStream::with_input(b"\ POST / HTTP/1.1\r\n\ Host: example.domain\r\n\ Transfer-Encoding: chunked\r\n\ @@ -152,6 +158,7 @@ mod tests { 0\r\n\ \r\n" ); + let mut stream = BufReader::new(&mut mock as &mut NetworkStream); let req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap(); @@ -177,7 +184,7 @@ mod tests { /// is returned. #[test] fn test_invalid_chunk_size_not_hex_digit() { - let mut stream = MockStream::with_input(b"\ + let mut mock = MockStream::with_input(b"\ POST / HTTP/1.1\r\n\ Host: example.domain\r\n\ Transfer-Encoding: chunked\r\n\ @@ -187,6 +194,7 @@ mod tests { 0\r\n\ \r\n" ); + let mut stream = BufReader::new(&mut mock as &mut NetworkStream); let req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap(); @@ -197,7 +205,7 @@ mod tests { /// returned. #[test] fn test_invalid_chunk_size_extension() { - let mut stream = MockStream::with_input(b"\ + let mut mock = MockStream::with_input(b"\ POST / HTTP/1.1\r\n\ Host: example.domain\r\n\ Transfer-Encoding: chunked\r\n\ @@ -207,6 +215,7 @@ mod tests { 0\r\n\ \r\n" ); + let mut stream = BufReader::new(&mut mock as &mut NetworkStream); let req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap(); @@ -217,7 +226,7 @@ mod tests { /// the chunk size, the chunk is correctly read. #[test] fn test_chunk_size_with_extension() { - let mut stream = MockStream::with_input(b"\ + let mut mock = MockStream::with_input(b"\ POST / HTTP/1.1\r\n\ Host: example.domain\r\n\ Transfer-Encoding: chunked\r\n\ @@ -227,6 +236,7 @@ mod tests { 0\r\n\ \r\n" ); + let mut stream = BufReader::new(&mut mock as &mut NetworkStream); let req = Request::new(&mut stream, sock("127.0.0.1:80")).unwrap(); diff --git a/src/uri.rs b/src/uri.rs index 03589a2077..a6a2aa344b 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -1,5 +1,9 @@ //! HTTP RequestUris +use std::str::FromStr; use url::Url; +use url::ParseError as UrlError; + +use error::HttpError; /// The Request-URI of a Request's StartLine. /// @@ -45,3 +49,40 @@ pub enum RequestUri { Star, } +impl FromStr for RequestUri { + type Err = HttpError; + + fn from_str(s: &str) -> Result { + match s.as_bytes() { + [] => Err(HttpError::HttpUriError(UrlError::InvalidCharacter)), + [b'*'] => Ok(RequestUri::Star), + [b'/', ..] => Ok(RequestUri::AbsolutePath(s.to_string())), + bytes if bytes.contains(&b'/') => { + Ok(RequestUri::AbsoluteUri(try!(Url::parse(s)))) + } + _ => { + let mut temp = "http://".to_string(); + temp.push_str(s); + try!(Url::parse(&temp[..])); + todo!("compare vs u.authority()"); + Ok(RequestUri::Authority(s.to_string())) + } + + } + } +} + +#[test] +fn test_uri_fromstr() { + use error::HttpResult; + fn read(s: &str, result: HttpResult) { + assert_eq!(s.parse(), result); + } + + read("*", Ok(RequestUri::Star)); + read("http://hyper.rs/", Ok(RequestUri::AbsoluteUri(Url::parse("http://hyper.rs/").unwrap()))); + read("hyper.rs", Ok(RequestUri::Authority("hyper.rs".to_string()))); + read("/", Ok(RequestUri::AbsolutePath("/".to_string()))); +} + +