summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2016-09-14 19:02:45 +0200
committerYorhel <git@yorhel.nl>2016-09-14 19:02:45 +0200
commit31915270b3eb7fe211cc500f97dc4a2ebaa3e91d (patch)
tree984271198c7d5e40dc4e5e2444c0fa241539aa34 /src
parent60e0eaab3674e88c7ff13a82ee44d524dbe59477 (diff)
Add initial request parsing + request objectHEADmaster
Diffstat (limited to 'src')
-rw-r--r--src/itf_http.rs57
-rw-r--r--src/main.rs3
-rw-r--r--src/request.rs176
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""));
+}