diff --git a/lib/src/method.rs b/lib/src/method.rs index eeeafc36..b5103070 100644 --- a/lib/src/method.rs +++ b/lib/src/method.rs @@ -19,17 +19,17 @@ pub enum Method { } impl Method { - pub fn from_hyp(method: HyperMethod) -> Option { + pub fn from_hyp(method: &HyperMethod) -> Option { match method { - HyperMethod::Get => Some(Get), - HyperMethod::Put => Some(Put), - HyperMethod::Post => Some(Post), - HyperMethod::Delete => Some(Delete), - HyperMethod::Options => Some(Options), - HyperMethod::Head => Some(Head), - HyperMethod::Trace => Some(Trace), - HyperMethod::Connect => Some(Connect), - HyperMethod::Patch => Some(Patch), + &HyperMethod::Get => Some(Get), + &HyperMethod::Put => Some(Put), + &HyperMethod::Post => Some(Post), + &HyperMethod::Delete => Some(Delete), + &HyperMethod::Options => Some(Options), + &HyperMethod::Head => Some(Head), + &HyperMethod::Trace => Some(Trace), + &HyperMethod::Connect => Some(Connect), + &HyperMethod::Patch => Some(Patch), _ => None } } diff --git a/lib/src/request.rs b/lib/src/request.rs index 6334aedd..8b9d8143 100644 --- a/lib/src/request.rs +++ b/lib/src/request.rs @@ -3,6 +3,7 @@ use param::FromParam; pub use hyper::server::Request as HyperRequest; +#[derive(Clone)] pub struct Request<'a> { params: Vec<&'a str>, pub uri: &'a str, diff --git a/lib/src/response/empty.rs b/lib/src/response/empty.rs index efabb5c5..94af4497 100644 --- a/lib/src/response/empty.rs +++ b/lib/src/response/empty.rs @@ -10,11 +10,20 @@ impl Empty { } impl Responder for Empty { - fn respond<'a>(&mut self, mut res: HyperResponse<'a, HyperFresh>) { + fn respond<'a>(&mut self, mut res: FreshHyperResponse<'a>) -> Outcome<'a> { res.headers_mut().set(header::ContentLength(0)); *(res.status_mut()) = self.0; let mut stream = res.start().unwrap(); stream.write_all(b"").unwrap(); + Outcome::Complete + } +} + +pub struct Forward; + +impl Responder for Forward { + fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a> { + Outcome::FailForward(res) } } diff --git a/lib/src/response/forward.rs b/lib/src/response/forward.rs new file mode 100644 index 00000000..e69de29b diff --git a/lib/src/response/mod.rs b/lib/src/response/mod.rs index 1bdec868..67534f5e 100644 --- a/lib/src/response/mod.rs +++ b/lib/src/response/mod.rs @@ -2,6 +2,7 @@ mod empty; mod responder; mod redirect; mod with_status; +mod outcome; pub use hyper::server::Response as HyperResponse; pub use hyper::net::Fresh as HyperFresh; @@ -9,12 +10,15 @@ pub use hyper::status::StatusCode; pub use hyper::header; pub use self::responder::Responder; -pub use self::empty::Empty; +pub use self::empty::{Empty, Forward}; pub use self::redirect::Redirect; pub use self::with_status::StatusResponse; +pub use self::outcome::Outcome; use std::ops::{Deref, DerefMut}; +pub type FreshHyperResponse<'a> = HyperResponse<'a, HyperFresh>; + pub struct Response<'a>(Box); impl<'a> Response<'a> { @@ -27,6 +31,10 @@ impl<'a> Response<'a> { Response(Box::new(StatusResponse::new(status, body))) } + pub fn forward() -> Response<'a> { + Response(Box::new(Forward)) + } + pub fn with_raw_status(status: u16, body: T) -> Response<'a> { let status_code = StatusCode::from_u16(status); diff --git a/lib/src/response/outcome.rs b/lib/src/response/outcome.rs new file mode 100644 index 00000000..bfa0df9b --- /dev/null +++ b/lib/src/response/outcome.rs @@ -0,0 +1,88 @@ +use response::*; + +use term_painter::Color::*; +use term_painter::ToStyle; +use std::fmt; + +pub enum Outcome<'h> { + Complete, + FailStop, + FailForward(HyperResponse<'h, HyperFresh>), +} + +impl<'h> Outcome<'h> { + pub fn as_str(&self) -> &'static str { + match self { + &Outcome::Complete => "Complete", + &Outcome::FailStop => "FailStop", + &Outcome::FailForward(..) => "FailForward", + } + } + + pub fn is_forward(&self) -> bool { + match self { + &Outcome::FailForward(_) => true, + _ => false + } + } + + pub fn map_forward(self, f: F) + where F: FnOnce(FreshHyperResponse<'h>) { + match self { + Outcome::FailForward(res) => f(res), + _ => { /* nothing */ } + } + } + + pub fn map_forward_or(self, default: R, f: F) -> R + where F: FnOnce(FreshHyperResponse<'h>) -> R { + match self { + Outcome::FailForward(res) => f(res), + _ => default + } + } + + pub fn is_failure(&self) -> bool { + self == &Outcome::FailStop + } + + pub fn is_complete(&self) -> bool { + self == &Outcome::Complete + } + + fn as_int(&self) -> isize { + match self { + &Outcome::Complete => 0, + &Outcome::FailStop => 1, + &Outcome::FailForward(..) => 2, + } + } +} + +impl<'h> PartialEq for Outcome<'h> { + fn eq(&self, other: &Outcome<'h>) -> bool { + self.as_int() == other.as_int() + } +} + +impl<'h> fmt::Debug for Outcome<'h> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Outcome::{}", self.as_str()) + } +} + +impl<'h> fmt::Display for Outcome<'h> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + &Outcome::Complete => { + write!(f, "{}", Green.paint("Complete")) + }, + &Outcome::FailStop => { + write!(f, "{}", Red.paint("Failed")) + }, + &Outcome::FailForward(..) => { + write!(f, "{}", Yellow.paint("Forwarding")) + }, + } + } +} diff --git a/lib/src/response/redirect.rs b/lib/src/response/redirect.rs index d2ab0a9d..fdb0b18c 100644 --- a/lib/src/response/redirect.rs +++ b/lib/src/response/redirect.rs @@ -22,11 +22,12 @@ impl Redirect { } impl<'a> Responder for Redirect { - fn respond<'b>(&mut self, mut res: HyperResponse<'b, HyperFresh>) { + fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { res.headers_mut().set(header::ContentLength(0)); res.headers_mut().set(header::Location(self.1.clone())); *(res.status_mut()) = self.0; res.send(b"").unwrap(); + Outcome::Complete } } diff --git a/lib/src/response/responder.rs b/lib/src/response/responder.rs index bcefeea0..7a3103c3 100644 --- a/lib/src/response/responder.rs +++ b/lib/src/response/responder.rs @@ -8,24 +8,26 @@ use std::fmt; // In particular, we want to try the next ranked route when when parsing // parameters doesn't work out. pub trait Responder { - fn respond<'a>(&mut self, mut res: HyperResponse<'a, HyperFresh>); + fn respond<'a>(&mut self, mut res: FreshHyperResponse<'a>) -> Outcome<'a>; } impl<'a> Responder for &'a str { - fn respond<'b>(&mut self, res: HyperResponse<'b, HyperFresh>) { + fn respond<'b>(&mut self, res: FreshHyperResponse<'b>) -> Outcome<'b> { res.send(self.as_bytes()).unwrap(); + Outcome::Complete } } impl Responder for String { - fn respond<'b>(&mut self, res: HyperResponse<'b, HyperFresh>) { + fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a> { res.send(self.as_bytes()).unwrap(); + Outcome::Complete } } // FIXME: Should we set a content-type here? Safari needs text/html to render. impl Responder for File { - fn respond<'b>(&mut self, mut res: HyperResponse<'b, HyperFresh>) { + fn respond<'a>(&mut self, mut res: FreshHyperResponse<'a>) -> Outcome<'a> { let size = self.metadata().unwrap().len(); res.headers_mut().set(header::ContentLength(size)); @@ -36,36 +38,38 @@ impl Responder for File { let mut stream = res.start().unwrap(); stream.write_all(&v).unwrap(); + Outcome::Complete } } impl Responder for Option { - fn respond<'b>(&mut self, res: HyperResponse<'b, HyperFresh>) { + fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a> { if self.is_none() { println!("Option is none."); // TODO: Should this be a 404 or 500? return Empty::new(StatusCode::NotFound).respond(res) } - self.as_mut().unwrap().respond(res); + self.as_mut().unwrap().respond(res) } } impl Responder for Result { // prepend with `default` when using impl specialization - default fn respond<'b>(&mut self, res: HyperResponse<'b, HyperFresh>) { + default fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) + -> Outcome<'a> { if self.is_err() { println!("Error: {:?}", self.as_ref().err().unwrap()); // TODO: Should this be a 404 or 500? return Empty::new(StatusCode::NotFound).respond(res) } - self.as_mut().unwrap().respond(res); + self.as_mut().unwrap().respond(res) } } impl Responder for Result { - fn respond<'b>(&mut self, res: HyperResponse<'b, HyperFresh>) { + fn respond<'a>(&mut self, res: FreshHyperResponse<'a>) -> Outcome<'a> { match self { &mut Ok(ref mut responder) => responder.respond(res), &mut Err(ref mut responder) => responder.respond(res) diff --git a/lib/src/response/with_status.rs b/lib/src/response/with_status.rs index a1f2ae5b..b5c52f29 100644 --- a/lib/src/response/with_status.rs +++ b/lib/src/response/with_status.rs @@ -15,9 +15,9 @@ impl StatusResponse { } impl Responder for StatusResponse { - fn respond<'b>(&mut self, mut res: HyperResponse<'b, HyperFresh>) { + fn respond<'b>(&mut self, mut res: FreshHyperResponse<'b>) -> Outcome<'b> { *(res.status_mut()) = self.status; - self.responder.respond(res); + self.responder.respond(res) } } diff --git a/lib/src/rocket.rs b/lib/src/rocket.rs index 5f5bb9c3..94206aec 100644 --- a/lib/src/rocket.rs +++ b/lib/src/rocket.rs @@ -1,5 +1,5 @@ use super::*; -use response::{HyperResponse, HyperFresh}; +use response::FreshHyperResponse; use request::HyperRequest; use catcher; @@ -9,6 +9,7 @@ use term_painter::Color::*; use term_painter::ToStyle; use hyper::uri::RequestUri as HyperRequestUri; +use hyper::method::Method as HyperMethod; use hyper::server::Server as HyperServer; use hyper::server::Handler as HyperHandler; @@ -19,46 +20,94 @@ pub struct Rocket { catchers: HashMap, } +fn uri_is_absolute(uri: &HyperRequestUri) -> bool { + match uri { + &HyperRequestUri::AbsolutePath(_) => true, + _ => false + } +} + +fn unwrap_absolute_path<'a>(uri: &'a HyperRequestUri) -> &'a str { + match uri { + &HyperRequestUri::AbsolutePath(ref s) => s.as_str(), + _ => panic!("Can only accept absolute paths!") + } +} + +fn method_is_valid(method: &HyperMethod) -> bool { + Method::from_hyp(method).is_some() +} + impl HyperHandler for Rocket { - fn handle<'a, 'k>(&'a self, mut req: HyperRequest<'a, 'k>, - res: HyperResponse<'a, HyperFresh>) { - println!("{:?} {:?}", Green.paint(&req.method), Blue.paint(&req.uri)); + fn handle<'a, 'k>(&'a self, req: HyperRequest<'a, 'k>, + res: FreshHyperResponse<'a>) { + println!("{:?} '{}'", Green.paint(&req.method), Blue.paint(&req.uri)); - let mut buf = vec![]; - req.read_to_end(&mut buf); // FIXME: Simple DOS attack here. - if let HyperRequestUri::AbsolutePath(uri_string) = req.uri { - if let Some(method) = Method::from_hyp(req.method) { - let uri_str = uri_string.as_str(); - let route = self.router.route(method, uri_str); + let finalize = |mut req: HyperRequest, _res: FreshHyperResponse| { + let mut buf = vec![]; + // FIXME: Simple DOS attack here. Working around Hyper bug. + let _ = req.read_to_end(&mut buf); + }; - if route.is_some() { - let route = route.unwrap(); - let params = route.get_params(uri_str); - let request = Request::new(params, uri_str, &buf); - - println!("{}", Green.paint("\t=> Dispatching request.")); - // FIXME: Responder should be able to say it didn't work. - return (route.handler)(request).respond(res); - } else { - // FIXME: Try next highest ranking route, not just 404. - let request = Request::new(vec![], uri_str, &buf); - let handler_404 = self.catchers.get(&404).unwrap().handler; - - let msg = "\t=> Dispatch failed. Returning 404."; - println!("{}", Red.paint(msg)); - return handler_404(request).respond(res); - } - } - - println!("{}", Yellow.paint("\t=> Debug: Method::from_hyp failed!")); + if !uri_is_absolute(&req.uri) { + println!("{}", Red.paint("\t=> Internal failure. Bad URI.")); + println!("{} {:?}", Yellow.paint("\t=> Debug:"), req.uri); + return finalize(req, res); } - println!("{}", Red.paint("\t=> Internal failure. Bad method or path.")); - Response::server_error().respond(res); + if !method_is_valid(&req.method) { + println!("{}", Yellow.paint("\t=> Internal failure. Bad method.")); + println!("{} {:?}", Yellow.paint("\t=> Debug:"), req.method); + return finalize(req, res); + } + + return self.dispatch(req, res); } } impl Rocket { + fn dispatch<'h, 'k>(&self, mut req: HyperRequest<'h, 'k>, + mut res: FreshHyperResponse<'h>) { + // We read all of the contents now because we have to do it at some + // point thanks to Hyper. FIXME: Simple DOS attack here. + let mut buf = vec![]; + let _ = req.read_to_end(&mut buf); + + // Extract the method, uri, and try to find a route. + let method = Method::from_hyp(&req.method).unwrap(); + let uri = unwrap_absolute_path(&req.uri); + let route = self.router.route(method, uri); + + // A closure which we call when we know there is no route. + let handle_not_found = |response: FreshHyperResponse| { + let request = Request::new(vec![], uri, &buf); + let handler_404 = self.catchers.get(&404).unwrap().handler; + println!("{}", Red.paint("\t<= Dispatch failed. Returning 404.")); + handler_404(request).respond(response); + }; + + // No route found. Handle the not_found error and return. + if route.is_none() { + println!("{}", Red.paint("\t=> No matching routes.")); + return handle_not_found(res); + } + + // Okay, we've got a route. Unwrap it, generate a request, and try to + // dispatch. TODO: keep trying lower ranked routes before dispatching a + // not found error. + println!("\t=> {}", Magenta.paint("Dispatching request.")); + let route = route.unwrap(); + let params = route.get_params(uri); + let request = Request::new(params, uri, &buf); + let outcome = (route.handler)(request).respond(res); + + println!("\t=> {} {}", White.paint("Outcome:"), outcome); + outcome.map_forward(|res| { + println!("{}", Red.paint("\t=> No further matching routes.")); + handle_not_found(res); + }); + } + pub fn new(address: &'static str, port: isize) -> Rocket { Rocket { address: address, @@ -68,7 +117,8 @@ impl Rocket { } } - pub fn mount(&mut self, base: &'static str, routes: Vec) -> &mut Self { + pub fn mount(&mut self, base: &'static str, routes: Vec) + -> &mut Self { println!("🛰 {} '{}':", Magenta.paint("Mounting"), Blue.paint(base)); for mut route in routes { let path = format!("{}/{}", base, route.path.as_str()); diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs index d938c0b6..daa371ba 100644 --- a/lib/src/router/mod.rs +++ b/lib/src/router/mod.rs @@ -30,6 +30,8 @@ impl Router { // TODO: Make a `Router` trait with this function. Rename this `Router` // struct to something like `RocketRouter`. If that happens, returning a // `Route` structure is inflexible. Have it be an associated type. + // FIXME: Figure out a way to get more than one route, i.e., to correctly + // handle ranking. pub fn route<'b>(&'b self, method: Method, uri: &str) -> Option<&'b Route> { let mut matched_route: Option<&Route> = None; @@ -37,7 +39,7 @@ impl Router { let num_segments = path.segment_count(); if let Some(routes) = self.routes.get(&(method, num_segments)) { for route in routes.iter().filter(|r| r.collides_with(uri)) { - println!("\t=> Matched {} to: {}", uri, route); + println!("\t=> {} {}", Magenta.paint("Matched:"), route); if let Some(existing_route) = matched_route { if route.rank > existing_route.rank { matched_route = Some(route); diff --git a/macros/src/route_decorator.rs b/macros/src/route_decorator.rs index ede8d492..ce067079 100644 --- a/macros/src/route_decorator.rs +++ b/macros/src/route_decorator.rs @@ -359,7 +359,7 @@ pub fn route_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, let param_fn_item = quote_stmt!(ecx, let $param_ident: $param_ty = match _req.get_param($i) { Ok(v) => v, - Err(_) => return rocket::Response::not_found() + Err(_) => return rocket::Response::forward() }; ).unwrap();