diff options
author | Yorhel <git@yorhel.nl> | 2016-09-14 19:02:45 +0200 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2016-09-14 19:02:45 +0200 |
commit | 31915270b3eb7fe211cc500f97dc4a2ebaa3e91d (patch) | |
tree | 984271198c7d5e40dc4e5e2444c0fa241539aa34 /src | |
parent | 60e0eaab3674e88c7ff13a82ee44d524dbe59477 (diff) |
Diffstat (limited to 'src')
-rw-r--r-- | src/itf_http.rs | 57 | ||||
-rw-r--r-- | src/main.rs | 3 | ||||
-rw-r--r-- | src/request.rs | 176 |
3 files changed, 231 insertions, 5 deletions
diff --git a/src/itf_http.rs b/src/itf_http.rs index 0d261a2..ec051c7 100644 --- a/src/itf_http.rs +++ b/src/itf_http.rs @@ -1,13 +1,25 @@ use mio::Event; use mio::tcp::TcpStream; use std::net::SocketAddr; +use std::str::FromStr; use std::sync::Arc; +use httparse; +use request; use iostream::IoStream; use listener::Interface; use eventloop::{Machine,Context,TToken,Action}; +const MAX_HEADERS: usize = 128; + +// This isn't really a hard maximum on the length of the headers, it's more a "stop reading stuff +// from the network if we've at least read this many bytes and still haven't received a complete +// header" limit. The header can exceed this limit if the last network read got us more bytes than +// MAX_HEADER_LEN. +const MAX_HEADER_LEN: usize = 4096; + + pub struct ItfHttp { io: IoStream, addr: SocketAddr, @@ -37,6 +49,44 @@ impl ItfHttp { itf: itf, } } + + // TODO: This error value is both ugly, inefficient and doesn't provide the info necessary to + // determine what kind of response we're supposed to give on error. + fn get_req(&mut self) -> Result<Option<request::Request>, String> { + // I'm not totally comfortable with having a fixed maximum on the number of headers and + // with allocating 4 KiB on the stack. But alas, it's what everyone else does. + let mut headers = [httparse::EMPTY_HEADER; MAX_HEADERS]; + + // Some weird indirection here to handle lifetime entanglement with the httparse object, + // input buffer and the headers list. + { + let mut parser = httparse::Request::new(&mut headers); + let res = try!(parser.parse(&self.io.rbuf[..]).map_err(|e| format!("{}", e))); + if let httparse::Status::Complete(size) = res { + Ok(Some((size, request::Request::new( + self.addr.ip(), + try!(request::Method::from_str(parser.method.unwrap()).map_err(|e| format!("{}", e))), + parser.path.unwrap(), + parser.version.unwrap() + )))) + } else { + if self.io.rbuf.len() > MAX_HEADER_LEN { + return Err("Headers too long".to_string()) + } + Ok(None) + } + }.map(|x| x.map(|(size, mut r)| { + for h in &headers[..] { + if *h == httparse::EMPTY_HEADER { + break; + } + r.headers.data.push(request::Header::new(h.name, h.value)); + } + r.headers.data.sort(); + self.io.rbuf.consume(size); + r + })) + } } @@ -44,10 +94,9 @@ impl Machine for ItfHttp { fn handle(&mut self, ctx: &mut Context, ev: Event) -> Option<Action> { try_rm!(self, self.io.handle(ctx, ev)); - // This is where we parse the stuff, generate a request object, throw the request object - // through some handlers to get a response and send it back. But for now let's just act - // like we wanted to be an echo server all along. - let _ = self.io.rbuf.write_to(&mut self.io.wbuf); + if let Some(req) = try_rm!(self, self.get_req()) { + debug!("{} {} {}", self.addr, req.method, req.path); + } self.io.set_ioreg(ctx); None diff --git a/src/main.rs b/src/main.rs index 8672f4b..5cd0a86 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,6 @@ #![allow(unused_mut)] #![allow(unused_imports)] // */ - #[macro_use] extern crate nom; #[macro_use] extern crate log; extern crate env_logger; @@ -12,12 +11,14 @@ extern crate getopts; extern crate mio; extern crate slab; extern crate netbuf; +extern crate httparse; mod iostream; mod config; mod eventloop; mod listener; mod itf_http; +mod request; use std::process::exit; use std::io::prelude::*; diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000..f5bc6ff --- /dev/null +++ b/src/request.rs @@ -0,0 +1,176 @@ +use std; +use std::cmp::Ordering; +use std::ascii::AsciiExt; +use std::net::IpAddr; + +// XXX: Use an arena allocator for the entire request object? Might be more +// efficient than an allocation for each header. But arenas have their own +// limitations (fatter pointers, unused capacity, narrower lifetimes), so I +// wonder how much it'll actually help in this case. + + +#[derive(Copy,Clone,PartialEq,Eq,Debug)] +pub enum Error { + InvalidMethod, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + Error::InvalidMethod => f.write_str("invalid method"), + } + } +} + +pub type Result<T> = std::result::Result<T,Error>; + + + +// A header (name + value) is stored in a single allocation for efficiency. +// The name is lower-cased and stored in front of the buffer, followed by the +// raw value. +// +// If case-preserving header storage turns out to be desirable, then the Eq/Ord +// traits need to be implemented to do case-insensitive comparison and the +// lowercasing in Header::new() should be removed. Everything else should +// adjust automatically. +#[derive(Debug)] +pub struct Header { + namelen: usize, + buf: Box<[u8]>, +} + + +// Ordering and equality is defined on the name of the header. +impl PartialEq for Header { fn eq(&self, other: &Header) -> bool { self.name() == other.name() } } +impl PartialOrd for Header { fn partial_cmp(&self, other: &Header) -> Option<Ordering> { Some(self.cmp(other)) } } +impl Eq for Header { } +impl Ord for Header { fn cmp(&self, other: &Header) -> Ordering { self.name().cmp(other.name()) } } + + +impl Header { + pub fn new(name: &str, value: &[u8]) -> Header { + let mut v = Vec::with_capacity(name.len() + value.len()); + v.extend(name.as_bytes().iter().map(u8::to_ascii_lowercase)); + v.extend_from_slice(value); + Header { + namelen: name.len(), + buf: v.into_boxed_slice(), + } + } + + pub fn name(&self) -> &str { + unsafe { + std::str::from_utf8_unchecked(&self.buf[..self.namelen]) + } + } + + pub fn value(&self) -> &[u8] { + &self.buf[self.namelen..] + } +} + + +pub struct Headers { + pub data: Vec<Header>, +} + + +// Bulk-adding headers efficiently: +// 1. headers.data.push(..) +// 2. headers.data.sort() +// Headers are ordered by name, so a binary search can find stuff. +impl Headers { + pub fn new() -> Headers { + Headers{ data: Vec::new() } + } + + // TODO: + // - insert_sorted(Header) + // - find(&str) -> Iter + // Finds all occurrences of the given header name (case-insensitive) in + // the order that they were added. +} + + + + +// Methods that we support +// Complete list: http://www.iana.org/assignments/http-methods/http-methods.xhtml +#[derive(Debug,Copy,Clone,PartialEq,Eq)] +pub enum Method { + GET, + HEAD, + POST, + PUT, + DELETE, + // CONNECT, // Forget it, we're not that kind of proxy + OPTIONS, + // TRACE, // I'm 12 what is this +} + + +impl std::str::FromStr for Method { + type Err = Error; + fn from_str(s: &str) -> Result<Method> { + match s { + "GET" => Ok(Method::GET), + "HEAD" => Ok(Method::HEAD), + "POST" => Ok(Method::POST), + "PUT" => Ok(Method::PUT), + "DELETE" => Ok(Method::DELETE), + "OPTIONS" => Ok(Method::OPTIONS), + _ => Err(Error::InvalidMethod), + } + } +} + +impl std::fmt::Display for Method { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + + + +pub struct Request { + pub client: IpAddr, + pub method: Method, + pub version: u8, // 0 for HTTP 1.0, 1 for HTTP 1.1; as in httparse + pub path: Box<str>, + pub headers: Headers, +} + + +impl Request { + pub fn new(client: IpAddr, method: Method, path: &str, version: u8) -> Request { + Request { + client: client, + method: method, + path: String::from(path).into_boxed_str(), + version: version, + headers: Headers::new(), + } + } +} + + + + + +#[test] +fn test_header() { + assert_eq!(std::mem::size_of::<Header>(), 3*std::mem::size_of::<usize>()); + + let h = Header::new("Content-Type", b"text/html; charset=UTF8"); + assert_eq!(h.name(), "content-type"); + assert_eq!(h.value(), b"text/html; charset=UTF8"); + + // Empty name is technically invalid, but let's test for it anyway + let h = Header::new("", b""); + assert_eq!(h.name(), ""); + assert_eq!(h.value(), b""); + + assert_eq!(Header::new("X-Whatever", b"val"), Header::new("x-WHATEVER", b"something else")); + assert!(Header::new("a-Whatever", b"") < Header::new("Xstuff", b"")); +} |