implementation
This commit is contained in:
14
.travis.yml
Normal file
14
.travis.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
language: rust
|
||||||
|
|
||||||
|
after_success: |
|
||||||
|
[ $TRAVIS_BRANCH = master ] &&
|
||||||
|
[ $TRAVIS_PULL_REQUEST = false ] &&
|
||||||
|
cargo doc &&
|
||||||
|
echo '<meta http-equiv=refresh content=0;url=hyper/index.html>' > target/doc/index.html &&
|
||||||
|
sudo pip install ghp-import &&
|
||||||
|
ghp-import -n target/doc &&
|
||||||
|
git push -fq https://${TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
|
||||||
|
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- secure: kHuPGsSt14Y7TTy+4NeNMQ4yhENXm38OM26G0ZER870QVOQH8cBZk9a9jgA36F8CGkGAMkFJ5lQw5RginQX01zaCev765XqCF8VvToXq9n/Vg8+oxR5LepC1ybY06yd7AuW/znB6cnQ8BB8HJK5FvZJ1PqH+yubzyyada8c/sVQ=
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# hyper
|
# hyper
|
||||||
|
|
||||||
|
[](https://travis-ci.org/seanmonstar/hyper)
|
||||||
|
|
||||||
An HTTP library for Rust.
|
An HTTP library for Rust.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
41
examples/client.rs
Normal file
41
examples/client.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
extern crate hyper;
|
||||||
|
|
||||||
|
use std::os;
|
||||||
|
use std::io::stdout;
|
||||||
|
use std::io::util::copy;
|
||||||
|
|
||||||
|
use hyper::Url;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = os::args();
|
||||||
|
match args.len() {
|
||||||
|
2 => (),
|
||||||
|
_ => {
|
||||||
|
println!("Usage: client <url>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = match Url::parse(args[1].as_slice()) {
|
||||||
|
Ok(url) => {
|
||||||
|
println!("GET {}...", url)
|
||||||
|
url
|
||||||
|
},
|
||||||
|
Err(e) => fail!("Invalid URL: {}", e)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let req = match hyper::get(url) {
|
||||||
|
Ok(req) => req,
|
||||||
|
Err(err) => fail!("Failed to connect: {}", err)
|
||||||
|
};
|
||||||
|
let mut res = req.send().unwrap();
|
||||||
|
|
||||||
|
println!("Response: {}", res.status);
|
||||||
|
println!("{}", res.headers);
|
||||||
|
match copy(&mut res, &mut stdout()) {
|
||||||
|
Ok(..) => (),
|
||||||
|
Err(e) => fail!("Stream failure: {}", e)
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
40
examples/server.rs
Normal file
40
examples/server.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
extern crate hyper;
|
||||||
|
extern crate debug;
|
||||||
|
|
||||||
|
use std::io::{IoResult};
|
||||||
|
use std::io::util::copy;
|
||||||
|
use std::io::net::ip::Ipv4Addr;
|
||||||
|
|
||||||
|
use hyper::method::{Get, Post};
|
||||||
|
use hyper::server::{Server, Handler, Request, Response};
|
||||||
|
|
||||||
|
struct Echo;
|
||||||
|
|
||||||
|
impl Handler for Echo {
|
||||||
|
fn handle(&mut self, mut req: Request, mut res: Response) -> IoResult<()> {
|
||||||
|
match &req.uri {
|
||||||
|
&hyper::uri::AbsolutePath(ref path) => match (&req.method, path.as_slice()) {
|
||||||
|
(&Get, "/") | (&Get, "/echo") => {
|
||||||
|
try!(res.write_str("Try POST /echo"));
|
||||||
|
return res.end();
|
||||||
|
},
|
||||||
|
(&Post, "/echo") => (), // fall through, fighting mutable borrows
|
||||||
|
_ => {
|
||||||
|
res.status = hyper::status::NotFound;
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => return res.end()
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("copying...");
|
||||||
|
try!(copy(&mut req, &mut res));
|
||||||
|
println!("copied...");
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let server = Server::http(Ipv4Addr(127, 0, 0, 1), 1337);
|
||||||
|
server.listen(Echo);
|
||||||
|
}
|
||||||
22
src/client/mod.rs
Normal file
22
src/client/mod.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
//! # Client
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use method::{Get, Method};
|
||||||
|
|
||||||
|
pub use self::request::Request;
|
||||||
|
pub use self::response::Response;
|
||||||
|
use {HttpResult};
|
||||||
|
|
||||||
|
pub mod request;
|
||||||
|
pub mod response;
|
||||||
|
|
||||||
|
|
||||||
|
/// Create a GET client request.
|
||||||
|
pub fn get(url: Url) -> HttpResult<Request> {
|
||||||
|
request(Get, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a client request.
|
||||||
|
pub fn request(method: Method, url: Url) -> HttpResult<Request> {
|
||||||
|
Request::new(method, url)
|
||||||
|
}
|
||||||
106
src/client/request.rs
Normal file
106
src/client/request.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
//! # Client Requests
|
||||||
|
use std::io::net::tcp::TcpStream;
|
||||||
|
use std::io::IoResult;
|
||||||
|
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use method;
|
||||||
|
use header::{Headers, Host};
|
||||||
|
use rfc7230::LINE_ENDING;
|
||||||
|
use version;
|
||||||
|
use {HttpResult, HttpUriError};
|
||||||
|
use super::{Response};
|
||||||
|
|
||||||
|
|
||||||
|
/// A client request to a remote server.
|
||||||
|
pub struct Request {
|
||||||
|
/// The method of this request.
|
||||||
|
pub method: method::Method,
|
||||||
|
/// The headers that will be sent with this request.
|
||||||
|
pub headers: Headers,
|
||||||
|
/// The target URI for this request.
|
||||||
|
pub url: Url,
|
||||||
|
/// The HTTP version of this request.
|
||||||
|
pub version: version::HttpVersion,
|
||||||
|
headers_written: bool,
|
||||||
|
body: TcpStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
|
||||||
|
/// Create a new client request.
|
||||||
|
pub fn new(method: method::Method, url: Url) -> HttpResult<Request> {
|
||||||
|
debug!("{} {}", method, url);
|
||||||
|
let host = match url.serialize_host() {
|
||||||
|
Some(host) => host,
|
||||||
|
None => return Err(HttpUriError)
|
||||||
|
};
|
||||||
|
debug!("host={}", host);
|
||||||
|
let port = match url.port_or_default() {
|
||||||
|
Some(port) => port,
|
||||||
|
None => return Err(HttpUriError)
|
||||||
|
};
|
||||||
|
debug!("port={}", port);
|
||||||
|
|
||||||
|
let stream = try_io!(TcpStream::connect(host.as_slice(), port));
|
||||||
|
let mut headers = Headers::new();
|
||||||
|
headers.set(Host(host));
|
||||||
|
Ok(Request {
|
||||||
|
method: method,
|
||||||
|
headers: headers,
|
||||||
|
url: url,
|
||||||
|
version: version::Http11,
|
||||||
|
headers_written: false,
|
||||||
|
body: stream
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_head(&mut self) -> IoResult<()> {
|
||||||
|
if self.headers_written {
|
||||||
|
debug!("headers previsouly written, nooping");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.headers_written = true;
|
||||||
|
|
||||||
|
let uri = self.url.serialize_path().unwrap();
|
||||||
|
debug!("writing head: {} {} {}", self.method, uri, self.version);
|
||||||
|
try!(write!(self.body, "{} {} {}", self.method, uri, self.version))
|
||||||
|
try!(self.body.write(LINE_ENDING));
|
||||||
|
|
||||||
|
debug!("{}", self.headers);
|
||||||
|
|
||||||
|
for (name, header) in self.headers.iter() {
|
||||||
|
try!(write!(self.body, "{}: {}", name, header));
|
||||||
|
try!(self.body.write(LINE_ENDING));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.body.write(LINE_ENDING)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Completes writing the request, and returns a response to read from.
|
||||||
|
///
|
||||||
|
/// Consumes the Request.
|
||||||
|
pub fn send(mut self) -> HttpResult<Response> {
|
||||||
|
try_io!(self.flush());
|
||||||
|
try_io!(self.body.close_write());
|
||||||
|
Response::new(self.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Writer for Request {
|
||||||
|
fn write(&mut self, msg: &[u8]) -> IoResult<()> {
|
||||||
|
if !self.headers_written {
|
||||||
|
try!(self.write_head());
|
||||||
|
}
|
||||||
|
self.body.write(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> IoResult<()> {
|
||||||
|
if !self.headers_written {
|
||||||
|
try!(self.write_head());
|
||||||
|
}
|
||||||
|
self.body.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
71
src/client/response.rs
Normal file
71
src/client/response.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//! # Client Responses
|
||||||
|
use std::io::{Reader, IoResult};
|
||||||
|
use std::io::net::tcp::TcpStream;
|
||||||
|
|
||||||
|
use header::{mod, ContentLength, TransferEncoding, Chunked};
|
||||||
|
use rfc7230::{read_status_line, HttpReader, SizedReader, ChunkedReader, EofReader};
|
||||||
|
use status;
|
||||||
|
use version;
|
||||||
|
use {HttpResult};
|
||||||
|
|
||||||
|
/// A response for a client request to a remote server.
|
||||||
|
pub struct Response {
|
||||||
|
/// The status from the server.
|
||||||
|
pub status: status::StatusCode,
|
||||||
|
/// The headers from the server.
|
||||||
|
pub headers: header::Headers,
|
||||||
|
/// The HTTP version of this response from the server.
|
||||||
|
pub version: version::HttpVersion,
|
||||||
|
body: HttpReader<TcpStream>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
|
||||||
|
/// Creates a new response from a server.
|
||||||
|
pub fn new(mut tcp: TcpStream) -> HttpResult<Response> {
|
||||||
|
let (version, status) = try!(read_status_line(&mut tcp));
|
||||||
|
let mut headers = try!(header::Headers::from_raw(&mut tcp));
|
||||||
|
|
||||||
|
debug!("{} {}", version, status);
|
||||||
|
debug!("{}", headers);
|
||||||
|
|
||||||
|
let body = if headers.has::<TransferEncoding>() {
|
||||||
|
match headers.get_ref::<TransferEncoding>() {
|
||||||
|
Some(&TransferEncoding(ref codings)) => {
|
||||||
|
if codings.len() > 1 {
|
||||||
|
debug!("TODO: handle other codings: {}", codings);
|
||||||
|
};
|
||||||
|
|
||||||
|
if codings.contains(&Chunked) {
|
||||||
|
ChunkedReader(tcp, None)
|
||||||
|
} else {
|
||||||
|
debug!("not chucked. read till eof");
|
||||||
|
EofReader(tcp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => unreachable!()
|
||||||
|
}
|
||||||
|
} else if headers.has::<ContentLength>() {
|
||||||
|
match headers.get_ref::<ContentLength>() {
|
||||||
|
Some(&ContentLength(len)) => SizedReader(tcp, len),
|
||||||
|
None => unreachable!()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("neither Transfer-Encoding nor Content-Length");
|
||||||
|
EofReader(tcp)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Response {
|
||||||
|
status: status,
|
||||||
|
version: version,
|
||||||
|
headers: headers,
|
||||||
|
body: body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reader for Response {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> IoResult<uint> {
|
||||||
|
self.body.read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
532
src/header.rs
Normal file
532
src/header.rs
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
//! # Headers
|
||||||
|
//!
|
||||||
|
//! hyper has the opinion that Headers should be strongly-typed, because that's
|
||||||
|
//! why we're using Rust in the first place. To set or get any header, an object
|
||||||
|
//! must implement the `Header` trait from this module. Several common headers
|
||||||
|
//! are already provided, such as `Host`, `ContentType`, `UserAgent`, and others.
|
||||||
|
//!
|
||||||
|
//! ## Mime
|
||||||
|
//!
|
||||||
|
//! Several header fields use MIME values for their contents. Keeping with the
|
||||||
|
//! strongly-typed theme, the [mime](http://seanmonstar.github.io/mime.rs) crate
|
||||||
|
//! is used, such as `ContentType(pub Mime)`.
|
||||||
|
use std::any::Any;
|
||||||
|
use std::ascii::OwnedAsciiExt;
|
||||||
|
use std::char::is_lowercase;
|
||||||
|
use std::fmt::{mod, Show};
|
||||||
|
use std::from_str::{FromStr, from_str};
|
||||||
|
use std::mem::{transmute, transmute_copy};
|
||||||
|
use std::raw::TraitObject;
|
||||||
|
use std::str::{from_utf8, SendStr, Slice};
|
||||||
|
use std::string::raw;
|
||||||
|
use std::collections::hashmap::{HashMap, Entries};
|
||||||
|
|
||||||
|
use mime::Mime;
|
||||||
|
|
||||||
|
use rfc7230::read_header;
|
||||||
|
use {HttpResult};
|
||||||
|
|
||||||
|
/// A trait for any object that will represent a header field and value.
|
||||||
|
pub trait Header: Any {
|
||||||
|
/// Returns the name of the header field this belongs to.
|
||||||
|
fn header_name(marker: Option<Self>) -> SendStr;
|
||||||
|
/// Parse a header from a raw stream of bytes.
|
||||||
|
///
|
||||||
|
/// It's possible that a request can include a header field more than once,
|
||||||
|
/// and in that case, the slice will have a length greater than 1. However,
|
||||||
|
/// it's not necessarily the case that a Header is *allowed* to have more
|
||||||
|
/// than one field value. If that's the case, you **should** return `None`
|
||||||
|
/// if `raw.len() > 1`.
|
||||||
|
fn parse_header(raw: &[Vec<u8>]) -> Option<Self>;
|
||||||
|
/// Format a header to be output into a TcpStream.
|
||||||
|
fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A trait for downcasting owned TraitObjects to another type without checking
|
||||||
|
/// `AnyRefExt::is::<T>(t)` first.
|
||||||
|
trait UncheckedAnyRefExt<'a> {
|
||||||
|
/// This will downcast an object to another type without checking that it is
|
||||||
|
/// legal. Do not call this unless you are ABSOLUTE SURE of the types.
|
||||||
|
unsafe fn downcast_ref_unchecked<T: 'static>(self) -> &'a T;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> UncheckedAnyRefExt<'a> for &'a Header {
|
||||||
|
#[inline]
|
||||||
|
unsafe fn downcast_ref_unchecked<T: 'static>(self) -> &'a T {
|
||||||
|
let to: TraitObject = transmute_copy(&self);
|
||||||
|
transmute(to.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header_name<T: Header>() -> SendStr {
|
||||||
|
let name = Header::header_name(None::<T>);
|
||||||
|
debug_assert!(name.as_slice().chars().all(|c| c == '-' || is_lowercase(c)),
|
||||||
|
"Header names should be lowercase: {}", name);
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A map of header fields on requests and responses.
|
||||||
|
pub struct Headers {
|
||||||
|
data: HashMap<SendStr, Item>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Headers {
|
||||||
|
|
||||||
|
/// Creates a new, empty headers map.
|
||||||
|
pub fn new() -> Headers {
|
||||||
|
Headers {
|
||||||
|
data: HashMap::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn from_raw<R: Reader>(rdr: &mut R) -> HttpResult<Headers> {
|
||||||
|
let mut headers = Headers::new();
|
||||||
|
loop {
|
||||||
|
match try!(read_header(rdr)) {
|
||||||
|
Some((name, value)) => {
|
||||||
|
// read_header already checks that name is a token, which
|
||||||
|
// means its safe utf8
|
||||||
|
let name = unsafe {
|
||||||
|
raw::from_utf8(name)
|
||||||
|
}.into_ascii_lower().into_maybe_owned();
|
||||||
|
match headers.data.find_or_insert(name, Raw(vec![])) {
|
||||||
|
&Raw(ref mut pieces) => pieces.push(value),
|
||||||
|
// at this point, Raw is the only thing that has been inserted
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a header field to the corresponding value.
|
||||||
|
///
|
||||||
|
/// The field is determined by the type of the value being set.
|
||||||
|
pub fn set<H: Header + 'static>(&mut self, value: H) {
|
||||||
|
self.data.insert(header_name::<H>(), Typed(box value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a clone of the header field's value, if it exists.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hyper::header::{Headers, ContentType};
|
||||||
|
/// # let mut headers = Headers::new();
|
||||||
|
/// let content_type = headers.get::<ContentType>();
|
||||||
|
/// ```
|
||||||
|
pub fn get<H: Header + Clone + 'static>(&mut self) -> Option<H> {
|
||||||
|
self.get_ref().map(|v: &H| v.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the header field's value, if it exists.
|
||||||
|
pub fn get_ref<H: Header + 'static>(&mut self) -> Option<&H> {
|
||||||
|
self.data.find_mut(&header_name::<H>()).and_then(|item| {
|
||||||
|
debug!("get_ref, name={}, val={}", header_name::<H>(), item);
|
||||||
|
let header = match *item {
|
||||||
|
Raw(ref raw) => match Header::parse_header(raw.as_slice()) {
|
||||||
|
Some::<H>(h) => {
|
||||||
|
h
|
||||||
|
},
|
||||||
|
None => return None
|
||||||
|
},
|
||||||
|
Typed(..) => return Some(item)
|
||||||
|
};
|
||||||
|
*item = Typed(box header as Box<Header + 'static>);
|
||||||
|
Some(item)
|
||||||
|
}).and_then(|item| {
|
||||||
|
debug!("downcasting {}", item);
|
||||||
|
let ret = match *item {
|
||||||
|
Typed(ref val) => {
|
||||||
|
unsafe {
|
||||||
|
Some(val.downcast_ref_unchecked())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Raw(..) => unreachable!()
|
||||||
|
};
|
||||||
|
debug!("returning {}", ret.is_some());
|
||||||
|
ret
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a boolean of whether a certain header is in the map.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hyper::header::{Headers, ContentType};
|
||||||
|
/// # let mut headers = Headers::new();
|
||||||
|
/// let has_type = headers.has::<ContentType>();
|
||||||
|
/// ```
|
||||||
|
pub fn has<H: Header + 'static>(&self) -> bool {
|
||||||
|
self.data.contains_key(&header_name::<H>())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a header from the map, if one existed. Returns true if a header has been removed.
|
||||||
|
pub fn remove<H: Header>(&mut self) -> bool {
|
||||||
|
self.data.remove(&Header::header_name(None::<H>))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over the header fields.
|
||||||
|
pub fn iter<'a>(&'a self) -> HeadersItems<'a> {
|
||||||
|
HeadersItems {
|
||||||
|
inner: self.data.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Show for Headers {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
try!("Headers {\n".fmt(fmt));
|
||||||
|
for (k, v) in self.iter() {
|
||||||
|
try!(write!(fmt, "\t{}: {}\n", k, v));
|
||||||
|
}
|
||||||
|
"}".fmt(fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An `Iterator` over the fields in a `Headers` map.
|
||||||
|
pub struct HeadersItems<'a> {
|
||||||
|
inner: Entries<'a, SendStr, Item>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator<(&'a SendStr, HeaderView<'a>)> for HeadersItems<'a> {
|
||||||
|
fn next(&mut self) -> Option<(&'a SendStr, HeaderView<'a>)> {
|
||||||
|
match self.inner.next() {
|
||||||
|
Some((k, v)) => Some((k, HeaderView(v))),
|
||||||
|
None => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returned with the `HeadersItems` iterator.
|
||||||
|
pub struct HeaderView<'a>(&'a Item);
|
||||||
|
|
||||||
|
impl<'a> fmt::Show for HeaderView<'a> {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let HeaderView(item) = *self;
|
||||||
|
item.fmt(fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collection for Headers {
|
||||||
|
fn len(&self) -> uint {
|
||||||
|
self.data.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mutable for Headers {
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.data.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Item {
|
||||||
|
Raw(Vec<Vec<u8>>),
|
||||||
|
Typed(Box<Header + 'static>)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Show for Item {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Raw(ref v) => {
|
||||||
|
for part in v.iter() {
|
||||||
|
try!(fmt.write(part.as_slice()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
Typed(ref h) => h.fmt_header(fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// common headers
|
||||||
|
|
||||||
|
/// The `Host` header.
|
||||||
|
///
|
||||||
|
/// HTTP/1.1 requires that all requests include a `Host` header, and so hyper
|
||||||
|
/// client requests add one automatically.
|
||||||
|
///
|
||||||
|
/// Currently is just a String, but it should probably become a better type,
|
||||||
|
/// like url::Host or something.
|
||||||
|
#[deriving(Clone, PartialEq, Show)]
|
||||||
|
pub struct Host(pub String);
|
||||||
|
|
||||||
|
impl Header for Host {
|
||||||
|
fn header_name(_: Option<Host>) -> SendStr {
|
||||||
|
Slice("host")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_header(raw: &[Vec<u8>]) -> Option<Host> {
|
||||||
|
from_one_raw_str(raw).map(|s| Host(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let Host(ref value) = *self;
|
||||||
|
value.fmt(fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `Content-Length` header.
|
||||||
|
///
|
||||||
|
/// Simply a wrapper around a `uint`.
|
||||||
|
#[deriving(Clone, PartialEq, Show)]
|
||||||
|
pub struct ContentLength(pub uint);
|
||||||
|
|
||||||
|
impl Header for ContentLength {
|
||||||
|
fn header_name(_: Option<ContentLength>) -> SendStr {
|
||||||
|
Slice("content-length")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_header(raw: &[Vec<u8>]) -> Option<ContentLength> {
|
||||||
|
from_one_raw_str(raw).map(|u| ContentLength(u))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let ContentLength(ref value) = *self;
|
||||||
|
value.fmt(fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `Content-Type` header.
|
||||||
|
///
|
||||||
|
/// Used to describe the MIME type of message body. Can be used with both
|
||||||
|
/// requests and responses.
|
||||||
|
#[deriving(Clone, PartialEq, Show)]
|
||||||
|
pub struct ContentType(pub Mime);
|
||||||
|
|
||||||
|
impl Header for ContentType {
|
||||||
|
fn header_name(_: Option<ContentType>) -> SendStr {
|
||||||
|
Slice("content-type")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_header(raw: &[Vec<u8>]) -> Option<ContentType> {
|
||||||
|
from_one_raw_str(raw).map(|mime| ContentType(mime))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let ContentType(ref value) = *self;
|
||||||
|
value.fmt(fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `Accept` header.
|
||||||
|
///
|
||||||
|
/// The `Accept` header is used to tell a server which content-types the client
|
||||||
|
/// is capable of using. It can be a comma-separated list of `Mime`s, and the
|
||||||
|
/// priority can be indicated with a `q` parameter.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hyper::header::{Headers, Accept};
|
||||||
|
/// use hyper::mime::{Mime, Text, Html, Xml};
|
||||||
|
/// # let mut headers = Headers::new();
|
||||||
|
/// headers.set(Accept(vec![ Mime(Text, Html, vec![]), Mime(Text, Xml, vec![]) ]));
|
||||||
|
/// ```
|
||||||
|
#[deriving(Clone, PartialEq, Show)]
|
||||||
|
pub struct Accept(pub Vec<Mime>);
|
||||||
|
|
||||||
|
impl Header for Accept {
|
||||||
|
fn header_name(_: Option<Accept>) -> SendStr {
|
||||||
|
Slice("accept")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_header(_raw: &[Vec<u8>]) -> Option<Accept> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let Accept(ref value) = *self;
|
||||||
|
let last = value.len() - 1;
|
||||||
|
for (i, mime) in value.iter().enumerate() {
|
||||||
|
try!(mime.fmt(fmt));
|
||||||
|
if i < last {
|
||||||
|
try!(", ".fmt(fmt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `Connection` header.
|
||||||
|
///
|
||||||
|
/// Describes whether the socket connection should be closed or reused after
|
||||||
|
/// this request/response is completed.
|
||||||
|
#[deriving(Clone, PartialEq, Show)]
|
||||||
|
pub enum Connection {
|
||||||
|
/// The `keep-alive` connection value.
|
||||||
|
KeepAlive,
|
||||||
|
/// The `close` connection value.
|
||||||
|
Close
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Connection {
|
||||||
|
fn from_str(s: &str) -> Option<Connection> {
|
||||||
|
debug!("Connection::from_str =? {}", s);
|
||||||
|
match s {
|
||||||
|
"keep-alive" => Some(KeepAlive),
|
||||||
|
"close" => Some(Close),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Header for Connection {
|
||||||
|
fn header_name(_: Option<Connection>) -> SendStr {
|
||||||
|
Slice("connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_header(raw: &[Vec<u8>]) -> Option<Connection> {
|
||||||
|
from_one_raw_str(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
KeepAlive => "keep-alive",
|
||||||
|
Close => "close",
|
||||||
|
}.fmt(fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `Transfer-Encoding` header.
|
||||||
|
///
|
||||||
|
/// This header describes the encoding of the message body. It can be
|
||||||
|
/// comma-separated, including multiple encodings.
|
||||||
|
///
|
||||||
|
/// ```notrust
|
||||||
|
/// Transfer-Encoding: gzip, chunked
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// According to the spec, if a `Content-Length` header is not included,
|
||||||
|
/// this header should include `chunked` as the last encoding.
|
||||||
|
///
|
||||||
|
/// The implementation uses a vector of `Encoding` values.
|
||||||
|
#[deriving(Clone, PartialEq, Show)]
|
||||||
|
pub struct TransferEncoding(pub Vec<Encoding>);
|
||||||
|
|
||||||
|
/// A value to be used with the `Transfer-Encoding` header.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use hyper::header::{Headers, TransferEncoding, Gzip, Chunked};
|
||||||
|
/// # let mut headers = Headers::new();
|
||||||
|
/// headers.set(TransferEncoding(vec![Gzip, Chunked]));
|
||||||
|
#[deriving(Clone, PartialEq, Show)]
|
||||||
|
pub enum Encoding {
|
||||||
|
/// The `chunked` encoding.
|
||||||
|
Chunked,
|
||||||
|
|
||||||
|
// TODO: implement this in `HttpReader`.
|
||||||
|
/// The `gzip` encoding.
|
||||||
|
Gzip,
|
||||||
|
/// The `deflate` encoding.
|
||||||
|
Deflate,
|
||||||
|
/// The `compress` encoding.
|
||||||
|
Compress,
|
||||||
|
/// Some other encoding that is less common, can be any String.
|
||||||
|
EncodingExt(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Encoding {
|
||||||
|
fn from_str(s: &str) -> Option<Encoding> {
|
||||||
|
match s {
|
||||||
|
"chunked" => Some(Chunked),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Header for TransferEncoding {
|
||||||
|
fn header_name(_: Option<TransferEncoding>) -> SendStr {
|
||||||
|
Slice("transfer-encoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_header(raw: &[Vec<u8>]) -> Option<TransferEncoding> {
|
||||||
|
if raw.len() != 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// we JUST checked that raw.len() == 1, so raw[0] WILL exist.
|
||||||
|
match from_utf8(unsafe { raw.as_slice().unsafe_get(0).as_slice() }) {
|
||||||
|
Some(s) => {
|
||||||
|
Some(TransferEncoding(s.as_slice()
|
||||||
|
.split(&[',', ' '])
|
||||||
|
.filter_map(from_str)
|
||||||
|
.collect()))
|
||||||
|
}
|
||||||
|
None => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let TransferEncoding(ref parts) = *self;
|
||||||
|
let last = parts.len() - 1;
|
||||||
|
for (i, part) in parts.iter().enumerate() {
|
||||||
|
try!(part.fmt(fmt));
|
||||||
|
if i < last {
|
||||||
|
try!(", ".fmt(fmt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `User-Agent` header field.
|
||||||
|
///
|
||||||
|
/// They can contain any value, so it just wraps a `String`.
|
||||||
|
#[deriving(Clone, PartialEq, Show)]
|
||||||
|
pub struct UserAgent(pub String);
|
||||||
|
|
||||||
|
impl Header for UserAgent {
|
||||||
|
fn header_name(_: Option<UserAgent>) -> SendStr {
|
||||||
|
Slice("user-agent")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_header(raw: &[Vec<u8>]) -> Option<UserAgent> {
|
||||||
|
from_one_raw_str(raw).map(|s| UserAgent(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_header(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let UserAgent(ref value) = *self;
|
||||||
|
value.fmt(fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_one_raw_str<T: FromStr>(raw: &[Vec<u8>]) -> Option<T> {
|
||||||
|
if raw.len() != 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// we JUST checked that raw.len() == 1, so raw[0] WILL exist.
|
||||||
|
match from_utf8(unsafe { raw.as_slice().unsafe_get(0).as_slice() }) {
|
||||||
|
Some(s) => FromStr::from_str(s),
|
||||||
|
None => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io::MemReader;
|
||||||
|
use mime::{Mime, Text, Plain};
|
||||||
|
use super::{Headers, Header, ContentLength, ContentType};
|
||||||
|
|
||||||
|
fn mem(s: &str) -> MemReader {
|
||||||
|
MemReader::new(s.as_bytes().to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_raw() {
|
||||||
|
let mut headers = Headers::from_raw(&mut mem("Content-Length: 10\r\n\r\n")).unwrap();
|
||||||
|
assert_eq!(headers.get_ref(), Some(&ContentLength(10)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_content_type() {
|
||||||
|
let content_type = Header::parse_header(["text/plain".as_bytes().to_vec()].as_slice());
|
||||||
|
assert_eq!(content_type, Some(ContentType(Mime(Text, Plain, vec![]))));
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/lib.rs
Normal file
77
src/lib.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
//! # hyper
|
||||||
|
#![feature(macro_rules, phase)]
|
||||||
|
#![warn(missing_doc)]
|
||||||
|
#![deny(warnings)]
|
||||||
|
#![experimental]
|
||||||
|
|
||||||
|
extern crate url;
|
||||||
|
#[phase(plugin,link)] extern crate log;
|
||||||
|
#[cfg(test)] extern crate test;
|
||||||
|
|
||||||
|
pub use std::io::net::ip::{SocketAddr, IpAddr, Ipv4Addr, Ipv6Addr, Port};
|
||||||
|
pub use mimewrapper::mime;
|
||||||
|
pub use url::Url;
|
||||||
|
pub use client::{get};
|
||||||
|
pub use server::Server;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::io::IoError;
|
||||||
|
|
||||||
|
use std::rt::backtrace;
|
||||||
|
|
||||||
|
|
||||||
|
macro_rules! try_io(
|
||||||
|
($e:expr) => (match $e { Ok(v) => v, Err(e) => return Err(::HttpIoError(e)) })
|
||||||
|
)
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct Trace;
|
||||||
|
|
||||||
|
impl fmt::Show for Trace {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let _ = backtrace::write(fmt);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! trace(
|
||||||
|
($($arg:tt)*) => (if cfg!(not(ndebug)) {
|
||||||
|
format_args!(|args| log!(5, "{}\n{}", args, ::Trace), $($arg)*)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
pub mod client;
|
||||||
|
pub mod method;
|
||||||
|
pub mod header;
|
||||||
|
pub mod server;
|
||||||
|
pub mod status;
|
||||||
|
pub mod uri;
|
||||||
|
pub mod version;
|
||||||
|
|
||||||
|
mod rfc7230;
|
||||||
|
|
||||||
|
mod mimewrapper {
|
||||||
|
extern crate mime;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Result type often returned from methods that can have `HttpError`s.
|
||||||
|
pub type HttpResult<T> = Result<T, HttpError>;
|
||||||
|
|
||||||
|
/// A set of errors that can occur parsing HTTP streams.
|
||||||
|
#[deriving(Show, PartialEq, Clone)]
|
||||||
|
pub enum HttpError {
|
||||||
|
/// An invalid `Method`, such as `GE,T`.
|
||||||
|
HttpMethodError,
|
||||||
|
/// An invalid `RequestUri`, such as `exam ple.domain`.
|
||||||
|
HttpUriError,
|
||||||
|
/// 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),
|
||||||
|
}
|
||||||
|
|
||||||
99
src/method.rs
Normal file
99
src/method.rs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
//! # HTTP Method
|
||||||
|
use std::fmt;
|
||||||
|
use std::from_str::FromStr;
|
||||||
|
|
||||||
|
/// The Request Method (VERB)
|
||||||
|
///
|
||||||
|
/// Currently includes 8 variants representing the 8 methods defined in
|
||||||
|
/// [RFC 7230](https://tools.ietf.org/html/rfc7231#section-4.1), plus PATCH,
|
||||||
|
/// and an Extension variant for all extensions.
|
||||||
|
///
|
||||||
|
/// It may make sense to grow this to include all variants currently
|
||||||
|
/// registered with IANA, if they are at all common to use.
|
||||||
|
#[deriving(Clone, PartialEq)]
|
||||||
|
pub enum Method {
|
||||||
|
/// OPTIONS
|
||||||
|
Options,
|
||||||
|
/// GET
|
||||||
|
Get,
|
||||||
|
/// POST
|
||||||
|
Post,
|
||||||
|
/// PUT
|
||||||
|
Put,
|
||||||
|
/// DELETE
|
||||||
|
Delete,
|
||||||
|
/// HEAD
|
||||||
|
Head,
|
||||||
|
/// TRACE
|
||||||
|
Trace,
|
||||||
|
/// CONNECT
|
||||||
|
Connect,
|
||||||
|
/// PATCH
|
||||||
|
Patch,
|
||||||
|
/// Method extentions. An example would be `let m = Extension("FOO".to_string())`.
|
||||||
|
Extension(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Method {
|
||||||
|
/// Whether a method is considered "safe", meaning the request is
|
||||||
|
/// essentially read-only.
|
||||||
|
///
|
||||||
|
/// See [the spec](https://tools.ietf.org/html/rfc7231#section-4.2.1)
|
||||||
|
/// for more words.
|
||||||
|
pub fn safe(&self) -> bool {
|
||||||
|
match *self {
|
||||||
|
Get | Head | Options | Trace => true,
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a method is considered "idempotent", meaning the request has
|
||||||
|
/// the same result is executed multiple times.
|
||||||
|
///
|
||||||
|
/// See [the spec](https://tools.ietf.org/html/rfc7231#section-4.2.2) for
|
||||||
|
/// more words.
|
||||||
|
pub fn idempotent(&self) -> bool {
|
||||||
|
if self.safe() {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
match *self {
|
||||||
|
Put | Delete => true,
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Method {
|
||||||
|
fn from_str(s: &str) -> Option<Method> {
|
||||||
|
Some(match s {
|
||||||
|
"OPTIONS" => Options,
|
||||||
|
"GET" => Get,
|
||||||
|
"POST" => Post,
|
||||||
|
"PUT" => Put,
|
||||||
|
"DELETE" => Delete,
|
||||||
|
"HEAD" => Head,
|
||||||
|
"TRACE" => Trace,
|
||||||
|
"CONNECT" => Connect,
|
||||||
|
"PATCH" => Patch,
|
||||||
|
_ => Extension(s.to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Show for Method {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Options => "OPTIONS",
|
||||||
|
Get => "GET",
|
||||||
|
Post => "POST",
|
||||||
|
Put => "PUT",
|
||||||
|
Delete => "DELETE",
|
||||||
|
Head => "HEAD",
|
||||||
|
Trace => "TRACE",
|
||||||
|
Connect => "CONNECT",
|
||||||
|
Patch => "PATCH",
|
||||||
|
Extension(ref s) => s.as_slice()
|
||||||
|
}.fmt(fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/mime.rs
Normal file
1
src/mime.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
660
src/rfc7230.rs
Normal file
660
src/rfc7230.rs
Normal file
@@ -0,0 +1,660 @@
|
|||||||
|
use std::cmp::min;
|
||||||
|
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))
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
return Err(io::standard_error(io::EndOfFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
let to_read = min(rem, buf.len());
|
||||||
|
let count = try!(body.read(buf.mut_slice_to(to_read)));
|
||||||
|
|
||||||
|
rem -= count;
|
||||||
|
*opt_remaining = if rem > 0 { Some(rem) } else { None };
|
||||||
|
Ok(count)
|
||||||
|
},
|
||||||
|
EofReader(ref mut body) => {
|
||||||
|
body.read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
debug!("TODO: chunk extension byte={}", ext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static SP: u8 = b' ';
|
||||||
|
pub static CR: u8 = b'\r';
|
||||||
|
pub static LF: u8 = b'\n';
|
||||||
|
pub static STAR: u8 = b'*';
|
||||||
|
pub static LINE_ENDING: &'static [u8] = &[CR, LF];
|
||||||
|
|
||||||
|
/// 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@b'A'..b'Z') => {
|
||||||
|
state = MsExt;
|
||||||
|
s.push_char(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,
|
||||||
|
|
||||||
|
(MsExt, b@b'A'..b'Z') => s.push_char(b as char),
|
||||||
|
|
||||||
|
(_, b@b'A'..b'Z') => {
|
||||||
|
s = state.as_slice().to_string();
|
||||||
|
s.push_char(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_char(b as char);
|
||||||
|
loop {
|
||||||
|
match try_io!(stream.read_byte()) {
|
||||||
|
SP => {
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
CR | LF => {
|
||||||
|
return Err(HttpUriError)
|
||||||
|
},
|
||||||
|
b => s.push_char(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) => {
|
||||||
|
debug!("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.
|
||||||
|
///
|
||||||
|
/// 2 vectors of u8s, divided by COLON (`:`). The first vector is guaranteed
|
||||||
|
/// to be all `token`s. See `is_token_char` source for all valid characters.
|
||||||
|
pub type RawHeaderLine = (Vec<u8>, Vec<u8>);
|
||||||
|
|
||||||
|
/// Read a RawHeaderLine from a Reader.
|
||||||
|
///
|
||||||
|
/// From [spec](https://tools.ietf.org/html/rfc7230#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 = vec![];
|
||||||
|
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),
|
||||||
|
_nontoken => return Err(HttpHeaderError)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ows = true; //optional whitespace
|
||||||
|
|
||||||
|
//TODO: handling 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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::MemReader;
|
||||||
|
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".as_bytes().to_vec(),
|
||||||
|
"rust-lang.org".as_bytes().to_vec()))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
83
src/server/mod.rs
Normal file
83
src/server/mod.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
//! # Server
|
||||||
|
use std::io::net::tcp::TcpListener;
|
||||||
|
use std::io::{Acceptor, Listener, IoResult};
|
||||||
|
use std::io::net::ip::{IpAddr, Port};
|
||||||
|
|
||||||
|
pub use self::request::Request;
|
||||||
|
pub use self::response::Response;
|
||||||
|
|
||||||
|
pub mod request;
|
||||||
|
pub mod response;
|
||||||
|
|
||||||
|
/// A server can listen on a TCP socket.
|
||||||
|
///
|
||||||
|
/// Once listening, it will create a `Request`/`Response` pair for each
|
||||||
|
/// incoming connection, and hand them to the provided handler.
|
||||||
|
pub struct Server {
|
||||||
|
ip: IpAddr,
|
||||||
|
port: Port
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
|
||||||
|
/// Creates a server to be used for `http` conenctions.
|
||||||
|
pub fn http(ip: IpAddr, port: Port) -> Server {
|
||||||
|
Server {
|
||||||
|
ip: ip,
|
||||||
|
port: port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Binds to a socket, and starts handling connections.
|
||||||
|
pub fn listen<H: Handler>(&self, mut handler: H) {
|
||||||
|
let listener = match TcpListener::bind(self.ip.to_string().as_slice(), self.port) {
|
||||||
|
Ok(listener) => listener,
|
||||||
|
Err(err) => fail!("Listen failed: {}", err)
|
||||||
|
};
|
||||||
|
let mut acceptor = listener.listen();
|
||||||
|
|
||||||
|
for conn in acceptor.incoming() {
|
||||||
|
match conn {
|
||||||
|
Ok(stream) => {
|
||||||
|
debug!("Incoming stream");
|
||||||
|
let clone = stream.clone();
|
||||||
|
let req = match Request::new(stream) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(err) => {
|
||||||
|
error!("creating Request: {}", err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut res = Response::new(clone);
|
||||||
|
res.version = req.version;
|
||||||
|
match handler.handle(req, res) {
|
||||||
|
Ok(..) => debug!("Stream handled"),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error from handler: {}", e)
|
||||||
|
//TODO try to send a status code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
error!("Connection failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handler that can handle incoming requests for a server.
|
||||||
|
pub trait Handler {
|
||||||
|
/// 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(&mut self, req: Request, res: Response) -> IoResult<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Handler for |Request, Response|: 'a -> IoResult<()> {
|
||||||
|
fn handle(&mut self, req: Request, res: Response) -> IoResult<()> {
|
||||||
|
(*self)(req, res)
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/server/request.rs
Normal file
72
src/server/request.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
//! # Server Requests
|
||||||
|
//!
|
||||||
|
//! These are requests that a `hyper::Server` receives, and include its method,
|
||||||
|
//! target URI, headers, and message body.
|
||||||
|
use std::io::{Reader, IoResult};
|
||||||
|
use std::io::net::ip::SocketAddr;
|
||||||
|
use std::io::net::tcp::TcpStream;
|
||||||
|
|
||||||
|
use {HttpResult};
|
||||||
|
use version::{HttpVersion};
|
||||||
|
use method;
|
||||||
|
use header::{Headers, ContentLength};
|
||||||
|
use rfc7230::{read_request_line};
|
||||||
|
use rfc7230::{HttpReader, SizedReader, ChunkedReader};
|
||||||
|
use uri::RequestUri;
|
||||||
|
|
||||||
|
/// A request bundles several parts of an incoming TCP stream, given to a `Handler`.
|
||||||
|
pub struct Request {
|
||||||
|
/// The IP address of the remote connection.
|
||||||
|
pub remote_addr: SocketAddr,
|
||||||
|
/// The `Method`, such as `Get`, `Post`, etc.
|
||||||
|
pub method: method::Method,
|
||||||
|
/// The headers of the incoming request.
|
||||||
|
pub headers: Headers,
|
||||||
|
/// The target request-uri for this request.
|
||||||
|
pub uri: RequestUri,
|
||||||
|
/// The version of HTTP for this request.
|
||||||
|
pub version: HttpVersion,
|
||||||
|
body: HttpReader<TcpStream>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
|
||||||
|
/// Create a new Request, reading the StartLine and Headers so they are
|
||||||
|
/// immediately useful.
|
||||||
|
pub fn new(mut tcp: TcpStream) -> HttpResult<Request> {
|
||||||
|
let (method, uri, version) = try!(read_request_line(&mut tcp));
|
||||||
|
let mut headers = try!(Headers::from_raw(&mut tcp));
|
||||||
|
|
||||||
|
debug!("{} {} {}", method, uri, version);
|
||||||
|
debug!("{}", headers);
|
||||||
|
|
||||||
|
let remote_addr = try_io!(tcp.peer_name());
|
||||||
|
|
||||||
|
// TODO: handle Transfer-Encoding
|
||||||
|
let body = if headers.has::<ContentLength>() {
|
||||||
|
match headers.get_ref::<ContentLength>() {
|
||||||
|
Some(&ContentLength(len)) => SizedReader(tcp, len),
|
||||||
|
None => unreachable!()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ChunkedReader(tcp, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Request {
|
||||||
|
remote_addr: remote_addr,
|
||||||
|
method: method,
|
||||||
|
uri: uri,
|
||||||
|
headers: headers,
|
||||||
|
version: version,
|
||||||
|
body: body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reader for Request {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> IoResult<uint> {
|
||||||
|
self.body.read(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
81
src/server/response.rs
Normal file
81
src/server/response.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
//! # Server Responses
|
||||||
|
//!
|
||||||
|
//! These are responses sent by a `hyper::Server` to clients, after
|
||||||
|
//! receiving a request.
|
||||||
|
use std::io::IoResult;
|
||||||
|
use std::io::net::tcp::TcpStream;
|
||||||
|
|
||||||
|
use header;
|
||||||
|
use status;
|
||||||
|
use version;
|
||||||
|
use rfc7230::{CR, LF, LINE_ENDING};
|
||||||
|
|
||||||
|
|
||||||
|
/// The outgoing half for a Tcp connection, created by a `Server` and given to a `Handler`.
|
||||||
|
pub struct Response {
|
||||||
|
/// The status code for the request.
|
||||||
|
pub status: status::StatusCode,
|
||||||
|
/// The outgoing headers on this response.
|
||||||
|
pub headers: header::Headers,
|
||||||
|
/// The HTTP version of this response.
|
||||||
|
pub version: version::HttpVersion,
|
||||||
|
|
||||||
|
headers_written: bool, // TODO: can this check be moved to compile time?
|
||||||
|
body: TcpStream
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
|
||||||
|
/// Creates a new Response that can be used to write to a network stream.
|
||||||
|
pub fn new(tcp: TcpStream) -> Response {
|
||||||
|
Response {
|
||||||
|
status: status::Ok,
|
||||||
|
version: version::Http11,
|
||||||
|
headers: header::Headers::new(),
|
||||||
|
headers_written: false,
|
||||||
|
body: tcp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_head(&mut self) -> IoResult<()> {
|
||||||
|
if self.headers_written {
|
||||||
|
debug!("headers previsouly written, nooping");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.headers_written = true;
|
||||||
|
debug!("writing head: {} {}", self.version, self.status);
|
||||||
|
try!(write!(self.body, "{} {}{}{}", self.version, self.status, CR as char, LF as char));
|
||||||
|
|
||||||
|
for (name, header) in self.headers.iter() {
|
||||||
|
debug!("headers {}: {}", name, header);
|
||||||
|
try!(write!(self.body, "{}: {}", name, header));
|
||||||
|
try!(self.body.write(LINE_ENDING));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.body.write(LINE_ENDING)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flushes all writing of a response to the client.
|
||||||
|
pub fn end(&mut self) -> IoResult<()> {
|
||||||
|
try!(self.flush());
|
||||||
|
self.body.close_write()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Writer for Response {
|
||||||
|
fn write(&mut self, msg: &[u8]) -> IoResult<()> {
|
||||||
|
if !self.headers_written {
|
||||||
|
try!(self.write_head());
|
||||||
|
}
|
||||||
|
debug!("write {:u} bytes", msg.len());
|
||||||
|
self.body.write(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> IoResult<()> {
|
||||||
|
if !self.headers_written {
|
||||||
|
try!(self.write_head());
|
||||||
|
}
|
||||||
|
self.body.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
1769
src/status.rs
Normal file
1769
src/status.rs
Normal file
File diff suppressed because it is too large
Load Diff
45
src/uri.rs
Normal file
45
src/uri.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
//! # RequestUri
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// The Request-URI of a Request's StartLine.
|
||||||
|
///
|
||||||
|
/// From Section 5.3, Request Target:
|
||||||
|
/// > Once an inbound connection is obtained, the client sends an HTTP
|
||||||
|
/// > request message (Section 3) with a request-target derived from the
|
||||||
|
/// > target URI. There are four distinct formats for the request-target,
|
||||||
|
/// > depending on both the method being requested and whether the request
|
||||||
|
/// > is to a proxy.
|
||||||
|
/// >
|
||||||
|
/// > request-target = origin-form
|
||||||
|
/// > / absolute-form
|
||||||
|
/// > / authority-form
|
||||||
|
/// > / asterisk-form
|
||||||
|
#[deriving(Show, PartialEq, Clone)]
|
||||||
|
pub enum RequestUri {
|
||||||
|
/// The most common request target, an absolute path and optional query.
|
||||||
|
///
|
||||||
|
/// For example, the line `GET /where?q=now HTTP/1.1` would parse the URI
|
||||||
|
/// as `AbsolutePath("/where?q=now".to_string())`.
|
||||||
|
AbsolutePath(String),
|
||||||
|
|
||||||
|
/// An absolute URI. Used in conjunction with proxies.
|
||||||
|
///
|
||||||
|
/// > When making a request to a proxy, other than a CONNECT or server-wide
|
||||||
|
/// > OPTIONS request (as detailed below), a client MUST send the target
|
||||||
|
/// > URI in absolute-form as the request-target.
|
||||||
|
///
|
||||||
|
/// An example StartLine with an `AbsoluteUri` would be
|
||||||
|
/// `GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1`.
|
||||||
|
AbsoluteUri(Url),
|
||||||
|
|
||||||
|
/// The authority form is only for use with `CONNECT` requests.
|
||||||
|
///
|
||||||
|
/// An example StartLine: `CONNECT www.example.com:80 HTTP/1.1`.
|
||||||
|
Authority(String),
|
||||||
|
|
||||||
|
/// The star is used to target the entire server, instead of a specific resource.
|
||||||
|
///
|
||||||
|
/// This is only used for a server-wide `OPTIONS` request.
|
||||||
|
Star,
|
||||||
|
}
|
||||||
|
|
||||||
29
src/version.rs
Normal file
29
src/version.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
//! # HTTP Versions
|
||||||
|
//!
|
||||||
|
//! Instead of relying on typo-prone Strings, use expected HTTP versions as
|
||||||
|
//! the `HttpVersion` enum.
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Represents a version of the HTTP spec.
|
||||||
|
#[deriving(PartialEq, PartialOrd)]
|
||||||
|
pub enum HttpVersion {
|
||||||
|
/// `HTTP/0.9`
|
||||||
|
Http09,
|
||||||
|
/// `HTTP/1.0`
|
||||||
|
Http10,
|
||||||
|
/// `HTTP/1.1`
|
||||||
|
Http11,
|
||||||
|
/// `HTTP/2.0`
|
||||||
|
Http20
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Show for HttpVersion {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Http09 => "HTTP/0.9",
|
||||||
|
Http10 => "HTTP/1.0",
|
||||||
|
Http11 => "HTTP/1.1",
|
||||||
|
Http20 => "HTTP/2.0",
|
||||||
|
}.fmt(fmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user