798 lines
23 KiB
Rust
798 lines
23 KiB
Rust
//! Pieces pertaining to the HTTP message protocol.
|
|
use std::cmp::min;
|
|
use std::fmt;
|
|
use std::io::{mod, Reader, IoResult};
|
|
use std::u16;
|
|
|
|
use url::Url;
|
|
|
|
use method;
|
|
use status;
|
|
use uri;
|
|
use version::{HttpVersion, Http09, Http10, Http11, Http20};
|
|
use {HttpResult, HttpMethodError, HttpVersionError, HttpIoError, HttpUriError};
|
|
use {HttpHeaderError, HttpStatusError};
|
|
|
|
/// Readers to handle different Transfer-Encodings.
|
|
///
|
|
/// If a message body does not include a Transfer-Encoding, it *should*
|
|
/// include a Content-Length header.
|
|
pub enum HttpReader<R> {
|
|
/// A Reader used when a Content-Length header is passed with a positive integer.
|
|
SizedReader(R, uint),
|
|
/// A Reader used when Transfer-Encoding is `chunked`.
|
|
ChunkedReader(R, Option<uint>),
|
|
/// A Reader used for responses that don't indicate a length or chunked.
|
|
/// Note: This should only used for `Response`s. It is illegal for a
|
|
/// `Request` to be made with both `Content-Length` and
|
|
/// `Transfer-Encoding: chunked` missing, as explained from the spec:
|
|
///
|
|
/// > If a Transfer-Encoding header field is present in a response and
|
|
/// > the chunked transfer coding is not the final encoding, the
|
|
/// > message body length is determined by reading the connection until
|
|
/// > it is closed by the server. If a Transfer-Encoding header field
|
|
/// > is present in a request and the chunked transfer coding is not
|
|
/// > the final encoding, the message body length cannot be determined
|
|
/// > reliably; the server MUST respond with the 400 (Bad Request)
|
|
/// > status code and then close the connection.
|
|
EofReader(R),
|
|
}
|
|
|
|
impl<R: Reader> Reader for HttpReader<R> {
|
|
fn read(&mut self, buf: &mut [u8]) -> IoResult<uint> {
|
|
match *self {
|
|
SizedReader(ref mut body, ref mut remaining) => {
|
|
debug!("Sized read, remaining={}", remaining);
|
|
if *remaining == 0 {
|
|
Err(io::standard_error(io::EndOfFile))
|
|
} else {
|
|
let num = try!(body.read(buf));
|
|
if num > *remaining {
|
|
*remaining = 0;
|
|
} else {
|
|
*remaining -= num;
|
|
}
|
|
Ok(num)
|
|
}
|
|
},
|
|
ChunkedReader(ref mut body, ref mut opt_remaining) => {
|
|
let mut rem = match *opt_remaining {
|
|
Some(ref rem) => *rem,
|
|
// None means we don't know the size of the next chunk
|
|
None => try!(read_chunk_size(body))
|
|
};
|
|
debug!("Chunked read, remaining={}", rem);
|
|
|
|
if rem == 0 {
|
|
// chunk of size 0 signals the end of the chunked stream
|
|
// if the 0 digit was missing from the stream, it would
|
|
// be an InvalidInput error instead.
|
|
debug!("end of chunked");
|
|
return Err(io::standard_error(io::EndOfFile));
|
|
}
|
|
|
|
let to_read = min(rem, buf.len());
|
|
let count = try!(body.read(buf.slice_to_mut(to_read)));
|
|
|
|
rem -= count;
|
|
*opt_remaining = if rem > 0 {
|
|
Some(rem)
|
|
} else {
|
|
try!(eat(body, LINE_ENDING));
|
|
None
|
|
};
|
|
Ok(count)
|
|
},
|
|
EofReader(ref mut body) => {
|
|
body.read(buf)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn eat<R: Reader>(rdr: &mut R, bytes: &[u8]) -> IoResult<()> {
|
|
for &b in bytes.iter() {
|
|
match try!(rdr.read_byte()) {
|
|
byte if byte == b => (),
|
|
_ => return Err(io::standard_error(io::InvalidInput))
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Chunked chunks start with 1*HEXDIGIT, indicating the size of the chunk.
|
|
fn read_chunk_size<R: Reader>(rdr: &mut R) -> IoResult<uint> {
|
|
let mut size = 0u;
|
|
let radix = 16;
|
|
let mut in_ext = false;
|
|
loop {
|
|
match try!(rdr.read_byte()) {
|
|
b@b'0'...b'9' if !in_ext => {
|
|
size *= radix;
|
|
size += (b - b'0') as uint;
|
|
},
|
|
b@b'a'...b'f' if !in_ext => {
|
|
size *= radix;
|
|
size += (b + 10 - b'a') as uint;
|
|
},
|
|
b@b'A'...b'F' if !in_ext => {
|
|
size *= radix;
|
|
size += (b + 10 - b'A') as uint;
|
|
},
|
|
CR => {
|
|
match try!(rdr.read_byte()) {
|
|
LF => break,
|
|
_ => return Err(io::standard_error(io::InvalidInput))
|
|
}
|
|
},
|
|
ext => {
|
|
in_ext = true;
|
|
todo!("chunk extension byte={}", ext);
|
|
}
|
|
}
|
|
}
|
|
debug!("chunk size={}", size);
|
|
Ok(size)
|
|
}
|
|
|
|
/// Writers to handle different Transfer-Encodings.
|
|
pub enum HttpWriter<W: Writer> {
|
|
/// A no-op Writer, used initially before Transfer-Encoding is determined.
|
|
ThroughWriter(W),
|
|
/// A Writer for when Transfer-Encoding includes `chunked`.
|
|
ChunkedWriter(W),
|
|
/// A Writer for when Content-Length is set.
|
|
///
|
|
/// Enforces that the body is not longer than the Content-Length header.
|
|
SizedWriter(W, uint),
|
|
}
|
|
|
|
impl<W: Writer> HttpWriter<W> {
|
|
/// Unwraps the HttpWriter and returns the underlying Writer.
|
|
#[inline]
|
|
pub fn unwrap(self) -> W {
|
|
match self {
|
|
ThroughWriter(w) => w,
|
|
ChunkedWriter(w) => w,
|
|
SizedWriter(w, _) => w
|
|
}
|
|
}
|
|
|
|
/// Ends the HttpWriter, and returns the underlying Writer.
|
|
///
|
|
/// A final `write()` is called with an empty message, and then flushed.
|
|
/// The ChunkedWriter variant will use this to write the 0-sized last-chunk.
|
|
#[inline]
|
|
pub fn end(mut self) -> IoResult<W> {
|
|
try!(self.write(&[]));
|
|
try!(self.flush());
|
|
Ok(self.unwrap())
|
|
}
|
|
}
|
|
|
|
impl<W: Writer> Writer for HttpWriter<W> {
|
|
#[inline]
|
|
fn write(&mut self, msg: &[u8]) -> IoResult<()> {
|
|
match *self {
|
|
ThroughWriter(ref mut w) => w.write(msg),
|
|
ChunkedWriter(ref mut w) => {
|
|
let chunk_size = msg.len();
|
|
try!(write!(w, "{:X}{}{}", chunk_size, CR as char, LF as char));
|
|
try!(w.write(msg));
|
|
w.write(LINE_ENDING)
|
|
},
|
|
SizedWriter(ref mut w, ref mut remaining) => {
|
|
let len = msg.len();
|
|
if len > *remaining {
|
|
let len = *remaining;
|
|
*remaining = 0;
|
|
try!(w.write(msg.slice_to(len))); // msg[...len]
|
|
Err(io::standard_error(io::ShortWrite(len)))
|
|
} else {
|
|
*remaining -= len;
|
|
w.write(msg)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn flush(&mut self) -> IoResult<()> {
|
|
match *self {
|
|
ThroughWriter(ref mut w) => w.flush(),
|
|
ChunkedWriter(ref mut w) => w.flush(),
|
|
SizedWriter(ref mut w, _) => w.flush(),
|
|
}
|
|
}
|
|
}
|
|
|
|
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 [u8] = &[CR, LF];
|
|
|
|
/// A `Show`able struct to easily write line endings to a formatter.
|
|
pub struct LineEnding;
|
|
|
|
impl fmt::Show for LineEnding {
|
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
|
fmt.write(LINE_ENDING)
|
|
}
|
|
}
|
|
|
|
impl AsSlice<u8> for LineEnding {
|
|
fn as_slice(&self) -> &[u8] {
|
|
LINE_ENDING
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
}
|
|
|
|
// omg
|
|
enum MethodState {
|
|
MsStart,
|
|
MsG,
|
|
MsGE,
|
|
MsGET,
|
|
MsP,
|
|
MsPO,
|
|
MsPOS,
|
|
MsPOST,
|
|
MsPU,
|
|
MsPUT,
|
|
MsPA,
|
|
MsPAT,
|
|
MsPATC,
|
|
MsPATCH,
|
|
MsH,
|
|
MsHE,
|
|
MsHEA,
|
|
MsHEAD,
|
|
MsD,
|
|
MsDE,
|
|
MsDEL,
|
|
MsDELE,
|
|
MsDELET,
|
|
MsDELETE,
|
|
MsT,
|
|
MsTR,
|
|
MsTRA,
|
|
MsTRAC,
|
|
MsTRACE,
|
|
MsO,
|
|
MsOP,
|
|
MsOPT,
|
|
MsOPTI,
|
|
MsOPTIO,
|
|
MsOPTION,
|
|
MsOPTIONS,
|
|
MsC,
|
|
MsCO,
|
|
MsCON,
|
|
MsCONN,
|
|
MsCONNE,
|
|
MsCONNEC,
|
|
MsCONNECT,
|
|
MsExt
|
|
}
|
|
|
|
// omg
|
|
impl MethodState {
|
|
fn as_slice(&self) -> &str {
|
|
match *self {
|
|
MsG => "G",
|
|
MsGE => "GE",
|
|
MsGET => "GET",
|
|
MsP => "P",
|
|
MsPO => "PO",
|
|
MsPOS => "POS",
|
|
MsPOST => "POST",
|
|
MsPU => "PU",
|
|
MsPUT => "PUT",
|
|
MsPA => "PA",
|
|
MsPAT => "PAT",
|
|
MsPATC => "PATC",
|
|
MsPATCH => "PATCH",
|
|
MsH => "H",
|
|
MsHE => "HE",
|
|
MsHEA => "HEA",
|
|
MsHEAD => "HEAD",
|
|
MsD => "D",
|
|
MsDE => "DE",
|
|
MsDEL => "DEL",
|
|
MsDELE => "DELE",
|
|
MsDELET => "DELET",
|
|
MsDELETE => "DELETE",
|
|
MsT => "T",
|
|
MsTR => "TR",
|
|
MsTRA => "TRA",
|
|
MsTRAC => "TRAC",
|
|
MsTRACE => "TRACE",
|
|
MsO => "O",
|
|
MsOP => "OP",
|
|
MsOPT => "OPT",
|
|
MsOPTI => "OPTI",
|
|
MsOPTIO => "OPTIO",
|
|
MsOPTION => "OPTION",
|
|
MsOPTIONS => "OPTIONS",
|
|
MsC => "C",
|
|
MsCO => "CO",
|
|
MsCON => "CON",
|
|
MsCONN => "CONN",
|
|
MsCONNE => "CONNE",
|
|
MsCONNEC => "CONNEC",
|
|
MsCONNECT => "CONNECT",
|
|
MsStart | MsExt => unreachable!()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Read a `Method` from a raw stream, such as `GET`.
|
|
pub fn read_method<R: Reader>(stream: &mut R) -> HttpResult<method::Method> {
|
|
let mut s = String::new();
|
|
let mut state = MsStart;
|
|
|
|
// omg
|
|
loop {
|
|
match (state, try_io!(stream.read_byte())) {
|
|
(MsStart, b'G') => state = MsG,
|
|
(MsStart, b'P') => state = MsP,
|
|
(MsStart, b'H') => state = MsH,
|
|
(MsStart, b'O') => state = MsO,
|
|
(MsStart, b'T') => state = MsT,
|
|
(MsStart, b'C') => state = MsC,
|
|
(MsStart, b'D') => state = MsD,
|
|
(MsStart, b@b'A'...b'Z') => {
|
|
state = MsExt;
|
|
s.push(b as char)
|
|
},
|
|
|
|
(MsG, b'E') => state = MsGE,
|
|
(MsGE, b'T') => state = MsGET,
|
|
|
|
(MsP, b'O') => state = MsPO,
|
|
(MsPO, b'S') => state = MsPOS,
|
|
(MsPOS, b'T') => state = MsPOST,
|
|
|
|
(MsP, b'U') => state = MsPU,
|
|
(MsPU, b'T') => state = MsPUT,
|
|
|
|
(MsP, b'A') => state = MsPA,
|
|
(MsPA, b'T') => state = MsPAT,
|
|
(MsPAT, b'C') => state = MsPATC,
|
|
(MsPATC, b'H') => state = MsPATCH,
|
|
|
|
(MsH, b'E') => state = MsHE,
|
|
(MsHE, b'A') => state = MsHEA,
|
|
(MsHEA, b'D') => state = MsHEAD,
|
|
|
|
(MsO, b'P') => state = MsOP,
|
|
(MsOP, b'T') => state = MsOPT,
|
|
(MsOPT, b'I') => state = MsOPTI,
|
|
(MsOPTI, b'O') => state = MsOPTIO,
|
|
(MsOPTIO, b'N') => state = MsOPTION,
|
|
(MsOPTION, b'S') => state = MsOPTIONS,
|
|
|
|
(MsT, b'R') => state = MsTR,
|
|
(MsTR, b'A') => state = MsTRA,
|
|
(MsTRA, b'C') => state = MsTRAC,
|
|
(MsTRAC, b'E') => state = MsTRACE,
|
|
|
|
(MsC, b'O') => state = MsCO,
|
|
(MsCO, b'N') => state = MsCON,
|
|
(MsCON, b'N') => state = MsCONN,
|
|
(MsCONN, b'E') => state = MsCONNE,
|
|
(MsCONNE, b'C') => state = MsCONNEC,
|
|
(MsCONNEC, b'T') => state = MsCONNECT,
|
|
|
|
(MsD, b'E') => state = MsDE,
|
|
(MsDE, b'L') => state = MsDEL,
|
|
(MsDEL, b'E') => state = MsDELE,
|
|
(MsDELE, b'T') => state = MsDELET,
|
|
(MsDELET, b'E') => state = MsDELETE,
|
|
|
|
(MsExt, b@b'A'...b'Z') => s.push(b as char),
|
|
|
|
(_, b@b'A'...b'Z') => {
|
|
s = state.as_slice().to_string();
|
|
s.push(b as char);
|
|
},
|
|
|
|
(MsGET, SP) => return Ok(method::Get),
|
|
(MsPOST, SP) => return Ok(method::Post),
|
|
(MsPUT, SP) => return Ok(method::Put),
|
|
(MsPATCH, SP) => return Ok(method::Patch),
|
|
(MsHEAD, SP) => return Ok(method::Head),
|
|
(MsDELETE, SP) => return Ok(method::Delete),
|
|
(MsTRACE, SP) => return Ok(method::Trace),
|
|
(MsOPTIONS, SP) => return Ok(method::Options),
|
|
(MsCONNECT, SP) => return Ok(method::Connect),
|
|
(MsExt, SP) => return Ok(method::Extension(s)),
|
|
|
|
(_, _) => return Err(HttpMethodError)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Read a `RequestUri` from a raw stream.
|
|
pub fn read_uri<R: Reader>(stream: &mut R) -> HttpResult<uri::RequestUri> {
|
|
let mut b = try_io!(stream.read_byte());
|
|
while b == SP {
|
|
b = try_io!(stream.read_byte());
|
|
}
|
|
|
|
let mut s = String::new();
|
|
if b == STAR {
|
|
try!(expect(stream.read_byte(), SP));
|
|
return Ok(uri::Star)
|
|
} else {
|
|
s.push(b as char);
|
|
loop {
|
|
match try_io!(stream.read_byte()) {
|
|
SP => {
|
|
break;
|
|
},
|
|
CR | LF => {
|
|
return Err(HttpUriError)
|
|
},
|
|
b => s.push(b as char)
|
|
}
|
|
}
|
|
}
|
|
|
|
if s.as_slice().starts_with("/") {
|
|
Ok(uri::AbsolutePath(s))
|
|
} else if s.as_slice().contains("/") {
|
|
match Url::parse(s.as_slice()) {
|
|
Ok(u) => Ok(uri::AbsoluteUri(u)),
|
|
Err(_e) => {
|
|
debug!("URL err {}", _e);
|
|
Err(HttpUriError)
|
|
}
|
|
}
|
|
} else {
|
|
let mut temp = "http://".to_string();
|
|
temp.push_str(s.as_slice());
|
|
match Url::parse(temp.as_slice()) {
|
|
Ok(_u) => {
|
|
todo!("compare vs u.authority()");
|
|
Ok(uri::Authority(s))
|
|
}
|
|
Err(_e) => {
|
|
debug!("URL err {}", _e);
|
|
Err(HttpUriError)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
/// Read the `HttpVersion` from a raw stream, such as `HTTP/1.1`.
|
|
pub fn read_http_version<R: Reader>(stream: &mut R) -> HttpResult<HttpVersion> {
|
|
try!(expect(stream.read_byte(), b'H'));
|
|
try!(expect(stream.read_byte(), b'T'));
|
|
try!(expect(stream.read_byte(), b'T'));
|
|
try!(expect(stream.read_byte(), b'P'));
|
|
try!(expect(stream.read_byte(), b'/'));
|
|
|
|
match try_io!(stream.read_byte()) {
|
|
b'0' => {
|
|
try!(expect(stream.read_byte(), b'.'));
|
|
try!(expect(stream.read_byte(), b'9'));
|
|
Ok(Http09)
|
|
},
|
|
b'1' => {
|
|
try!(expect(stream.read_byte(), b'.'));
|
|
match try_io!(stream.read_byte()) {
|
|
b'0' => Ok(Http10),
|
|
b'1' => Ok(Http11),
|
|
_ => Err(HttpVersionError)
|
|
}
|
|
},
|
|
b'2' => {
|
|
try!(expect(stream.read_byte(), b'.'));
|
|
try!(expect(stream.read_byte(), b'0'));
|
|
Ok(Http20)
|
|
},
|
|
_ => Err(HttpVersionError)
|
|
}
|
|
}
|
|
|
|
/// The raw bytes when parsing a header line.
|
|
///
|
|
/// A String and Vec<u8>, divided by COLON (`:`). The String is guaranteed
|
|
/// to be all `token`s. See `is_token_char` source for all valid characters.
|
|
pub type RawHeaderLine = (String, Vec<u8>);
|
|
|
|
/// 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<R: Reader>(stream: &mut R) -> HttpResult<Option<RawHeaderLine>> {
|
|
let mut name = String::new();
|
|
let mut value = vec![];
|
|
|
|
loop {
|
|
match try_io!(stream.read_byte()) {
|
|
CR if name.len() == 0 => {
|
|
match try_io!(stream.read_byte()) {
|
|
LF => return Ok(None),
|
|
_ => return Err(HttpHeaderError)
|
|
}
|
|
},
|
|
b':' => break,
|
|
b if is_token(b) => name.push(b as char),
|
|
_nontoken => return Err(HttpHeaderError)
|
|
};
|
|
}
|
|
|
|
let mut ows = true; //optional whitespace
|
|
|
|
todo!("handle obs-folding (gross!)");
|
|
loop {
|
|
match try_io!(stream.read_byte()) {
|
|
CR => break,
|
|
LF => return Err(HttpHeaderError),
|
|
b' ' if ows => {},
|
|
b => {
|
|
ows = false;
|
|
value.push(b)
|
|
}
|
|
};
|
|
}
|
|
|
|
match try_io!(stream.read_byte()) {
|
|
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<R: Reader>(stream: &mut R) -> HttpResult<RequestLine> {
|
|
let method = try!(read_method(stream));
|
|
let uri = try!(read_uri(stream));
|
|
let version = try!(read_http_version(stream));
|
|
|
|
if try_io!(stream.read_byte()) != CR {
|
|
return Err(HttpVersionError);
|
|
}
|
|
if try_io!(stream.read_byte()) != 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, status::StatusCode);
|
|
|
|
/// 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<R: Reader>(stream: &mut R) -> HttpResult<StatusLine> {
|
|
let version = try!(read_http_version(stream));
|
|
if try_io!(stream.read_byte()) != SP {
|
|
return Err(HttpVersionError);
|
|
}
|
|
let code = try!(read_status(stream));
|
|
|
|
Ok((version, code))
|
|
}
|
|
|
|
/// Read the StatusCode from a stream.
|
|
pub fn read_status<R: Reader>(stream: &mut R) -> HttpResult<status::StatusCode> {
|
|
let code = [
|
|
try_io!(stream.read_byte()),
|
|
try_io!(stream.read_byte()),
|
|
try_io!(stream.read_byte()),
|
|
];
|
|
|
|
let code = match u16::parse_bytes(code.as_slice(), 10) {
|
|
Some(num) => match FromPrimitive::from_u16(num) {
|
|
Some(code) => code,
|
|
None => return Err(HttpStatusError)
|
|
},
|
|
None => return Err(HttpStatusError)
|
|
};
|
|
|
|
// reason is purely for humans, so just consume it till we get to CRLF
|
|
loop {
|
|
match try_io!(stream.read_byte()) {
|
|
CR => match try_io!(stream.read_byte()) {
|
|
LF => break,
|
|
_ => return Err(HttpStatusError)
|
|
},
|
|
_ => ()
|
|
}
|
|
}
|
|
|
|
Ok(code)
|
|
}
|
|
|
|
#[inline]
|
|
fn expect(r: IoResult<u8>, expected: u8) -> HttpResult<()> {
|
|
match r {
|
|
Ok(b) if b == expected => Ok(()),
|
|
Ok(_) => Err(HttpVersionError),
|
|
Err(e) => Err(HttpIoError(e))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::io::{mod, MemReader, MemWriter};
|
|
use test::Bencher;
|
|
use uri::{RequestUri, Star, AbsoluteUri, AbsolutePath, Authority};
|
|
use method;
|
|
use status;
|
|
use version::{HttpVersion, Http10, Http11, Http20};
|
|
use {HttpResult, HttpVersionError};
|
|
use url::Url;
|
|
|
|
use super::{read_method, read_uri, read_http_version, read_header, RawHeaderLine, read_status};
|
|
|
|
fn mem(s: &str) -> MemReader {
|
|
MemReader::new(s.as_bytes().to_vec())
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_method() {
|
|
fn read(s: &str, m: method::Method) {
|
|
assert_eq!(read_method(&mut mem(s)), Ok(m));
|
|
}
|
|
|
|
read("GET /", method::Get);
|
|
read("POST /", method::Post);
|
|
read("PUT /", method::Put);
|
|
read("HEAD /", method::Head);
|
|
read("OPTIONS /", method::Options);
|
|
read("CONNECT /", method::Connect);
|
|
read("TRACE /", method::Trace);
|
|
read("PATCH /", method::Patch);
|
|
read("FOO /", method::Extension("FOO".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_uri() {
|
|
fn read(s: &str, result: HttpResult<RequestUri>) {
|
|
assert_eq!(read_uri(&mut mem(s)), 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<HttpVersion>) {
|
|
assert_eq!(read_http_version(&mut mem(s)), result);
|
|
}
|
|
|
|
read("HTTP/1.0", Ok(Http10));
|
|
read("HTTP/1.1", Ok(Http11));
|
|
read("HTTP/2.0", Ok(Http20));
|
|
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<status::StatusCode>) {
|
|
assert_eq!(read_status(&mut mem(s)), result);
|
|
}
|
|
|
|
read("200 OK\r\n", Ok(status::Ok));
|
|
}
|
|
|
|
#[test]
|
|
fn test_read_header() {
|
|
fn read(s: &str, result: HttpResult<Option<RawHeaderLine>>) {
|
|
assert_eq!(read_header(&mut mem(s)), result);
|
|
}
|
|
|
|
read("Host: rust-lang.org\r\n", Ok(Some(("Host".to_string(),
|
|
"rust-lang.org".as_bytes().to_vec()))));
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_chunked() {
|
|
use std::str::from_utf8;
|
|
let mut w = super::ChunkedWriter(MemWriter::new());
|
|
w.write(b"foo bar").unwrap();
|
|
w.write(b"baz quux herp").unwrap();
|
|
let buf = w.end().unwrap().unwrap();
|
|
let s = from_utf8(buf.as_slice()).unwrap();
|
|
assert_eq!(s, "7\r\nfoo bar\r\nD\r\nbaz quux herp\r\n0\r\n\r\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_sized() {
|
|
use std::str::from_utf8;
|
|
let mut w = super::SizedWriter(MemWriter::new(), 8);
|
|
w.write(b"foo bar").unwrap();
|
|
assert_eq!(w.write(b"baz"), Err(io::standard_error(io::ShortWrite(1))));
|
|
|
|
let buf = w.end().unwrap().unwrap();
|
|
let s = from_utf8(buf.as_slice()).unwrap();
|
|
assert_eq!(s, "foo barb");
|
|
}
|
|
|
|
#[bench]
|
|
fn bench_read_method(b: &mut Bencher) {
|
|
b.bytes = b"CONNECT ".len() as u64;
|
|
b.iter(|| assert_eq!(read_method(&mut mem("CONNECT ")), Ok(method::Connect)));
|
|
}
|
|
|
|
}
|