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()))); +} + +