Skip to content

Instantly share code, notes, and snippets.

@m4rw3r
Last active January 13, 2016 06:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save m4rw3r/9816747 to your computer and use it in GitHub Desktop.
Save m4rw3r/9816747 to your computer and use it in GitHub Desktop.
http.rs: Simple HTTP-library for Rust
use std::fmt;
use std::ascii::StrAsciiExt;
#[deriving(Clone,Eq)]
pub enum HeaderName {
Accept,
AcceptCharset,
AcceptEncoding,
AcceptLanguage,
Age,
Allow,
Authorization,
CacheControl,
Connection,
ContentBase,
ContentEncoding,
ContentLanguage,
ContentLength,
ContentLocation,
ContentMD5,
ContentRange,
ContentTransferEncoding,
ContentType,
Cookie,
Date,
ETag,
Expect,
Expires,
From,
Host,
IfMatch,
IfModifiedSince,
IfNoneMatch,
IfRange,
IfUnmodifiedSince,
LastModified,
Location,
MaxForwards,
Pragma,
ProxyAuthenticate,
ProxyAuthorization,
Public,
Range,
Referer,
RetryAfter,
Server,
SetCookie,
TE,
Trailer,
TransferEncoding,
Upgrade,
UserAgent,
Vary,
Via,
WWWAuthenticate,
Warning,
Custom(~str)
}
impl HeaderName {
fn as_str(&self) -> ~str {
match *self {
Accept => ~"Accept",
AcceptCharset => ~"Accept-Charset",
AcceptEncoding => ~"Accept-Encoding",
AcceptLanguage => ~"Accept-Language",
Age => ~"Age",
Allow => ~"Allow",
Authorization => ~"Authorization",
CacheControl => ~"Cache-Control",
Connection => ~"Connection",
ContentBase => ~"Content-Base",
ContentEncoding => ~"Content-Encoding",
ContentLanguage => ~"Content-Language",
ContentLength => ~"Content-Length",
ContentLocation => ~"Content-Location",
ContentMD5 => ~"Content-MD5",
ContentRange => ~"Content-Range",
ContentTransferEncoding => ~"Content-Transfer-Encoding",
ContentType => ~"Content-Type",
Cookie => ~"Cookie",
Date => ~"Date",
ETag => ~"ETag",
Expect => ~"Expect",
Expires => ~"Expires",
From => ~"From",
Host => ~"Host",
IfMatch => ~"If-Match",
IfModifiedSince => ~"If-Modified-Since",
IfNoneMatch => ~"If-None-Match",
IfRange => ~"If-Range",
IfUnmodifiedSince => ~"If-Unmodified-Since",
LastModified => ~"Last-Modified",
Location => ~"Location",
MaxForwards => ~"Max-Forwards",
Pragma => ~"Pragma",
ProxyAuthenticate => ~"Proxy-Authenticate",
ProxyAuthorization => ~"Proxy-Authorization",
Public => ~"Public",
Range => ~"Range",
Referer => ~"Referer",
RetryAfter => ~"Retry-After",
Server => ~"Server",
SetCookie => ~"Set-Cookie",
TE => ~"TE",
Trailer => ~"Trailer",
TransferEncoding => ~"Transfer-Encoding",
Upgrade => ~"Upgrade",
UserAgent => ~"UserAgent",
Vary => ~"Vary",
Via => ~"Via",
WWWAuthenticate => ~"WWW-Authenticate",
Warning => ~"Warning",
Custom(ref string) => string.to_owned()
}
}
}
impl fmt::Show for HeaderName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.as_str().fmt(f)
}
}
pub fn from_str(string: &str) -> HeaderName {
match string.to_ascii_lower().as_slice() {
"accept" => Accept,
"accept-charset" => AcceptCharset,
"accept-encoding" => AcceptEncoding,
"accept-language" => AcceptLanguage,
"age" => Age,
"allow" => Allow,
"authorization" => Authorization,
"cache-control" => CacheControl,
"connection" => Connection,
"content-base" => ContentBase,
"content-encoding" => ContentEncoding,
"content-language" => ContentLanguage,
"content-length" => ContentLength,
"content-location" => ContentLocation,
"content-md5" => ContentMD5,
"content-range" => ContentRange,
"content-transfer-encoding" => ContentTransferEncoding,
"content-type" => ContentType,
"cookie" => Cookie,
"date" => Date,
"etag" => ETag,
"expect" => Expect,
"expires" => Expires,
"from" => From,
"host" => Host,
"if-match" => IfMatch,
"if-modified-since" => IfModifiedSince,
"if-none-match" => IfNoneMatch,
"if-range" => IfRange,
"if-unmodified-since" => IfUnmodifiedSince,
"last-modified" => LastModified,
"location" => Location,
"max-forwards" => MaxForwards,
"pragma" => Pragma,
"proxy-authenticate" => ProxyAuthenticate,
"proxy-authorization" => ProxyAuthorization,
"public" => Public,
"range" => Range,
"referer" => Referer,
"retry-after" => RetryAfter,
"server" => Server,
"set-cookie" => SetCookie,
"te" => TE,
"trailer" => Trailer,
"transfer-encoding" => TransferEncoding,
"upgrade" => Upgrade,
"useragent" => UserAgent,
"vary" => Vary,
"via" => Via,
"warning" => Warning,
"www-authenticate" => WWWAuthenticate,
_ => Custom(string.into_owned())
}
}
#[cfg(test)]
mod test {
}
#![feature(macro_rules)]
use std::io::{Buffer,IoError};
use std::iter::Iterator;
pub mod header;
pub mod verb {
#[deriving(Clone,Eq,Show)]
pub enum Verb {
DELETE,
GET,
HEAD,
OPTIONS,
PATCH,
POST,
PUT,
UNKNOWN
}
}
macro_rules! try( ($e:expr) => (match $e { Ok(e) => e, Err(e) => return Err(e) }) )
#[deriving(Clone,Eq,Show)]
pub enum Version {
HTTP10,
HTTP11
}
#[deriving(Clone,Eq,Show)]
pub struct Header {
key: ~str,
value: ~str
}
#[deriving(Clone,Eq,Show)]
pub struct Request {
method: verb::Verb,
version: Version,
uri: ~str,
headers: ~[Header]
}
#[deriving(Eq,Show)]
pub enum ParseError {
InvalidRequestLine,
InvalidHeader,
UnsupportedProtocolVersion,
UnknownVerb,
MissingHostHeader,
Error(IoError)
}
#[inline]
fn parseVerb(string: &str) -> Result<verb::Verb, ParseError> {
match string {
"DELETE" => Ok(verb::DELETE),
"GET" => Ok(verb::GET),
"HEAD" => Ok(verb::HEAD),
"OPTIONS" => Ok(verb::OPTIONS),
"PATCH" => Ok(verb::PATCH),
"POST" => Ok(verb::POST),
"PUT" => Ok(verb::PUT),
_ => {
return Err(UnknownVerb)
}
}
}
#[inline]
fn parseVersion(version: &str) -> Result<Version, ParseError> {
match version {
"HTTP/1.0\r\n" => Ok(HTTP10),
"HTTP/1.1\r\n" => Ok(HTTP11),
_ => {
return Err(UnsupportedProtocolVersion)
}
}
}
#[inline]
fn parseRequestLine(string: ~str) -> Result<(verb::Verb, ~str, Version), ParseError> {
let strings: ~[&str] = string.split(' ').collect();
match strings.len() {
3 => Ok((try!(parseVerb(strings[0])), strings[1].trim().into_owned(), try!(parseVersion(strings[2])))),
_ => Err(InvalidRequestLine)
}
}
#[inline]
fn parseHeader(string: ~str) -> Result<Header, ParseError> {
let strings: ~[&str] = string.splitn(':', 1).collect();
match strings.len() {
2 => Ok(Header{key: strings[0].to_owned(), value: strings[1].trim().to_owned()}),
_ => Err(InvalidHeader)
}
}
pub fn parseRequest<T:Buffer>(buffer: &mut T) -> Result<~Request, ParseError> {
let mut lines = buffer.lines().take_while(|line| match line {
&Ok(ref s) => "\r\n" != *s,
&Err(_) => true
}).map(|ioresult| match ioresult {
Ok(s) => Ok(s),
Err(e) => Err(Error(e))
});
let (verb, url, version) = try!(parseRequestLine(try!(lines.next().unwrap_or(Ok(~"")))));
/* TODO: Merge multiline headers */
let headers: ~[Header] = try!(std::result::collect(lines.map(|header| parseHeader(try!(header)))));
match version {
HTTP11 => if ! headers.iter().any(|h| h.key == ~"Host") {
return Err(MissingHostHeader)
},
HTTP10 => {}
}
Ok(~Request{method: verb, version: version, uri: url, headers: headers})
}
#[cfg(test)]
mod test {
use std::io::MemReader;
use std::str;
use verb::{DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT};
use {parseRequest,HTTP11,HTTP10,Header,MissingHostHeader};
#[test]
fn basicGet() {
let req = ~"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
let mut data = ~MemReader::new(req.into_bytes());
let request = parseRequest(data);
match request {
Ok(request) => {
assert!(request.method == GET);
assert!(request.version == HTTP11);
assert!(request.uri == ~"/");
assert!(request.headers == ~[Header{key: ~"Host", value: ~"example.com"}]);
}
Err(error) => {
fail!(format!("Got {} instead of Header.", error.to_str()));
}
}
}
#[test]
fn missingHostHeader11() {
let req = ~"GET / HTTP/1.1\r\n\r\n";
let mut data = ~MemReader::new(req.into_bytes());
let request = parseRequest(data);
assert!(request == Err(MissingHostHeader));
}
#[test]
fn missingHostHeader10() {
let req = ~"GET / HTTP/1.0\r\n\r\n";
let mut data = ~MemReader::new(req.into_bytes());
let request = parseRequest(data);
match request {
Ok(request) => {
assert!(request.method == GET);
assert!(request.version == HTTP10);
assert!(request.uri == ~"/");
assert!(request.headers == ~[]);
}
Err(error) => {
fail!(format!("Got {} instead of Header.", error.to_str()));
}
}
}
#[test]
fn testRemaining() {
let req = ~"GET / HTTP/1.1\r\nHost: example.com\r\n\r\nThis is some content here\r\nMore content";
let mut data = ~MemReader::new(req.into_bytes());
let request = parseRequest(data);
match request {
Ok(request) => {
assert!(request.method == GET);
assert!(request.version == HTTP11);
assert!(request.uri == ~"/");
assert!(request.headers == ~[Header{key: ~"Host", value: ~"example.com"}]);
assert!(str::from_utf8(data.fill_buf().unwrap()) == Some(&"This is some content here\r\nMore content"));
}
Err(error) => {
fail!(format!("Got {} instead of Header.", error.to_str()));
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment