diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..b088f40e
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,14 @@
+language: rust
+
+after_success: |
+ [ $TRAVIS_BRANCH = master ] &&
+ [ $TRAVIS_PULL_REQUEST = false ] &&
+ cargo doc &&
+ echo '' > 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=
diff --git a/README.md b/README.md
index 8efa69cc..3c9987fd 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# hyper
+[](https://travis-ci.org/seanmonstar/hyper)
+
An HTTP library for Rust.
## License
diff --git a/examples/client.rs b/examples/client.rs
new file mode 100644
index 00000000..f436eacb
--- /dev/null
+++ b/examples/client.rs
@@ -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 ");
+ 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)
+ };
+
+}
diff --git a/examples/server.rs b/examples/server.rs
new file mode 100644
index 00000000..b24be5b1
--- /dev/null
+++ b/examples/server.rs
@@ -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);
+}
diff --git a/src/client/mod.rs b/src/client/mod.rs
new file mode 100644
index 00000000..e51306a3
--- /dev/null
+++ b/src/client/mod.rs
@@ -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(Get, url)
+}
+
+/// Create a client request.
+pub fn request(method: Method, url: Url) -> HttpResult {
+ Request::new(method, url)
+}
diff --git a/src/client/request.rs b/src/client/request.rs
new file mode 100644
index 00000000..621e079d
--- /dev/null
+++ b/src/client/request.rs
@@ -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 {
+ 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 {
+ 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()
+ }
+}
+
diff --git a/src/client/response.rs b/src/client/response.rs
new file mode 100644
index 00000000..92bf2068
--- /dev/null
+++ b/src/client/response.rs
@@ -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,
+}
+
+impl Response {
+
+ /// Creates a new response from a server.
+ pub fn new(mut tcp: TcpStream) -> HttpResult {
+ 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::() {
+ match headers.get_ref::() {
+ 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::() {
+ match headers.get_ref::() {
+ 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 {
+ self.body.read(buf)
+ }
+}
diff --git a/src/header.rs b/src/header.rs
new file mode 100644
index 00000000..1e34c507
--- /dev/null
+++ b/src/header.rs
@@ -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) -> 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]) -> Option;
+ /// 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)` 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(self) -> &'a T;
+}
+
+impl<'a> UncheckedAnyRefExt<'a> for &'a Header {
+ #[inline]
+ unsafe fn downcast_ref_unchecked(self) -> &'a T {
+ let to: TraitObject = transmute_copy(&self);
+ transmute(to.data)
+ }
+}
+
+fn header_name() -> SendStr {
+ let name = Header::header_name(None::);
+ 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
+}
+
+impl Headers {
+
+ /// Creates a new, empty headers map.
+ pub fn new() -> Headers {
+ Headers {
+ data: HashMap::new()
+ }
+ }
+
+ #[doc(hidden)]
+ pub fn from_raw(rdr: &mut R) -> HttpResult {
+ 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(&mut self, value: H) {
+ self.data.insert(header_name::(), 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::();
+ /// ```
+ pub fn get(&mut self) -> Option {
+ self.get_ref().map(|v: &H| v.clone())
+ }
+
+ /// Get a reference to the header field's value, if it exists.
+ pub fn get_ref(&mut self) -> Option<&H> {
+ self.data.find_mut(&header_name::()).and_then(|item| {
+ debug!("get_ref, name={}, val={}", header_name::(), item);
+ let header = match *item {
+ Raw(ref raw) => match Header::parse_header(raw.as_slice()) {
+ Some::(h) => {
+ h
+ },
+ None => return None
+ },
+ Typed(..) => return Some(item)
+ };
+ *item = Typed(box header as Box);
+ 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::();
+ /// ```
+ pub fn has(&self) -> bool {
+ self.data.contains_key(&header_name::())
+ }
+
+ /// Removes a header from the map, if one existed. Returns true if a header has been removed.
+ pub fn remove(&mut self) -> bool {
+ self.data.remove(&Header::header_name(None::))
+ }
+
+ /// 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>),
+ Typed(Box)
+}
+
+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) -> SendStr {
+ Slice("host")
+ }
+
+ fn parse_header(raw: &[Vec]) -> Option {
+ 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) -> SendStr {
+ Slice("content-length")
+ }
+
+ fn parse_header(raw: &[Vec]) -> Option {
+ 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) -> SendStr {
+ Slice("content-type")
+ }
+
+ fn parse_header(raw: &[Vec]) -> Option {
+ 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);
+
+impl Header for Accept {
+ fn header_name(_: Option) -> SendStr {
+ Slice("accept")
+ }
+
+ fn parse_header(_raw: &[Vec]) -> Option {
+ 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 {
+ debug!("Connection::from_str =? {}", s);
+ match s {
+ "keep-alive" => Some(KeepAlive),
+ "close" => Some(Close),
+ _ => None
+ }
+ }
+}
+
+impl Header for Connection {
+ fn header_name(_: Option) -> SendStr {
+ Slice("connection")
+ }
+
+ fn parse_header(raw: &[Vec]) -> Option {
+ 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);
+
+/// 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 {
+ match s {
+ "chunked" => Some(Chunked),
+ _ => None
+ }
+ }
+}
+
+impl Header for TransferEncoding {
+ fn header_name(_: Option) -> SendStr {
+ Slice("transfer-encoding")
+ }
+
+ fn parse_header(raw: &[Vec]) -> Option {
+ 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) -> SendStr {
+ Slice("user-agent")
+ }
+
+ fn parse_header(raw: &[Vec]) -> Option {
+ 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(raw: &[Vec]) -> Option {
+ 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![]))));
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 00000000..f6870a3a
--- /dev/null
+++ b/src/lib.rs
@@ -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 = Result;
+
+/// 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),
+}
+
diff --git a/src/method.rs b/src/method.rs
new file mode 100644
index 00000000..5dd9372a
--- /dev/null
+++ b/src/method.rs
@@ -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 {
+ 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)
+ }
+}
diff --git a/src/mime.rs b/src/mime.rs
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/src/mime.rs
@@ -0,0 +1 @@
+
diff --git a/src/rfc7230.rs b/src/rfc7230.rs
new file mode 100644
index 00000000..88f4e0dc
--- /dev/null
+++ b/src/rfc7230.rs
@@ -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 {
+ /// 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),
+ /// 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 Reader for HttpReader {
+ fn read(&mut self, buf: &mut [u8]) -> IoResult {
+ 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(rdr: &mut R) -> IoResult {
+ 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(stream: &mut R) -> HttpResult {
+ 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(stream: &mut R) -> HttpResult {
+ 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(stream: &mut R) -> HttpResult {
+ 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, Vec);
+
+/// 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(stream: &mut R) -> HttpResult